| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | -- 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 | 
					
						
							| 
									
										
										
										
											2023-06-26 16:53:44 +10:00
										 |  |  | -- the LADXR Archipelago client to be able to talk to EmuHawk instead of RetroArch | 
					
						
							|  |  |  | -- by reproducing the RetroArch API with EmuHawk's Lua interface. | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | -- | 
					
						
							|  |  |  | -- 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: | 
					
						
							| 
									
										
										
										
											2023-06-26 16:53:44 +10:00
										 |  |  | --  Load this script in EmuHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script", or drag+drop) | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | -- | 
					
						
							|  |  |  | -- 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 | 
					
						
							| 
									
										
										
										
											2023-06-26 16:53:44 +10:00
										 |  |  | -- cores supported by EmuHawk, please let me know. Note that GET_STATUS, at the very least, will | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | -- have to be adjusted. | 
					
						
							|  |  |  | -- | 
					
						
							|  |  |  | -- | 
					
						
							|  |  |  | -- NOTE: | 
					
						
							| 
									
										
										
										
											2023-06-26 16:53:44 +10:00
										 |  |  | --  EmuHawk's Lua API is very trigger-happy on throwing exceptions. | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | --  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") | 
					
						
							| 
									
										
										
										
											2023-05-03 14:35:14 -07:00
										 |  |  | local udp = socket.socket.udp() | 
					
						
							|  |  |  | require('common') | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2023-06-26 16:53:44 +10:00
										 |  |  |                 -- simply replaced by the hash that EmuHawk _does_ make available. | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 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+)") | 
					
						
							| 
									
										
										
										
											2023-05-03 14:35:14 -07:00
										 |  |  |             address = stripPrefix(address, "0x") | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  |             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+)") | 
					
						
							| 
									
										
										
										
											2023-05-03 14:35:14 -07:00
										 |  |  |             address = stripPrefix(address, "0x") | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  |             address = tonumber(address, 16) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             local to_write = {} | 
					
						
							|  |  |  |             local i = 1 | 
					
						
							|  |  |  |             for byte_str in string.gmatch(data, "%S+") do | 
					
						
							|  |  |  |                 if i > 2 then | 
					
						
							| 
									
										
										
										
											2023-05-03 14:35:14 -07:00
										 |  |  |                     byte_str = stripPrefix(byte_str, "0x") | 
					
						
							| 
									
										
										
										
											2023-03-30 15:55:38 +02:00
										 |  |  |                     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 |