mirror of
https://github.com/libretro/mame2016-libretro.git
synced 2024-11-27 02:30:46 +00:00
Add experimental cheat lua plugin [Carl]
This commit is contained in:
parent
ead741a06e
commit
55c9596f4c
11
plugins/cheat/conv_cheat.lua
Normal file
11
plugins/cheat/conv_cheat.lua
Normal 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
433
plugins/cheat/init.lua
Normal 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
10
plugins/cheat/plugin.json
Normal 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
217
plugins/cheat/xml_conv.lua
Normal 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
|
Loading…
Reference in New Issue
Block a user