-- original file found in a GPLv3 code repository, unclear if this is the intended license nor who the authors are -- SNI modifications by Berserker, jsd1982; modifications licensed under MIT License -- version 3 changes Read response from JSON to HEX -- lua 5.1/5.4 shim by zig; modifications licensed under MIT and WTFPL -- version 4 enhances the message processing loop to allow for more than one message per game frame function get_lua_version() local major, minor = _VERSION:match("Lua (%d+)%.(%d+)") assert(tonumber(major) == 5) if tonumber(minor) >= 4 then return "5-4" end return "5-1" end if not event then is_snes9x = true memory.usememorydomain = function() -- snes9x always uses "System Bus" domain, which cannot be switched end else if emu.getsystemid() ~= "SNES" then print("Connector only for BSNES Core within Bizhawk, sorry.") end local current_engine = nil; -- client.get_lua_engine is new if client.get_lua_engine ~= nil then current_engine = client.get_lua_engine(); -- emu.getluacore for old BizHawk elseif emu.getluacore ~= nil then current_engine = emu.getluacore(); end if current_engine ~= nil and current_engine ~= "LuaInterface" and get_lua_version() ~= "5-4" then print("Wrong Lua Core. Found " .. current_engine .. ", was expecting LuaInterface. ") print("Please go to Config -> Customize -> Advanced and select Lua+LuaInterface.") print("Once set, restart Bizhawk.") end end function readbyterange(addr, length, domain) local mtable; local mstart = 0; local mend = length - 1; if is_snes9x then mtable = memory.readbyterange(addr, length); mstart = 1 mend = length else -- jsd: wrap around address by domain size: local domainsize = memory.getmemorydomainsize(domain) while addr >= domainsize do addr = addr - domainsize end mtable = memory.readbyterange(addr, length, domain) mstart = 0; mend = length - 1; end -- jsd: format output in 2-char hex per byte: local toret = {}; for i=mstart, mend do table.insert(toret, string.format("%02x", mtable[i])) end return toret end function writebyte(addr, value, domain) if is_snes9x then memory.writebyte(addr, value) else -- jsd: wrap around address by domain size: local domainsize = memory.getmemorydomainsize(domain) while addr >= domainsize do addr = addr - domainsize end memory.writebyte(addr, value, domain) end end function get_os() local the_os, ext, arch if package.config:sub(1,1) == "\\" then the_os, ext = "windows", "dll" arch = os.getenv"PROCESSOR_ARCHITECTURE" else -- TODO: macos? the_os, ext = "linux", "so" arch = "x86_64" -- TODO: read ELF header from /proc/$PID/exe to get arch end if arch:find("64") ~= nil then arch = "x64" else arch = "x86" end return the_os, ext, arch end function get_socket_path() local the_os, ext, arch = get_os() -- for some reason ./ isn't working, so use a horrible hack to get the pwd local pwd = (io.popen and io.popen("cd"):read'*l') or "." return pwd .. "/" .. arch .. "/socket-" .. the_os .. "-" .. get_lua_version() .. "." .. ext end local socket_path = get_socket_path() print("loading " .. socket_path) local socket = assert(package.loadlib(socket_path, "luaopen_socket_core"))() local connection local host = os.getenv("SNI_LUABRIDGE_LISTEN_HOST") or '127.0.0.1' local port = os.getenv("SNI_LUABRIDGE_LISTEN_PORT") or 65398 local connected = false local name = "Unnamed" memory.usememorydomain("System Bus") local function onMessage(s) local parts = {} for part in string.gmatch(s, '([^|]+)') do parts[#parts + 1] = part end if parts[1] == "Read" then local adr = tonumber(parts[2]) local length = tonumber(parts[3]) local domain if is_snes9x ~= true then domain = parts[4] end local byteRange = readbyterange(adr, length, domain) connection:send(table.concat(byteRange) .. "\n") elseif parts[1] == "Write" then local adr = tonumber(parts[2]) local domain local offset = 2 if is_snes9x ~= true then domain = parts[3] offset = 3 end for k, v in pairs(parts) do if k > offset then writebyte(adr + k - offset - 1, tonumber(v), domain) end end elseif parts[1] == "SetName" then name = parts[2] print("My name is " .. name .. "!") elseif parts[1] == "Message" then print(parts[2]) elseif parts[1] == "Version" then if is_snes9x then connection:send("Version|SNI Connector|4|Snes9x\n") else connection:send("Version|SNI Connector|4|Bizhawk\n") end elseif is_snes9x ~= true then if parts[1] == "Reset" then print("Rebooting core...") client.reboot_core() elseif parts[1] == "Pause" then print("Pausing...") client.pause() elseif parts[1] == "Unpause" then print("Unpausing...") client.unpause() elseif parts[1] == "PauseToggle" then print("Toggling pause...") client.togglepause() end end end local connectionBackOff = 0 local localIP, localPort, localFam local function doclose() if connection ~= nil then print('Closing connection to SNI from ' .. localIP .. ':' .. localPort .. ' (' .. localFam .. ')') connection:close() print('Connection to SNI is closed from ' .. localIP .. ':' .. localPort .. ' (' .. localFam .. ')') localIP, localPort, localFam = nil, nil, nil connection = nil end connected = false end local function main() if not connected then if connectionBackOff > 0 then connectionBackOff = connectionBackOff - 1 return end connectionBackOff = 60 * 10 local err print('Connecting to SNI at ' .. host .. ':' .. port .. ' ...') connection, err = socket:tcp() if err ~= nil then print(err) print('Waiting 10 seconds...') return end connection:setoption('keepalive', true) connection:setoption('tcp-nodelay', true) connection:settimeout(0.01) local returnCode, err = connection:connect(host, port) if err ~= nil then print("Error while connecting: " .. err) print('Waiting 10 seconds...') connected = false return end connected = true localIP, localPort, localFam = connection:getsockname() if localFam == nil then localFam = 'inet' end print('Connected to SNI from ' .. localIP .. ':' .. localPort .. ' (' .. localFam .. ')') connection:settimeout(0) return end local empty = false local iters = 0 while iters <= 8 do iters = iters + 1 local s, status = connection:receive('*l') if status == 'timeout' then return elseif status == 'closed' then print('SNI closed the connection') doclose() print('Waiting 10 seconds...') return elseif s then onMessage(s) end end end local function onexit() doclose() end if is_snes9x then -- snes9x-rr: emu.registerexit(onexit) emu.registerbefore(main) else -- bizhawk: event.onexit(onexit) while true do main() emu.frameadvance() end end