local _p = print local function print(data) _p(data .. "") end return function(server) local me = { open = false, } local function responseCheckpointInfo(requestId, checkpt, hit) local r = {} r[#r+1] = server.uint32ToLittleEndian(checkpt.num) r[#r+1] = server.boolToLittleEndian(hit) r[#r+1] = server.uint16ToLittleEndian(checkpt.start) r[#r+1] = server.uint16ToLittleEndian(checkpt.finish) r[#r+1] = server.boolToLittleEndian(checkpt.stop) r[#r+1] = server.boolToLittleEndian(checkpt.enabled) r[#r+1] = server.uint8ToLittleEndian(checkpt.op) r[#r+1] = server.boolToLittleEndian(checkpt.temp) r[#r+1] = server.uint32ToLittleEndian(checkpt.hitCount) r[#r+1] = server.uint32ToLittleEndian(checkpt.ignoreCount) r[#r+1] = server.boolToLittleEndian(checkpt.condition ~= nil) r[#r+1] = server.uint32ToLittleEndian(checkpt.memspace) server.response(server.responseType.CHECKPOINT_INFO, server.errorType.OK, requestId, table.concat(r)) end 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 function responseRegisterInfo(requestId) if requestId == nil then error("request id was nil", 2) end local r = {} local regs = emu.getState().cpu local count = 0 for name, meta in pairs(regMeta) do count = count + 1 end r[#r+1] = server.uint16ToLittleEndian(count) local itemSize = 3 for name, meta in pairs(regMeta) do r[#r+1] = server.uint8ToLittleEndian(itemSize) r[#r+1] = server.uint8ToLittleEndian(meta.id) r[#r+1] = server.uint16ToLittleEndian(regs[name]) end server.response(server.responseType.REGISTER_INFO, server.errorType.OK, requestId, table.concat(r)) end local function responseStopped() local pc = emu.getState().cpu.pc server.response(server.responseType.STOPPED, server.errorType.OK, server.EVENT_ID, server.uint16ToLittleEndian(pc)) end local function responseResumed() local pc = emu.getState().cpu.pc server.response(server.responseType.RESUMED, server.errorType.OK, server.EVENT_ID, server.uint16ToLittleEndian(pc)) end function me.monitorOpened() if me.open then return end me.open = true print("Monitor opened") local pc = emu.getState().cpu.pc print(string.format("PC: %04x", pc)) responseRegisterInfo(server.EVENT_ID) responseStopped(server.EVENT_ID) end function me.monitorClosed() if not me.open then return end me.open = false print("Monitor closed") responseResumed(server.EVENT_ID) end local dumps = {} local function processDump(command) if command.length < 3 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local saveRoms = server.readBool(command.body, 1) local saveDisks = server.readBool(command.body, 2) local filenameLength = server.readUint8(command.body, 3) if command.length < 3 + filenameLength then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local filename = command.body:sub(4, 4 + filenameLength - 1) print("Dumping "..filename) local dereg local callback = function() dumps[filename] = emu.saveSavestate() server.response(server.responseType.DUMP, server.errorType.OK, command.requestId, nil) print("Dumped "..filename) emu.removeEventCallback(dereg, emu.eventType.startFrame) end dereg = emu.addEventCallback(callback, emu.eventType.startFrame) end local function processUndump(command) if command.length < 1 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local filenameLength = server.readUint8(command.body, 1) if command.length < 1 + filenameLength then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local filename = command.body:sub(2, 2 + filenameLength - 1) local dump = dumps[filename] if dump == nil then server.errorResponse(server.errorType.CMD_FAILURE, command.requestId) return end local dereg local callback = function() emu.loadSavestate(dump) local pc = emu.getState().cpu.pc server.response(server.responseType.UNDUMP, server.errorType.OK, command.requestId, server.uint16ToLittleEndian(pc)) emu.removeEventCallback(dereg, emu.eventType.startFrame) end dereg = emu.addEventCallback(callback, emu.eventType.startFrame) end local function processAdvanceInstructions(command) if command.length < 3 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local stepOverSubroutines = server.readBool(command.body, 1) local count = server.readUint16(command.body, 2) server.stepping = true if stepOverSubroutines then -- FIXME emu.execute(count, emu.executeCountType.cpuInstructions) else emu.execute(count, emu.executeCountType.cpuInstructions) end server.running = true server.response(server.responseType.ADVANCE_INSTRUCTIONS, server.errorType.OK, command.requestId, nil) me.monitorClosed() end -- FIXME This should take instruction level into account local function processExecuteUntilReturn(command) -- FIXME server.stepping = true emu.execute(1, emu.executeCountType.cpuInstructions) server.running = true server.response(server.responseType.EXECUTE_UNTIL_RETURN, server.errorType.OK, command.requestId, nil) me.monitorClosed() end local function processPing(command) server.response(server.responseType.PING, server.errorType.OK, command.requestId, nil) end local memspaces = { MAIN = 0, INVALID = -1, } local function getRequestedMemspace(requestedMemspace) if requestedMemspace == 0 then return memspaces.MAIN else return memspaces.INVALID end end local banks = { default = 0, cpu = 1, ppu = 2, palette = 3, oam = 4, secondaryOam = 5 } local function validateBanknum(memspace, banknum) return memspace == memspaces.MAIN and banknum >= banks.default and banknum <= banks.secondaryOam end local function ignoreRegister(meta) return false end local function processRegistersAvailable(command) if command.length < 1 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local requestedMemspace = server.readUint8(command.body, 1) local memspace = getRequestedMemspace(requestedMemspace) if memspace == memspaces.INVALID then server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) return end local responseRegCount = 0; for name, meta in pairs(regMeta) do if not ignoreRegister(meta) then responseRegCount = responseRegCount + 1 end end local r = {} r[#r+1] = server.uint16ToLittleEndian(responseRegCount) for name, meta in pairs(regMeta) do if not ignoreRegister(meta) then local itemSize = meta.name:len() + 3 r[#r+1] = server.uint8ToLittleEndian(itemSize) r[#r+1] = server.uint8ToLittleEndian(meta.id) r[#r+1] = server.uint8ToLittleEndian(meta.size * 8) r[#r+1] = server.writeString(meta.name); end end server.response(server.responseType.REGISTERS_AVAILABLE, server.errorType.OK, command.requestId, table.concat(r)); end local function processBanksAvailable(command) local responseBankCount = 0; for name, id in pairs(banks) do responseBankCount = responseBankCount + 1 end local r = {} r[#r+1] = server.uint16ToLittleEndian(responseBankCount) for name, id in pairs(banks) do local itemSize = name:len() + 3 r[#r+1] = server.uint8ToLittleEndian(itemSize) r[#r+1] = server.uint16ToLittleEndian(id) r[#r+1] = server.writeString(name); end server.response(server.responseType.BANKS_AVAILABLE, server.errorType.OK, command.requestId, table.concat(r)); end local function processDisplayGet(command) print("Taking screenshot") if command.apiVersion < 0x02 then print("API Version error") server.errorResponse(server.errorType.INVALID_API_VERSION, command.requestId) return end local infoLength = 13; local shot = emu.takeScreenshot() print("Screenshot taken") local r = {} -- Length of fields before display buffer r[#r+1] = server.uint32ToLittleEndian(infoLength) -- Full width of buffer r[#r+1] = server.uint16ToLittleEndian(256) -- Full height of buffer r[#r+1] = server.uint16ToLittleEndian(240) -- X offset of the inner part of the screen r[#r+1] = server.uint16ToLittleEndian(0) -- Y offset of the inner part of the screen r[#r+1] = server.uint16ToLittleEndian(0) -- Width of the inner part of the screen r[#r+1] = server.uint16ToLittleEndian(256) -- Height of the inner part of the screen r[#r+1] = server.uint16ToLittleEndian(240) -- Bits per pixel of image r[#r+1] = server.uint8ToLittleEndian(24) -- Length of display buffer r[#r+1] = server.uint32ToLittleEndian(shot:len()) -- Buffer Data in requested format r[#r+1] = shot server.response(server.responseType.DISPLAY_GET, server.errorType.OK, command.requestId, table.concat(r)); end local function processExit(command) server.response(server.responseType.EXIT, server.errorType.OK, command.requestId, nil) me.monitorClosed() server.running = true end local function processQuit(command) server.response(server.responseType.QUIT, server.errorType.OK, command.requestId, nil) os.exit(0) end local function processReset(command) server.running = true emu.reset() server.response(server.responseType.RESET, server.errorType.OK, command.requestId, nil) me.monitorClosed() end local function processMemorySet(command) local newSidefx = server.readBool(command.body, 1) local startAddress = server.readUint16(command.body, 2) local endAddress = server.readUint16(command.body, 4) if startAddress > endAddress then server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) return end local length = endAddress - startAddress + 1 if command.length < length + 8 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local requestedMemspace = server.readUint8(command.body, 6) local memspace = getRequestedMemspace(requestedMemspace) if memspace == memspaces.INVALID then server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) return end local requestedBanknum = server.readUint16(command.body, 7) if not validateBanknum(memspace, requestedBanknum) then server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) return end local banknum = requestedBanknum if banknum == banks.default then banknum = banks.cpu end if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then banknum = banknum + 0x100 end banknum = banknum - 1 for i = 0,length - 1,1 do local val = command.body:byte(8 + i + 1) print(val) emu.write(startAddress + i, val, banknum) end server.response(server.responseType.MEM_SET, server.errorType.OK, command.requestId, nil) end local function processExit(command) server.running = true server.response(server.responseType.EXIT, server.errorType.OK, command.requestId, nil) me.monitorClosed() end local function processReset(command) server.running = true emu.reset() server.response(server.responseType.RESET, server.errorType.OK, command.requestId, nil) me.monitorClosed() end local function processMemorySet(command) local newSidefx = server.readBool(command.body, 1) local startAddress = server.readUint16(command.body, 2) local endAddress = server.readUint16(command.body, 4) if startAddress > endAddress then server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) return end local length = endAddress - startAddress + 1 if command.length < length + 8 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local requestedMemspace = server.readUint8(command.body, 6) local memspace = getRequestedMemspace(requestedMemspace) if memspace == memspaces.INVALID then server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) return end local requestedBanknum = server.readUint16(command.body, 7) if not validateBanknum(memspace, requestedBanknum) then server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) return end local banknum = requestedBanknum if banknum == banks.default then banknum = banks.cpu end if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then banknum = banknum + 0x100 end banknum = banknum - 1 for i = 0,length - 1,1 do local val = command.body:byte(8 + i + 1) print(val) emu.write(startAddress + i, val, banknum) end server.response(server.responseType.MEM_SET, server.errorType.OK, command.requestId, nil) end local function processMemoryGet(command) if command.length < 8 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local newSidefx = server.readBool(command.body, 1) local startAddress = server.readUint16(command.body, 2) local endAddress = server.readUint16(command.body, 4) if startAddress > endAddress then server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) return end local requestedMemspace = server.readUint8(command.body, 6) local memspace = getRequestedMemspace(requestedMemspace) if memspace == memspaces.INVALID then server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) return end local length = endAddress - startAddress + 1 local requestedBanknum = server.readUint16(command.body, 7) if not validateBanknum(memspace, requestedBanknum) then server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) return end local banknum = requestedBanknum local r = {} r[#r+1] = server.uint16ToLittleEndian(length) if banknum == banks.default then banknum = banks.cpu end if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then banknum = banknum + 0x100 end banknum = banknum - 1 local remainingByte = length % 2 ~= 0 for addr=startAddress,endAddress-1,2 do r[#r+1] = server.uint16ToLittleEndian(emu.readWord(addr, banknum)) end if remainingByte then r[#r+1] = server.uint8ToLittleEndian(emu.read(addr, banknum)) end server.response(server.responseType.MEM_GET, server.errorType.OK, command.requestId, table.concat(r)) end nextTrap = 1 traps = {} local operation = { READ = 0x01, WRITE = 0x02, EXEC = 0x04, } 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 if trap.op & operation.EXEC ~= 0 then trap.execRegistration = emu.addMemoryCallback(function() if trap.enabled then trapHandle(trap) end end, emu.memCallbackType.cpuExec, trap.start, trap.finish) end if trap.op & operation.READ ~= 0 then trap.readRegistration = emu.addMemoryCallback(function() if trap.enabled then trapHandle(trap) end end, emu.memCallbackType.cpuRead, trap.start, trap.finish) end if trap.op & operation.WRITE ~= 0 then trap.writeRegistration = emu.addMemoryCallback(function() if trap.enabled then trapHandle(trap) end end, emu.memCallbackType.cpuRead, trap.start, trap.finish) end return trap end local function removeCheckpoint(num) local trap for i=#traps,1,-1 do trap = traps[i] if trap.num == num then table.remove(traps, i) break end trap = nil end if trap == nil then return false end if trap.execRegistration ~= nil then emu.removeMemoryCallback(trap.execRegistration, emu.memCallbackType.cpuExec, trap.start, trap.finish) end if trap.readRegistration ~= nil then emu.removeMemoryCallback(trap.readRegistration, emu.memCallbackType.cpuRead, trap.start, trap.finish) end if trap.writeRegistration ~= nil then emu.removeMemoryCallback(trap.writeRegistration, emu.memCallbackType.cpuWrite, trap.start, trap.finish) end return true end local function toggleCheckpoint(enable, num) for i=#traps,1,-1 do trap = traps[i] if trap.num == num then break end trap = nil end if trap == nil then return false end trap.enabled = enable return true end local function processCheckpointGet(command) if command.length < 4 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local num = server.readUint32(command.body, 1) for i=#traps,1,-1 do trap = traps[i] if trap.num == num then break end trap = nil end if not trap then server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) return end responseCheckpointInfo(command.requestId, trap, false) end local function processCheckpointSet(command) if command.length < 8 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end -- Ignore the memspace - byte 9 local checkpt = addCheckpoint( server.readUint16(command.body, 1), server.readUint16(command.body, 3), server.readBool(command.body, 5), server.readBool(command.body, 6), server.readUint8(command.body, 7), server.readBool(command.body, 8) ) responseCheckpointInfo(command.requestId, checkpt, 0) end local function processCheckpointDelete(command) if command.length < 4 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local brkNum = server.readUint32(command.body, 1) local success = removeCheckpoint(brkNum) if not success then server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) return end server.response(server.responseType.CHECKPOINT_DELETE, server.errorType.OK, command.requestId, nil) end local function processCheckpointList(command) for i = 1,#traps,1 do responseCheckpointInfo(command.requestId, traps[i], false) end local r = server.uint32ToLittleEndian(#traps) server.response(server.responseType.CHECKPOINT_LIST, server.errorType.OK, command.requestId, r) end local function processCheckpointToggle(command) if command.length < 5 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local num = server.readUint32(command.body, 1) local enable = server.readBool(command.body, 5) if not toggleCheckpoint(enable, num) then server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) return end server.response(server.responseType.CHECKPOINT_TOGGLE, server.errorType.OK, command.requestId, nil) end local function validateRegister(memspace, regId) return memspace == memspaces.MAIN and regId >= regMeta.a.id and regId <= regMeta.status.id end local function processRegistersGet(command) if command.length < 1 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local requestedMemspace = server.readUint8(command.body, 1) local memspace = getRequestedMemspace(requestedMemspace) if memspace == memspaces.INVALID then server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) return end responseRegisterInfo(command.requestId, memspace) end local function processRegistersSet(command) local headerSize = 3 local count = server.readUint16(command.body, 2) if command.length < headerSize + count * (3 + 1) then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end local requestedMemspace = server.readUint8(command.body, 1) local memspace = getRequestedMemspace(requestedMemspace) if memspace == memspaces.INVALID then server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) return end local bodyCursor = headerSize + 1 local state = emu.getState() for i=1,count do local itemSize = server.readUint8(command.body, bodyCursor + 0) local regId = server.readUint8(command.body, bodyCursor + 1) local regVal = server.readUint16(command.body, bodyCursor + 2) if itemSize < 3 then server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) return end if not validateRegister(memspace, regId) then server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) return end for name, meta in pairs(regMeta) do if meta.id == regId then state.cpu[name] = regVal break end end bodyCursor = bodyCursor + itemSize + 1 end emu.setState(state) responseRegisterInfo(command.requestId, memspace) end function me.processCommand(apiVersion, bodyLength, remainingHeader, body) local command = {} command.apiVersion = apiVersion if command.apiVersion < 0x01 or command.apiVersion > 0x02 then server.errorResponse(server.errorType.INVALID_API_VERSION, command.requestId) end command.length = bodyLength command.requestId = server.readUint32(remainingHeader, 1) command.type = server.readUint8(remainingHeader, 5) command.body = body local prettyType = "" for k, v in pairs(server.commandType) do if v == command.type then prettyType = k break end end print(string.format("Command start: %02x (%s)", command.type, prettyType)) local ct = command.type if ct == server.commandType.MEM_GET then processMemoryGet(command) elseif ct == server.commandType.MEM_SET then processMemorySet(command) elseif ct == server.commandType.CHECKPOINT_GET then processCheckpointGet(command) elseif ct == server.commandType.CHECKPOINT_SET then processCheckpointSet(command) elseif ct == server.commandType.CHECKPOINT_DELETE then processCheckpointDelete(command) elseif ct == server.commandType.CHECKPOINT_LIST then processCheckpointList(command) elseif ct == server.commandType.CHECKPOINT_TOGGLE then processCheckpointToggle(command) elseif ct == server.commandType.REGISTERS_GET then processRegistersGet(command) elseif ct == server.commandType.REGISTERS_SET then processRegistersSet(command) --[[ elseif ct == server.commandType.DUMP then processDump(command) elseif ct == server.commandType.UNDUMP then processUndump(command) ]] elseif ct == server.commandType.ADVANCE_INSTRUCTIONS then processAdvanceInstructions(command) elseif ct == server.commandType.EXECUTE_UNTIL_RETURN then processExecuteUntilReturn(command) elseif ct == server.commandType.PING then processPing(command) elseif ct == server.commandType.REGISTERS_AVAILABLE then processRegistersAvailable(command) elseif ct == server.commandType.BANKS_AVAILABLE then processBanksAvailable(command) elseif ct == server.commandType.DISPLAY_GET then processDisplayGet(command) elseif ct == server.commandType.EXIT then processExit(command) elseif ct == server.commandType.QUIT then processQuit(command) elseif ct == server.commandType.RESET then processReset(command) else server.errorResponse(server.errorType.CMD_INVALID_TYPE, command.requestId) print(string.format("unknown command: %02x, skipping command length %d", command.type, command.length)) end print(string.format("Command finished: %02x", command.type)) end function trapHandle(trap) if server.conn == nil then return end responseCheckpointInfo(server.EVENT_ID, trap, true) if trap.stop then me.monitorOpened() else return end if server.running then server.running = false server.deregisterFrameCallback() print("Break called by trap") emu.breakExecution() end end return me end