This commit is contained in:
bryanthaboi
2026-08-18 09:20:07 -04:00
parent 675971068e
commit 55616fc03d
14 changed files with 194 additions and 31 deletions
+4 -1
View File
@@ -120,7 +120,7 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics)_test\.lua$|tests/engine/platform_nx)'; then
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics|cache_fs_gold_nx_load)_test\.lua$|tests/engine/platform_nx)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
@@ -151,6 +151,9 @@ jobs:
luajit tests/engine/assets_version_fallback_test.lua
luajit tests/engine/nx_generated_guard_test.lua
luajit tests/engine/nx_yellow_boot_test.lua
luajit tests/engine/cache_fs_gold_nx_load_test.lua
luajit tests/engine/cache_fs_blue_mount_test.lua
luajit tests/engine/switch_diagnostics_test.lua
switch-build:
name: Switch fused build
+3 -1
View File
@@ -169,6 +169,7 @@ the NX runtime modules `src/core/NxAssetOverlay.lua`, `src/core/Platform.lua`,
`tests/engine/assets_version_fallback_test.lua`,
`tests/engine/nx_generated_guard_test.lua`,
`tests/engine/nx_yellow_boot_test.lua`,
`tests/engine/cache_fs_gold_nx_load_test.lua`,
`tests/engine/switch_diagnostics_test.lua`, `tests/engine/platform_nx_*`,
or the Switch-related workflow YAML), CI runs:
@@ -179,7 +180,8 @@ or the Switch-related workflow YAML), CI runs:
`luajit tests/switch_transfer_docs_test.lua`, and the NX engine suites
headlessly (`luajit tests/engine/assets_version_fallback_test.lua`,
`luajit tests/engine/nx_generated_guard_test.lua`,
`luajit tests/engine/nx_yellow_boot_test.lua`).
`luajit tests/engine/nx_yellow_boot_test.lua`,
`luajit tests/engine/cache_fs_gold_nx_load_test.lua`).
2. **Fused NRO build** only on the **main** repository
(`bryanthaboi/gen1recomp`), on the self-hosted Mac runner
(`scripts/build_switch.sh --fetch --fused`), and only when the workflow
+1
View File
@@ -74,6 +74,7 @@ run_tier "T0 switch transfer docs gate" "$LUA" tests/switch_transfer_docs_test.l
run_tier "T0 NX asset overlay fallback" "$LUA" tests/engine/assets_version_fallback_test.lua
run_tier "T0 NX generated-path static guard" "$LUA" tests/engine/nx_generated_guard_test.lua
run_tier "T0 NX Yellow/Blue boot (dynamic paths)" "$LUA" tests/engine/nx_yellow_boot_test.lua
run_tier "T0 NX Gold cache load (maps.lua prefix)" "$LUA" tests/engine/cache_fs_gold_nx_load_test.lua
run_tier "T0 touch-controls pad cursor" "$LUA" tests/engine/touch_controls_pad_cursor_test.lua
run_tier "T1/T2 engine invariants + parity gates" "$LUA" tests/run_engine.lua
run_tier "T4 mod-SDK" "$LUA" tests/run_modkit.lua
+5 -5
View File
@@ -120,11 +120,11 @@ local function visibleBaseState(stack)
end
local function loadGenerated(path)
local chunk = love.filesystem.load(path)
if not chunk then return nil end
local ok, data = pcall(chunk)
if ok then return data end
return nil
-- CacheFs.loadActive, not love.filesystem.load: Gold's cache lives under
-- gold/ and fused NX often cannot mount that tree onto data/generated/.
local CacheFs = require("src.import.CacheFs")
local data = CacheFs.loadActive(path)
return data
end
-- NewGame (engine/menus/intro_menu.asm) calls OakSpeech, and OakSpeech's first
+24 -10
View File
@@ -1,10 +1,10 @@
-- NX-only asset overlay: fused love-nx cannot reliably mount
-- blue|yellow/assets/generated onto the un-prefixed assets/generated, so
-- blue|yellow|gold/{assets,data}/generated onto the un-prefixed paths, so
-- instead of teaching every call site about versioned caches, this module
-- wraps EVERY read-side love entry point that accepts a filesystem path
-- once at boot: any string path under assets/generated/ that does not
-- resolve falls back to the active version's prefixed copy
-- (yellow|blue/assets/generated/...). Covering the whole read surface --
-- once at boot: any string path under assets/generated/ or data/generated/
-- that does not resolve falls back to the active version's prefixed copy
-- (yellow|blue|gold/... ). Covering the whole read surface --
-- not just the loaders we happened to need -- is what keeps future states
-- and mods inside the fallback without anyone updating this file.
--
@@ -18,24 +18,38 @@
-- * the chip-audio worker (src/core/chip_worker.lua) is a separate Lua
-- state without these wrappers; ChipAudio.slimAudio hands it the prefix
-- explicitly as audio.programPrefix.
-- * data/generated module loads go through CacheFs.readActive, which
-- already implements the same fallback for require bytes.
-- * Gen 1 Data:load and Gold Game2/World go through CacheFs.readActive /
-- CacheFs.loadActive, which implement the same fallback for Lua bytes.
-- data/generated is still rewritten here so any leftover
-- love.filesystem.load("data/generated/...") call (the Gold intro /
-- naming / maps hole on 0.2.4) stays inside the overlay.
local GameVersion = require("src.core.GameVersion")
local GENERATED = "assets/generated/"
local GENERATED_PREFIXES = {
"assets/generated/",
"data/generated/",
}
local NxAssetOverlay = {}
local originals -- raw love functions, non-nil while installed
-- Resolve `path` to the versioned copy when the un-prefixed file is missing
-- and the active version (Blue/Yellow) carries it. Returns nil when the
-- caller's path should be used untouched (non-generated path, Red, the real
-- and the active version (Blue/Yellow/Gold) carries it. Returns nil when the
-- caller's path should be used untouched (non-generated path, the real
-- file exists, or no versioned copy).
local function versioned(path)
if type(path) ~= "string" then return nil end
if path:sub(1, #GENERATED) ~= GENERATED then return nil end
local generated = false
for i = 1, #GENERATED_PREFIXES do
local gen = GENERATED_PREFIXES[i]
if path:sub(1, #gen) == gen then
generated = true
break
end
end
if not generated then return nil end
local prefix = GameVersion.cachePrefix()
if prefix == "" then return nil end
if originals.getInfo(path) then return nil end
+19 -7
View File
@@ -220,6 +220,9 @@ function SwitchDiagnostics.probeAssets(version)
"assets/generated/tilesets/reds_house.png",
"assets/generated/sprites/red.png",
"assets/generated/sprites/monster.png",
"data/generated/maps.lua",
"data/generated/oak_speech.lua",
"data/generated/font.lua",
}
for _, path in ipairs(samples) do
local versioned = prefix ~= "" and (prefix .. path) or path
@@ -230,17 +233,26 @@ function SwitchDiagnostics.probeAssets(version)
lines[#lines + 1] = "versioned=" .. probeInfo(filesystem, versioned)
end
lines[#lines + 1] = "resolve=" .. tostring(resolved)
lines[#lines + 1] = "newImage=" .. probeOpen("image", resolved)
lines[#lines + 1] = "newImageData=" .. probeOpen("imageData", resolved)
if prefix ~= "" and resolved ~= versioned then
lines[#lines + 1] = "newImage_versioned=" .. probeOpen("image", versioned)
lines[#lines + 1] = "newImageData_versioned=" .. probeOpen("imageData", versioned)
if path:sub(-4) == ".lua" then
local CacheFs = require("src.import.CacheFs")
local loaded, err = CacheFs.loadActive(path)
lines[#lines + 1] = "loadActive=" .. (loaded ~= nil and "ok"
or ("FAIL " .. tostring(err):gsub("%s+", " "):sub(1, 160)))
else
lines[#lines + 1] = "newImage=" .. probeOpen("image", resolved)
lines[#lines + 1] = "newImageData=" .. probeOpen("imageData", resolved)
if prefix ~= "" and resolved ~= versioned then
lines[#lines + 1] = "newImage_versioned=" .. probeOpen("image", versioned)
lines[#lines + 1] = "newImageData_versioned=" .. probeOpen("imageData", versioned)
end
end
end
-- Shallow listing so we can see if the extract tree exists at all.
local roots = { "yellow", "blue", "assets", "yellow/assets/generated",
"yellow/assets/generated/sprites", "blue/assets/generated/sprites" }
local roots = { "yellow", "blue", "gold", "assets", "yellow/assets/generated",
"yellow/assets/generated/sprites", "blue/assets/generated/sprites",
"gold/assets/generated", "gold/assets/generated/sprites",
"gold/data/generated" }
for _, dir in ipairs(roots) do
local info = filesystem.getInfo(dir)
if info and info.type == "directory" and filesystem.getDirectoryItems then
+27 -1
View File
@@ -305,7 +305,7 @@ function CacheFs.read(rel)
end
-- Read cache-relative `rel` for the active GameVersion when PhysFS may hide
-- prefixed Blue/Yellow trees (fused NX mount hole). Same order Data:load
-- prefixed Blue/Yellow/Gold trees (fused NX mount hole). Same order Data:load
-- already used: active version prefix with CacheFs.prefix cleared, then
-- `rel` under the caller's CacheFs.prefix. Returns the bytes or nil.
function CacheFs.readActive(rel)
@@ -322,6 +322,32 @@ function CacheFs.readActive(rel)
return nil
end
-- Load a generated Lua table the way Data:load does: versioned save-dir
-- bytes first (gold/data/generated/maps.lua), then the un-prefixed path.
-- Game2/World used love.filesystem.load("data/generated/...") which misses
-- on fused NX when the gold/ overlay mount fails -- intro art still loads
-- via NxAssetOverlay, but oak_speech.lua / font.lua / maps.lua do not.
function CacheFs.loadActive(rel)
local bytes = CacheFs.readActive(rel)
if type(bytes) == "string" then
local GameVersion = require("src.core.GameVersion")
local loader = loadstring or load
local chunk, err = loader(bytes, "@" .. GameVersion.cachePrefix() .. rel)
if not chunk then return nil, err end
local ok, value = pcall(chunk)
if not ok then return nil, value end
return value
end
if love and love.filesystem and love.filesystem.load then
local chunk, err = love.filesystem.load(rel)
if not chunk then return nil, err end
local ok, value = pcall(chunk)
if not ok then return nil, value end
return value
end
return nil, "Could not open file " .. rel .. ". Does not exist."
end
-- does cache-relative `rel` exist as a file?
function CacheFs.exists(rel)
rel = withPrefix(rel)
+4 -5
View File
@@ -396,11 +396,10 @@ local function givePokeMon(data, speciesIndex, level, itemIndex)
end
local function loadGenerated(path)
local chunk, err = love.filesystem.load(path)
if not chunk then return nil, err end
local ok, value = pcall(chunk)
if not ok then return nil, value end
return value
-- Same NX gold/ fallback Game2 uses. World:load is what surfaces
-- "Gold cache incomplete" when maps.lua is invisible at the unprefixed path.
local CacheFs = require("src.import.CacheFs")
return CacheFs.loadActive(path)
end
-- Paste the 9-tile roof sheet over atlas tiles $0a-$12.
@@ -110,6 +110,18 @@ clearPath("yellow/" .. PNG)
eq(love.filesystem.read(PNG), "blue-png-bytes",
"overlay maps generated reads to blue/ for Blue")
-- Gold data/generated: fused NX hides gold/maps.lua at the unprefixed path,
-- which is the "Gold cache incomplete / maps.lua Does not exist" crash.
GameVersion.set("gold")
local MAPS = "data/generated/maps.lua"
love.filesystem.write("gold/" .. MAPS, "return { NEW_BARK_TOWN = true }")
local mapsChunk = love.filesystem.load(MAPS)
eq(type(mapsChunk) == "function" and mapsChunk().NEW_BARK_TOWN or nil, true,
"wrapped filesystem.load resolves gold/data/generated/maps.lua")
eq(love.filesystem.read(MAPS), "return { NEW_BARK_TOWN = true }",
"wrapped filesystem.read returns gold maps.lua bytes")
clearPath("gold/" .. MAPS)
-- Red has no prefix: nothing is rewritten
GameVersion.set("red")
clearPath("blue/" .. PNG)
+11
View File
@@ -49,5 +49,16 @@ check(CacheFs.mountVersion("yellow") == true, "mountVersion(yellow) returns true
eq(love.filesystem.read("assets/generated/fonts/font.png"), "yellow-font",
"Yellow mount exposes fonts/font.png at the unprefixed path")
-- Gold-only: same overlay contract (the Switch intro/maps.lua hole)
love.filesystem._mounts = {}
GameVersion.set("gold")
love.filesystem.write("gold/data/generated/maps.lua", "return { ok = true }")
love.filesystem.write("gold/assets/generated/fonts/font.png", "gold-font")
check(CacheFs.mountVersion("gold") == true, "mountVersion(gold) returns true")
eq(love.filesystem.read("assets/generated/fonts/font.png"), "gold-font",
"Gold mount exposes fonts/font.png at the unprefixed path")
eq(love.filesystem.read("data/generated/maps.lua"), "return { ok = true }",
"Gold mount exposes maps.lua at the unprefixed path")
GameVersion.set("red")
T.finish()
@@ -0,0 +1,57 @@
-- Gold on fused NX: gold/data/generated exists, but the overlay mount
-- onto data/generated often fails. Game2/World used to love.filesystem.load
-- the unprefixed path and crash with "Gold cache incomplete / maps.lua
-- Does not exist" after a textless intro. CacheFs.loadActive must read the
-- versioned file without any mount.
-- Self-contained: luajit tests/engine/cache_fs_gold_nx_load_test.lua
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 CacheFs = require("src.import.CacheFs")
local GameVersion = require("src.core.GameVersion")
local savedVersion = GameVersion.get()
local savedPrefix = CacheFs.prefix
love.filesystem._mounts = {}
GameVersion.set("gold")
CacheFs.prefix = GameVersion.cachePrefix()
love.filesystem.write("gold/data/generated/maps.lua",
"return { NEW_BARK_TOWN = { id = 1 } }")
love.filesystem.write("gold/data/generated/oak_speech.lua",
"return { text = { _OakText1 = 'Hello!' } }")
love.filesystem.write("gold/data/generated/font.lua",
"return { width = 8 }")
-- Unprefixed path is a miss: the NX mount hole.
eq(love.filesystem.read("data/generated/maps.lua"), nil,
"unprefixed maps.lua is missing (mount hole)")
eq(love.filesystem.load("data/generated/maps.lua"), nil,
"love.filesystem.load misses unprefixed maps.lua")
local maps, mapsErr = CacheFs.loadActive("data/generated/maps.lua")
check(maps ~= nil, "loadActive finds gold/data/generated/maps.lua ("
.. tostring(mapsErr) .. ")")
eq(maps and maps.NEW_BARK_TOWN and maps.NEW_BARK_TOWN.id, 1,
"loadActive returns the Gold maps table")
local oak = CacheFs.loadActive("data/generated/oak_speech.lua")
eq(oak and oak.text and oak.text._OakText1, "Hello!",
"loadActive returns oak_speech.lua from gold/")
local font = CacheFs.loadActive("data/generated/font.lua")
eq(font and font.width, 8,
"loadActive returns font.lua from gold/")
love.filesystem.remove("gold/data/generated/maps.lua")
love.filesystem.remove("gold/data/generated/oak_speech.lua")
love.filesystem.remove("gold/data/generated/font.lua")
CacheFs.prefix = savedPrefix
GameVersion.set(savedVersion)
T.finish()
+3
View File
@@ -16,5 +16,8 @@ check(CacheFs.read("data/generated/audio.lua") == nil,
"read is a nil miss headless, not a crash")
check(CacheFs.readActive("data/generated/audio.lua") == nil,
"readActive is a nil miss headless, not a crash")
local loaded, loadErr = CacheFs.loadActive("data/generated/audio.lua")
check(loaded == nil,
"loadActive is a nil miss headless, not a crash (" .. tostring(loadErr) .. ")")
T.finish()
+19
View File
@@ -89,6 +89,23 @@ check(probe:find("resolve=yellow/assets/generated/fonts/font.png", 1, true) ~= n
"probe records versioned font path visibility")
check(not probe:find(string.char(0xEA, 0x9B), 1, true),
"probe log contains no ROM-like binary")
check(probe:find("data/generated/maps.lua", 1, true) ~= nil,
"probe records Gold/Gen2 maps.lua visibility")
check(probe:find("loadActive=", 1, true) ~= nil,
"probe uses loadActive for generated Lua instead of newImage")
-- Gold maps.lua / gold/ folders: fused NX intro/naming/overworld crash.
GameVersion.set("gold")
love.filesystem.write("gold/data/generated/maps.lua", "return { NEW_BARK_TOWN = true }")
love.filesystem.write("gold/data/generated/oak_speech.lua", "return {}")
SwitchDiagnostics.probeAssets("gold")
probe = love.filesystem.read("nx-asset-probe.log") or ""
check(probe:find("cachePrefix=gold/", 1, true) ~= nil, "probe records gold prefix")
check(probe:find("data/generated/maps.lua", 1, true) ~= nil,
"probe lists maps.lua (the Gold cache incomplete path)")
check(probe:find("list gold", 1, true) ~= nil, "probe lists gold/ folder")
check(probe:find("list gold/data/generated", 1, true) ~= nil,
"probe lists gold/data/generated/")
love.system = { getOS = function() return "OS X" end }
Platform._resetForTests()
@@ -104,5 +121,7 @@ love.filesystem.remove("nx-asset-probe.log")
love.filesystem.remove("yellow/assets/generated/fonts/font.png")
love.filesystem.remove("yellow/assets/generated/tilesets/reds_house.png")
love.filesystem.remove("yellow/assets/generated/sprites/red.png")
love.filesystem.remove("gold/data/generated/maps.lua")
love.filesystem.remove("gold/data/generated/oak_speech.lua")
T.finish()
+5 -1
View File
@@ -26,7 +26,7 @@ end
-- Also gates the NX runtime modules and the NX engine suites so an NX
-- runtime regression cannot slip past switch-selftest / switch-build.
local SWITCH_PATH_REGEX =
[[^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics)_test\.lua$|tests/engine/platform_nx)]]
[[^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics|cache_fs_gold_nx_load)_test\.lua$|tests/engine/platform_nx)]]
local ci = read(".github/workflows/ci.yml")
local release = read(".github/workflows/release.yml")
@@ -50,6 +50,7 @@ for _, fragment in ipairs({
"nx_generated_guard",
"nx_yellow_boot",
"switch_diagnostics",
"cache_fs_gold_nx_load",
"tests/engine/platform_nx",
}) do
mustContain(ci, fragment, "ci.yml path regex NX fragment")
@@ -79,6 +80,7 @@ do
mustContain(block, "luajit tests/engine/assets_version_fallback_test.lua", "switch-selftest")
mustContain(block, "luajit tests/engine/nx_generated_guard_test.lua", "switch-selftest")
mustContain(block, "luajit tests/engine/nx_yellow_boot_test.lua", "switch-selftest")
mustContain(block, "luajit tests/engine/cache_fs_gold_nx_load_test.lua", "switch-selftest")
mustNotContain(block, "continue-on-error:", "switch-selftest")
end
@@ -185,6 +187,7 @@ mustContain(test_sh, "T0 switch transfer docs gate", "scripts/test.sh")
mustContain(test_sh, "tests/engine/assets_version_fallback_test.lua", "scripts/test.sh")
mustContain(test_sh, "tests/engine/nx_generated_guard_test.lua", "scripts/test.sh")
mustContain(test_sh, "tests/engine/nx_yellow_boot_test.lua", "scripts/test.sh")
mustContain(test_sh, "tests/engine/cache_fs_gold_nx_load_test.lua", "scripts/test.sh")
mustContain(build_doc, "tests/switch_ci_workflows_test.lua", "switch-build.md path list")
mustContain(build_doc, "tests/switch_transfer_docs_test.lua", "switch-build.md path list")
@@ -197,6 +200,7 @@ for _, path in ipairs({
"tests/engine/assets_version_fallback_test.lua",
"tests/engine/nx_generated_guard_test.lua",
"tests/engine/nx_yellow_boot_test.lua",
"tests/engine/cache_fs_gold_nx_load_test.lua",
"tests/engine/switch_diagnostics_test.lua",
"tests/engine/platform_nx_*",
}) do