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
|
-- hiscore.lua
-- by borgar@borgar.net, WTFPL license
--
-- This uses MAME's built-in Lua scripting to implment
-- high-score saving with hiscore.dat infom just as older
-- builds did in the past.
--
local exports = {}
exports.name = "hiscore"
exports.version = "1.0.0"
exports.description = "Hiscore"
exports.license = "WTFPL license"
exports.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()
hiscoredata_path = "hiscore.dat";
hiscore_path = "hi";
current_checksum = 0;
default_checksum = 0;
scores_have_been_read = false;
mem_check_passed = false;
current_game = "";
positions = {};
function parse_table ( dsting )
local _table = {};
for line in string.gmatch(dsting, '([^\n]+)') do
local cpu, mem;
cputag, space, offs, len, chk_st, chk_ed = string.match(line, '^@([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)');
cpu = manager:machine().devices[cputag];
if not cpu then
return nil;
end
mem = cpu.spaces[space];
if not mem then
return nil;
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),
};
end
return _table;
end
function read_hiscore_dat ()
file = io.open( hiscoredata_path, "r" );
if not file then
file = io.open( hiscore_plugin_path .. "/hiscore.dat", "r" );
end
if emu.softname() ~= "" then
rm_match = '^' .. emu.romname() .. ',' .. emu.softname() .. ':';
else
rm_match = '^' .. emu.romname() .. ':';
end
cluster = "";
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
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
function get_file_name ()
if emu.softname() ~= "" then
r = hiscore_path .. '/' .. emu.romname() .. "_" .. emu.softname() .. ".hi";
else
r = hiscore_path .. '/' .. emu.romname() .. ".hi";
end
return r;
end
function write_scores ( posdata )
print("write_scores")
local output = io.open(get_file_name(), "wb");
if not output then
-- attempt to create the directory, and try again
os.execute( "mkdir " .. hiscore_path );
output = io.open(get_file_name(), "wb");
end
print("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
print("write_scores end")
end
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
function check_scores ( posdata )
local r = 0;
-- commonly the first entry will be for the entire table
-- so it will only trigger a write once a player enters
-- his/her name in.
row = positions[1];
for i=0,row["size"]-1 do
r = r + row["mem"]:read_u8( row["addr"] + i );
end
return r;
end
function init ()
if not scores_have_been_read then
if check_mem( positions ) then
default_checksum = check_scores( positions );
if read_scores( positions ) then
print( "scores read", "OK" );
else
-- likely there simply isn't a .hi file around yet
print( "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;
function tick ()
-- set up scores if they have been
init();
-- only allow save check to run when
if mem_check_passed 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();
-- print( "SAVE SCORES EVENT!", last_write_time );
end
end
end
end
function reset()
-- the notifier will still be attached even if the running game has no hiscore.dat entry
if mem_check_passed and (emu.romname() == current_game) then
local checksum = check_scores(positions)
if checksum ~= current_checksum and checksum ~= default_checksum then
write_scores(positions)
end
end
current_game = ""
mem_check_passed = false
scores_have_been_read = false;
end
emu.register_start(function()
current_game = ""
mem_check_passed = false
scores_have_been_read = false;
last_write_time = -10
print("Starting " .. emu.gamename())
current_game = emu.romname()
dat = read_hiscore_dat()
if dat and dat ~= "" then
print( "found hiscore.dat entry for " .. emu.romname() );
positions = parse_table( dat );
if not positions then
print("hiscore.dat parse error");
return;
end
end
emu.sethook( tick, "frame" );
end)
emu.register_stop(function()
reset()
end)
emu.register_prestart(function()
reset()
end)
end
return exports
|