-- 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", "L", "l", "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 getmetatable(devtable[devcur].space).__name:match("device_t") 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 getmetatable(devtable[devcur].space).__name:match("device_t") 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 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 = "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("\n\n\n\n", 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 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