plugins: Rewrote timer plugin fixing multiple issues.

Added emulated time recording as well as wall clock time.

Fixed recording time for multiple software items per system.  An
incorrect constraint on the database table meant that time was only
being recorded for a single software item per system.

Detect the "empty" driver so the time spent at the selection menu isn't
recorded (you'd get multiple entries for this due to the way options
leak when returning to the system selection menu).

Included schema migration code to update existing timer plugin
databases.  Also replaced some unnecessary floating point code with
integer maths, added log messages, and made the plugin unload unload its
database access code during emulation.

Changed other plugins' use of paths with trailing slashes as this causes
stat to fail on Windows.
This commit is contained in:
Vas Crabb 2021-11-06 05:20:59 +11:00
parent 3bb8e3adc7
commit 07e55935cf
8 changed files with 325 additions and 114 deletions

View File

@ -1,7 +1,7 @@
.. _plugins-timer:
Timer Plugin
============
Game Play Timer Plugin
======================
The timer plugin records the total time spent emulating each combination of a
system and a software list item, as well as the number of times each combination
@ -9,12 +9,12 @@ has been launched. To see the statistics, bring up the main menu (press **Tab**
during emulation by default), choose **Plugin Options**, and then choose
**Timer**.
Note that this plugin records wall clock time, not emulated time. That is, it
records the real time duration elapsed while the emulation is running, according
to the host OS. This may be shorter than the elapsed emulated time if you turn
off throttling or use MAMEs “fast forward” feature, or it may be longer than
the elapsed emulated time if you pause the emulation of if the emulation is too
demanding to run at full speed.
This plugin records wall clock time (the real time duration elapsed while
emulation is running, according to the host OS) as well as emulated time. The
elapsed wall clock time may be shorter than the elapsed emulated time if you
turn off throttling or use MAMEs “fast forward” feature, or it may be longer
than the elapsed emulated time if you pause the emulation of if the emulation is
too demanding to run at full speed.
The statistics are stored in the file **timer.db** in the **timer** folder
inside your plugin data folder (see the

View File

@ -1,7 +1,7 @@
local lib = {}
local function get_settings_path()
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/autofire/'
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/autofire'
end
local function get_settings_filename()
@ -52,7 +52,7 @@ end
function lib:load_settings()
local buttons = {}
local json = require('json')
local filename = get_settings_path() .. get_settings_filename()
local filename = get_settings_path() .. '/' .. get_settings_filename()
local file = io.open(filename, 'r')
if not file then
return buttons
@ -81,14 +81,14 @@ function lib:save_settings(buttons)
emu.print_error(string.format('Error saving autofire settings: "%s" is not a directory', path))
return
end
local filename = path .. '/' .. get_settings_filename()
if #buttons == 0 then
os.remove(path .. get_settings_filename())
os.remove(filename)
return
end
local json = require('json')
local settings = serialize_settings(buttons)
local data = json.stringify(settings, {indent = true})
local filename = path .. get_settings_filename()
local file = io.open(filename, 'w')
if not file then
emu.print_error(string.format('Error saving autofire settings: error opening file "%s" for writing', filename))

View File

@ -23,7 +23,7 @@ end
function hiscore.startplugin()
local function get_data_path()
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/hiscore/'
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/hiscore'
end
-- configuration
@ -35,7 +35,7 @@ function hiscore.startplugin()
if config_read then
return true
end
local filename = get_data_path() .. 'plugin.cfg'
local filename = get_data_path() .. '/plugin.cfg'
local file = io.open(filename, 'r')
if file then
local json = require('json')
@ -67,7 +67,7 @@ function hiscore.startplugin()
end
local settings = { only_save_at_exit = not timed_save }
-- TODO: other settings?
local filename = path .. 'plugin.cfg'
local filename = path .. '/plugin.cfg'
local json = require('json')
local data = json.stringify(settings, { indent = true })
local file = io.open(filename, 'w')
@ -210,9 +210,9 @@ function hiscore.startplugin()
local r;
if emu.softname() ~= "" then
local soft = emu.softname():match("([^:]*)$")
r = get_data_path() .. emu.romname() .. "_" .. soft .. ".hi";
r = get_data_path() .. '/' .. emu.romname() .. "_" .. soft .. ".hi";
else
r = get_data_path() .. emu.romname() .. ".hi";
r = get_data_path() .. '/' .. emu.romname() .. ".hi";
end
return r;
end
@ -223,7 +223,7 @@ function hiscore.startplugin()
local output = io.open(get_file_name(), "wb");
if not output then
-- attempt to create the directory, and try again
lfs.mkdir( get_data_path() );
lfs.mkdir(get_data_path());
output = io.open(get_file_name(), "wb");
end
emu.print_verbose("hiscore: write_scores output")

View File

@ -5,7 +5,7 @@
-- Helpers
local function settings_path()
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/inputmacro/'
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/inputmacro'
end
local function settings_filename()
@ -101,7 +101,7 @@ end
local lib = { }
function lib:load_settings()
filename = settings_path() .. settings_filename()
filename = settings_path() .. '/' .. settings_filename()
local file = io.open(filename, 'r')
if not file then
return { }
@ -133,7 +133,7 @@ function lib:save_settings(macros)
emu.print_error(string.format('Error saving input macros: "%s" is not a directory', path))
return
end
filename = path .. settings_filename()
filename = path .. '/' .. settings_filename()
if #macros == 0 then
os.remove(filename)

View File

@ -34,7 +34,7 @@ function timecode.startplugin()
local function get_settings_path()
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/timecode/'
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/timecode'
end
@ -50,7 +50,7 @@ function timecode.startplugin()
set_default_hotkey()
-- try to open configuration file
local cfgname = get_settings_path() .. 'plugin.cfg'
local cfgname = get_settings_path() .. '/plugin.cfg'
local cfgfile = io.open(cfgname, 'r')
if not cfgfile then
return -- probably harmless, configuration just doesn't exist yet
@ -97,7 +97,7 @@ function timecode.startplugin()
settings.hotkey = hotkey_cfg
end
local data = json.stringify(settings, { indent = true })
local cfgname = path .. 'plugin.cfg'
local cfgname = path .. '/plugin.cfg'
local cfgfile = io.open(cfgname, 'w')
if not cfgfile then
emu.print_error(string.format('Error saving timecode recorder settings: error opening file "%s" for writing', cfgname))

View File

@ -1,111 +1,73 @@
-- license:BSD-3-Clause
-- copyright-holders:Carl
require('lfs')
local sqlite3 = require('lsqlite3')
local exports = {}
exports.name = "timer"
exports.version = "0.0.2"
exports.description = "Game play timer"
exports.license = "BSD-3-Clause"
exports.author = { name = "Carl" }
-- copyright-holders:Vas Crabb
local exports = {
name = 'timer',
version = '0.0.3',
description = 'Game play timer',
license = 'BSD-3-Clause',
author = { name = 'Vas Crabb' } }
local timer = exports
function timer.startplugin()
local dir = emu.subst_env(manager.options.entries.homepath:value())
local timer_db = dir .. "/timer/timer.db"
local timer_started = false
local total_time = 0
local start_time = 0
local play_count = 0
local emu_total = emu.attotime()
local function save()
total_time = total_time + (os.time() - start_time)
local reference = 0
local lastupdate
local db = assert(sqlite3.open(timer_db))
local insert_stmt = assert( db:prepare("INSERT OR IGNORE INTO timer VALUES (?, ?, 0, 0)") )
insert_stmt:bind_values(emu.romname(), emu.softname())
insert_stmt:step()
insert_stmt:reset()
local update_stmt = assert( db:prepare("UPDATE timer SET total_time=?, play_count=? WHERE driver=? AND software=?") )
update_stmt:bind_values(total_time, play_count,emu.romname(), emu.softname())
update_stmt:step()
update_stmt:reset()
assert(db:close() == sqlite3.OK)
end
emu.register_start(function()
local file
if timer_started then
save()
end
timer_started = true
lfs.mkdir(dir .. '/timer')
local db = assert(sqlite3.open(timer_db))
local found=false
db:exec([[select * from sqlite_master where name='timer';]], function(...) found=true return 0 end)
if not found then
db:exec[[ CREATE TABLE timer (
driver VARCHAR(32) PRIMARY KEY,
software VARCHAR(40),
total_time INTEGER NOT NULL,
play_count INTEGER NOT NULL
); ]]
end
local stmt, row
stmt = db:prepare("SELECT total_time, play_count FROM timer WHERE driver = ? AND software = ?")
stmt:bind_values(emu.romname(), emu.softname())
if (stmt:step() == sqlite3.ROW) then
row = stmt:get_named_values()
play_count = row.play_count
total_time = row.total_time
else
play_count = 0
total_time = 0
end
assert(db:close() == sqlite3.OK)
start_time = os.time()
play_count = play_count + 1
end)
emu.register_stop(function()
timer_started = false
save()
total_time = 0
play_count = 0
end)
local function sectohms(time)
local hrs = math.floor(time / 3600)
local min = math.floor((time % 3600) / 60)
local hrs = time // 3600
local min = (time % 3600) // 60
local sec = time % 60
return string.format(_p("plugin-timer", "%03d:%02d:%02d"), hrs, min, sec)
return string.format(_p('plugin-timer', '%03d:%02d:%02d'), hrs, min, sec)
end
local lastupdate
local function menu_populate()
lastupdate = os.time()
local time = lastupdate - start_time
local refname = (reference == 0) and _p('plugin-timer', 'Wall clock') or _p('plugin-timer', 'Emulated time')
local time = (reference == 0) and (lastupdate - start_time) or manager.machine.time.seconds
local total = (reference == 0) and (total_time + time) or (manager.machine.time + emu_total).seconds
return
{{ _p("plugin-timer", "Current time"), sectohms(time), "off" },
{ _p("plugin-timer", "Total time"), sectohms(total_time + time), "off" },
{ _p("plugin-timer", "Play Count"), play_count, "off" }},
{
{ _p("plugin-timer", "Reference"), refname, (reference == 0) and 'r' or 'l' },
{ '---', '', '' },
{ _p("plugin-timer", "Current time"), sectohms(time), "off" },
{ _p("plugin-timer", "Total time"), sectohms(total), "off" },
{ _p("plugin-timer", "Play Count"), play_count, "off" } },
nil,
"idle"
end
local function menu_callback(index, event)
if (index == 1) and ((event == 'left') or (event == 'right') or (event == 'select')) then
reference = reference ~ 1
return true
end
return os.time() > lastupdate
end
emu.register_start(
function()
if emu.romname() ~= '___empty' then
start_time = os.time()
local persister = require('timer/timer_persist')
total_time, play_count, emu_total = persister:start_session()
end
end)
emu.register_stop(
function()
if emu.romname() ~= '___empty' then
local persister = require('timer/timer_persist')
persister:update_totals(start_time)
end
end)
emu.register_menu(menu_callback, menu_populate, _p("plugin-timer", "Timer"))
end

View File

@ -1,10 +1,10 @@
{
"plugin": {
"name": "timer",
"description": "Timer plugin",
"version": "0.0.1",
"author": "Carl",
"type": "plugin",
"start": "false"
}
"plugin": {
"name": "timer",
"description": "Game play timer",
"version": "0.0.3",
"author": "Vas Crabb",
"type": "plugin",
"start": "false"
}
}

View File

@ -0,0 +1,249 @@
-- license:BSD-3-Clause
-- copyright-holders:Vas Crabb
local sqlite3 = require('lsqlite3')
local function check_schema(db)
local create_statement =
[[CREATE TABLE timer (
driver VARCHAR(32) NOT NULL,
softlist VARCHAR(24) NOT NULL DEFAULT '',
software VARCHAR(16) NOT NULL DEFAULT '',
total_time INTEGER NOT NULL DEFAULT 0,
play_count INTEGER NOT NULL DEFAULT 0,
emu_sec INTEGER NOT NULL DEFAULT 0,
emu_nsec INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (driver, softlist, software));]]
-- create table if it doesn't exist yet
local table_found = false
db:exec(
[[SELECT * FROM sqlite_master WHERE type = 'table' AND name='timer';]],
function()
table_found = true
end)
if not table_found then
emu.print_verbose('Creating timer database table')
db:exec(create_statement)
return
end
-- check recently added columns
local have_softlist = false
local have_emu_sec = false
local have_emu_nsec = false
db:exec(
[[PRAGMA table_info(timer);]],
function(udata, n, vals, cols)
for i, name in ipairs(cols) do
if name == 'name' then
if vals[i] == 'softlist' then
have_softlist = true
elseif vals[i] == 'emu_sec' then
have_emu_sec = true
elseif vals[i] == 'emu_nsec' then
have_emu_nsec = true
end
return 0
end
end
return 0
end)
if not have_softlist then
emu.print_verbose('Adding softlist column to timer database')
db:exec([[ALTER TABLE timer ADD COLUMN softlist VARCHAR(24) NOT NULL DEFAULT '';]])
local to_split = { }
db:exec(
[[SELECT DISTINCT software FROM timer WHERE software LIKE '%:%';]],
function(udata, n, vals)
table.insert(to_split, vals[1])
return 0
end)
if #to_split > 0 then
local update = db:prepare([[UPDATE timer SET softlist = ?, software = ? WHERE software = ?;]])
for i, softname in ipairs(to_split) do
local x, y = softname:find(':')
local softlist = softname:sub(1, x - 1)
local software = softname:sub(y + 1)
update:bind_values(softlist, software, softname)
update:step()
update:reset()
end
end
end
if not have_emu_sec then
emu.print_verbose('Adding emu_sec column to timer database')
db:exec([[ALTER TABLE timer ADD COLUMN emu_sec INTEGER NOT NULL DEFAULT 0;]])
end
if not have_emu_nsec then
emu.print_verbose('Adding emu_nsec column to timer database')
db:exec([[ALTER TABLE timer ADD COLUMN emu_nsec INTEGER NOT NULL DEFAULT 0;]])
end
-- check the required columns are in the primary key
local index_name
db:exec(
[[SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'timer';]],
function(udata, n, vals)
index_name = vals[1]
end)
local index_good
if index_name then
local driver_indexed = false
local softlist_indexed = false
local software_indexed = false
db:exec(
string.format([[PRAGMA index_info('%s');]], index_name), -- can't use prepared statement for PRAGMA
function(udata, n, vals, cols)
for i, name in ipairs(cols) do
if name == 'name' then
if vals[i] == 'driver' then
driver_indexed = true
elseif vals[i] == 'softlist' then
softlist_indexed = true
elseif vals[i] == 'software' then
software_indexed = true
end
return 0
end
end
return 0
end)
index_good = driver_indexed and softlist_indexed and software_indexed
end
-- if the required columns are not indexed, migrate to a new table with desired primary key
if not index_good then
emu.print_verbose('Re-indexing timer database table')
db:exec([[DROP TABLE IF EXISTS timer_old;]])
db:exec([[ALTER TABLE timer RENAME TO timer_old;]])
db:exec(create_statement)
db:exec(
[[INSERT
INTO timer (driver, softlist, software, total_time, play_count, emu_sec, emu_nsec)
SELECT driver, softlist, software, total_time, play_count, emu_sec, emu_nsec FROM timer_old;]])
db:exec([[DROP TABLE timer_old;]])
end
end
local function open_database()
-- make sure settings directory exists
local dir = emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/timer'
local attr = lfs.attributes(dir)
if not attr then
lfs.mkdir(dir)
elseif attr.mode ~= 'directory' then
emu.print_error(string.format('Error opening timer database: "%s" is not a directory', dir))
return nil
end
-- open database
local filename = dir .. '/timer.db'
local db = sqlite3.open(filename)
if not db then
emu.print_error(string.format('Error opening timer database file "%s"', filename))
return nil
end
-- make sure schema is up-to-date
check_schema(db)
return db
end
local function get_software()
local softname = emu.softname()
local i, j = softname:find(':')
if i then
return softname:sub(1, i - 1), softname:sub(j + 1)
else
-- FIXME: need a way to get the implicit software list when no colon in the option value
return '', softname
end
end
local function get_current(db)
local statement = db:prepare(
[[SELECT
total_time, play_count, emu_sec, emu_nsec
FROM timer
WHERE driver = ? AND softlist = ? AND software = ?;]])
statement:bind_values(emu.romname(), get_software())
local result
if statement:step() == sqlite3.ROW then
result = statement:get_named_values()
end
statement:reset()
return result
end
local lib = { }
function lib:start_session()
-- open database
local db = open_database()
if not db then
return 0, 0, emu.attotime()
end
-- get existing values
local row = get_current(db)
local update
if row then
update = db:prepare(
[[UPDATE timer
SET play_count = play_count + 1
WHERE driver = ? AND softlist = ? AND software = ?;]])
else
row = { total_time = 0, play_count = 0, emu_sec = 0, emu_nsec = 0 }
update = db:prepare(
[[INSERT
INTO timer (driver, softlist, software, total_time, play_count, emu_sec, emu_nsec)
VALUES (?, ?, ?, 0, 1, 0, 0);]])
end
update:bind_values(emu.romname(), get_software())
update:step()
update:reset()
return row.total_time, row.play_count + 1, emu.attotime.from_seconds(row.emu_sec) + emu.attotime.from_nsec(row.emu_nsec)
end
function lib:update_totals(start)
-- open database
local db = open_database()
if not db then
return
end
-- get existing values
local row = get_current(db)
if not row then
row = { total_time = 0, play_count = 1, emu_sec = 0, emu_nsec = 0 }
end
-- calculate new totals
local emu_total = emu.attotime.from_seconds(row.emu_sec) + emu.attotime.from_nsec(row.emu_nsec) + manager.machine.time
row.total_time = os.time() - start + row.total_time
row.emu_sec = emu_total.seconds
row.emu_nsec = emu_total.nsec
-- update database
local update = db:prepare(
[[INSERT OR REPLACE
INTO timer (driver, softlist, software, total_time, play_count, emu_sec, emu_nsec)
VALUES (?, ?, ?, ?, ?, ?, ?);]])
local softlist, software = get_software()
update:bind_values(emu.romname(), softlist, software, row.total_time, row.play_count, row.emu_sec, row.emu_nsec)
update:step()
update:reset()
-- close database
if db:close() ~= sqlite3.OK then
emu.print_error('Error closing timer database')
end
end
return lib