1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
|
-- 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
local reset_subscription, frame_subscription, stop_subscription
function hiscore.set_folder(path)
hiscore_plugin_path = path
end
function hiscore.startplugin()
local function get_data_path()
return 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
reset_subscription = emu.add_machine_reset_notifier(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)
frame_subscription = emu.add_machine_frame_notifier(function ()
if found_hiscore_entry then
tick()
end
end)
stop_subscription = emu.add_machine_stop_notifier(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
|