mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
af47e19e1a
The launcher spent ~9ms per frame building and drawing, and the Find Mods tab could hang the window for minutes. Both had the same root cause: a retained UI tree rebuilt every frame, and blocking curl calls made from the draw path. Replace the vendored FlexLove engine (28.5k lines) with src/ui/kit/ (Kit, Theme, Layout, Loader). The kit caches Text objects and all measurement, allocates nothing in the steady state, and draws flat. Build+draw is now under 1ms at every window size and on every tab (POKEPORT_LAUNCHER_PROF). Move every network call off the render thread onto a love.thread pool (src/net/Fetch.lua): mod index fetches, per-mod release checks, find-tab stats, thumbnails and mod installs. Mod indexes prewarm at boot so the Find Mods tab is populated before it is opened. Paginate every list -- mods, find, save slots, settings, release notes, versions -- with the page size derived from the real viewport height, so a 500-mod index costs what a 10-mod one does. Scrolling is gone. Anything that waits now raises a non-dismissable loader; per-row background work shows an inline spinner instead. The in-app updater moves to the top right beside the settings gear and pulses when an update is waiting. Theme is black with white outlines, no gradients or glows, and solid colour-coded embossed buttons with bold labels. The game tabs keep their cartridge colours. Everything is 1.3x larger. The save editor shares the theme, and adding an item there is now a searchable pop-up like adding a Pokemon. Also: - Reset rebinds, in Settings and under Touch Controls. Rebinds are additive (Input:applyBindings layers them over the defaults), so there was no in-game way to undo one. - Launch options: --game red [--slot N] / POKEPORT_GAME boots straight into a game for shortcuts and frontends, falling back to that game's tab when its ROM is not imported. Fixes found while porting: - Ellipsis and letterspacing truncated bytes, not codepoints, so a multi-byte mod name crashed the first frame on a Japanese index. Measurement no longer throws on malformed input either. - The new font set missed UiFont's kana fallback, rendering translated builds as tofu. - Fetch workers idle in Channel:demand() and LOVE waits for live threads at exit, so the process outlived the window; quitting mid-download also waited on curl's 300s ceiling. Shut the pool down in love.quit and bound its transfer timeouts. - In one column the save-slot card drew below the fold, over the footer, with no scrollbar left to reach it. The two FlexLove engine tests guarded a scroll manager and an auto-height propagation bug that no longer exist; replace them with a kit suite covering page bounds, viewport sizing and UTF-8 truncation, and retarget the NX test to assert the dependency is gone rather than that its perf guards are set.
170 lines
6.4 KiB
Lua
170 lines
6.4 KiB
Lua
-- Ratchet: player-visible text in the engine stays reachable from a mod.
|
|
--
|
|
-- Every string the engine writes itself goes through src/core/Strings.lua so
|
|
-- a translation can replace it (#186, #245). That is easy to land once and
|
|
-- easy to erode: the next battle message someone adds is a bare literal
|
|
-- unless something notices, and nothing notices, because a bare literal
|
|
-- works perfectly in English.
|
|
--
|
|
-- So this gate re-runs the sweep's own test. In the files whose job is
|
|
-- drawing text, a literal carrying a line marker (\n, \f, \v) has to be
|
|
-- inside a Strings(...) or Strings.source(...) call. The exceptions below
|
|
-- are the shapes that legitimately carry a marker without being prose, and
|
|
-- the list only shrinks: an entry that no longer matches anything fails too,
|
|
-- so it cannot rot into a standing excuse.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
local FsIo = require("tests.fs_io")
|
|
|
|
-- Files that put text in front of the player. Everywhere else the same
|
|
-- literal shapes are ids, log lines, wire framing and asset paths.
|
|
local WATCHED_DIRS = {
|
|
"src/battle", "src/world", "src/ui", "src/inventory", "src/pokemon",
|
|
"src/script",
|
|
}
|
|
local WATCHED_FILES = {
|
|
"src/link/LinkState.lua", "src/link/LinkBattle.lua",
|
|
"src/link/Tournament.lua", "src/link/Net.lua",
|
|
"src/mods/ManagerState.lua", "src/import/RomImporter.lua",
|
|
"src/core/DiscordPresence.lua",
|
|
}
|
|
|
|
-- A marker in one of these is not prose. Keep the reason with the rule:
|
|
-- a bare pattern list is unreviewable six months from now.
|
|
local ALLOWED = {
|
|
{ pattern = "gmatch", why = "iterating lines, the marker is the separator" },
|
|
{ pattern = "gsub", why = "rewriting markers, not printing them" },
|
|
{ pattern = ":find%(", why = "searching for a marker" },
|
|
{ pattern = ":match%(", why = "matching on a marker" },
|
|
{ pattern = "table%.concat", why = "joining pages with \\f" },
|
|
{ pattern = '%.%. "\\f"', why = "page-join glue between two texts" },
|
|
{ pattern = "error%(", why = "a developer error, never drawn" },
|
|
{ pattern = "Logger%.", why = "a log line, never drawn" },
|
|
{ pattern = "io%.stderr", why = "a dev-harness diagnostic, never drawn "
|
|
.. "(POKEPORT_LAUNCHER_PROF's frame timings)" },
|
|
{ pattern = '== "\\v"', why = "comparing against a marker, not printing it" },
|
|
{ pattern = "txBuf", why = "newline-delimited wire framing, not text" },
|
|
}
|
|
|
|
-- Whole files inside a watched directory that are exempt, with the reason.
|
|
local EXEMPT_FILES = {
|
|
["src/ui/OakSpeech.lua"] =
|
|
"the _OakSpeech* literals are fallbacks for extracted text ids, so they "
|
|
.. "are already translatable through the text registry",
|
|
}
|
|
|
|
local function isAllowed(line)
|
|
for _, rule in ipairs(ALLOWED) do
|
|
if line:find(rule.pattern) then return rule end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Blank out every Strings(...) / Strings.source(...) call span, parens
|
|
-- balanced, so a call wrapped across lines counts as covered. A per-line
|
|
-- test reported the continuation lines of three real calls as misses.
|
|
--
|
|
-- romText(...) counts as a router too: it prefers the line the importer
|
|
-- extracted from the ROM and hands its literal straight to Strings(...)
|
|
-- whenever that label is absent (a cache built before it, or a dataset-less
|
|
-- unit test), so the literal is still catalog-backed and a translation mod
|
|
-- still reaches it. Blanking the whole span is safe -- the only literals
|
|
-- inside are the pokered label and that fallback.
|
|
local function stripStringsCalls(body)
|
|
local out, i, n = {}, 1, #body
|
|
while i <= n do
|
|
local s, e = body:find("Strings%.?s?o?u?r?c?e?%(", i)
|
|
local rs = body:find("romText%(", i)
|
|
if rs and (not s or rs < s) then s, e = rs, nil end
|
|
if not s then out[#out + 1] = body:sub(i) break end
|
|
out[#out + 1] = body:sub(i, s - 1)
|
|
local depth, j = 0, body:find("%(", s)
|
|
while j and j <= n do
|
|
local c = body:sub(j, j)
|
|
if c == "(" then depth = depth + 1
|
|
elseif c == ")" then
|
|
depth = depth - 1
|
|
if depth == 0 then break end
|
|
end
|
|
j = j + 1
|
|
end
|
|
j = j or n
|
|
-- keep the newlines so reported line numbers stay honest
|
|
out[#out + 1] = (body:sub(s, j):gsub("[^\n]", ""))
|
|
i = j + 1
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
-- literals carrying a line marker, ignoring ones already inside a Strings call
|
|
local function offenders(body)
|
|
body = stripStringsCalls(body)
|
|
local out = {}
|
|
local lineno = 0
|
|
for line in (body .. "\n"):gmatch("(.-)\n") do
|
|
lineno = lineno + 1
|
|
local code = line:gsub("^%s*", "")
|
|
local isComment = code:sub(1, 2) == "--"
|
|
if not isComment then
|
|
-- a double-quoted literal containing \n, \f or \v
|
|
for literal in line:gmatch('"[^"]*"') do
|
|
if literal:find("\\[nfv]") and not isAllowed(line) then
|
|
out[#out + 1] = { line = lineno, text = code:sub(1, 90) }
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local files = {}
|
|
for _, dir in ipairs(WATCHED_DIRS) do
|
|
for _, path in ipairs(FsIo.luaFilesUnder(dir)) do files[#files + 1] = path end
|
|
end
|
|
for _, path in ipairs(WATCHED_FILES) do files[#files + 1] = path end
|
|
|
|
T.check(#files > 20, "the gate actually found the watched files")
|
|
|
|
local usedRules, bad = {}, {}
|
|
local scanned = 0
|
|
for _, path in ipairs(files) do
|
|
if not EXEMPT_FILES[path] then
|
|
local handle = io.open(path, "rb")
|
|
if handle then
|
|
local body = handle:read("*a")
|
|
handle:close()
|
|
scanned = scanned + 1
|
|
for _, hit in ipairs(offenders(body)) do
|
|
bad[#bad + 1] = ("%s:%d %s"):format(path, hit.line, hit.text)
|
|
end
|
|
-- track which allowances are still earning their place
|
|
for line in (body .. "\n"):gmatch("(.-)\n") do
|
|
local rule = isAllowed(line)
|
|
if rule then usedRules[rule.pattern] = true end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
T.check(scanned > 20, "the gate read the files it listed")
|
|
T.check(#bad == 0,
|
|
("%d player-visible literal(s) are not routed through Strings(...):\n %s")
|
|
:format(#bad, table.concat(bad, "\n ")))
|
|
|
|
-- the ratchet half: an allowance nothing matches any more is dead weight
|
|
for _, rule in ipairs(ALLOWED) do
|
|
T.check(usedRules[rule.pattern] ~= nil,
|
|
("the %q allowance (%s) matches nothing any more -- delete it")
|
|
:format(rule.pattern, rule.why))
|
|
end
|
|
|
|
for path, why in pairs(EXEMPT_FILES) do
|
|
local handle = io.open(path, "rb")
|
|
T.check(handle ~= nil, ("exempt file %s still exists (%s)"):format(path, why))
|
|
if handle then handle:close() end
|
|
end
|
|
|
|
T.finish("gate_strings_coverage")
|