mame/plugins/cheatfind/init.lua
Vas Crabb 2851d7d6cd (nw) Improve localisation:
* Change makefile rules to treat mame.pot as a target so rules can depend on it
* Put mame.pot inside the build directory so it will get cleaned
* Couldn't get xgettext to scrape lua and C++ in the same command and still remove stale strings
* Use larger strings and format specifiers to fix some localisation issues
  - Issue with "None" lacking context in Russian and Turkish translations
  - Issue with "Not implemented" changing depending on the noun in Serbian
  - Issues with lua plugins not allowing for languages with different grammar/punctuation

Strings that need to be translated after this change - most of these are existing text that's been made into larger chunks or reworded slightly:

"Mechanical Machine\tYes\n"

"Mechanical Machine\tNo\n"

"Requires Artwork\tYes\n"

"Requires Artwork\tNo\n"

"Requires Clickable Artwork\tYes\n"

"Requires Clickable Artwork\tNo\n"

"Support Cocktail\tYes\n"

"Support Cocktail\tNo\n"

"Driver is BIOS\tYes\n"

"Driver is BIOS\tNo\n"

"Support Save\tYes\n"

"Support Save\tNo\n"

"Screen Orientation\tVertical\n"

"Screen Orientation\tHorizontal\n"

"Requires CHD\tYes\n"

"Requires CHD\tNo\n"

"ROM Audit Result\tOK\n"

"ROM Audit Result\tBAD\n"

"Samples Audit Result\tNone Needed\n"

"Samples Audit Result\tOK\n"

"Samples Audit Result\tBAD\n"

"ROM Audit Disabled\t\n"
"Samples Audit Disabled\t\n"

"Activated: %s = %s"

"Activated: %s"

"Enabled: %s"

"Disabled: %s"

"%s added"

"Default name is %s"

"Cheat written to %s and added to cheat.simple"

"Unable to write file\n"
"Ensure that cheatpath folder exists"
2017-10-03 10:49:30 +11:00

797 lines
24 KiB
Lua

-- license:BSD-3-Clause
-- copyright-holders:Carl
-- This includes a library of functions to be used at the Lua console as cf.getspaces() etc...
local exports = {}
exports.name = "cheatfind"
exports.version = "0.0.1"
exports.description = "Cheat finder helper library"
exports.license = "The BSD 3-Clause License"
exports.author = { name = "Carl" }
local cheatfind = exports
function cheatfind.startplugin()
local cheat = {}
-- return table of devices and spaces
function cheat.getspaces()
local spaces = {}
for tag, device in pairs(manager:machine().devices) do
if device.spaces then
spaces[tag] = {}
for name, space in pairs(device.spaces) do
spaces[tag][name] = space
end
end
end
return spaces
end
-- return table of ram devices
function cheat.getram()
local ram = {}
for tag, device in pairs(manager:machine().devices) do
if device:shortname() == "ram" then
ram[tag] = {}
ram[tag].dev = device
ram[tag].size = emu.item(device.items["0/m_size"]):read(0)
end
end
return ram
end
-- return table of share regions
function cheat.getshares()
local shares = {}
for tag, share in pairs(manager:machine():memory().shares) do
shares[tag] = share
end
return shares
end
-- save data block
function cheat.save(space, start, size)
local data = { block = "", start = start, size = size, space = space }
if getmetatable(space).__name:match("device_t") then
if space:shortname() == "ram" then
data.block = emu.item(space.items["0/m_pointer"]):read_block(start, size)
if not data.block then
return nil
end
end
else
local block = ""
local temp = {}
local j = 1
for i = start, start + size do
if j < 65536 then
temp[j] = string.pack("B", space:read_u8(i, true))
j = j + 1
else
block = block .. table.concat(temp) .. string.pack("B", space:read_u8(i, true))
temp = {}
j = 1
end
end
block = block .. table.concat(temp)
data.block = block
end
return data
end
-- compare two data blocks, format is as lua string.unpack, bne and beq val is table of masks
function cheat.comp(newdata, olddata, oper, format, val, bcd)
local ret = {}
local ref = {} -- this is a helper for comparing two match lists
local bitmask = nil
local cfoper = {
lt = function(a, b, val) return (a < b and val == 0) or (val > 0 and (a + val) == b) end,
gt = function(a, b, val) return (a > b and val == 0) or (val > 0 and (a - val) == b) end,
eq = function(a, b, val) return a == b end,
ne = function(a, b, val) return (a ~= b and val == 0) or
(val > 0 and ((a - val) == b or (a + val) == b)) end,
ltv = function(a, b, val) return a < val end,
gtv = function(a, b, val) return a > val end,
eqv = function(a, b, val) return a == val end,
nev = function(a, b, val) return a ~= val end }
function cfoper.bne(a, b, val, addr)
if type(val) ~= "table" then
bitmask = a ~ b
return bitmask ~= 0
elseif not val[addr] then
return false
else
bitmask = (a ~ b) & val[addr]
return bitmask ~= 0
end
end
function cfoper.beq(a, b, val, addr)
if type(val) ~= "table" then
bitmask = ~a ~ b
return bitmask ~= 0
elseif not val[addr] then
return false
else
bitmask = (~a ~ b) & val[addr]
return bitmask ~= 0
end
end
local function check_bcd(val)
local a = val + 0x0666666666666666
a = a ~ val
return (a & 0x1111111111111110) == 0
end
local function frombcd(val)
local result = 0
local mul = 1
while val ~= 0 do
result = result + ((val % 16) * mul)
val = val >> 4
mul = mul * 10
end
return result
end
if not newdata and oper:sub(3, 3) == "v" then
newdata = olddata
end
if olddata.start ~= newdata.start or olddata.size ~= newdata.size or not cfoper[oper] then
return {}
end
if not val then
val = 0
end
for i = 1, olddata.size do
local oldstat, old = pcall(string.unpack, format, olddata.block, i)
local newstat, new = pcall(string.unpack, format, newdata.block, i)
if oldstat and newstat then
local oldc, newc = old, new
local comp = false
local addr = olddata.start + i - 1
if not bcd or (check_bcd(old) and check_bcd(new)) then
if bcd then
oldc = frombcd(old)
newc = frombcd(new)
end
if cfoper[oper](newc, oldc, val, addr) then
ret[#ret + 1] = { addr = addr,
oldval = old,
newval = new,
bitmask = bitmask }
ref[ret[#ret].addr] = #ret
end
end
end
end
return ret, ref
end
local function check_val(oper, val, matches)
if oper ~= "beq" and oper ~= "bne" then
return val
elseif not matches or not matches[1].bitmask then
return nil
end
local masks = {}
for num, match in pairs(matches) do
masks[match.addr] = match.bitmask
end
return masks
end
-- compare two blocks and filter by table of previous matches
function cheat.compnext(newdata, olddata, oldmatch, oper, format, val, bcd)
local matches, refs = cheat.comp(newdata, olddata, oper, format, check_val(oper, val, oldmatch), bcd)
local nonmatch = {}
local oldrefs = {}
for num, match in pairs(oldmatch) do
oldrefs[match.addr] = num
end
for addr, ref in pairs(refs) do
if not oldrefs[addr] then
nonmatch[ref] = true
refs[addr] = nil
else
matches[ref].oldval = oldmatch[oldrefs[addr]].oldval
end
end
local resort = {}
for num, match in pairs(matches) do
if not nonmatch[num] then
resort[#resort + 1] = match
end
end
return resort
end
-- compare a data block to the current state
function cheat.compcur(olddata, oper, format, val, bcd)
local newdata = cheat.save(olddata.space, olddata.start, olddata.size, olddata.space)
return cheat.comp(newdata, olddata, oper, format, val, bcd)
end
-- compare a data block to the current state and filter
function cheat.compcurnext(olddata, oldmatch, oper, format, val, bcd)
local newdata = cheat.save(olddata.space, olddata.start, olddata.size, olddata.space)
return cheat.compnext(newdata, olddata, oldmatch, oper, format, val, bcd)
end
_G.cf = cheat
local devtable = {}
local devsel = 1
local devcur = 1
local formtable = { "B", "b", "<H", ">H", "<h", ">h", "<L", ">L", "<l", ">l", "<J", ">J", "<j", ">j" }
local formname = { "u8", "s8", "little u16", "big u16", "little s16", "big s16",
"little u32", "big u32", "little s32", "big s32", "little u64", "big u64", "little s64", "big s64" }
local width = 1
local bcd = 0
local optable = { "lt", "gt", "eq", "ne", "beq", "bne", "ltv", "gtv", "eqv", "nev" }
local opsel = 1
local value = 0
local leftop = 2
local rightop = 1
local matches = {}
local matchsel = 0
local matchpg = 0
local menu_blocks = {}
local watches = {}
local menu_func
local cheat_save
local name = 1
local name_player = 1
local name_type = 1
local function start()
devtable = {}
devsel = 1
devcur = 1
width = 1
bcd = 0
opsel = 1
value = 0
leftop = 2
rightop = 1
matches = {}
matchsel = 0
matchpg = 0
menu_blocks = {}
watches = {}
local space_table = cheat.getspaces()
for tag, list in pairs(space_table) do
if list.program then
local ram = {}
for num, entry in pairs(list.program.map) do
if entry.writetype == "ram" then
ram[#ram + 1] = { offset = entry.offset, size = entry.endoff - entry.offset }
end
end
if next(ram) then
if tag == ":maincpu" then
table.insert(devtable, 1, { tag = tag, space = list.program, ram = ram })
else
devtable[#devtable + 1] = { tag = tag, space = list.program, ram = ram }
end
end
end
end
space_table = cheat.getram()
for tag, ram in pairs(space_table) do
devtable[#devtable + 1] = { tag = tag, space = ram.dev, ram = {{ offset = 0, size = ram.size }} }
end
space_table = cheat.getshares()
for tag, share in pairs(space_table) do
devtable[#devtable + 1] = { tag = tag, space = share, ram = {{ offset = 0, size = share.size }} }
end
end
emu.register_start(start)
local function menu_populate()
local menu = {}
local function menu_prepare()
local menu_list = {}
menu_func = {}
for num, func in ipairs(menu) do
local item, f = func()
if item then
menu_list[#menu_list + 1] = item
menu_func[#menu_list] = f
end
end
return menu_list
end
local function menu_lim(val, min, max, menuitem)
if min == max then
menuitem[3] = 0
elseif val == min then
menuitem[3] = "r"
elseif val == max then
menuitem[3] = "l"
else
menuitem[3] = "lr"
end
end
local function incdec(event, val, min, max)
local ret
if event == "left" and val ~= min then
val = val - 1
ret = true
elseif event == "right" and val ~= max then
val = val + 1
ret = true
end
return val, ret
end
if cheat_save then
local cplayer = { "All", "P1", "P2", "P3", "P4" }
local ctype = { "Infinite Credits", "Infinite Time", "Infinite Lives", "Infinite Energy", "Infinite Ammo", "Infinite Bombs", "Invincibility" }
menu[#menu + 1] = function() return { _("Save Cheat"), "", "off" }, nil end
menu[#menu + 1] = function() return { "---", "", "off" }, nil end
menu[#menu + 1] = function()
local c = { _("Default"), _("Custom") }
local m = { _("Cheat Name"), c[name], 0 }
menu_lim(name, 1, #c, m)
local function f(event)
local r
name, r = incdec(event, name, 1, #c)
if (event == "select" or event == "comment") and name == 1 then
manager:machine():popmessage(string.format(_("Default name is %s"), cheat_save.name))
end
return r
end
return m, f
end
if name == 2 then
menu[#menu + 1] = function()
local m = { _("Player"), cplayer[name_player], 0 }
menu_lim(name_player, 1, #cplayer, m)
return m, function(event) local r name_player, r = incdec(event, name_player, 1, #cplayer) return r end
end
menu[#menu + 1] = function()
local m = { _("Type"), ctype[name_type], 0 }
menu_lim(name_type, 1, #ctype, m)
return m, function(event) local r name_type, r = incdec(event, name_type, 1, #ctype) return r end
end
end
menu[#menu + 1] = function()
local m = { _("Save"), "", 0 }
local function f(event)
if event == "select" then
local desc
local written = false
if name == 2 then
if cplayer[name_player] == "All" then
desc = ctype[name_type]
else
desc = cplayer[name_player] .. " " .. ctype[name_type]
end
else
desc = cheat_save.name
end
local filename = cheat_save.filename .. "_" .. desc
local file = io.open(filename .. ".json", "w")
if file then
file:write(string.format(cheat_save.json, desc))
file:close()
if not devtable[devcur].space.shortname then -- no xml or simple for ram_device cheat
file = io.open(filename .. ".xml", "w")
file:write(string.format(cheat_save.xml, desc))
file:close()
file = io.open(cheat_save.path .. "/cheat.simple", "a")
file:write(string.format(cheat_save.simple, desc))
file:close()
manager:machine():popmessage(string.format(_("Cheat written to %s and added to cheat.simple"), cheat_save.filename))
end
written = true
elseif not devtable[devcur].space.shortname then
file = io.open(cheat_save.path .. "/cheat.simple", "a")
if file then
file:write(string.format(cheat_save.simple, desc))
file:close()
manager:machine():popmessage(_("Cheat added to cheat.simple"))
written = true
end
end
if not written then
manager:machine():popmessage(_("Unable to write file\nEnsure that cheatpath folder exists"))
end
cheat_save = nil
return true
end
return false
end
return m, f
end
menu[#menu + 1] = function() return { _("Cancel"), "", 0 }, function(event) if event == "select" then cheat_save = nil return true end end end
return menu_prepare()
end
menu[#menu + 1] = function()
local m = { _("CPU or RAM"), devtable[devsel].tag, 0 }
menu_lim(devsel, 1, #devtable, m)
local function f(event)
if (event == "left" or event == "right") and #menu_blocks ~= 0 then
manager:machine():popmessage(_("Changes to this only take effect when \"Start new search\" is selected"))
end
devsel = incdec(event, devsel, 1, #devtable)
return true
end
return m, f
end
menu[#menu + 1] = function()
local function f(event)
local ret = false
if event == "select" then
menu_blocks = {}
matches = {}
devcur = devsel
for num, region in ipairs(devtable[devcur].ram) do
menu_blocks[num] = {}
menu_blocks[num][1] = cheat.save(devtable[devcur].space, region.offset, region.size)
end
manager:machine():popmessage(_("Data cleared and current state saved"))
watches = {}
leftop = 2
rightop = 1
matchsel = 0
return true
end
end
return { _("Start new search"), "", 0 }, f
end
if #menu_blocks ~= 0 then
menu[#menu + 1] = function() return { "---", "", "off" }, nil end
menu[#menu + 1] = function()
local function f(event)
if event == "select" then
for num, region in ipairs(devtable[devcur].ram) do
menu_blocks[num][#menu_blocks[num] + 1] = cheat.save(devtable[devcur].space, region.offset, region.size)
end
manager:machine():popmessage(_("Current state saved"))
leftop = (leftop == #menu_blocks[1]) and #menu_blocks[1] + 1 or leftop
rightop = (rightop == #menu_blocks[1] - 1) and #menu_blocks[1] or rightop
devsel = devcur
return true
end
end
return { _("Save current -- #") .. #menu_blocks[1] + 1, "", 0 }, f
end
menu[#menu + 1] = function()
local function f(event)
if event == "select" then
local count = 0
if #matches == 0 then
matches[1] = {}
for num = 1, #menu_blocks do
if leftop == #menu_blocks[1] + 1 then
matches[1][num] = cheat.compcur(menu_blocks[num][rightop], optable[opsel],
formtable[width], value, bcd == 1)
else
matches[1][num] = cheat.comp(menu_blocks[num][leftop], menu_blocks[num][rightop],
optable[opsel], formtable[width], value, bcd == 1)
end
count = count + #matches[1][num]
end
else
lastmatch = matches[#matches]
matches[#matches + 1] = {}
for num = 1, #menu_blocks do
if leftop == #menu_blocks[1] + 1 then
matches[#matches][num] = cheat.compcurnext(menu_blocks[num][rightop], lastmatch[num],
optable[opsel], formtable[width], value, bcd == 1)
else
matches[#matches][num] = cheat.compnext(menu_blocks[num][leftop], menu_blocks[num][rightop],
lastmatch[num], optable[opsel], formtable[width], value, bcd == 1)
end
count = count + #matches[#matches][num]
end
end
manager:machine():popmessage(count .. _(" total matches found"))
matches[#matches].count = count
matchpg = 0
devsel = devcur
return true
end
end
return { _("Compare"), "", 0 }, f
end
menu[#menu + 1] = function()
local m = { _("Left operand"), leftop, "" }
menu_lim(leftop, 1, #menu_blocks[1] + 1, m)
if leftop == #menu_blocks[1] + 1 then
m[2] = _("Current")
end
return m, function(event) local r leftop, r = incdec(event, leftop, 1, #menu_blocks[1] + 1) return r end
end
menu[#menu + 1] = function()
local m = { _("Operator"), optable[opsel], "" }
menu_lim(opsel, 1, #optable, m)
local function f(event)
local r
opsel, r = incdec(event, opsel, 1, #optable)
if event == "left" or event == "right" or event == "comment" then
if optable[opsel] == "lt" then
manager:machine():popmessage(_("Left less than right, value is difference"))
elseif optable[opsel] == "gt" then
manager:machine():popmessage(_("Left greater than right, value is difference"))
elseif optable[opsel] == "eq" then
manager:machine():popmessage(_("Left equal to right"))
elseif optable[opsel] == "ne" then
manager:machine():popmessage(_("Left not equal to right, value is difference"))
elseif optable[opsel] == "beq" then
manager:machine():popmessage(_("Left equal to right with bitmask"))
elseif optable[opsel] == "bne" then
manager:machine():popmessage(_("Left not equal to right with bitmask"))
elseif optable[opsel] == "ltv" then
manager:machine():popmessage(_("Left less than value"))
elseif optable[opsel] == "gtv" then
manager:machine():popmessage(_("Left greater than value"))
elseif optable[opsel] == "eqv" then
manager:machine():popmessage(_("Left equal to value"))
elseif optable[opsel] == "nev" then
manager:machine():popmessage(_("Left not equal to value"))
end
end
return r
end
return m, f
end
menu[#menu + 1] = function()
if optable[opsel]:sub(3, 3) == "v" then
return nil
end
local m = { _("Right operand"), rightop, "" }
menu_lim(rightop, 1, #menu_blocks[1], m)
return m, function(event) local r rightop, r = incdec(event, rightop, 1, #menu_blocks[1]) return r end
end
menu[#menu + 1] = function()
if optable[opsel] == "bne" or optable[opsel] == "beq" or optable[opsel] == "eq" then
return nil
end
local m = { _("Value"), value, "" }
local max = 100 -- max value?
menu_lim(value, 0, max, m)
if value == 0 and optable[opsel]:sub(3, 3) ~= "v" then
m[2] = _("Any")
end
return m, function(event) local r value, r = incdec(event, value, 0, max) return r end
end
menu[#menu + 1] = function() return { "---", "", "off" }, nil end
menu[#menu + 1] = function()
local m = { _("Data Format"), formname[width], 0 }
menu_lim(width, 1, #formtable, m)
return m, function(event) local r width, r = incdec(event, width, 1, #formtable) return r end
end
menu[#menu + 1] = function()
if optable[opsel] == "bne" or optable[opsel] == "beq" then
return nil
end
local m = { "BCD", _("Off"), 0 }
menu_lim(bcd, 0, 1, m)
if bcd == 1 then
m[2] = _("On")
end
return m, function(event) local r bcd, r = incdec(event, bcd, 0, 1) return r end
end
if #matches ~= 0 then
menu[#menu + 1] = function()
local function f(event)
if event == "select" then
matches[#matches] = nil
matchpg = 0
return true
end
end
return { _("Undo last search -- #") .. #matches, "", 0 }, f
end
menu[#menu + 1] = function() return { "---", "", "off" }, nil end
menu[#menu + 1] = function()
local m = { _("Match block"), matchsel, "" }
menu_lim(matchsel, 0, #matches[#matches], m)
if matchsel == 0 then
m[2] = _("All")
end
local function f(event)
local r
matchsel, r = incdec(event, matchsel, 0, #matches[#matches])
if r then
matchpg = 0
end
return r
end
return m, f
end
local function mpairs(sel, list, start)
if #list == 0 then
return function() end, nil, nil
end
if sel ~= 0 then
list = {list[sel]}
end
local function mpairs_it(list, i)
local match
i = i + 1
local sel = i + start
for j = 1, #list do
if sel <= #list[j] then
match = list[j][sel]
break
else
sel = sel - #list[j]
end
end
if not match then
return
end
return i, match
end
return mpairs_it, list, 0
end
local bitwidth = formtable[width]:sub(2, 2):lower()
if bitwidth == "h" then
bitwidth = " %04x"
elseif bitwidth == "l" then
bitwidth = " %08x"
elseif bitwidth == "j" then
bitwidth = " %016x"
else
bitwidth = " %02x"
end
local function match_exec(match)
local dev = devtable[devcur]
local cheat = { desc = string.format(_("Test cheat at addr %08X"), match.addr), script = {} }
local wid = formtable[width]:sub(2, 2):lower()
local widchar
local form
if wid == "h" then
wid = "u16"
form = "%08x %04x"
widchar = "w"
elseif wid == "l" then
wid = "u32"
form = "%08x %08x"
widchart = "d"
elseif wid == "j" then
wid = "u64"
form = "%08x %016x"
widchar = "q"
else
wid = "u8"
form = "%08x %02x"
widchar = "b"
end
if dev.space.shortname then
cheat.ram = { ram = dev.tag }
cheat.script.run = "ram:write(" .. match.addr .. "," .. match.newval .. ")"
else
cheat.space = { cpu = { tag = dev.tag, type = "program" } }
cheat.script.run = "cpu:write_" .. wid .. "(" .. match.addr .. "," .. match.newval .. ", true)"
end
if match.mode == 1 then
if not _G.ce then
manager:machine():popmessage(_("Cheat engine not available"))
else
_G.ce.inject(cheat)
end
elseif match.mode == 2 then
cheat_save = {}
menu = 1
menu_player = 1
menu_type = 1
local setname = emu.romname()
if emu.softname() ~= "" then
for name, image in pairs(manager:machine().images) do
if image:exists() and image:software_list_name() ~= "" then
setname = image:software_list_name() .. "/" .. emu.softname()
end
end
end
-- lfs.env_replace is defined in boot.lua
cheat_save.path = lfs.env_replace(manager:machine():options().entries.cheatpath:value()):match("([^;]+)")
cheat_save.filename = string.format("%s/%s", cheat_save.path, setname)
cheat_save.name = cheat.desc
local json = require("json")
cheat.desc = "%s"
cheat_save.json = json.stringify({[1] = cheat}, {indent = true})
cheat_save.xml = string.format("<mamecheat version=1>\n<cheat desc=\"%%s\">\n<script state=\"run\">\n<action>%s.pp%s@%X=%X</action>\n</script>\n</cheat>\n</mamecheat>", dev.tag:sub(2), widchar, match.addr, match.newval)
cheat_save.simple = string.format("%s,%s,%X,%s,%X,%%s\n", setname, dev.tag, match.addr, widchar, match.newval)
manager:machine():popmessage(_("Default name is ") .. cheat_save.name)
return true
else
local func = "return space:read"
local env = { space = devtable[devcur].space }
if not dev.space.shortname then
func = func .. "_" .. wid
end
func = func .. "(" .. match.addr .. ")"
watches[#watches + 1] = { addr = match.addr, func = load(func, func, "t", env), format = form }
return true
end
return false
end
for num2, match in mpairs(matchsel, matches[#matches], matchpg * 100) do
if num2 > 100 then
break
end
menu[#menu + 1] = function()
if not match.mode then
match.mode = 1
end
local modes = { _("Test"), _("Write"), _("Watch") }
local m = { string.format("%08x" .. bitwidth .. bitwidth, match.addr, match.oldval,
match.newval), modes[match.mode], 0 }
menu_lim(match.mode, 1, #modes, m)
local function f(event)
local r
match.mode, r = incdec(event, match.mode, 1, 3)
if event == "select" then
r = match_exec(match)
end
return r
end
return m, f
end
end
if matches[#matches].count > 100 then
menu[#menu + 1] = function()
local m = { _("Page"), matchpg, 0 }
local max
if matchsel == 0 then
max = math.ceil(matches[#matches].count / 100)
else
max = #matches[#matches][matchsel]
end
menu_lim(matchpg, 0, max, m)
local function f(event)
matchpg, r = incdec(event, matchpg, 0, max)
return r
end
return m, f
end
end
end
if #watches ~= 0 then
menu[#menu + 1] = function()
return { _("Clear Watches"), "", 0 }, function(event) if event == "select" then watches = {} return true end end
end
end
end
return menu_prepare()
end
local function menu_callback(index, event)
return menu_func[index](event)
end
emu.register_menu(menu_callback, menu_populate, _("Cheat Finder"))
emu.register_frame_done(function ()
local tag, screen = next(manager:machine().screens)
local height = mame_manager:ui():get_line_height()
for num, watch in ipairs(watches) do
screen:draw_text("left", num * height, string.format(watch.format, watch.addr, watch.func()))
end
end)
end
return exports