MAME/src/mame_bridge.lua
Tolik 6b6a5b0f9c
Some checks failed
CI (macOS) / build-macos (push) Waiting to run
CI (Windows) / build-windows (gcc, gcc-x64, g++, mame, UCRT64, windows-latest, mingw-w64-ucrt-x86_64, mame) (push) Waiting to run
Check #include guards / validate (push) Has been cancelled
Vibe changes over upstream MAME (squashed)
Содержит правки из следующих коммитов:
  - WIP: all-in-one
  - Janko: cache on
  - DeZog fix
  - Revert "drop mlz"
  - emu/debug/debugcpu.cpp,sinclair/spectrum.cpp: Guarded pointer accessors
  - sinclair/sprinter.cpp tmkonf
  - dma delay under investigation
  - harddisks-shareable.diff
  - ignore
  - плагин и скрипты для управления MAME by Claude code (MCP)
  - много всего
  - mouse release
2026-05-26 22:09:47 +10:00

154 lines
5.6 KiB
Lua

-- mame_bridge.lua
-- File-IPC bridge that exposes MAME's debugger to an external MCP server.
-- Launch MAME like:
-- mame <system> -debug -autoboot_script /path/to/mame_bridge.lua
--
-- Protocol: the MCP server drops a request file <DIR>/req_<id>.txt containing
-- one command line; this script processes it on each emulated frame and writes
-- the reply to <DIR>/resp_<id>.txt .
--
-- Env vars:
-- MAME_MCP_DIR IPC directory (default /tmp/mame_mcp)
-- MAME_MCP_CPU CPU device tag (default :maincpu)
local DIR = os.getenv("MAME_MCP_DIR") or "/tmp/mame_mcp"
local CPUTAG = os.getenv("MAME_MCP_CPU") or ":maincpu"
local SPACE = "program"
-- Z80 (and 8080-ish) registers worth reporting. Missing ones are skipped.
local REGS = { "PC","SP","AF","BC","DE","HL","IX","IY",
"AF2","BC2","DE2","HL2","I","R","IM","IFF1","IFF2","WZ" }
os.execute('mkdir -p "' .. DIR .. '" 2>/dev/null')
local lfs_ok, lfs = pcall(require, "lfs")
----------------------------------------------------------------------- helpers
local function read_file(p)
local f = io.open(p, "rb"); if not f then return nil end
local d = f:read("*a"); f:close(); return d
end
local function write_atomic(path, data)
local tmp = path .. ".tmp"
local f = io.open(tmp, "wb"); if not f then return false end
f:write(data); f:close()
os.rename(tmp, path)
return true
end
local function cpu() return manager.machine.devices[CPUTAG] end
local function dbg() return manager.machine.debugger end
local function space() return cpu().spaces[SPACE] end
-- Run a debugger console command and return any new console output it produced.
local function run_cmd(cmdstr)
local d = dbg()
local before = #d.consolelog
d:command(cmdstr)
local lines = {}
for i = before + 1, #d.consolelog do lines[#lines + 1] = d.consolelog[i] end
return table.concat(lines, "\n")
end
----------------------------------------------------------------------- actions
local function do_regs()
local c, out = cpu(), {}
for _, r in ipairs(REGS) do
local ok, ent = pcall(function() return c.state[r] end)
if ok and ent then
local ok2, v = pcall(function() return ent.value end)
if ok2 and v then out[#out + 1] = string.format("%s=0x%X", r, v) end
end
end
return table.concat(out, " ")
end
local function do_mem(addr, len)
local s, t = space(), {}
if len > 4096 then len = 4096 end
for i = 0, len - 1 do
local ok, b = pcall(function() return s:read_u8(addr + i) end)
t[#t + 1] = string.format("%02X", ok and b or 0)
end
return table.concat(t)
end
local function do_setmem(addr, hex)
local s, i = space(), 0
for byte in hex:gmatch("%x%x") do
s:write_u8(addr + i, tonumber(byte, 16)); i = i + 1
end
return "ok wrote " .. i .. " byte(s)"
end
local function do_status()
local st = dbg().execution_state
local pc = "?"
local ok, v = pcall(function() return cpu().state["PC"].value end)
if ok then pc = string.format("0x%X", v) end
return "state=" .. st .. " PC=" .. pc
end
local function do_dasm(addr, nbytes)
local tmp = DIR .. "/_dasm.tmp"
run_cmd(string.format("dasm %s,0x%X,%d", tmp, addr, nbytes))
local d = read_file(tmp) or "(no output)"
os.remove(tmp)
return d
end
--------------------------------------------------------------------- dispatch
local function dispatch(line)
if not manager.machine.debugger then
return "ERROR: debugger not enabled (start MAME with -debug)"
end
local words = {}
for w in line:gmatch("%S+") do words[#words + 1] = w end
local verb = words[1]
local ok, res = pcall(function()
if verb == "regs" then return do_regs()
elseif verb == "mem" then return do_mem(tonumber(words[2]), tonumber(words[3]))
elseif verb == "setmem" then return do_setmem(tonumber(words[2]), words[3])
elseif verb == "bp" then
local cond = line:match("^%S+%s+%S+%s+(.+)$")
return run_cmd("bpset " .. words[2] .. (cond and ("," .. cond) or ""))
elseif verb == "bpclr" then return run_cmd("bpclear " .. words[2])
elseif verb == "bplist" then return run_cmd("bplist")
elseif verb == "wp" then return run_cmd("wpset " .. words[2] .. "," .. words[3] .. "," .. words[4])
elseif verb == "wpclr" then return run_cmd("wpclear " .. words[2])
elseif verb == "step" then return run_cmd("step " .. (words[2] or "1"))
elseif verb == "over" then return run_cmd("over " .. (words[2] or "1"))
elseif verb == "out" then return run_cmd("out")
elseif verb == "cont" then dbg().execution_state = "run"; return "running"
elseif verb == "pause" then dbg().execution_state = "stop"; return "stopped"
elseif verb == "status" then return do_status()
elseif verb == "dasm" then return do_dasm(tonumber(words[2]), tonumber(words[3] or "32"))
elseif verb == "cmd" then return run_cmd(line:match("^cmd%s+(.+)$") or "")
else return "ERROR: unknown command '" .. tostring(verb) .. "'" end
end)
return ok and tostring(res) or ("ERROR: " .. tostring(res))
end
------------------------------------------------------------------------- pump
local function poll()
if not lfs_ok then return end
for entry in lfs.dir(DIR) do
local id = entry:match("^req_(%d+)%.txt$")
if id then
local reqp = DIR .. "/" .. entry
local data = read_file(reqp)
if data then
local reply = dispatch(data)
write_atomic(DIR .. "/resp_" .. id .. ".txt", reply)
os.remove(reqp)
end
end
end
end
-- Keep the subscription alive for the lifetime of the script.
local _sub = emu.add_machine_frame_notifier(function() poll() end)
emu.print_info("mame_bridge.lua active. IPC dir = " .. DIR)