Poke_Transporter_GB/tools/link_helper.lua
2026-04-14 17:26:35 -04:00

581 lines
19 KiB
Lua

--- PTGB Link Debug Helper
---
--- This Lua script is designed to connect PTGB to Gen 1/2 on mGBA.
--- It requires a development build of mGBA.
---
--- How to use:
--- 1. Load PTGB in mGBA
--- 2. Tools -> Scripting -> Load script, select this file.
--- 3. Check that it says "PTGB Link Debug Script", otherwise your mGBA may lack Lua support
--- 4. Repeat, but for the Gen 1/2 game.
---
--- Most of the code is adapted from the demo scripts provided with mGBA.
console:log("-- PTGB Link Debug Script --")
-- Version check. The latest release as of writing is 0.10.5, which lacks watchpoint support. --
if C.WATCHPOINT_TYPE == nil then
console:error("This script requires a more recent build of mGBA!")
console:log("You can get a development build at https://mgba.io/downloads.html#development-downloads")
console:log("If you want to build from source, remember to pass -DENABLE_SCRIPTING=1 to CMake.")
return
end
-- Reentrancy check
--
-- If you load a script twice, mGBA will execute the script in the same environment
-- which causes "address in use" errors.
--
-- Requires a development build of mGBA, as watchpoints are a new feature.
if _G["PTGB_REENTRANCY_TEST"] then
Stop()
end
_G["PTGB_REENTRANCY_TEST"] = true
--[[
PTGB in Debug mode uses the following protocol, since mGBA doesn't let us write to REG_SIOCNT properly.
1. PTGB will write a value to link_debug.output_byte
2. PTGB will write true to link_debug.has_output
3. The Lua script will detect this write via a watchpoint and send the byte to the GB part of the script
4. PTGB will wait on link_debug.has_input
5. When the Lua script receives a response, it will write the value to link_debug.input_byte, and write true
to link_debug.has_input.
]]--
-- Let users know the script is working --
console:log("Make sure you enable [insert option here] in the Debug Menu")
console:log("If you would like to log the sent bytes, type EnableOutput(\"filename\") in the console and hit Run")
-- Port used for the socket. Chosen at random.
PORT = 31415
server = nil
sock = nil
watchpoint = -1
defer_watchpoint = -1
output_file = nil
poll_callback = -1
MODE_IDLE = 0
MODE_SEND = 1
MODE_RECV = 2
--- Make sure these line up with the .link_debug addresses in PTGB.
LINK_DEBUG = 0x0203FFFC
LD_REG_SIOCNT = LINK_DEBUG
LD_REG_SIODATA8 = LINK_DEBUG + 2
function logf(fmt, ...)
local tag = "[UNK]"
if emu and emu:platform() == C.PLATFORM.GBA then
tag = "[GBA]"
elseif emu and emu:platform() == C.PLATFORM.GB then
tag = "[GBC]"
end
local str = string.format("%s " .. fmt, tag, ...)
console:log(str)
end
function EnableOutput(filename)
if output_file then
output_file:close()
end
output_file, err = io.open(filename, "wb")
if not output_file then
console:error("Could not open " .. filename .. " for writing: " .. err)
end
end
function SendByte(byte)
logf("SEND [0x%04x] 0x%02x", emu:currentFrame(), byte)
local bytes = string.char(byte)
if output_file then
output_file:write(bytes)
output_file:flush()
end
if sock then
sock:send(bytes)
end
end
function DisableWatchpoint()
if watchpoint ~= -1 then
emu:clearBreakpoint(watchpoint)
watchpoint = -1
end
end
function SocketError()
if sock then
console:error("Socket error")
DisableWatchpoint()
sock:close()
sock = nil
else
console:error("Unknown socket error")
end
end
function PollSocket()
if sock then
local status, err = sock:hasdata()
if err then
console:error("Socket error " .. err)
SocketError()
end
-- SIOCNT START writes can happen while the watchpoint is temporarily
-- disabled during reply handling. Polling here lets us recover
-- transfers that the watchpoint missed.
if emu and emu:platform() == C.PLATFORM.GBA then
GBA_Watchpoint()
end
end
end
function Stop()
logf("Stopping...")
DisableWatchpoint()
if poll_callback ~= -1 then
callbacks:remove(poll_callback)
poll_callback = -1
end
if sock then
sock:close()
sock = nil
end
if output_file then
output_file:close()
output_file = nil
end
if server then
server:close()
server = nil
end
end
frame_callback = -1
gba_transfer_pending = false
gba_last_start_state = false
function SerialExchangeByte(input)
if not sock then return false end
-- Check if ready --
local rSC = read_io(REG_SIOCNT)
if (rSC & SIO_START) == 0 then
if defer_watchpoint == -1 then
-- Defer the result until the game is ready. --
defer_watchpoint = emu:setWatchpoint(
function()
SerialExchangeByte(input)
end,
REG_SIOCNT,
C.WATCHPOINT_TYPE.WRITE
)
end
if frame_callback == -1 then
frame_callback = callbacks:add("frame", function() SerialExchangeByte(input) end)
else
-- SendByte(0xFF) -- fail
--callbacks:remove(frame_callback)
--frame_callback = -1
end
return false -- not ready --
end
-- Disable the watchpoint --
DisableWatchpoint()
if defer_watchpoint ~= -1 then
emu:clearBreakpoint(defer_watchpoint)
defer_watchpoint = -1
end
if frame_callback ~= -1 then
callbacks:remove(frame_callback)
frame_callback = -1
end
local output = read_io(REG_SIODATA8)
-- Log --
logf("RECV [0x%04x] 0x%02x", emu:currentFrame(), input)
-- Write the new byte --
write_io(REG_SIODATA8, input & 0xFF)
-- Clear the start and ready flags. --
rSC = (rSC & ~SIO_START)
write_io(REG_SIOCNT, rSC)
if emu:platform() == C.PLATFORM.GBA then
-- Treat the next SIOCNT START write as a new transaction.
gba_transfer_pending = false
gba_last_start_state = false
end
-- GB specific code --
if emu:platform() == C.PLATFORM.GB then
-- GBA sends first. --
SendByte(output)
-- Trigger an interrupt --
local rIF = read_io(REG_IF)
rIF = rIF | IF_SERIAL
write_io(REG_IF, rIF)
end
MODE = MODE_IDLE
EnableWatchpoint()
return true
end
function HandleReceive()
if not sock then return end
-- Receive the byte from the GB socket --
local p, err = sock:receive(1)
if p then
local input = string.byte(p)
-- DisableWatchpoint()
-- if emu:platform() == C.PLATFORM.GBA then
-- -- The GBA needs a second flag set to enable transfer --
-- local rSC = read_io(REG_SIOCNT)
-- rSC = rSC | SIO_READY
-- write_io(REG_SIOCNT, rSC)
-- end
-- Write the byte from the GB to link_debug.input_byte and write true to has_input --
SerialExchangeByte(input)
else
if err ~= socket.ERRORS.AGAIN then
SocketError()
end
end
end
--- Host (GBA) code ---
function GBA_Watchpoint()
-- if mode == MODE_RECV then return end
local rSC = read_io(REG_SIOCNT)
local start_set = (rSC & SIO_START) ~= 0
-- DisableWatchpoint()
-- rSC = rSC | SIO_READY
-- write_io(REG_SIOCNT, rSC)
-- EnableWatchpoint()
if not start_set then
gba_last_start_state = false
return
end
if not sock then return end
if gba_transfer_pending or gba_last_start_state then return end
gba_last_start_state = true
gba_transfer_pending = true
-- Send it to the GB --
is_sending = true
SendByte(read_io(REG_SIODATA8) & 0xFF)
end
function GBA_Accept() -- ST_Accept
if poll_callback ~= -1 then
callbacks:remove(poll_callback)
poll_callback = -1
end
if sock then
-- Reconnection --
DisableWatchpoint()
sock:close()
sock = nil
end
gba_transfer_pending = false
gba_last_start_state = false
-- Accept the connection --
while true do
local tmp_sock
tmp_sock, err = server:accept()
if err then
if err ~= socket.ERRORS.AGAIN then
console:error("Accept " .. err)
Stop()
return
end
else
-- Setup callbacks --
sock = tmp_sock
tmp_sock:add("received", HandleReceive)
tmp_sock:add("error", SocketError)
logf("Connected")
break
end
end
poll_callback = callbacks:add("frame", PollSocket)
-- Enable the watchpoint to start monitoring for link debug events. --
-- local rSC = read_io(REG_SIOCNT)
-- rSC = rSC | SIO_READY
-- write_io(REG_SIOCNT, rSC)
EnableWatchpoint()
end
function GBA_Setup()
if server ~= nil then
server:close()
server = nil
end
server, err = socket.bind(nil, PORT)
if err then
console:error("Bind error " .. err)
return false
end
local ok
ok, err = server:listen()
if err then
console:error("Error listening: " .. err)
server:close()
return false
else
logf("PTGB Link Debug Script: Listening on port %d", PORT)
server:add("received", GBA_Accept)
return true
end
end
--- GB client code ---
--[[
The GB requires us to emulate the link cable enough to trick Pokémon.
Thankfully, mGBA is not picky on the GB I/O registers, so this is a simple task.
]]--
--- Since mGBA is much more lenient on the GB link cable registers, we can modify memory directly. --
function GB_Error()
console:error("Socket error, disconnecting...")
if sock then
sock:close()
sock = nil
end
GB_TryConnect()
end
function GB_TryConnect()
local tmp_sock = socket.tcp()
if tmp_sock and tmp_sock:connect("127.0.0.1", PORT) then
logf("PTGB Link Debug Script: Connected via port %d", PORT)
if poll_callback ~= -1 then
callbacks:remove(poll_callback)
poll_callback = -1
end
poll_callback = callbacks:add("frame", PollSocket)
sock = tmp_sock
tmp_sock:add("received", HandleReceive)
tmp_sock:add("error", GB_Error)
return true
else
tmp_sock:close()
if poll_callback == -1 then
-- Poll each frame. Only print once --
logf("Waiting for connection...")
poll_callback = callbacks:add("frame", GB_TryConnect)
end
return false
end
end
-- mGBA doesn't let you get the No-Intro title from Lua, sadly. --
ROM_INFO = {
-- CRC32 Game Language Rev. No-Intro title --
-- Red --
[0x13652705] = { "Red", "Japanese", 0, "Pocket Monsters - Aka (Japan) (SGB Enhanced)" },
[0xb77be1e0] = { "Red", "Japanese", 1, "Pocket Monsters - Aka (Japan) (Rev 1) (SGB Enhanced)" },
[0x9f7fdd53] = { "Red", "English", 0, "Pokemon - Red Version (USA, Europe) (SGB Enhanced)" },
[0xd8507d8a] = { "Red", "Spanish", 0, "Pokemon - Edicion Roja (Spain) (SGB Enhanced)" },
[0x89197825] = { "Red", "German", 0, "Pokemon - Rote Edition (Germany) (SGB Enhanced)" },
[0x337fce11] = { "Red", "French", 0, "Pokemon - Version Rouge (France) (SGB Enhanced)" },
[0x2945aceb] = { "Red", "Italian", 0, "Pokemon - Versione Rossa (Italy) (SGB Enhanced)" },
-- Green --
[0xbaeacd2b] = { "Green", "Japanese", 0, "Pocket Monsters - Midori (Japan) (SGB Enhanced)" },
[0x37ae8dc4] = { "Green", "Japanese", 1, "Pocket Monsters - Midori (Japan) (Rev 1) (SGB Enhanced)" },
-- Blue --
[0xe4468d14] = { "Blue", "Japanese", 0, "Pocket Monsters - Ao (Japan) (SGB Enhanced)" },
[0xd6da8a1a] = { "Blue", "English", 0, "Pokemon - Blue Version (USA, Europe) (SGB Enhanced)" },
[0xd95416f9] = { "Blue", "Spanish", 0, "Pokemon - Edicion Azul (Spain) (SGB Enhanced)" },
[0x9c336307] = { "Blue", "German", 0, "Pokemon - Blaue Edition (Germany) (SGB Enhanced)" },
[0x50e2fc1d] = { "Blue", "French", 0, "Pokemon - Version Bleue (France) (SGB Enhanced)" },
[0x4d0984a9] = { "Blue", "Italian", 0, "Pokemon - Versione Blu (Italy) (SGB Enhanced)" },
-- Yellow --
[0x4ec85504] = { "Yellow", "Japanese", 0, "Pocket Monsters - Pikachu (Japan) (SGB Enhanced)" },
[0xa2545d33] = { "Yellow", "Japanese", 1, "Pocket Monsters - Pikachu (Japan) (Rev 1) (SGB Enhanced)" },
[0xfd3da7ff] = { "Yellow", "Japanese", 2, "Pocket Monsters - Pikachu (Japan) (Rev 2) (SGB Enhanced)" },
[0xe9e6483a] = { "Yellow", "Japanese", 3, "Pocket Monsters - Pikachu (Japan) (Rev 3) (SGB Enhanced)" },
[0x7d527d62] = { "Yellow", "English", 0, "Pokemon - Yellow Version - Special Pikachu Edition (USA, Europe) (CGB+SGB Enhanced)" },
[0x964b7a10] = { "Yellow", "Spanish", 0, "Pokemon - Edicion Amarilla - Edicion Especial Pikachu (Spain) (CGB+SGB Enhanced)" },
[0x7a01e45a] = { "Yellow", "German", 0, "Pokemon - Gelbe Edition - Special Pikachu Edition (Germany) (CGB+SGB Enhanced)" },
[0xd03426e9] = { "Yellow", "French", 0, "Pokemon - Version Jaune - Edition Speciale Pikachu (France) (CGB+SGB Enhanced)" },
[0x8b56fe33] = { "Yellow", "Italian", 0, "Pokemon - Versione Gialla - Speciale Edizione Pikachu (Italy) (CGB+SGB Enhanced)" },
-- Gold --
[0x524478d4] = { "Gold", "Japanese", 0, "Pocket Monsters Kin (Japan) (SGB Enhanced) (GB Compatible)" },
[0x4ef7f2a5] = { "Gold", "Japanese", 1, "Pocket Monsters Kin (Japan) (Rev 1) (SGB Enhanced) (GB Compatible)" },
[0x6bde3c3e] = { "Gold", "English", 0, "Pokemon - Gold Version (USA, Europe) (SGB Enhanced) (GB Compatible)" },
[0x3434a92b] = { "Gold", "Spanish", 0, "Pokemon - Edicion Oro (Spain) (SGB Enhanced) (GB Compatible)" },
[0x4889dfaa] = { "Gold", "German", 0, "Pokemon - Goldene Edition (Germany) (SGB Enhanced) (GB Compatible)" },
[0x37a70702] = { "Gold", "French", 0, "Pokemon - Version Or (France) (SGB Enhanced) (GB Compatible)" },
[0x4c184ce3] = { "Gold", "Italian", 0, "Pokemon - Versione Oro (Italy) (SGB Enhanced) (GB Compatible)" },
[0x249a7a66] = { "Gold", "Korean", 0, "Pocket Monsters Geum (Korea)" },
-- Silver --
[0xbe1b928a] = { "Silver", "Japanese", 0, "Pocket Monsters Gin (Japan) (SGB Enhanced) (GB Compatible)" },
[0x0aea5383] = { "Silver", "Japanese", 1, "Pocket Monsters Gin (Japan) (Rev 1) (SGB Enhanced) (GB Compatible)" },
[0x8ad48636] = { "Silver", "English", 0, "Pokemon - Silver Version (USA, Europe) (SGB Enhanced) (GB Compatible)" },
[0x1d9faac5] = { "Silver", "Spanish", 0, "Pokemon - Edicion Plata (Spain) (SGB Enhanced) (GB Compatible)" },
[0x96c9db95] = { "Silver", "German", 0, "Pokemon - Silberne Edition (Germany) (SGB Enhanced) (GB Compatible)" },
[0xe0c216ea] = { "Silver", "French", 0, "Pokemon - Version Argent (France) (SGB Enhanced) (GB Compatible)" },
[0xcba6d2d4] = { "Silver", "Italian", 0, "Pokemon - Versione Argento (Italy) (SGB Enhanced) (GB Compatible)" },
[0x9cc1f90f] = { "Silver", "Korean", 0, "Pocket Monsters Eun (Korea)" },
-- Crystal --
[0x270c4ecc] = { "Crystal", "Japanese", 0, "Pocket Monsters - Crystal Version (Japan)" },
[0xee6f5188] = { "Crystal", "English (US)", 0, "Pokemon - Crystal Version (USA)" },
[0x3358e30a] = { "Crystal", "English (US/EU)", 1, "Pokemon - Crystal Version (USA, Europe) (Rev 1)" },
[0xbb6dd80c] = { "Crystal", "English (AUS)", 0, "Pokemon - Crystal Version (Australia)" },
[0xff0a6f8a] = { "Crystal", "Spanish", 0, "Pokemon - Edicion Cristal (Spain)" },
[0x616d85de] = { "Crystal", "German", 0, "Pokemon - Kristall-Edition (Germany)" },
[0x878b2aa7] = { "Crystal", "French", 0, "Pokemon - Version Cristal (France)" },
[0xd45ac039] = { "Crystal", "Italian", 0, "Pokemon - Versione Cristallo (Italy)" },
}
function GB_Setup()
-- Try to identify the ROM --
local crc32_raw = emu:checksum(C.CHECKSUM.CRC32)
-- mGBA returns a binary string in big endian, convert it to a number --
local crc32 = (string.byte(crc32_raw, 1) << 24) |
(string.byte(crc32_raw, 2) << 16) |
(string.byte(crc32_raw, 3) << 8) |
(string.byte(crc32_raw, 4) << 0)
if ROM_INFO[crc32] then
-- Print info --
local game_type = ROM_INFO[crc32][1]
local game_lang = ROM_INFO[crc32][2]
local game_rev = ROM_INFO[crc32][3]
local game_title = ROM_INFO[crc32][4]
logf("Detected an official Pokémon game!")
logf(" - CRC32: 0x%08x", crc32)
logf(" - ROM name: %s", game_title)
logf(" - Game: %s", game_type)
logf(" - Language: %s", game_lang)
logf(" - Revision: %d", game_rev)
else
logf("Warning: Unknown CRC32 0x%08x", crc32)
end
GB_TryConnect()
return true
end
function Setup()
if emu then
logf("Setting up...")
if emu:platform() == C.PLATFORM.GBA then
REG_SIODATA8 = LD_REG_SIODATA8
REG_SIOCNT = LD_REG_SIOCNT
-- PTGB doesn't actually use serial interrupts, and we can't manually
-- set the interrupts.
REG_IF = 0
SIO_START = 0x0080
SIO_READY = 0x0000
IF_SERIAL = 0x0080
function write_io(addr, val)
emu:write16(addr, val)
end
function read_io(addr)
return emu:read16(addr)
end
function EnableWatchpoint()
watchpoint = emu:setWatchpoint(GBA_Watchpoint, REG_SIOCNT, C.WATCHPOINT_TYPE.WRITE)
end
if not GBA_Setup() then
console:error("Setup failed!")
end
else
REG_SIODATA8 = 0xFF01
REG_SIOCNT = 0xFF02
REG_IF = 0xFF0F
SIO_START = 0x80
SIO_READY = 0x00
IF_SERIAL = 0x08
function write_io(addr, val)
emu:write8(addr, val)
end
function read_io(addr)
return emu:read8(addr)
end
function EnableWatchpoint()
-- watchpoint = emu:setWatchpoint(GB_Watchpoint, REG_SIOCNT, C.WATCHPOINT_TYPE.WRITE)
end
if not GB_Setup() then
console:error("Setup failed!")
end
end
end
end
callbacks:add("start", Setup)
callbacks:add("stop", Stop)
if emu then
Setup()
end