138 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
		
		
			
		
	
	
			138 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
|   | -- 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 |