-- license:BSD-3-Clause -- copyright-holders:Carl -- -- json cheat file format -- [{ -- "desc": "text", -- "parameter": { -- "min": "minval(0)", -- "max": "maxval(numitems)", -- "step": "stepval(1)", -- "item" [{ -- "value": "itemval(index*stepval+minval)", -- "text": "text" -- }, -- ... ] -- }, -- "cpu": { -- "varname": "tag" -- ... -- } -- "space": { -- "varname": { -- "tag": "tag", -- "type": "program|data|io" -- }, -- ... -- }, -- "screen": { -- "varname": "tag", -- ... -- }, -- "region": { -- "varname": "tag", -- ... -- }, -- "ram": { -- "varname": "tag", -- ... -- }, -- "share": { -- "varname": "tag", -- ... -- }, -- "script": { -- "on|off|run|change": "script", -- ... -- }, -- "comment": "text" -- }, -- ... ] -- -- Scripts are lua scripts with a limited api. Most library functions are unavailable. -- Like the XML cheats, param is the current parameter value and variables are shared between scripts within a cheat -- Differences from XML cheats: -- - actions are only one line which include the entire script -- - "condexpr" is replaced with lua control statements (if-then-else-end) -- - variables are only limited by the limits of the lua interperter, you can have strings and tables -- - the address spaces in the "space" blocks are accessible to the script if included, -- same with regions (the "m" space in debug expr) -- - frame is replaced by screen:frame_number() so if you use frame a screen needs to be in the device section -- - output is a function and argindex isn't supported, output args need to be explicit and a screen device -- must be provided -- - cpu is only used for break and watch points, if it is defined and the debugger is not enabled (-debugger none is enough) -- it will disable the cheat only if a point is set, check var for nil first -- - watch points require the address space that you want to set the watch on, wptype is "r"-read, "w"-write or "rw"-both local exports = {} exports.name = "cheat" exports.version = "0.0.1" exports.description = "Cheat plugin" exports.license = "BSD-3-Clause" exports.author = { name = "Carl" } local cheat = exports local reset_subscription, stop_subscription, frame_subscription function cheat.set_folder(path) cheat.path = path end function cheat.startplugin() local cheats = {} local output = {} local line = 0 local start_time = 0 local stop = true local cheatname = "" local consolelog = nil local consolelast = 0 local perodicset = false local watches = {} local breaks = {} local inputs = {} local function load_cheats() local filename = emu.romname() local newcheats = {} local file = emu.file(manager.machine.options.entries.cheatpath:value():gsub("([^;]+)", "%1;%1/cheat") , 1) for name, image in pairs(manager.machine.images) do if image.exists and image.software_list_name ~= "" then filename = image.software_list_name .. "/" .. image.filename end end cheatname = filename local function add(addcheats) if not next(newcheats) then newcheats = addcheats else for num, cheat in pairs(addcheats) do newcheats[#newcheats + 1] = cheat end end end for scrfile in lfs.dir(cheat.path) do local name = string.match(scrfile, "^(cheat_.*).lua$") if name then local conv = require("cheat/" .. name) if conv then local ret = file:open(conv.filename(filename)) while not ret do add(conv.conv_cheat(file:read(file:size()))) ret = file:open_next() end end end end return newcheats end local function load_hotkeys() local json = require("json") local file = io.open(manager.machine.options.entries.cheatpath:value():match("([^;]+)") .. "/" .. cheatname .. "_hotkeys.json", "r") if not file then return end local hotkeys = json.parse(file:read("a")) for num, val in ipairs(hotkeys) do for num, cheat in pairs(cheats) do if val.desc == cheat.desc then cheat.hotkeys = {pressed = false, keys = manager.machine.input:seq_from_tokens(val.keys)} end end end end local function save_hotkeys() local hotkeys = {} for num, cheat in ipairs(cheats) do if cheat.hotkeys then local hotkey = {desc = cheat.desc, keys = manager.machine.input:seq_to_tokens(cheat.hotkeys.keys)} if hotkey.keys ~= "" then hotkeys[#hotkeys + 1] = hotkey end end end local path = manager.machine.options.entries.cheatpath:value():match("([^;]+)") local filepath = path .. "/" .. cheatname .. "_hotkeys.json" if #hotkeys > 0 then local json = require("json") local attr = lfs.attributes(path) if not attr then lfs.mkdir(path) elseif attr.mode ~= "directory" then -- uhhh? return end if cheatname:find("/", 1, true) then local softpath = path .. "/" .. cheatname:match("([^/]+)") attr = lfs.attributes(softpath) if not attr then lfs.mkdir(softpath) elseif attr.mode ~= "directory" then -- uhhh? return end end local file = io.open(filepath, "w+") if file then file:write(json.stringify(hotkeys, {indent = true})) file:close() end else local attr = lfs.attributes(filepath) if attr and (attr.mode == "file") then local json = require("json") local file = io.open(filepath, "w+") if file then file:write(json.stringify(hotkeys, {indent = true})) file:close() end end end end local function cheat_error(cheat, msg) emu.print_error("error cheat script error: \"" .. cheat.desc .. "\" " .. msg) cheat.desc = cheat.desc .. " error" cheat.script = nil cheat.enabled = nil return end local function run_if(cheat, func) if func then local stat, err = pcall(func) if not stat then cheat_error(cheat, err) end return func end return false end local function draw_text(screen, x, y, color, form, ...) local str = form:format(...) if y == "auto" then y = line line = line + 1 end if not screen then emu.print_verbose("draw_text: invalid screen") return end if type(x) == "string" then y = y * mame_manager.ui.line_height end output[#output + 1] = { type = "text", scr = screen, x = x, y = y, str = str, color = color } end local function draw_line(screen, x1, y1, x2, y2, color) if not screen then emu.print_verbose("draw_line: invalid screen") return end output[#output + 1] = { type = "line", scr = screen, x1 = x1, x2 = x2, y1 = y1, y2 = y2, color = color } end local function draw_box(screen, x1, y1, x2, y2, bgcolor, linecolor) if not screen then emu.print_verbose("draw_box: invalid screen") return end output[#output + 1] = { type = "box", scr = screen, x1 = x1, x2 = x2, y1 = y1, y2 = y2, bgcolor = bgcolor, linecolor = linecolor } end local function tobcd(val) local result = 0 local shift = 0 while val ~= 0 do result = result + ((val % 10) << shift) val = val / 10 shift = shift + 4 end return result 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 local function time() return emu.time() - start_time end local function periodiccb() local last = consolelast local msg = consolelog[#consolelog] consolelast = #consolelog if #consolelog > last and msg:find("Stopped at", 1, true) then local point = tonumber(msg:match("Stopped at breakpoint ([0-9]+)")) if not point then point = tonumber(msg:match("Stopped at watchpoint ([0-9]+")) if not point then return -- ?? end local wp = watches[point] if wp then run_if(wp.cheat, wp.func) -- go in case a debugger other than "none" is enabled -- don't use an b/wpset action because that will supress the b/wp index manager.machine.debugger.execution_state = "run" end else local bp = breaks[point] if bp then run_if(bp.cheat, bp.func) manager.machine.debugger.execution_state = "run" end end end end local function bpset(cheat, dev, addr, func) if cheat.is_oneshot then error("bpset not permitted in oneshot cheat") return end local idx = dev.debug:bpset(addr) breaks[idx] = {cheat = cheat, func = func, dev = dev} end local function wpset(cheat, dev, space, wptype, addr, len, func) if cheat.is_oneshot then error("wpset not permitted in oneshot cheat") return end if not space.name then error("bad space in wpset") return end local idx = dev.debug:wpset(space, wptype, addr, len) watches[idx] = {cheat = cheat, func = func, dev = dev} end local function bwpclr(cheat) if not manager.machine.debugger then return end for num, bp in pairs(breaks) do if cheat == bp.cheat then bp.dev.debug:bpclr(num) end end for num, wp in pairs(watches) do if cheat == wp.cheat then wp.dev.debug:wpclr(num) end end end local function input_trans(list) local xlate = { start = {}, stop = {}, last = 0 } local function errout(port, field) cheat:set_enabled(false) error(port .. field .. " not found") return end for num, entry in ipairs(list) do if entry.port:sub(1, 1) ~= ":" then entry.port = ":" .. entry.port end local port = manager.machine.ioport.ports[entry.port] if not port then errout(entry.port, entry.field) end local field = port.fields[entry.field] if not field then errout(entry.port, entry.field) end if not xlate.start[entry.start] then xlate.start[entry.start] = {} end if not xlate.stop[entry.stop] then xlate.stop[entry.stop] = {} end local start = xlate.start[entry.start] local stop = xlate.stop[entry.stop] local ent = { port = port, field = field } stop[#stop + 1] = ent start[#start + 1] = ent if entry.stop > xlate.last then xlate.last = entry.stop end end return xlate end local function input_run(cheat, list) if not cheat.is_oneshot then cheat.enabled = false error("input_run only allowed in one shot cheats") return end local _, screen = next(manager.machine.screens) list.begin = screen:frame_number() inputs[#inputs + 1] = list end local function param_calc(param) if param.item then if not param.item[param.index] then -- uh oh param.index = 1 end param.value = param.item[param.index].value return end param.value = param.min + (param.step * (param.index - 1)) if param.value > param.max then param.value = param.max end end -- return is current state, ui change local function set_enabled(cheat, state) if cheat.is_oneshot then if state then if cheat.parameter and cheat.script.change and cheat.parameter.index ~= 0 then param_calc(cheat.parameter) cheat.cheat_env.param = cheat.parameter.value cheat.script.change() elseif not cheat.parameter and cheat.script.on then cheat.script.on() end end return false, false end if cheat.enabled == state then return state, false end if not state then cheat.enabled = false run_if(cheat, cheat.script.off) bwpclr(cheat) else cheat.enabled = true run_if(cheat, cheat.script.on) end return state, true end -- return is current index, ui change local function set_index(cheat, index) local param = cheat.parameter local oldindex = param.index if (index < 0) or (index > param.last) or (param.index == index) then return param.index, false end param.index = index if index == 0 then cheat.cheat_env.param = param.min cheat:set_enabled(false) else if oldindex == 0 then cheat:set_enabled(true) end param_calc(param) cheat.cheat_env.param = param.value if not cheat.is_oneshot then run_if(cheat, cheat.script.change) end end return index, true end local function parse_cheat(cheat) cheat.cheat_env = { draw_text = draw_text, draw_line = draw_line, draw_box = draw_box, tobcd = tobcd, frombcd = frombcd, pairs = pairs, ipairs = ipairs, outputs = manager.machine.output, time = time, input_trans = input_trans, input_run = function(list) input_run(cheat, list) end, os = { time = os.time, date = os.date, difftime = os.difftime }, table = { insert = table.insert, remove = table.remove }, string = { format = string.format, char = string.char } } cheat.enabled = false cheat.set_enabled = set_enabled; cheat.get_enabled = function(cheat) return cheat.enabled end cheat.is_oneshot = cheat.script and not cheat.script.run and not cheat.script.off -- verify scripts are valid first if not cheat.script then return end for name, script in pairs(cheat.script) do script, err = load(script, cheat.desc .. name, "t", cheat.cheat_env) if not script then cheat_error(cheat, err) return end cheat.script[name] = script end -- initialize temp[0-9] for backward compatbility reasons for i = 0, 9 do cheat.cheat_env["temp" .. i] = 0 end if cheat.cpu then cheat.cpudev = {} for name, tag in pairs(cheat.cpu) do if manager.machine.debugger then local dev = manager.machine.devices[tag] if not dev or not dev.debug then cheat_error(cheat, "missing or invalid device " .. tag) return end cheat.cheat_env[name] = { bpset = function(addr, func) bpset(cheat, dev, addr, func) end, wpset = function(space, wptype, addr, len, func) wpset(cheat, dev, space, wptype, addr, len, func) end, regs = dev.state } cheat.bp = {} cheat.wp = {} if not periodicset then emu.register_periodic(periodic_cb) periodicset = true end end end end if cheat.space then for name, space in pairs(cheat.space) do local cpu, mem cpu = manager.machine.devices[space.tag] if not cpu then cheat_error(cheat, "missing device " .. space.tag) return end if space.type then mem = cpu.spaces[space.type] else space.type = "program" mem = cpu.spaces["program"] end if not mem then cheat_error(cheat, "missing space " .. space.type) return end cheat.cheat_env[name] = mem end end if cheat.screen then for name, screen in pairs(cheat.screen) do local scr = manager.machine.screens[screen] if screen == "ui" then scr = manager.machine.render.ui_container elseif not scr then local tag local nxt, coll = manager.machine.screens:pairs() tag, scr = nxt(coll) -- get any screen end cheat.cheat_env[name] = scr end end if cheat.region then for name, region in pairs(cheat.region) do local mem = manager.machine.memory.regions[region] if not mem then cheat_error(cheat, "missing region " .. region) return end cheat.cheat_env[name] = mem end end if cheat.ram then for name, tag in pairs(cheat.ram) do local ram = manager.machine.devices[tag] if not ram then cheat_error(cheat, "missing ram device " .. tag) return end cheat.cheat_env[name] = emu.item(ram.items["0/m_pointer"]) end end if cheat.share then for name, tag in pairs(cheat.share) do local share = manager.machine.memory.shares[tag] if not share then cheat_error(cheat, "missing share " .. share) return end cheat.cheat_env[name] = share end end local param = cheat.parameter if not param then return end cheat.set_index = set_index; cheat.set_value = function(cheat, value) local idx = ((value - cheat.parameter.min) / cheat.parameter.step) + 1 local chg = false if math.integer(idx) == idx then idx, chg = cheat:set_index(idx) end return cheat.parameter.value, chg end cheat.get_index = function(cheat) return cheat.parameter.index end cheat.get_value = function(cheat) return cheat.parameter.value end param.min = tonumber(param.min) or 0 param.max = tonumber(param.max) or #param.item param.step = tonumber(param.step) or 1 if param.item then for count, item in pairs(param.item) do if not item.value then item.value = (count * param.step) + param.min else item.value = tonumber(item.value) end end param.last = #param.item else param.last = ((param.max - param.min) / param.step) + 1 end param.index = 0 param.value = param.min cheat.cheat_env.param = param.min end local hotkeymenu = false local hotkeylist = {} local commonui local poller local function menu_populate() local menu = {} if hotkeymenu then local ioport = manager.machine.ioport local input = manager.machine.input menu[1] = {_("Select cheat to set hotkey"), "", "off"} menu[2] = {string.format(_("Press %s to clear hotkey"), manager.ui:get_general_input_setting(ioport:token_to_input_type("UI_CLEAR"))), "", "off"} menu[3] = {"---", "", "off"} hotkeylist = {} local function hkcbfunc(cheat, event) if poller then if poller:poll() then if poller.sequence then cheat.hotkeys = { pressed = false, keys = poller.sequence } end poller = nil return true end elseif event == "clear" then cheat.hotkeys = nil return true elseif event == "select" then if not commonui then commonui = require('commonui') end poller = commonui.switch_polling_helper() return true end return false end for num, cheat in ipairs(cheats) do if cheat.script then local setting = cheat.hotkeys and input:seq_name(cheat.hotkeys.keys) or _("None") menu[#menu + 1] = {cheat.desc, setting, ""} hotkeylist[#hotkeylist + 1] = function(event) return hkcbfunc(cheat, event) end end end menu[#menu + 1] = {"---", "", ""} menu[#menu + 1] = {_("Done"), "", ""} if poller then return poller:overlay(menu) else return menu end end for num, cheat in ipairs(cheats) do menu[num] = {} menu[num][1] = cheat.desc if not cheat.parameter then if not cheat.script then if cheat.desc == "" then menu[num][1] = "---" end menu[num][2] = "" menu[num][3] = "off" elseif cheat.is_oneshot then menu[num][2] = _("Set") menu[num][3] = "" else if cheat.enabled then menu[num][2] = _("On") menu[num][3] = "l" else menu[num][2] = _("Off") menu[num][3] = "r" end end else if cheat.parameter.index == 0 then if cheat.is_oneshot then menu[num][2] = _("Set") else menu[num][2] = _("Off") end menu[num][3] = "r" else if cheat.parameter.item then menu[num][2] = cheat.parameter.item[cheat.parameter.index].text else menu[num][2] = cheat.parameter.value end menu[num][3] = "l" if cheat.parameter.index < cheat.parameter.last then menu[num][3] = "lr" end end end end menu[#menu + 1] = {"---", "", ""} menu[#menu + 1] = {_("Set hotkeys"), "", ""} menu[#menu + 1] = {_("Reset All"), "", ""} menu[#menu + 1] = {_("Reload All"), "", ""} return menu end local function menu_callback(index, event) manager.machine:popmessage() if hotkeymenu then if event == "back" then hotkeymenu = false return true else index = index - 3 if index >= 1 and index <= #hotkeylist then hotkeylist[index](event) return true elseif index == #hotkeylist + 2 and event == "select" then hotkeymenu = false return true end end return false end if index > #cheats and event == "select" then index = index - #cheats if index == 2 then hotkeymenu = true elseif index == 3 then for num, cheat in pairs(cheats) do cheat:set_enabled(false) if cheat.parameter then cheat:set_index(0) end end elseif index == 4 then for num, cheat in pairs(cheats) do cheat:set_enabled(false) end cheats = load_cheats() for num, cheat in pairs(cheats) do parse_cheat(cheat) end load_hotkeys() end return true end local cheat = cheats[index] if not cheat then return false end if event == "up" or event == "down" or event == "comment" then if cheat.comment then manager.machine:popmessage(string.format(_("Cheat Comment:\n%s"), cheat.comment)) end elseif event == "left" then if cheat.parameter then local idx, chg = cheat:set_index(cheat:get_index() - 1) return chg else if not cheat.is_oneshot then local state, chg = cheat:set_enabled(false) return chg end return false end elseif event == "right" then if cheat.parameter then local idx, chg = cheat:set_index(cheat:get_index() + 1) return chg else if not cheat.is_oneshot then local state, chg = cheat:set_enabled(true) return chg end return false end elseif event == "select" then if cheat.is_oneshot then cheat:set_enabled(true) if cheat.parameter and cheat.script.change and cheat:get_index() ~= 0 then local itemtext if cheat.parameter.item then itemtext = cheat.parameter.item[cheat.parameter.index].text else itemtext = cheat.parameter.value end manager.machine:popmessage(string.format(_("Activated: %s = %s"), cheat.desc, itemtext)) elseif not cheat.parameter and cheat.script.on then manager.machine:popmessage(string.format(_("Activated: %s"), cheat.desc)) end end end return false end emu.register_menu(function(index, event) return menu_callback(index, event) end, function() return menu_populate() end, _("Cheat")) reset_subscription = emu.add_machine_reset_notifier(function () if not stop then return end stop = false start_time = emu.time() cheats = load_cheats() local json = require("json") local file = io.open(manager.machine.options.entries.cheatpath:value():match("([^;]+)") .. "/output.json", "w") if file then file:write(json.stringify(cheats, {indent = true})) file:close() end for num, cheat in pairs(cheats) do parse_cheat(cheat) end load_hotkeys() if manager.machine.debugger then consolelog = manager.machine.debugger.consolelog consolelast = 0 end end) stop_subscription = emu.add_machine_stop_notifier(function () stop = true consolelog = nil save_hotkeys() end) frame_subscription = emu.add_machine_frame_notifier(function () if stop then return end for num, cheat in pairs(cheats) do if cheat.enabled then run_if(cheat, cheat.script.run) end if cheat.hotkeys and cheat.hotkeys.keys then if manager.machine.input:seq_pressed(cheat.hotkeys.keys) then if not cheat.hotkeys.pressed then if cheat.is_oneshot then if not run_if(cheat, cheat.script.change) then run_if(cheat, cheat.script.on) end manager.machine:popmessage(string.format(_("Activated: %s"), cheat.desc)) elseif not cheat.enabled then cheat.enabled = true run_if(cheat, cheat.script.on) manager.machine:popmessage(string.format(_("Enabled: %s"), cheat.desc)) else cheat.enabled = false run_if(cheat, cheat.script.off) bwpclr(cheat) manager.machine:popmessage(string.format(_("Disabled: %s"), cheat.desc)) end end cheat.hotkeys.pressed = true else cheat.hotkeys.pressed = false end end end for num, input in pairs(inputs) do local _, screen = next(manager.machine.screens) local framenum = screen:frame_number() - input.begin local enttab = input.start[framenum] if enttab then for num, entry in pairs(enttab) do entry.field:set_value(1) end end enttab = input.stop[framenum] if enttab then for num, entry in pairs(enttab) do entry.field:set_value(0) end end if framenum >= input.last then table.remove(inputs, num) end end end) emu.register_frame_done(function() if stop then return end line = 0 for num, draw in pairs(output) do if draw.type == "text" then if not draw.color then draw.scr:draw_text(draw.x, draw.y, draw.str) else draw.scr:draw_text(draw.x, draw.y, draw.str, draw.color) end elseif draw.type == "line" then draw.scr:draw_line(draw.x1, draw.y1, draw.x2, draw.y2, draw.color) elseif draw.type == "box" then draw.scr:draw_box(draw.x1, draw.y1, draw.x2, draw.y2, draw.linecolor, draw.bgcolor) end end output = {} end) local ce = {} -- interface to script cheat engine function ce.inject(newcheat) cheats[#cheats + 1] = newcheat parse_cheat(newcheat) manager.machine:popmessage(string.format(_("%s added"), newcheat.desc)) end function ce.get(index) local cheat = cheats[index] if not cheat then return nil end local intf = { get_enabled = function() return cheat:get_enabled() end, set_enabled = function(status) return cheat:set_enabled(status) end, desc = cheat.desc, is_oneshot = cheat.is_oneshot, comment = cheat.comment, get_hotkeys = function() if cheat.hotkeys then return cheat.hotkeys.keys end return nil end, set_hotkeys = function(seq) cheat.hotkeys = { pressed = false, keys = manager.machine.input:seq_clean(seq) } end } if cheat.script then intf.script = {} if cheat.script.on then intf.script.on = true end if cheat.script.off then intf.script.off = true end if cheat.script.run then intf.script.run = true end if cheat.script.change then intf.script.change = true end end if cheat.parameter then intf.parameter = {} intf.get_value = function() return cheat:get_value() end intf.set_value = function(value) return cheat:set_value(value) end intf.get_index = function() return cheat:get_index() end intf.set_index = function(index) return cheat:set_index(index) end intf.parameter.min = cheat.parameter.min intf.parameter.max = cheat.parameter.max intf.parameter.step = cheat.parameter.step if cheat.parameter.item then intf.parameter.item = {} for idx, item in pairs(cheat.parameter.item) do intf.parameter.item[idx] = {} intf.parameter.item[idx].text = cheat.parameter.item[idx].text intf.parameter.item[idx].value = cheat.parameter.item[idx].value end end end return intf end function ce.list() local list = {} for num, cheat in pairs(cheats) do list[num] = cheat.desc end return list end _G.emu.plugin.cheat = ce end return exports