mame/plugins/cheatfind/init.lua
2018-03-25 02:03:24 +11:00

827 lines
25 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, shift = 0 }
if getmetatable(space).__name:match("addr_space") then
data.shift = space.shift
end
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
if data.shift >= 0 then -- region or byte wide space
for i = start, start + (size << data.shift), 1 << data.shift do
if j < 65536 then
temp[j] = string.pack("B", space:read_u8(i))
j = j + 1
else
block = block .. table.concat(temp) .. string.pack("B", space:read_u8(i))
temp = {}
j = 1
end
end
elseif data.shift < 0 then
local s = -data.shift
local read = (s == 1) and space.read_u16 or (s == 2) and space.read_u32 or (s == 3) and space.read_u64 or space.read_u8
local pack = (s == 1) and "<H" or (s == 2) and "<L" or (s == 3) and "<J" or "B"
for i = start, start + (size >> s) do
if j < 65536 then
temp[j] = string.pack(pack, read(space, i))
j = j + 1
else
block = block .. table.concat(temp) .. string.pack(pack, read(space, i))
temp = {}
j = 1
end
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 = i - 1
if olddata.shift ~= 0 then
local s = olddata.shift
addr = (s < 0) and addr >> -s or (s > 0) and addr << s
end
addr = addr + olddata.start
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
for name, space in pairs(list) do
local ram = {}
for num, entry in pairs(space.map) do
if entry.writetype == "ram" then
ram[#ram + 1] = { offset = entry.offset, size = entry.endoff - entry.offset }
if space.shift > 0 then
ram[#ram].size = ram[#ram].size >> space.shift
elseif space.shift < 0 then
ram[#ram].size = ram[#ram].size << -space.shift
end
end
end
if next(ram) then
if tag == ":maincpu" and name == "program" then
table.insert(devtable, 1, { name = tag .. ", " .. name, tag = tag, sname = name, space = space, ram = ram })
else
devtable[#devtable + 1] = { name = tag .. ", " .. name, tag = tag, sname = name, space = space, ram = ram }
end
end
end
end
space_table = cheat.getram()
for tag, ram in pairs(space_table) do
devtable[#devtable + 1] = { tag = tag, name = "ram", 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, name = 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()
-- xml or simple are program space only
if not getmetatable(devtable[devcur].space).__name:match("device_t") and devtable[devcur].sname == "program" then
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"), filename))
end
written = true
elseif not getmetatable(devtable[devcur].space).__name:match("device_t") and devtable[devcur].sname == "program" 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].name, 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(string.format(_("%d total matches found"), count))
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 getmetatable(dev.space).__name:match("device_t") then
cheat.ram = { ram = dev.tag }
cheat.script.run = "ram:write(" .. match.addr .. "," .. match.newval .. ")"
else
cheat.space = { cpu = { tag = dev.tag, type = dev.sname } }
cheat.script.run = "cpu:write_" .. wid .. "(" .. match.addr .. "," .. match.newval .. ")"
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(string.format(_("Default name is %s"), cheat_save.name))
return true
else
local func = "return space:read"
local env = { space = devtable[devcur].space }
if not getmetatable(dev.space).__name:match("device_t") 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