From 55c9596f4c434eb615e71f62f610666629dc864b Mon Sep 17 00:00:00 2001 From: cracyc Date: Wed, 6 Apr 2016 17:03:11 -0500 Subject: [PATCH] Add experimental cheat lua plugin [Carl] --- plugins/cheat/conv_cheat.lua | 11 + plugins/cheat/init.lua | 433 +++++++++++++++++++++++++++++++++++ plugins/cheat/plugin.json | 10 + plugins/cheat/xml_conv.lua | 217 ++++++++++++++++++ 4 files changed, 671 insertions(+) create mode 100644 plugins/cheat/conv_cheat.lua create mode 100644 plugins/cheat/init.lua create mode 100644 plugins/cheat/plugin.json create mode 100644 plugins/cheat/xml_conv.lua diff --git a/plugins/cheat/conv_cheat.lua b/plugins/cheat/conv_cheat.lua new file mode 100644 index 0000000000..afc87bc55b --- /dev/null +++ b/plugins/cheat/conv_cheat.lua @@ -0,0 +1,11 @@ +xml = require("xml_conv") +json = dofile("../json/init.lua") + +function readAll(file) + local f = io.open(file, "rb") + local content = f:read("*all") + f:close() + return content +end + +print(json.stringify(xml.conv_cheat(readAll(arg[1])))) diff --git a/plugins/cheat/init.lua b/plugins/cheat/init.lua new file mode 100644 index 0000000000..5f38a85646 --- /dev/null +++ b/plugins/cheat/init.lua @@ -0,0 +1,433 @@ +-- 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" +-- }, +-- ... ] +-- }, +-- "space": { +-- "varname": { +-- "tag": "tag", +-- "type": "program|data|io" +-- }, +-- ... +-- }, +-- "screen": { +-- "varname": "tag" +-- }, +-- ... +-- "region": { +-- "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 + +local exports = {} +exports.name = "cheat" +exports.version = "0.0.1" +exports.description = "Cheat plugin" +exports.license = "The BSD 3-Clause License" +exports.author = { name = "Carl" } + +local cheat = exports + +function cheat.startplugin() + local cheats = {} + local output = {} + local line = 0 + + local function load_cheats() + local filename = emu.romname() + local json = require("json") + local file = emu.file(manager:machine():options().entries.cheatpath:value(), 1) + local function readAll(file) + local f = io.open(file, "rb") + if not f then + return nil + end + local content = f:read("*all") + f:close() + return content + end + + if emu.softname() ~= "" then + for name, image in manager:machine().images do + if image:exists() and image:software_entry() then + filename = filename .. "/" .. image:software_entry() + end + end + end + end + + if file:open(filename .. ".json") then + local xml = require("cheat/xml_conv") + + if file:open(filename .. ".xml") then + return {} + end + return xml.conv_cheat(file:read(file:size())) + end + + return json.parse(file:read(file:size())) + 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 + print("draw_text: invalid screen") + return + end + if type(x) == "string" then + y = y * manager:machine():ui():get_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 + print("draw_text: 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 + print("draw_text: 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 parse_cheat(cheat) + cheat.cheat_env = { draw_text = draw_text, + draw_line = draw_line, + draw_box = draw_box, + pairs = pairs } + cheat.enabled = false + -- verify scripts are valid first + if not cheat.script then + return + end + for name, script in pairs(cheat.script) do + script = load(script, cheat.desc .. name, "t", cheat.cheat_env) + if not script then + print("error loading cheat script: " .. cheat.desc) + cheat = nil + return + end + cheat.script[name] = script + 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 + print("error loading cheat script: " .. cheat.desc) + cheat = nil + return + end + if space.type then + mem = cpu.spaces[space.type] + else + mem = cpu.spaces["program"] + end + if not mem then + print("error loading cheat script: " .. cheat.desc) + cheat = nil + return + end + cheat.cheat_env[name] = mem + end + end + if cheat.screen then + for name, screen in pairs(cheat.screen) do + local scr + scr = manager:machine().screens[screen] + if not scr then + print("error loading cheat script: " .. cheat.desc) + cheat = nil + return + end + cheat.cheat_env[name] = scr + end + end + if cheat.region then + for name, region in pairs(cheat.region) do + local mem + mem = manager:machine():memory().regions[region] + if not mem then + print("error loading cheat script: " .. cheat.desc) + cheat = nil + return + end + cheat.cheat_env[name] = mem + end + end + local param = cheat.parameter + if not param then + return + end + if not param.min then + param.min = 0 + else + param.min = tonumber(param.min) + end + if not param.max then + param.max = #param.item + else + param.max = tonumber(param.max) + end + if not param.step then + param.step = 1 + else + param.step = tonumber(param.step) + end + 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 function menu_populate() + local menu = {} + for num, cheat in pairs(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] = 32 -- MENU_FLAG_DISABLE + elseif not cheat.script.run and not cheat.script.off then + menu[num][2] = "Set" + menu[num][3] = 0 + else + if cheat.enabled then + menu[num][2] = "On" + menu[num][3] = 1 -- MENU_FLAG_LEFT_ARROW + else + menu[num][2] = "Off" + menu[num][3] = 2 -- MENU_FLAG_RIGHT_ARROW + end + end + else + if cheat.parameter.index == 0 then + if not cheat.script.run and not cheat.script.off then + menu[num][2] = "Set" + else + menu[num][2] = "Off" + end + menu[num][3] = 2 + 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] = 1 + if cheat.parameter.index < cheat.parameter.last then + menu[num][3] = 3 + end + end + end + end + return menu + end + + local function menu_callback(index, event) + 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 + + 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("Cheat Comment:\n" .. cheat.comment) + end + elseif event == "left" then + if cheat.parameter then + local param = cheat.parameter + if param.index == 1 then + param.index = 0 + cheat.enabled = false + cheat.cheat_env.param = param.min + if cheat.script.off then + cheat.script.off() + end + return true + elseif param.index == 0 then + return false + end + param.index = param.index - 1 + param_calc(param) + cheat.cheat_env.param = param.value + if cheat.script.change and (cheat.script.run or cheat.script.off) then + cheat.script.change() + end + return true + else + if not cheat.script.run and not cheat.script.off then + return false + end + if cheat.enabled then + cheat.enabled = false + if cheat.script.off then + cheat.script.off() + end + return true + end + return false + end + elseif event == "right" then + if cheat.parameter then + local param = cheat.parameter + if param.index == 0 then + cheat.enabled = true + if cheat.script.on then + cheat.script.on() + end + elseif param.index == param.last then + return false + end + param.index = param.index + 1 + param_calc(param) + cheat.cheat_env.param = param.value + if cheat.script.change and (cheat.script.run or cheat.script.off) then + cheat.script.change() + end + return true + else + if not cheat.script.run and not cheat.script.off then + return false + end + if not cheat.enabled then + cheat.enabled = true + if cheat.script.on then + cheat.script.on() + end + return true + end + return false + end + elseif event == "select" then + if not cheat.script.run and not cheat.script.off 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() + local subtext + if cheat.parameter.item then + subtext = cheat.parameter.item[cheat.parameter.index] + else + subtext = cheat.parameter.value + end + manager:machine():popmessage("Activated: " .. cheat.desc .. " = " .. subtext) + elseif not cheat.parameter and cheat.script.on then + cheat.script.on() + manager:machine():popmessage("Activated: " .. 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") + + emu.register_start(function() + cheats = load_cheats() + for num, cheat in pairs(cheats) do + parse_cheat(cheat) + end + end) + + emu.register_frame(function() + for num, cheat in pairs(cheats) do + if cheat.enabled and cheat.script.run then + cheat.script.run() + end + end + end) + + emu.register_frame_done(function() + 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.x2, draw.y1, draw.y2, draw.color) + elseif draw.type = "box" then + draw.scr:draw_box(draw.x1, draw.x2, draw.y1, draw.y2, draw.bgcolor, draw.linecolor) + end + end + output = {} + end) +end + +return exports diff --git a/plugins/cheat/plugin.json b/plugins/cheat/plugin.json new file mode 100644 index 0000000000..4067fdbc47 --- /dev/null +++ b/plugins/cheat/plugin.json @@ -0,0 +1,10 @@ +{ + "plugin": { + "name": "cheat", + "description": "Cheat plugin", + "version": "0.0.1", + "author": "Carl", + "type": "plugin", + "start": "false" + } +} diff --git a/plugins/cheat/xml_conv.lua b/plugins/cheat/xml_conv.lua new file mode 100644 index 0000000000..72bbf63503 --- /dev/null +++ b/plugins/cheat/xml_conv.lua @@ -0,0 +1,217 @@ +local xml = {} + +-- basic xml parser for mamecheat only +local function xml_parse(data) + local cheat_str = data:match("(.*)") + + local function get_tags(str) + local arr = {} + while str ~= "" do + local tag, attr, stop + tag, attr, stop, str = str:match("<([%w!-]+) ?(.-)(/?)[ -]->(.*)") + + if not tag then + return arr + end + if tag:sub(0, 3) ~= "!--" then + local block = {} + if stop ~= "/" then + local nest + nest, str = str:match("(.-)(.*)") + local children = get_tags(nest) + if not next(children) then + nest = nest:gsub("", "") + nest = nest:gsub("^%s*(.-)%s*$", "%1") + block["text"] = nest + else + block = children + end + end + if attr then + for name, value in attr:gmatch("(%w-)=\"(.-)\"") do + block[name] = value:gsub("^%s*(.-)%s*$", "%1") + end + end + if not arr[tag] then + arr[tag] = {} + end + arr[tag][#arr[tag] + 1] = block + end + end + return arr + end + local xml_table = get_tags(cheat_str) + return xml_table +end + +function xml.conv_cheat(data) + local spaces, regions, output + data = xml_parse(data) + local function convert_expr(data) + local write = false + + local function convert_memref(cpu, space, width, addr, rw) + local direct = "" + if space == "p" then + fullspace = "program" + elseif space == "d" then + fullspace = "data" + elseif space == "i" then + fullspace = "io" + elseif space == "r" or space == "o" then + fullspace = "program" + direct = "direct_" + space = "p" + end + if width == "b" then + width = "u8" + elseif width == "w" then + width = "u16" + elseif width == "d" then + width = "u32" + elseif width == "q" then + width = "u64" + end + local cpuname = cpu:gsub(":", "_") + if space == "m" then + regions[cpuname .. space] = ":" .. cpu + else + spaces[cpuname .. space] = { tag = ":" .. cpu, type = fullspace } + end + if rw == "=" then + write = true + ret = cpuname .. space .. ":" .. "write_" .. direct .. width .. "(" .. addr .. "," + else + ret = cpuname .. space .. ":" .. "read_" .. direct .. width .. "(" .. addr .. ")" + end + if rw == "==" then + ret = ret .. "==" + end + return ret + end + + local function frame() + output = true + return "screen:frame_number()" + end + + data = data:lower() + data = data:gsub("lt", "<") + data = data:gsub("ge", ">=") + data = data:gsub("gt", ">") + data = data:gsub("le", "<=") + data = data:gsub("!=", "~=") + data = data:gsub("frame", frame) + data = data:gsub("band", "&") + data = data:gsub("bor", "|") + data = data:gsub("%f[%w](%x+)%f[%W]", "0x%1") + data = data:gsub("([%w:]-).([pmrodi3])([bwdq])@(0x%x+) *(=*)", convert_memref) + data = data:gsub("([%w:]-).([pmrodi3])([bwdq])@(%b()) *(=*)", convert_memref) + if write then + data = data .. ")" + end + return data + end + + local function convert_output(data) + local str = "draw_text(screen," + if data["align"] then + str = str .. data["align"] + else + str = str .. "\"left\"" + end + if data["line"] then + str = str .. ",\"" .. data["line"] .. "\"" + else + str = str .. ", \"auto\"" + end + str = str .. ", nil,\"" .. data["format"] .. "\"" + for count, block in pairs(data["argument"]) do + local expr = convert_expr(block["text"]) + if block["count"] then + for i = 0, block["count"] - 1 do + str = str .. "," .. expr:gsub("argindex", i) + end + else + str = str .. "," .. expr + end + end + return str .. ")" + end + + local function convert_script(data) + local str = "" + local state = "run" + for tag, block in pairs(data) do + if tag == "state" then + state = block + elseif tag == "action" then + for count, action in pairs(block) do + if action["condition"] then + str = str .. " if " .. convert_expr(action["condition"]) .. " then " + for expr in action["text"]:gmatch("([^,]+)") do + str = str .. convert_expr(expr) .. " " + end + str = str .. "end" + else + for expr in action["text"]:gmatch("([^,]+)") do + str = str .. " " .. convert_expr(expr) .. " " + end + end + end + elseif tag == "output" then + output = true + for count, output in pairs(block) do + if output["condition"] then + str = str .. " if " .. convert_expr(output["condition"]) .. " then " + str = str .. convert_output(output) .. " end " + else + str = str .. " " .. convert_output(output) .. " " + end + end + end + end + return state, str + end + + for count, cheat in pairs(data["cheat"]) do + spaces = {} + regions = {} + output = false + for tag, block in pairs(cheat) do + if tag == "comment" then + data["cheat"][count]["comment"] = block[1]["text"] + elseif tag == "script" then + local scripts = {} + for count2, script in pairs(block) do + local state, str = convert_script(script) + scripts[state] = str + end + data["cheat"][count]["script"] = scripts + elseif tag == "parameter" then + data["cheat"][count]["parameter"] = block[1] + end + end + if next(spaces) then + data["cheat"][count]["space"] = {} + for name, space in pairs(spaces) do + data["cheat"][count]["space"] = {} + data["cheat"][count]["space"][name] = { type = space["type"], tag = space["tag"] } + end + end + if next(regions) then + data["cheat"][count]["region"] = {} + for name, region in pairs(regions) do + data["cheat"][count]["region"] = {} + data["cheat"][count]["region"][name] = region + end + end + if output then + data["cheat"][count]["screen"] = {} + data["cheat"][count]["screen"]["screen"] = ":screen" + end + end + return data["cheat"] +end + +return xml