660 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			660 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
--[[
 | 
						|
Copyright (c) 2023 Zunawe
 | 
						|
 | 
						|
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
						|
of this software and associated documentation files (the "Software"), to deal
 | 
						|
in the Software without restriction, including without limitation the rights
 | 
						|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
						|
copies of the Software, and to permit persons to whom the Software is
 | 
						|
furnished to do so, subject to the following conditions:
 | 
						|
 | 
						|
The above copyright notice and this permission notice shall be included in all
 | 
						|
copies or substantial portions of the Software.
 | 
						|
 | 
						|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
						|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
						|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
						|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
						|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
						|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
						|
SOFTWARE.
 | 
						|
]]
 | 
						|
 | 
						|
local SCRIPT_VERSION = 1
 | 
						|
 | 
						|
-- Set to log incoming requests
 | 
						|
-- Will cause lag due to large console output
 | 
						|
local DEBUG = false
 | 
						|
 | 
						|
--[[
 | 
						|
This script expects to receive JSON and will send JSON back. A message should
 | 
						|
be a list of 1 or more requests which will be executed in order. Each request
 | 
						|
will have a corresponding response in the same order.
 | 
						|
 | 
						|
Every individual request and response is a JSON object with at minimum one
 | 
						|
field `type`. The value of `type` determines what other fields may exist.
 | 
						|
 | 
						|
To get the script version, instead of JSON, send "VERSION" to get the script
 | 
						|
version directly (e.g. "2").
 | 
						|
 | 
						|
#### Ex. 1
 | 
						|
 | 
						|
Request: `[{"type": "PING"}]`
 | 
						|
 | 
						|
Response: `[{"type": "PONG"}]`
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
#### Ex. 2
 | 
						|
 | 
						|
Request: `[{"type": "LOCK"}, {"type": "HASH"}]`
 | 
						|
 | 
						|
Response: `[{"type": "LOCKED"}, {"type": "HASH_RESPONSE", "value": "F7D18982"}]`
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
#### Ex. 3
 | 
						|
 | 
						|
Request:
 | 
						|
 | 
						|
```json
 | 
						|
[
 | 
						|
    {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
 | 
						|
    {"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
 | 
						|
]
 | 
						|
```
 | 
						|
 | 
						|
Response:
 | 
						|
 | 
						|
```json
 | 
						|
[
 | 
						|
    {"type": "GUARD_RESPONSE", "address": 100, "value": true},
 | 
						|
    {"type": "READ_RESPONSE", "value": "dGVzdA=="}
 | 
						|
]
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
#### Ex. 4
 | 
						|
 | 
						|
Request:
 | 
						|
 | 
						|
```json
 | 
						|
[
 | 
						|
    {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
 | 
						|
    {"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
 | 
						|
]
 | 
						|
```
 | 
						|
 | 
						|
Response:
 | 
						|
 | 
						|
```json
 | 
						|
[
 | 
						|
    {"type": "GUARD_RESPONSE", "address": 100, "value": false},
 | 
						|
    {"type": "GUARD_RESPONSE", "address": 100, "value": false}
 | 
						|
]
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
### Supported Request Types
 | 
						|
 | 
						|
- `PING`  
 | 
						|
    Does nothing; resets timeout.
 | 
						|
 | 
						|
    Expected Response Type: `PONG`
 | 
						|
 | 
						|
- `SYSTEM`  
 | 
						|
    Returns the system of the currently loaded ROM (N64, GBA, etc...).
 | 
						|
 | 
						|
    Expected Response Type: `SYSTEM_RESPONSE`
 | 
						|
 | 
						|
- `PREFERRED_CORES`  
 | 
						|
    Returns the user's default cores for systems with multiple cores. If the
 | 
						|
    current ROM's system has multiple cores, the one that is currently
 | 
						|
    running is very probably the preferred core.
 | 
						|
 | 
						|
    Expected Response Type: `PREFERRED_CORES_RESPONSE`
 | 
						|
 | 
						|
- `HASH`  
 | 
						|
    Returns the hash of the currently loaded ROM calculated by BizHawk.
 | 
						|
 | 
						|
    Expected Response Type: `HASH_RESPONSE`
 | 
						|
 | 
						|
- `GUARD`  
 | 
						|
    Checks a section of memory against `expected_data`. If the bytes starting
 | 
						|
    at `address` do not match `expected_data`, the response will have `value`
 | 
						|
    set to `false`, and all subsequent requests will not be executed and
 | 
						|
    receive the same `GUARD_RESPONSE`.
 | 
						|
 | 
						|
    Expected Response Type: `GUARD_RESPONSE`
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `address` (`int`): The address of the memory to check
 | 
						|
    - `expected_data` (string): A base64 string of contiguous data
 | 
						|
    - `domain` (`string`): The name of the memory domain the address
 | 
						|
    corresponds to
 | 
						|
 | 
						|
- `LOCK`  
 | 
						|
    Halts emulation and blocks on incoming requests until an `UNLOCK` request
 | 
						|
    is received or the client times out. All requests processed while locked
 | 
						|
    will happen on the same frame.
 | 
						|
 | 
						|
    Expected Response Type: `LOCKED`
 | 
						|
 | 
						|
- `UNLOCK`  
 | 
						|
    Resumes emulation after the current list of requests is done being
 | 
						|
    executed.
 | 
						|
 | 
						|
    Expected Response Type: `UNLOCKED`
 | 
						|
 | 
						|
- `READ`  
 | 
						|
    Reads an array of bytes at the provided address.
 | 
						|
 | 
						|
    Expected Response Type: `READ_RESPONSE`
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `address` (`int`): The address of the memory to read
 | 
						|
    - `size` (`int`): The number of bytes to read
 | 
						|
    - `domain` (`string`): The name of the memory domain the address
 | 
						|
    corresponds to
 | 
						|
 | 
						|
- `WRITE`  
 | 
						|
    Writes an array of bytes to the provided address.
 | 
						|
 | 
						|
    Expected Response Type: `WRITE_RESPONSE`
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `address` (`int`): The address of the memory to write to
 | 
						|
    - `value` (`string`): A base64 string representing the data to write
 | 
						|
    - `domain` (`string`): The name of the memory domain the address
 | 
						|
    corresponds to
 | 
						|
 | 
						|
- `DISPLAY_MESSAGE`  
 | 
						|
    Adds a message to the message queue which will be displayed using
 | 
						|
    `gui.addmessage` according to the message interval.
 | 
						|
 | 
						|
    Expected Response Type: `DISPLAY_MESSAGE_RESPONSE`
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `message` (`string`): The string to display
 | 
						|
 | 
						|
- `SET_MESSAGE_INTERVAL`  
 | 
						|
    Sets the minimum amount of time to wait between displaying messages.
 | 
						|
    Potentially useful if you add many messages quickly but want players
 | 
						|
    to be able to read each of them.
 | 
						|
 | 
						|
    Expected Response Type: `SET_MESSAGE_INTERVAL_RESPONSE`
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `value` (`number`): The number of seconds to set the interval to
 | 
						|
 | 
						|
 | 
						|
### Response Types
 | 
						|
 | 
						|
- `PONG`  
 | 
						|
    Acknowledges `PING`.
 | 
						|
 | 
						|
- `SYSTEM_RESPONSE`  
 | 
						|
    Contains the name of the system for currently running ROM.
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `value` (`string`): The returned system name
 | 
						|
 | 
						|
- `PREFERRED_CORES_RESPONSE`  
 | 
						|
    Contains the user's preferred cores for systems with multiple supported
 | 
						|
    cores. Currently includes NES, SNES, GB, GBC, DGB, SGB, PCE, PCECD, and
 | 
						|
    SGX.
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `value` (`{[string]: [string]}`): A dictionary map from system name to
 | 
						|
    core name
 | 
						|
 | 
						|
- `HASH_RESPONSE`  
 | 
						|
    Contains the hash of the currently loaded ROM calculated by BizHawk.
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `value` (`string`): The returned hash
 | 
						|
 | 
						|
- `GUARD_RESPONSE`  
 | 
						|
    The result of an attempted `GUARD` request.
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `value` (`boolean`): true if the memory was validated, false if not
 | 
						|
    - `address` (`int`): The address of the memory that was invalid (the same
 | 
						|
    address provided by the `GUARD`, not the address of the individual invalid
 | 
						|
    byte)
 | 
						|
 | 
						|
- `LOCKED`  
 | 
						|
    Acknowledges `LOCK`.
 | 
						|
 | 
						|
- `UNLOCKED`  
 | 
						|
    Acknowledges `UNLOCK`.
 | 
						|
 | 
						|
- `READ_RESPONSE`  
 | 
						|
    Contains the result of a `READ` request.
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `value` (`string`): A base64 string representing the read data
 | 
						|
 | 
						|
- `WRITE_RESPONSE`  
 | 
						|
    Acknowledges `WRITE`.
 | 
						|
 | 
						|
- `DISPLAY_MESSAGE_RESPONSE`  
 | 
						|
    Acknowledges `DISPLAY_MESSAGE`.
 | 
						|
 | 
						|
- `SET_MESSAGE_INTERVAL_RESPONSE`  
 | 
						|
    Acknowledges `SET_MESSAGE_INTERVAL`.
 | 
						|
 | 
						|
- `ERROR`  
 | 
						|
    Signifies that something has gone wrong while processing a request.
 | 
						|
 | 
						|
    Additional Fields:
 | 
						|
    - `err` (`string`): A description of the problem
 | 
						|
]]
 | 
						|
 | 
						|
local bizhawk_version = client.getversion()
 | 
						|
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
 | 
						|
bizhawk_major = tonumber(bizhawk_major)
 | 
						|
bizhawk_minor = tonumber(bizhawk_minor)
 | 
						|
if bizhawk_patch == "" then
 | 
						|
    bizhawk_patch = 0
 | 
						|
else
 | 
						|
    bizhawk_patch = tonumber(bizhawk_patch)
 | 
						|
end
 | 
						|
 | 
						|
local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
 | 
						|
lua_major = tonumber(lua_major)
 | 
						|
lua_minor = tonumber(lua_minor)
 | 
						|
 | 
						|
if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
 | 
						|
    require("lua_5_3_compat")
 | 
						|
end
 | 
						|
 | 
						|
local base64 = require("base64")
 | 
						|
local socket = require("socket")
 | 
						|
local json = require("json")
 | 
						|
 | 
						|
local SOCKET_PORT_FIRST = 43055
 | 
						|
local SOCKET_PORT_RANGE_SIZE = 5
 | 
						|
local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE
 | 
						|
 | 
						|
local STATE_NOT_CONNECTED = 0
 | 
						|
local STATE_CONNECTED = 1
 | 
						|
 | 
						|
local server = nil
 | 
						|
local client_socket = nil
 | 
						|
 | 
						|
local current_state = STATE_NOT_CONNECTED
 | 
						|
 | 
						|
local timeout_timer = 0
 | 
						|
local message_timer = 0
 | 
						|
local message_interval = 0
 | 
						|
local prev_time = 0
 | 
						|
local current_time = 0
 | 
						|
 | 
						|
local locked = false
 | 
						|
 | 
						|
local rom_hash = nil
 | 
						|
 | 
						|
function queue_push (self, value)
 | 
						|
    self[self.right] = value
 | 
						|
    self.right = self.right + 1
 | 
						|
end
 | 
						|
 | 
						|
function queue_is_empty (self)
 | 
						|
    return self.right == self.left
 | 
						|
end
 | 
						|
 | 
						|
function queue_shift (self)
 | 
						|
    value = self[self.left]
 | 
						|
    self[self.left] = nil
 | 
						|
    self.left = self.left + 1
 | 
						|
    return value
 | 
						|
end
 | 
						|
 | 
						|
function new_queue ()
 | 
						|
    local queue = {left = 1, right = 1}
 | 
						|
    return setmetatable(queue, {__index = {is_empty = queue_is_empty, push = queue_push, shift = queue_shift}})
 | 
						|
end
 | 
						|
 | 
						|
local message_queue = new_queue()
 | 
						|
 | 
						|
function lock ()
 | 
						|
    locked = true
 | 
						|
    client_socket:settimeout(2)
 | 
						|
end
 | 
						|
 | 
						|
function unlock ()
 | 
						|
    locked = false
 | 
						|
    client_socket:settimeout(0)
 | 
						|
end
 | 
						|
 | 
						|
request_handlers = {
 | 
						|
    ["PING"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "PONG"
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["SYSTEM"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "SYSTEM_RESPONSE"
 | 
						|
        res["value"] = emu.getsystemid()
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["PREFERRED_CORES"] = function (req)
 | 
						|
        local res = {}
 | 
						|
        local preferred_cores = client.getconfig().PreferredCores
 | 
						|
 | 
						|
        res["type"] = "PREFERRED_CORES_RESPONSE"
 | 
						|
        res["value"] = {}
 | 
						|
        res["value"]["NES"] = preferred_cores.NES
 | 
						|
        res["value"]["SNES"] = preferred_cores.SNES
 | 
						|
        res["value"]["GB"] = preferred_cores.GB
 | 
						|
        res["value"]["GBC"] = preferred_cores.GBC
 | 
						|
        res["value"]["DGB"] = preferred_cores.DGB
 | 
						|
        res["value"]["SGB"] = preferred_cores.SGB
 | 
						|
        res["value"]["PCE"] = preferred_cores.PCE
 | 
						|
        res["value"]["PCECD"] = preferred_cores.PCECD
 | 
						|
        res["value"]["SGX"] = preferred_cores.SGX
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["HASH"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "HASH_RESPONSE"
 | 
						|
        res["value"] = rom_hash
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["GUARD"] = function (req)
 | 
						|
        local res = {}
 | 
						|
        local expected_data = base64.decode(req["expected_data"])
 | 
						|
        local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"])
 | 
						|
 | 
						|
        local data_is_validated = true
 | 
						|
        for i, byte in ipairs(actual_data) do
 | 
						|
            if byte ~= expected_data[i] then
 | 
						|
                data_is_validated = false
 | 
						|
                break
 | 
						|
            end
 | 
						|
        end
 | 
						|
 | 
						|
        res["type"] = "GUARD_RESPONSE"
 | 
						|
        res["value"] = data_is_validated
 | 
						|
        res["address"] = req["address"]
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["LOCK"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "LOCKED"
 | 
						|
        lock()
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["UNLOCK"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "UNLOCKED"
 | 
						|
        unlock()
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["READ"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "READ_RESPONSE"
 | 
						|
        res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"]))
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["WRITE"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "WRITE_RESPONSE"
 | 
						|
        memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"])
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["DISPLAY_MESSAGE"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "DISPLAY_MESSAGE_RESPONSE"
 | 
						|
        message_queue:push(req["message"])
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["SET_MESSAGE_INTERVAL"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE"
 | 
						|
        message_interval = req["value"]
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
 | 
						|
    ["default"] = function (req)
 | 
						|
        local res = {}
 | 
						|
 | 
						|
        res["type"] = "ERROR"
 | 
						|
        res["err"] = "Unknown command: "..req["type"]
 | 
						|
 | 
						|
        return res
 | 
						|
    end,
 | 
						|
}
 | 
						|
 | 
						|
function process_request (req)
 | 
						|
    if request_handlers[req["type"]] then
 | 
						|
        return request_handlers[req["type"]](req)
 | 
						|
    else
 | 
						|
        return request_handlers["default"](req)
 | 
						|
    end
 | 
						|
end
 | 
						|
 | 
						|
-- Receive data from AP client and send message back
 | 
						|
function send_receive ()
 | 
						|
    local message, err = client_socket:receive()
 | 
						|
 | 
						|
    -- Handle errors
 | 
						|
    if err == "closed" then
 | 
						|
        if current_state == STATE_CONNECTED then
 | 
						|
            print("Connection to client closed")
 | 
						|
        end
 | 
						|
        current_state = STATE_NOT_CONNECTED
 | 
						|
        return
 | 
						|
    elseif err == "timeout" then
 | 
						|
        unlock()
 | 
						|
        return
 | 
						|
    elseif err ~= nil then
 | 
						|
        print(err)
 | 
						|
        current_state = STATE_NOT_CONNECTED
 | 
						|
        unlock()
 | 
						|
        return
 | 
						|
    end
 | 
						|
 | 
						|
    -- Reset timeout timer
 | 
						|
    timeout_timer = 5
 | 
						|
 | 
						|
    -- Process received data
 | 
						|
    if DEBUG then
 | 
						|
        print("Received Message ["..emu.framecount().."]: "..'"'..message..'"')
 | 
						|
    end
 | 
						|
 | 
						|
    if message == "VERSION" then
 | 
						|
        client_socket:send(tostring(SCRIPT_VERSION).."\n")
 | 
						|
    else
 | 
						|
        local res = {}
 | 
						|
        local data = json.decode(message)
 | 
						|
        local failed_guard_response = nil
 | 
						|
        for i, req in ipairs(data) do
 | 
						|
            if failed_guard_response ~= nil then
 | 
						|
                res[i] = failed_guard_response
 | 
						|
            else
 | 
						|
                -- An error is more likely to cause an NLua exception than to return an error here
 | 
						|
                local status, response = pcall(process_request, req)
 | 
						|
                if status then
 | 
						|
                    res[i] = response
 | 
						|
 | 
						|
                    -- If the GUARD validation failed, skip the remaining commands
 | 
						|
                    if response["type"] == "GUARD_RESPONSE" and not response["value"] then
 | 
						|
                        failed_guard_response = response
 | 
						|
                    end
 | 
						|
                else
 | 
						|
                    if type(response) ~= "string" then response = "Unknown error" end
 | 
						|
                    res[i] = {type = "ERROR", err = response}
 | 
						|
                end
 | 
						|
            end
 | 
						|
        end
 | 
						|
 | 
						|
        client_socket:send(json.encode(res).."\n")
 | 
						|
    end
 | 
						|
end
 | 
						|
 | 
						|
function initialize_server ()
 | 
						|
    local err
 | 
						|
    local port = SOCKET_PORT_FIRST
 | 
						|
    local res = nil
 | 
						|
 | 
						|
    server, err = socket.socket.tcp4()
 | 
						|
    while res == nil and port <= SOCKET_PORT_LAST do
 | 
						|
        res, err = server:bind("localhost", port)
 | 
						|
        if res == nil and err ~= "address already in use" then
 | 
						|
            print(err)
 | 
						|
            return
 | 
						|
        end
 | 
						|
 | 
						|
        if res == nil then
 | 
						|
            port = port + 1
 | 
						|
        end
 | 
						|
    end
 | 
						|
 | 
						|
    if port > SOCKET_PORT_LAST then
 | 
						|
        print("Too many instances of connector script already running. Exiting.")
 | 
						|
        return
 | 
						|
    end
 | 
						|
 | 
						|
    res, err = server:listen(0)
 | 
						|
 | 
						|
    if err ~= nil then
 | 
						|
        print(err)
 | 
						|
        return
 | 
						|
    end
 | 
						|
 | 
						|
    server:settimeout(0)
 | 
						|
end
 | 
						|
 | 
						|
function main ()
 | 
						|
    while true do
 | 
						|
        if server == nil then
 | 
						|
            initialize_server()
 | 
						|
        end
 | 
						|
 | 
						|
        current_time = socket.socket.gettime()
 | 
						|
        timeout_timer = timeout_timer - (current_time - prev_time)
 | 
						|
        message_timer = message_timer - (current_time - prev_time)
 | 
						|
        prev_time = current_time
 | 
						|
 | 
						|
        if message_timer <= 0 and not message_queue:is_empty() then
 | 
						|
            gui.addmessage(message_queue:shift())
 | 
						|
            message_timer = message_interval
 | 
						|
        end
 | 
						|
 | 
						|
        if current_state == STATE_NOT_CONNECTED then
 | 
						|
            if emu.framecount() % 30 == 0 then
 | 
						|
                print("Looking for client...")
 | 
						|
                local client, timeout = server:accept()
 | 
						|
                if timeout == nil then
 | 
						|
                    print("Client connected")
 | 
						|
                    current_state = STATE_CONNECTED
 | 
						|
                    client_socket = client
 | 
						|
                    server:close()
 | 
						|
                    server = nil
 | 
						|
                    client_socket:settimeout(0)
 | 
						|
                end
 | 
						|
            end
 | 
						|
        else
 | 
						|
            repeat
 | 
						|
                send_receive()
 | 
						|
            until not locked
 | 
						|
 | 
						|
            if timeout_timer <= 0 then
 | 
						|
                print("Client timed out")
 | 
						|
                current_state = STATE_NOT_CONNECTED
 | 
						|
            end
 | 
						|
        end
 | 
						|
 | 
						|
        coroutine.yield()
 | 
						|
    end
 | 
						|
end
 | 
						|
 | 
						|
event.onexit(function ()
 | 
						|
    print("\n-- Restarting Script --\n")
 | 
						|
    if server ~= nil then
 | 
						|
        server:close()
 | 
						|
    end
 | 
						|
end)
 | 
						|
 | 
						|
if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
 | 
						|
    print("Must use BizHawk 2.7.0 or newer")
 | 
						|
elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
 | 
						|
    print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
 | 
						|
else
 | 
						|
    if emu.getsystemid() == "NULL" then
 | 
						|
        print("No ROM is loaded. Please load a ROM.")
 | 
						|
        while emu.getsystemid() == "NULL" do
 | 
						|
            emu.frameadvance()
 | 
						|
        end
 | 
						|
    end
 | 
						|
 | 
						|
    rom_hash = gameinfo.getromhash()
 | 
						|
 | 
						|
    print("Waiting for client to connect. This may take longer the more instances of this script you have open at once.\n")
 | 
						|
 | 
						|
    local co = coroutine.create(main)
 | 
						|
    function tick ()
 | 
						|
        local status, err = coroutine.resume(co)
 | 
						|
 | 
						|
        if not status and err ~= "cannot resume dead coroutine" then
 | 
						|
            print("\nERROR: "..err)
 | 
						|
            print("Consider reporting this crash.\n")
 | 
						|
    
 | 
						|
            if server ~= nil then
 | 
						|
                server:close()
 | 
						|
            end
 | 
						|
 | 
						|
            co = coroutine.create(main)
 | 
						|
        end
 | 
						|
    end
 | 
						|
 | 
						|
    -- Gambatte has a setting which can cause script execution to become
 | 
						|
    -- misaligned, so for GB and GBC we explicitly set the callback on
 | 
						|
    -- vblank instead.
 | 
						|
    -- https://github.com/TASEmulators/BizHawk/issues/3711
 | 
						|
    if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then
 | 
						|
        event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
 | 
						|
    else
 | 
						|
        event.onframeend(tick)
 | 
						|
    end
 | 
						|
 | 
						|
    while true do
 | 
						|
        emu.frameadvance()
 | 
						|
    end
 | 
						|
end
 |