mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 12:40:21 +02:00
feat(debug): add opt-in Switch input diagnostics
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
||||||
|
|
||||||
|
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||||
|
|
||||||
local Game, EditorApp, Importer, TouchEditor
|
local Game, EditorApp, Importer, TouchEditor
|
||||||
|
|
||||||
local autopilot -- optional scripted-input dev tool (tests/autopilot.lua)
|
local autopilot -- optional scripted-input dev tool (tests/autopilot.lua)
|
||||||
@@ -268,6 +270,7 @@ function love.load(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
|
SwitchDiagnostics.maybeFlush(false)
|
||||||
if editorMode then return EditorApp.update(dt) end
|
if editorMode then return EditorApp.update(dt) end
|
||||||
if TouchEditor then return TouchEditor.update(dt) end
|
if TouchEditor then return TouchEditor.update(dt) end
|
||||||
if Importer then return Importer:update(dt) end
|
if Importer then return Importer:update(dt) end
|
||||||
@@ -342,48 +345,56 @@ function love.keyreleased(key)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function love.gamepadpressed(joystick, button)
|
function love.gamepadpressed(joystick, button)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("gamepadpressed", joystick, button)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:gamepadpressed(joystick, button) end
|
if Importer then return Importer:gamepadpressed(joystick, button) end
|
||||||
Game:gamepadpressed(joystick, button)
|
Game:gamepadpressed(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.gamepadreleased(joystick, button)
|
function love.gamepadreleased(joystick, button)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("gamepadreleased", joystick, button)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:gamepadreleased(joystick, button) end
|
if Importer then return Importer:gamepadreleased(joystick, button) end
|
||||||
Game:gamepadreleased(joystick, button)
|
Game:gamepadreleased(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.gamepadaxis(joystick, axis, value)
|
function love.gamepadaxis(joystick, axis, value)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("gamepadaxis", joystick, axis, { value = value })
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
||||||
Game:gamepadaxis(joystick, axis, value)
|
Game:gamepadaxis(joystick, axis, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickpressed(joystick, button)
|
function love.joystickpressed(joystick, button)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("joystickpressed", joystick, button)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:joystickpressed(joystick, button) end
|
if Importer then return Importer:joystickpressed(joystick, button) end
|
||||||
Game:joystickpressed(joystick, button)
|
Game:joystickpressed(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickreleased(joystick, button)
|
function love.joystickreleased(joystick, button)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("joystickreleased", joystick, button)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:joystickreleased(joystick, button) end
|
if Importer then return Importer:joystickreleased(joystick, button) end
|
||||||
Game:joystickreleased(joystick, button)
|
Game:joystickreleased(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickaxis(joystick, axis, value)
|
function love.joystickaxis(joystick, axis, value)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("joystickaxis", joystick, axis, { value = value })
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
||||||
Game:joystickaxis(joystick, axis, value)
|
Game:joystickaxis(joystick, axis, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickhat(joystick, hat, direction)
|
function love.joystickhat(joystick, hat, direction)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("joystickhat", joystick, hat, { direction = direction })
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
||||||
Game:joystickhat(joystick, hat, direction)
|
Game:joystickhat(joystick, hat, direction)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickremoved(joystick)
|
function love.joystickremoved(joystick)
|
||||||
|
SwitchDiagnostics.onJoystickEvent("joystickremoved", joystick)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
Game:joystickremoved(joystick)
|
Game:joystickremoved(joystick)
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
-- Opt-in Switch diagnostics: ring buffer + ≤1 Hz flush when switch-debug.txt exists.
|
||||||
|
-- Never logs ROM/save bytes — see spec SWNX-13/28.
|
||||||
|
|
||||||
|
local SwitchDiagnostics = {}
|
||||||
|
|
||||||
|
local MARKER = "switch-debug.txt"
|
||||||
|
local LOG_FILE = "switch.log"
|
||||||
|
local FLUSH_INTERVAL = 1.0
|
||||||
|
local RING_SIZE = 64
|
||||||
|
|
||||||
|
local enabled = nil
|
||||||
|
local buffer = {}
|
||||||
|
local bufCount = 0
|
||||||
|
local lastFlushAt = -math.huge
|
||||||
|
local identityLine = nil
|
||||||
|
|
||||||
|
local function fs()
|
||||||
|
return love and love.filesystem
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redactString(s)
|
||||||
|
if type(s) ~= "string" then return s end
|
||||||
|
for i = 1, #s do
|
||||||
|
local b = s:byte(i)
|
||||||
|
if b < 32 or b > 126 then return "<redacted>" end
|
||||||
|
end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sanitize(value, depth)
|
||||||
|
depth = depth or 0
|
||||||
|
if depth > 4 then return "<deep>" end
|
||||||
|
local t = type(value)
|
||||||
|
if t == "string" then return redactString(value) end
|
||||||
|
if t == "number" or t == "boolean" or value == nil then return value end
|
||||||
|
if t == "table" then
|
||||||
|
local out = {}
|
||||||
|
for k, v in pairs(value) do
|
||||||
|
local key = type(k) == "string" and k or tostring(k)
|
||||||
|
if key:lower():find("rom") or key:lower():find("save") then
|
||||||
|
out[key] = "<redacted>"
|
||||||
|
else
|
||||||
|
out[key] = sanitize(v, depth + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
return tostring(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function encodePayload(payload)
|
||||||
|
if payload == nil then return "" end
|
||||||
|
if type(payload) == "string" then return redactString(payload) end
|
||||||
|
local parts = {}
|
||||||
|
for k, v in pairs(sanitize(payload)) do
|
||||||
|
parts[#parts + 1] = tostring(k) .. "=" .. tostring(v)
|
||||||
|
end
|
||||||
|
table.sort(parts)
|
||||||
|
return table.concat(parts, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
function SwitchDiagnostics._resetForTests()
|
||||||
|
enabled = nil
|
||||||
|
buffer = {}
|
||||||
|
bufCount = 0
|
||||||
|
lastFlushAt = -math.huge
|
||||||
|
identityLine = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function SwitchDiagnostics.isEnabled()
|
||||||
|
if enabled ~= nil then return enabled end
|
||||||
|
local filesystem = fs()
|
||||||
|
if not filesystem then
|
||||||
|
enabled = false
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
enabled = filesystem.getInfo(MARKER) ~= nil
|
||||||
|
return enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
function SwitchDiagnostics.identityOverlay()
|
||||||
|
if identityLine then return identityLine end
|
||||||
|
local gitCommit = os.getenv("POKEPORT_GIT_COMMIT") or "unknown"
|
||||||
|
local loveNxTag = "11.5-nx1"
|
||||||
|
local buildVersion = "dev"
|
||||||
|
local filesystem = fs()
|
||||||
|
if filesystem then
|
||||||
|
local raw = filesystem.read("build-info.json")
|
||||||
|
if raw and raw ~= "" then
|
||||||
|
local ver = raw:match('"version"%s*:%s*"([^"]+)"')
|
||||||
|
if ver then buildVersion = ver end
|
||||||
|
local tag = raw:match('"loveNxTag"%s*:%s*"([^"]+)"')
|
||||||
|
if tag then loveNxTag = tag end
|
||||||
|
local commit = raw:match('"gitCommit"%s*:%s*"([^"]+)"')
|
||||||
|
if commit then gitCommit = commit end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
identityLine = ("gitCommit=%s loveNxTag=%s buildVersion=%s os=%s"):format(
|
||||||
|
gitCommit, loveNxTag, buildVersion,
|
||||||
|
love and love.system and love.system.getOS() or "unknown")
|
||||||
|
return identityLine
|
||||||
|
end
|
||||||
|
|
||||||
|
function SwitchDiagnostics.onEvent(kind, payload)
|
||||||
|
if not SwitchDiagnostics.isEnabled() then return end
|
||||||
|
bufCount = bufCount + 1
|
||||||
|
local slot = ((bufCount - 1) % RING_SIZE) + 1
|
||||||
|
buffer[slot] = ("%s %s"):format(tostring(kind), encodePayload(payload))
|
||||||
|
end
|
||||||
|
|
||||||
|
function SwitchDiagnostics.onJoystickEvent(kind, joystick, button, extra)
|
||||||
|
if not SwitchDiagnostics.isEnabled() then return end
|
||||||
|
local payload = { button = button }
|
||||||
|
if joystick then
|
||||||
|
if joystick.getGUID then payload.guid = joystick:getGUID() end
|
||||||
|
if joystick.isGamepad then payload.isGamepad = joystick:isGamepad() end
|
||||||
|
if joystick.getName then payload.name = joystick:getName() end
|
||||||
|
end
|
||||||
|
if extra then
|
||||||
|
for k, v in pairs(extra) do payload[k] = v end
|
||||||
|
end
|
||||||
|
SwitchDiagnostics.onEvent(kind, payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SwitchDiagnostics.maybeFlush(force, now)
|
||||||
|
if not SwitchDiagnostics.isEnabled() then return end
|
||||||
|
now = now or (love and love.timer and love.timer.getTime() or 0)
|
||||||
|
if not force and (now - lastFlushAt) < FLUSH_INTERVAL then return end
|
||||||
|
lastFlushAt = now
|
||||||
|
|
||||||
|
local filesystem = fs()
|
||||||
|
if not filesystem then return end
|
||||||
|
|
||||||
|
local lines = { SwitchDiagnostics.identityOverlay(), "---" }
|
||||||
|
local start = math.max(1, bufCount - RING_SIZE + 1)
|
||||||
|
for i = start, bufCount do
|
||||||
|
local slot = ((i - 1) % RING_SIZE) + 1
|
||||||
|
if buffer[slot] then lines[#lines + 1] = buffer[slot] end
|
||||||
|
end
|
||||||
|
filesystem.write(LOG_FILE, table.concat(lines, "\n") .. "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
return SwitchDiagnostics
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
-- Opt-in Switch input diagnostics (SWNX-13/28): marker file, ring buffer, flush cap.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check = T.check
|
||||||
|
local eq = T.eq
|
||||||
|
|
||||||
|
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||||
|
|
||||||
|
local function reset()
|
||||||
|
SwitchDiagnostics._resetForTests()
|
||||||
|
love.filesystem.remove("switch-debug.txt")
|
||||||
|
love.filesystem.remove("switch.log")
|
||||||
|
end
|
||||||
|
|
||||||
|
reset()
|
||||||
|
check(not SwitchDiagnostics.isEnabled(), "disabled without marker file")
|
||||||
|
|
||||||
|
love.filesystem.write("switch-debug.txt", "")
|
||||||
|
reset()
|
||||||
|
love.filesystem.write("switch-debug.txt", "")
|
||||||
|
check(SwitchDiagnostics.isEnabled(), "enabled when marker exists")
|
||||||
|
|
||||||
|
SwitchDiagnostics.onEvent("probe", { kind = "gamepadpressed", button = "a" })
|
||||||
|
SwitchDiagnostics.maybeFlush(true, 0)
|
||||||
|
local log = love.filesystem.read("switch.log") or ""
|
||||||
|
check(log:find("gamepadpressed", 1, true) ~= nil, "flush writes buffered events")
|
||||||
|
check(log:find("gitCommit=", 1, true) ~= nil, "identity includes gitCommit field")
|
||||||
|
check(log:find("loveNxTag=", 1, true) ~= nil, "identity includes loveNxTag field")
|
||||||
|
|
||||||
|
-- ROM-like byte sequences must never appear in diagnostics output.
|
||||||
|
local romSnippet = string.char(0xEA, 0x9B, 0xCA, 0xE6)
|
||||||
|
SwitchDiagnostics.onEvent("probe", { sample = romSnippet, note = "redacted" })
|
||||||
|
SwitchDiagnostics.maybeFlush(true, 1)
|
||||||
|
log = love.filesystem.read("switch.log") or ""
|
||||||
|
check(not log:find(romSnippet, 1, true),
|
||||||
|
"ROM bytes are stripped from diagnostic payloads")
|
||||||
|
|
||||||
|
-- Flush rate capped at 1 Hz unless forced.
|
||||||
|
reset()
|
||||||
|
love.filesystem.write("switch-debug.txt", "")
|
||||||
|
SwitchDiagnostics.onEvent("tick", { n = 1 })
|
||||||
|
SwitchDiagnostics.maybeFlush(true, 0.0)
|
||||||
|
SwitchDiagnostics.onEvent("tick", { n = 2 })
|
||||||
|
SwitchDiagnostics.maybeFlush(false, 0.5)
|
||||||
|
local logMid = love.filesystem.read("switch.log") or ""
|
||||||
|
SwitchDiagnostics.maybeFlush(false, 1.0)
|
||||||
|
local logLate = love.filesystem.read("switch.log") or ""
|
||||||
|
check(not logMid:find("n=2", 1, true), "flush waits until 1s elapsed")
|
||||||
|
check(logLate:find("n=2", 1, true) ~= nil, "flush includes events after 1s")
|
||||||
|
|
||||||
|
T.finish()
|
||||||
Reference in New Issue
Block a user