mame/plugins/hiscore/init.lua
Vas Crabb 07e55935cf 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.
2021-11-06 05:20:59 +11:00

375 lines
9.8 KiB
Lua

-- hiscore.lua
-- by borgar@borgar.net, CC0 license
--
-- This uses MAME's built-in Lua scripting to implement
-- high-score saving with hiscore.dat infom just as older
-- builds did in the past.
--
local exports = {
name = 'hiscore',
version = '1.0.1',
description = 'Hiscore',
license = 'CC0',
author = { name = 'borgar@borgar.net' } }
local hiscore = exports
local hiscore_plugin_path = ""
function hiscore.set_folder(path)
hiscore_plugin_path = path
end
function hiscore.startplugin()
local function get_data_path()
return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/hiscore'
end
-- configuration
local config_read = false
local timed_save = true
-- read configuration file from data directory
local function read_config()
if config_read then
return true
end
local filename = get_data_path() .. '/plugin.cfg'
local file = io.open(filename, 'r')
if file then
local json = require('json')
local parsed_settings = json.parse(file:read('a'))
file:close()
if parsed_settings then
if parsed_settings.only_save_at_exit and (parsed_settings.only_save_at_exit ~= 0) then
timed_save = false
end
-- TODO: other settings? maybe path overrides for hiscore.dat or the hiscore data?
config_read = true
return true
else
emu.print_error(string.format('Error loading hiscore plugin settings: error parsing file "%s" as JSON', filename))
end
end
return false
end
-- save configuration file
local function save_config()
local path = get_data_path()
local attr = lfs.attributes(path)
if not attr then
lfs.mkdir(path)
elseif attr.mode ~= 'directory' then
emu.print_error(string.format('Error saving hiscore plugin settings: "%s" is not a directory', path))
return
end
local settings = { only_save_at_exit = not timed_save }
-- TODO: other settings?
local filename = path .. '/plugin.cfg'
local json = require('json')
local data = json.stringify(settings, { indent = true })
local file = io.open(filename, 'w')
if not file then
emu.print_error(string.format('Error saving hiscore plugin settings: error opening file "%s" for writing', filename))
return
end
file:write(data)
file:close()
end
-- build menu
local function populate_menu()
local items = { }
local setting = timed_save and _p('plugin-hiscore', 'When updated') or _p('plugin-hiscore', 'On exit')
table.insert(items, { _p('plugin-hiscore', 'Hiscore Support Options'), '', 'off' })
table.insert(items, { '---', '', '' })
table.insert(items, { _p('plugin-hiscore', 'Save scores'), setting, timed_save and 'l' or 'r' })
return items
end
-- handle menu events
local function handle_menu(index, event)
if event == 'left' then
timed_save = false
return true
elseif event == 'right' then
timed_save = true
return true
end
return false
end
local hiscoredata_path = "hiscore.dat";
local current_checksum = 0;
local default_checksum = 0;
local scores_have_been_read = false;
local mem_check_passed = false;
local found_hiscore_entry = false;
local delaytime = 0;
local function parse_table ( dsting )
local _table = {}
for line in string.gmatch(dsting, '([^\n]+)') do
local delay = line:match('^@delay=([.%d]*)')
if delay and #delay > 0 then
delaytime = emu.time() + tonumber(delay)
else
local cpu, mem;
local cputag, space, offs, len, chk_st, chk_ed, fill = string.match(line, '^@([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),?(%x?%x?)');
cpu = manager.machine.devices[cputag];
if not cpu then
error(cputag .. " device not found")
end
local rgnname, rgntype = space:match("([^/]*)/?([^/]*)")
if rgntype == "share" then
mem = manager.machine.memory.shares[rgnname]
else
mem = cpu.spaces[space]
end
if not mem then
error(space .. " space not found")
end
_table[ #_table + 1 ] = {
mem = mem,
addr = tonumber(offs, 16),
size = tonumber(len, 16),
c_start = tonumber(chk_st, 16),
c_end = tonumber(chk_ed, 16),
fill = tonumber(fill, 16)
};
end
end
return _table;
end
local function read_hiscore_dat ()
local file = io.open( hiscoredata_path, "r" );
local rm_match;
if not file then
file = io.open( hiscore_plugin_path .. "/hiscore.dat", "r" );
end
if emu.softname() ~= "" then
local soft = emu.softname():match("([^:]*)$")
rm_match = '^' .. emu.romname() .. ',' .. soft .. ':';
else
rm_match = '^' .. emu.romname() .. ':';
end
local cluster = "";
local current_is_match = false;
if file then
repeat
line = file:read("*l");
if line then
-- remove comments
line = line:gsub( '[ \t\r\n]*;.+$', '' );
-- handle lines
if string.find(line, '^@') then -- data line
if current_is_match then
cluster = cluster .. "\n" .. line;
end
elseif string.find(line, rm_match) then --- match this game
current_is_match = true;
elseif string.find(line, '^[a-z0-9_]+:') then --- some game
if current_is_match and string.len(cluster) > 0 then
break; -- we're done
end
else --- empty line or garbage
-- noop
end
end
until not line;
file:close();
end
return cluster;
end
local function check_mem ( posdata )
if #posdata < 1 then
return false;
end
for ri,row in ipairs(posdata) do
-- must pass mem check
if row["c_start"] ~= row["mem"]:read_u8(row["addr"]) then
return false;
end
if row["c_end"] ~= row["mem"]:read_u8(row["addr"]+row["size"]-1) then
return false;
end
end
return true;
end
local function get_file_name()
local r;
if emu.softname() ~= "" then
local soft = emu.softname():match("([^:]*)$")
r = get_data_path() .. '/' .. emu.romname() .. "_" .. soft .. ".hi";
else
r = get_data_path() .. '/' .. emu.romname() .. ".hi";
end
return r;
end
local function write_scores ( posdata )
emu.print_verbose("hiscore: write_scores")
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());
output = io.open(get_file_name(), "wb");
end
emu.print_verbose("hiscore: write_scores output")
if output then
for ri,row in ipairs(posdata) do
t = {}
for i=0,row["size"]-1 do
t[i+1] = row["mem"]:read_u8(row["addr"] + i)
end
output:write(string.char(table.unpack(t)));
end
output:close();
end
emu.print_verbose("hiscore: write_scores end")
end
local function read_scores ( posdata )
local input = io.open(get_file_name(), "rb");
if input then
for ri,row in ipairs(posdata) do
local str = input:read(row["size"]);
for i=0,row["size"]-1 do
local b = str:sub(i+1,i+1):byte();
row["mem"]:write_u8( row["addr"] + i, b );
end
end
input:close();
return true;
end
return false;
end
local function check_scores ( posdata )
local r = 0;
for ri,row in ipairs(posdata) do
for i=0,row["size"]-1 do
r = r + row["mem"]:read_u8( row["addr"] + i );
end
end
return r;
end
local function init ()
if not scores_have_been_read then
if (delaytime <= emu.time()) and check_mem( positions ) then
default_checksum = check_scores( positions );
if read_scores( positions ) then
emu.print_verbose( "hiscore: scores read OK" );
else
-- likely there simply isn't a .hi file around yet
emu.print_verbose( "hiscore: scores read FAIL" );
end
scores_have_been_read = true;
current_checksum = check_scores( positions );
mem_check_passed = true;
else
-- memory check can fail while the game is still warming up
-- TODO: only allow it to fail N many times
end
end
end
local last_write_time = -10;
local function tick ()
-- set up scores if they have been
init();
-- only allow save check to run when
if mem_check_passed and timed_save then
-- The reason for this complicated mess is that
-- MAME does expose a hook for "exit". Once it does,
-- this should obviously just be done when the emulator
-- shuts down (or reboots).
local checksum = check_scores( positions );
if checksum ~= current_checksum and checksum ~= default_checksum then
-- 5 sec grace time so we don't clobber io and cause
-- latency. This would be bad as it would only ever happen
-- to players currently reaching a new highscore
if emu.time() > last_write_time + 5 then
write_scores( positions );
current_checksum = checksum;
last_write_time = emu.time();
-- emu.print_verbose( "SAVE SCORES EVENT!", last_write_time );
end
end
end
end
local function reset()
-- the notifier will still be attached even if the running game has no hiscore.dat entry
if mem_check_passed and found_hiscore_entry then
local checksum = check_scores(positions)
if checksum ~= current_checksum and checksum ~= default_checksum then
write_scores(positions)
end
end
found_hiscore_entry = false
mem_check_passed = false
scores_have_been_read = false;
end
emu.register_start(function()
found_hiscore_entry = false
mem_check_passed = false
scores_have_been_read = false;
last_write_time = -10
emu.print_verbose("Starting " .. emu.gamename())
read_config();
local dat = read_hiscore_dat()
if dat and dat ~= "" then
emu.print_verbose( "hiscore: found hiscore.dat entry for " .. emu.romname() );
res, positions = pcall(parse_table, dat);
if not res then
emu.print_error("hiscore: hiscore.dat parse error " .. positions);
return;
end
for i, row in pairs(positions) do
if row.fill then
for i=0,row["size"]-1 do
row["mem"]:write_u8(row["addr"] + i, row.fill)
end
end
end
found_hiscore_entry = true
end
end)
emu.register_frame(function()
if found_hiscore_entry then
tick()
end
end)
emu.register_stop(function()
reset()
save_config()
end)
emu.register_prestart(function()
reset()
end)
emu.register_menu(handle_menu, populate_menu, _p('plugin-hiscore', 'Hiscore Support'))
end
return exports