diff --git a/docs/source/plugins/timer.rst b/docs/source/plugins/timer.rst index 9576f0acc20..578dd2beb50 100644 --- a/docs/source/plugins/timer.rst +++ b/docs/source/plugins/timer.rst @@ -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 MAME’s “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 MAME’s “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 diff --git a/plugins/autofire/autofire_save.lua b/plugins/autofire/autofire_save.lua index 1ebe06561e1..5ca4cd217b6 100644 --- a/plugins/autofire/autofire_save.lua +++ b/plugins/autofire/autofire_save.lua @@ -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)) diff --git a/plugins/hiscore/init.lua b/plugins/hiscore/init.lua index 4101dd48708..652061c92cf 100644 --- a/plugins/hiscore/init.lua +++ b/plugins/hiscore/init.lua @@ -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") diff --git a/plugins/inputmacro/inputmacro_persist.lua b/plugins/inputmacro/inputmacro_persist.lua index 4e043852c51..6c2aa6fd188 100644 --- a/plugins/inputmacro/inputmacro_persist.lua +++ b/plugins/inputmacro/inputmacro_persist.lua @@ -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) diff --git a/plugins/timecode/init.lua b/plugins/timecode/init.lua index 084d3b3e8d6..09e49229cae 100644 --- a/plugins/timecode/init.lua +++ b/plugins/timecode/init.lua @@ -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)) diff --git a/plugins/timer/init.lua b/plugins/timer/init.lua index 19755f7a348..d39243da730 100644 --- a/plugins/timer/init.lua +++ b/plugins/timer/init.lua @@ -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 diff --git a/plugins/timer/plugin.json b/plugins/timer/plugin.json index 005a0fad9a4..6755f434f94 100644 --- a/plugins/timer/plugin.json +++ b/plugins/timer/plugin.json @@ -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" + } } diff --git a/plugins/timer/timer_persist.lua b/plugins/timer/timer_persist.lua new file mode 100644 index 00000000000..5ed33f03eb4 --- /dev/null +++ b/plugins/timer/timer_persist.lua @@ -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