diff --git a/data/lua/connector_ladx_bizhawk.lua b/data/lua/connector_ladx_bizhawk.lua new file mode 100644 index 00000000..e318015c --- /dev/null +++ b/data/lua/connector_ladx_bizhawk.lua @@ -0,0 +1,137 @@ +-- SPDX-FileCopyrightText: 2023 Wilhelm Schürmann +-- +-- 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 diff --git a/data/lua/core.dll b/data/lua/core.dll new file mode 100644 index 00000000..3e956957 Binary files /dev/null and b/data/lua/core.dll differ diff --git a/data/lua/socket.lua b/data/lua/socket.lua new file mode 100644 index 00000000..a98e9521 --- /dev/null +++ b/data/lua/socket.lua @@ -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) diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md index abd60dc8..2fbd67da 100644 --- a/worlds/ladx/docs/setup_en.md +++ b/worlds/ladx/docs/setup_en.md @@ -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 ![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) 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