Add experimental cheat lua plugin [Carl]

This commit is contained in:
cracyc 2016-04-06 17:03:11 -05:00
parent ead741a06e
commit 55c9596f4c
4 changed files with 671 additions and 0 deletions

View File

@ -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]))))

433
plugins/cheat/init.lua Normal file
View File

@ -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

10
plugins/cheat/plugin.json Normal file
View File

@ -0,0 +1,10 @@
{
"plugin": {
"name": "cheat",
"description": "Cheat plugin",
"version": "0.0.1",
"author": "Carl",
"type": "plugin",
"start": "false"
}
}

217
plugins/cheat/xml_conv.lua Normal file
View File

@ -0,0 +1,217 @@
local xml = {}
-- basic xml parser for mamecheat only
local function xml_parse(data)
local cheat_str = data:match("<mamecheat.->(.*)</ *mamecheat>")
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("(.-)</ *" .. tag .. " *>(.*)")
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