LADX: Add Lua connector for BizHawk (#1579)
This is a Lua script for BizHawk that implements the relevant parts of the RetroArch networking API used by the Archipelago LADX Client. socket.lua and core.dll are exact copies of the same files in data/lua/OOT and various other folders. There is a PR consolidating these into the base folder, which this commit is anticipating. LADX "just works"(tm) when this is loaded in Bizhawk.
This commit is contained in:
parent
59b78528a9
commit
20e80d06cf
|
@ -0,0 +1,137 @@
|
|||
-- SPDX-FileCopyrightText: 2023 Wilhelm Schürmann <wimschuermann@googlemail.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- This script attempts to implement the basic functionality needed in order for
|
||||
-- the LADXR Archipelago client to be able to talk to BizHawk instead of RetroArch
|
||||
-- by reproducing the RetroArch API with BizHawk's Lua interface.
|
||||
--
|
||||
-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c
|
||||
--
|
||||
-- Only
|
||||
-- VERSION
|
||||
-- GET_STATUS
|
||||
-- READ_CORE_MEMORY
|
||||
-- WRITE_CORE_MEMORY
|
||||
-- commands are supported right now.
|
||||
--
|
||||
-- USAGE:
|
||||
-- Load this script in BizHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script")
|
||||
--
|
||||
-- All inconsistencies (like missing newlines for some commands) of the RetroArch
|
||||
-- UDP API (network_cmd_enable) are reproduced as-is in order for clients written to work with
|
||||
-- RetroArch's current API to "just work"(tm).
|
||||
--
|
||||
-- This script has only been tested on GB(C). If you have made sure it works for N64 or other
|
||||
-- cores supported by BizHawk, please let me know. Note that GET_STATUS, at the very least, will
|
||||
-- have to be adjusted.
|
||||
--
|
||||
--
|
||||
-- NOTE:
|
||||
-- BizHawk's Lua API is very trigger-happy on throwing exceptions.
|
||||
-- Emulation will continue fine, but the RetroArch API layer will stop working. This
|
||||
-- is indicated only by an exception visible in the Lua console, which most players
|
||||
-- will probably not have in the foreground.
|
||||
--
|
||||
-- pcall(), the usual way to catch exceptions in Lua, doesn't appear to be supported at all,
|
||||
-- meaning that error/exception handling is not easily possible.
|
||||
--
|
||||
-- This means that a lot more error checking would need to happen before e.g. reading/writing
|
||||
-- memory. Since the end goal, according to AP's Discord, seems to be SNI integration of GB(C),
|
||||
-- no further fault-proofing has been done on this.
|
||||
--
|
||||
|
||||
|
||||
local socket = require("socket")
|
||||
local udp = socket.udp()
|
||||
|
||||
udp:setsockname('127.0.0.1', 55355)
|
||||
udp:settimeout(0)
|
||||
|
||||
|
||||
while true do
|
||||
-- Attempt to lessen the CPU load by only polling the UDP socket every x frames.
|
||||
-- x = 10 is entirely arbitrary, very little thought went into it.
|
||||
-- We could try to make use of client.get_approx_framerate() here, but the values returned
|
||||
-- seemed more or less arbitrary as well.
|
||||
--
|
||||
-- NOTE: Never mind the above, the LADXR Archipelago client appears to run into problems with
|
||||
-- interwoven GET_STATUS calls, leading to stopped communication.
|
||||
-- For GB(C), polling the socket on every frame is OK-ish, so we just do that.
|
||||
--
|
||||
--while emu.framecount() % 10 ~= 0 do
|
||||
-- emu.frameadvance()
|
||||
--end
|
||||
|
||||
local data, msg_or_ip, port_or_nil = udp:receivefrom()
|
||||
if data then
|
||||
-- "data" format is "COMMAND [PARAMETERS] [...]"
|
||||
local command = string.match(data, "%S+")
|
||||
if command == "VERSION" then
|
||||
-- 1.14 is the latest RetroArch release at the time of writing this, no other reason
|
||||
-- for choosing this here.
|
||||
udp:sendto("1.14.0\n", msg_or_ip, port_or_nil)
|
||||
elseif command == "GET_STATUS" then
|
||||
local status = "PLAYING"
|
||||
if client.ispaused() then
|
||||
status = "PAUSED"
|
||||
end
|
||||
|
||||
if emu.getsystemid() == "GBC" then
|
||||
-- Actual reply from RetroArch's API:
|
||||
-- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f"
|
||||
-- CRC32 isn't readily available through the Lua API. We could calculate
|
||||
-- it ourselves, but since LADXR doesn't make use of this field it is
|
||||
-- simply replaced by the hash that BizHawk _does_ make available.
|
||||
|
||||
udp:sendto(
|
||||
"GET_STATUS " .. status .. " game_boy," ..
|
||||
string.gsub(gameinfo.getromname(), "[%s,]", "_") ..
|
||||
",romhash=" ..
|
||||
gameinfo.getromhash() .. "\n",
|
||||
msg_or_ip, port_or_nil
|
||||
)
|
||||
else -- No ROM loaded
|
||||
-- NOTE: No newline is intentional here for 1:1 RetroArch compatibility
|
||||
udp:sendto("GET_STATUS CONTENTLESS", msg_or_ip, port_or_nil)
|
||||
end
|
||||
elseif command == "READ_CORE_MEMORY" then
|
||||
local _, address, length = string.match(data, "(%S+) (%S+) (%S+)")
|
||||
address = tonumber(address, 16)
|
||||
length = tonumber(length)
|
||||
|
||||
-- NOTE: mainmemory.read_bytes_as_array() would seem to be the obvious choice
|
||||
-- here instead, but it isn't. At least for Sameboy and Gambatte, the "main"
|
||||
-- memory differs (ROM vs WRAM).
|
||||
-- Using memory.read_bytes_as_array() and explicitly using the System Bus
|
||||
-- as the active memory domain solves this incompatibility, allowing us
|
||||
-- to hopefully use whatever GB(C) emulator we want.
|
||||
local mem = memory.read_bytes_as_array(address, length, "System Bus")
|
||||
local hex_string = ""
|
||||
for _, v in ipairs(mem) do
|
||||
hex_string = hex_string .. string.format("%02X ", v)
|
||||
end
|
||||
hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " "
|
||||
local reply = string.format("%s %02x %s\n", command, address, hex_string)
|
||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||
elseif command == "WRITE_CORE_MEMORY" then
|
||||
local _, address = string.match(data, "(%S+) (%S+)")
|
||||
address = tonumber(address, 16)
|
||||
|
||||
local to_write = {}
|
||||
local i = 1
|
||||
for byte_str in string.gmatch(data, "%S+") do
|
||||
if i > 2 then
|
||||
table.insert(to_write, tonumber(byte_str, 16))
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
memory.write_bytes_as_array(address, to_write, "System Bus")
|
||||
local reply = string.format("%s %02x %d\n", command, address, i - 3)
|
||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||
end
|
||||
end
|
||||
|
||||
emu.frameadvance()
|
||||
end
|
Binary file not shown.
|
@ -0,0 +1,132 @@
|
|||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX`
|
||||
- Software capable of loading and playing GBC ROM files
|
||||
- Currently only [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer) is supported.
|
||||
- Bizhawk support will come at a later date.
|
||||
- [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer.
|
||||
- Your American 1.0 ROM file, probably named `Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc`
|
||||
|
||||
## Installation Procedures
|
||||
|
@ -65,7 +65,7 @@ client, and will also create your ROM in the same place as your patch file.
|
|||
|
||||
### Connect to the client
|
||||
|
||||
##### RetroArch 1.10.3 or newer
|
||||
#### RetroArch 1.10.3 or newer
|
||||
|
||||
You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3.
|
||||
|
||||
|
@ -77,6 +77,13 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
|||

|
||||
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - Gameboy / Color (SameBoy)".
|
||||
|
||||
#### BizHawk 2.8 or newer (older versions untested)
|
||||
|
||||
1. With the ROM loaded, click on "Tools" --> "Lua Console"
|
||||
2. In the new window, click on "Script" --> "Open Script..."
|
||||
3. Navigate to the folder Archipelago is installed in, and choose data/lua/connector_ladx_bizhawk.lua
|
||||
4. Keep the Lua Console open during gameplay (minimizing it is fine!)
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
|
||||
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
|
||||
|
|
Loading…
Reference in New Issue