commit 9599d75b1437650998ee4db010c546670c9e1d3b Author: empathicqubit Date: Sat Oct 30 05:34:47 2021 +0200 Initial commit diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..6fad8ed --- /dev/null +++ b/main.lua @@ -0,0 +1,429 @@ +local _p = emu.log +local function print(data) + _p(data .. "") +end + +local socket = require("socket.core") + +local running = true + +local regMeta = { + a = { name = "A", id = 0, size = 1 }, + x = { name = "X", id = 1, size = 1 }, + y = { name = "Y", id = 2, size = 1 }, + pc = { name = "PC", id = 3, size = 2 }, + sp = { name = "SP", id = 4, size = 1 }, + status = { name = "FL", id = 5, size = 1 }, +} + +local host = os.getenv("FCEUX_REMOTE_HOST") or "localhost" +local port = os.getenv("FCEUX_REMOTE_PORT") or 9355 + +print("Binding to host '" ..host.. "' and port " ..port.. "...") + +local server = assert(socket.tcp()) +assert(server:bind(host, port)) +assert(server:listen(32)) +server:settimeout(0) + +local i, p = server:getsockname() +assert(i, p) + +print("Waiting for connection on " .. i .. ":" .. p .. "...") +local conn = nil + +local function boolToLittleEndian(b) + local value = 0 + if b then + value = 1 + end + return string.char(value & 0xff) +end + +local function uint8ToLittleEndian(value) + return string.char(value & 0xff) +end + +local function uint16ToLittleEndian(value) + return string.char(value & 0xff, (value >> 8) & 0xff) +end + +local function uint32ToLittleEndian(value) + return string.char(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff) +end + +local function readUint32(data, index) + return data:byte(index) + (data:byte(index + 1) << 8) + (data:byte(index + 2) << 16) + (data:byte(index + 3) << 24) +end + +local function readUint16(data, index) + return data:byte(index) + (data:byte(index + 1) << 8) +end + +local function readUint8(data, index) + return data:byte(index) +end + +local function readBool(data, index) + return data:byte(index) > 0 +end + +local errorType = { + OK = 0x00, + OBJECT_MISSING = 0x01, + INVALID_MEMSPACE = 0x02, + CMD_INVALID_LENGTH = 0x80, + INVALID_PARAMETER = 0x81, + CMD_INVALID_API_VERSION = 0x82, + CMD_INVALID_TYPE = 0x83, + CMD_FAILURE = 0x8f, +}; + +local commandType = { + CHECKPOINT_SET = 0x12, + + PING = 0x81, + + EXIT = 0xaa, +} + +local responseType = { + MEM_GET = 0x01, + MEM_SET = 0x02, + + CHECKPOINT_INFO = 0x11, + + CHECKPOINT_DELETE = 0x13, + CHECKPOINT_LIST = 0x14, + CHECKPOINT_TOGGLE = 0x15, + + CONDITION_SET = 0x22, + + REGISTER_INFO = 0x31, + + DUMP = 0x41, + UNDUMP = 0x42, + + RESOURCE_GET = 0x51, + RESOURCE_SET = 0x52, + + JAM = 0x61, + STOPPED = 0x62, + RESUMED = 0x63, + + ADVANCE_INSTRUCTIONS = 0x71, + KEYBOARD_FEED = 0x72, + EXECUTE_UNTIL_RETURN = 0x73, + + PING = 0x81, + BANKS_AVAILABLE = 0x82, + REGISTERS_AVAILABLE = 0x83, + DISPLAY_GET = 0x84, + VICE_INFO = 0x85, + + PALETTE_GET = 0x91, + + EXIT = 0xaa, + QUIT = 0xbb, + RESET = 0xcc, + AUTOSTART = 0xdd, +} + +local API_VERSION = 0x02 + +local EVENT_ID = 0xffffffff + +local function response(responseType, errorCode, requestId, body) + if conn == nil then + return + end + + local r = {} + r[#r+1] = string.char(0x02) + r[#r+1] = uint8ToLittleEndian(API_VERSION) + if body ~= nil then + r[#r+1] = uint32ToLittleEndian(body:len()) + else + r[#r+1] = uint32ToLittleEndian(0) + end + r[#r+1] = uint8ToLittleEndian(responseType) + r[#r+1] = uint8ToLittleEndian(errorCode) + r[#r+1] = uint32ToLittleEndian(requestId) + conn:send(table.concat(r)) + + if body ~= nil then + conn:send(body) + end +end + +local function responseCheckpointInfo(requestId, checkpt, hit) + local r = {} + -- FIXME exec load store + + r[#r+1] = uint32ToLittleEndian(checkpt.num) + r[#r+1] = boolToLittleEndian(hit) + + r[#r+1] = uint16ToLittleEndian(checkpt.start) + r[#r+1] = uint16ToLittleEndian(checkpt.finish) + r[#r+1] = boolToLittleEndian(checkpt.stop) + r[#r+1] = boolToLittleEndian(checkpt.enabled) + r[#r+1] = uint8ToLittleEndian(checkpt.op) + r[#r+1] = boolToLittleEndian(checkpt.temp) + + r[#r+1] = uint32ToLittleEndian(checkpt.hitCount) + r[#r+1] = uint32ToLittleEndian(checkpt.ignoreCount) + r[#r+1] = boolToLittleEndian(checkpt.condition ~= nil) + r[#r+1] = uint32ToLittleEndian(checkpt.memspace) + + response(responseType.CHECKPOINT_INFO, errorType.OK, requestId, table.concat(r)) +end + +local function responseRegisterInfo(requestId) + local r = {} + local regs = emu.getState().cpu + + local count = 0 + for name, meta in pairs(regMeta) do + count = count + 1 + end + + r[#r+1] = uint16ToLittleEndian(count) + + local itemSize = 3 + for name, meta in pairs(regMeta) do + r[#r+1] = uint8ToLittleEndian(itemSize) + r[#r+1] = uint8ToLittleEndian(meta.id) + r[#r+1] = uint16ToLittleEndian(regs[name]) + end + + response(responseType.REGISTER_INFO, errorType.OK, requestId, table.concat(r)) +end + +local function responseStopped() + local pc = emu.getState().cpu.pc + + response(responseType.STOPPED, errorType.OK, EVENT_ID, uint16ToLittleEndian(pc)) +end + +local function responseResumed() + local pc = emu.getState().cpu.pc + + response(responseType.RESUMED, errorType.OK, EVENT_ID, uint16ToLittleEndian(pc)) +end + +local function monitorOpened() + responseRegisterInfo(EVENT_ID) + responseStopped(EVENT_ID) +end + +local function monitorClosed() + responseRegisterInfo(EVENT_ID) + responseResumed(EVENT_ID) +end + +local function errorResponse(errorCode, requestId) + response(0, errorCode, requestId, nil) +end + +local function processPing(command) + response(responseType.PING, errorType.OK, command.requestId, nil) +end + +local function processExit(command) + running = true + + response(responseType.EXIT, errorType.OK, command.requestId, nil) + + monitorClosed() +end + +local nextTrap = 1 +local traps = {} +local op = { + EXEC = 4, + LOAD = 2, + STORE = 1, +} + +local trapHandle + +local function addCheckpoint(start, finish, stop, enabled, op, temp) + local num = nextTrap + nextTrap = nextTrap+1 + local trap = { + start = start, + finish = finish, + stop = stop, + enabled = enabled, + op = op, + temp = temp, + hitCount = 0, + ignoreCount = 0, + condition = nil, + memspace = 0, + num = num, + } + traps[#traps+1] = trap + -- FIXME op + trap.registration = emu.addMemoryCallback(function() + trapHandle(trap) + end, emu.memCallbackType.cpuExec, trap.start, trap.finish) + return trap +end + +local function processCheckpointSet(command) + if command.length < 8 then + errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + -- Ignore the memspace - byte 9 + local checkpt = addCheckpoint( + readUint16(command.body, 1), + readUint16(command.body, 3), + readBool(command.body, 5), + readBool(command.body, 6), + readUint8(command.body, 7), + readBool(command.body, 8) + ) + + responseCheckpointInfo(command.requestId, checkpt, 0) +end + +local function processCommand(apiVersion, bodyLength, remainingHeader, body) + local command = {} + command.apiVersion = apiVersion + if command.apiVersion < 0x01 or command.apiVersion > 0x02 then + errorResponse(errorType.INVALID_API_VERSION, command.requestId) + end + + command.length = bodyLength + + command.requestId = readUint32(remainingHeader, 1) + command.type = readUint8(remainingHeader, 5) + command.body = body + + print(string.format("Command type: %02x", command.type)) + + local command_type = command.type + + if command_type == commandType.CHECKPOINT_SET then + processCheckpointSet(command) + elseif command_type == commandType.PING then + processPing(command) + elseif command_type == commandType.EXIT then + processExit(command) + else + errorResponse(errorType.CMD_INVALID_TYPE, command.requestId) + print(string.format("unknown command: %d, skipping command length %d", command.type, command.length)) + end +end + +local function prepareCommand() + if running then + conn:settimeout(0) + else + conn:settimeout(-1) + end + + local data, status, partial = conn:receive(1) + if status == "timeout" then + return + elseif status == "closed" then + conn = nil + running = true + return + end + + if data:byte(1) ~= 0x02 then + return + end + + if running then + running = false + monitorOpened() + end + conn:settimeout(-1) + + data, status = conn:receive(1 + 4) + + local apiVersion = readUint8(data, 1) + + print(string.format("API Version: %02x", apiVersion)) + + local bodyLength = readUint32(data, 2) + + print(string.format("Body Size: %x", bodyLength)) + + local remainingHeaderSize = 5 + + local remainingHeader, status = conn:receive(remainingHeaderSize) + + local body + if bodyLength > 0 then + body, status = conn:receive(bodyLength) + end + + processCommand(apiVersion, bodyLength, remainingHeader, body) +end + +local function initConnection() + if conn == nil then + conn, err = server:accept() + if conn == nil or err ~= nil then + return + end + end +end + +local deregisterFrameCallback +local registerFrameCallback +local function frameHandle() + initConnection() + + if conn == nil then + return + end + + prepareCommand() + if not running then + deregisterFrameCallback() + emu.breakExecution() + registerFrameCallback() + end +end + +local frameCallback = nil +function registerFrameCallback() + frameCallback = emu.addEventCallback(frameHandle, emu.eventType.inputPolled) +end + +function deregisterFrameCallback() + emu.removeEventCallback(frameCallback, emu.eventType.inputPolled) +end + +function trapHandle(trap) + if conn == nil then + return + end + + running = false + + monitorOpened() + + deregisterFrameCallback() + emu.breakExecution() + registerFrameCallback() +end + +local function breakHandle() + repeat + prepareCommand() + until running + + emu.resume() +end + +registerFrameCallback() + +emu.addEventCallback(breakHandle, emu.eventType.codeBreak) \ No newline at end of file