mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
fix(switch): prefer Yellow/Blue asset bytes when mount overlay lies
Probe generated canaries after mountVersion and always readActive for prefixed caches so sprites are not blanked by empty PhysFS stubs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -409,6 +409,32 @@ local function mountGeneratedTrees(prefix)
|
||||
return mounted
|
||||
end
|
||||
|
||||
-- True when unprefixed generated cache is actually readable (not merely a
|
||||
-- directory stub PhysFS can see). Used after Blue/Yellow mounts so NX cannot
|
||||
-- silently boot with a broken overlay.
|
||||
local function generatedCacheVisible()
|
||||
if not (love and love.filesystem) then return false end
|
||||
local canaries = {
|
||||
"assets/generated/fonts/font.png",
|
||||
"data/generated/constants.lua",
|
||||
}
|
||||
for _, path in ipairs(canaries) do
|
||||
local bytes = love.filesystem.read(path)
|
||||
if type(bytes) == "string" and #bytes > 0 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function noteMountProbe(version, prefix, ok)
|
||||
local okReq, Diag = pcall(require, "src.debug.SwitchDiagnostics")
|
||||
if okReq and Diag and Diag.onEvent then
|
||||
Diag.onEvent(ok and "mount_probe_ok" or "mount_probe_failed", {
|
||||
version = tostring(version or ""),
|
||||
prefix = tostring(prefix or ""),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function CacheFs.mountVersion(version)
|
||||
local prefix = require("src.core.GameVersion").cachePrefix(version)
|
||||
local sub = prefix:gsub("/+$", "")
|
||||
@@ -432,6 +458,27 @@ function CacheFs.mountVersion(version)
|
||||
|
||||
-- Version-scoped generated trees → un-prefixed paths (Red prefix is "").
|
||||
mountGeneratedTrees(prefix)
|
||||
|
||||
-- Blue/Yellow: verify the overlay actually exposes generated files. A failed
|
||||
-- mount still returns true so Play can fall back through CacheFs.readActive,
|
||||
-- but we retry once and leave a SwitchDiagnostics breadcrumb when enabled.
|
||||
if sub ~= "" then
|
||||
if not generatedCacheVisible() then
|
||||
mountGeneratedTrees(prefix)
|
||||
end
|
||||
local ok = generatedCacheVisible()
|
||||
noteMountProbe(version, prefix, ok)
|
||||
if not ok then
|
||||
-- Last resort: root mount alone sometimes leaves assets/ hidden behind
|
||||
-- fused archive assets/; re-issue both mounts once more.
|
||||
if love.filesystem.mount and love.filesystem.getInfo(sub, "directory") then
|
||||
love.filesystem.mount(sub, "", false)
|
||||
end
|
||||
mountGeneratedTrees(prefix)
|
||||
noteMountProbe(version, prefix, generatedCacheVisible())
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
+15
-4
@@ -43,15 +43,26 @@ function Assets.resolve(path)
|
||||
return loader:derivedPath(rel) or path
|
||||
end
|
||||
|
||||
-- When PhysFS hides Blue/Yellow prefixed trees (fused NX), load generated
|
||||
-- asset bytes the same way Data:load does via CacheFs.readActive.
|
||||
-- When PhysFS hides or mis-exposes Blue/Yellow prefixed trees (fused NX),
|
||||
-- load generated asset bytes the same way Data:load does via CacheFs.readActive.
|
||||
-- Blue/Yellow prefer versioned bytes whenever present: getInfo can succeed on a
|
||||
-- broken overlay while sprites decode as blank (OBP keys white → transparent).
|
||||
local function generatedFileData(resolved)
|
||||
if type(resolved) ~= "string" or resolved:sub(1, #GENERATED) ~= GENERATED then
|
||||
return nil
|
||||
end
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local prefix = require("src.core.GameVersion").cachePrefix()
|
||||
if prefix ~= "" then
|
||||
local bytes = CacheFs.readActive(resolved)
|
||||
if type(bytes) == "string" and #bytes > 0 then
|
||||
return love.filesystem.newFileData(bytes, resolved)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
if exists(resolved) then return nil end
|
||||
local bytes = require("src.import.CacheFs").readActive(resolved)
|
||||
if type(bytes) ~= "string" then return nil end
|
||||
local bytes = CacheFs.readActive(resolved)
|
||||
if type(bytes) ~= "string" or #bytes == 0 then return nil end
|
||||
return love.filesystem.newFileData(bytes, resolved)
|
||||
end
|
||||
|
||||
|
||||
@@ -57,6 +57,15 @@ check(redImg ~= nil, "Assets.image loads Red tileset from unprefixed path")
|
||||
eq(love.filesystem.read(PNG), "red-png-bytes",
|
||||
"Red still stores generated assets at the save-dir root")
|
||||
|
||||
-- --- Stale unprefixed path must not win over Yellow bytes (NX mount lie)
|
||||
GameVersion.set("yellow")
|
||||
Assets.flush()
|
||||
love.filesystem.write(PNG, "") -- visible but empty → would bake blank sprites
|
||||
love.filesystem.write("yellow/" .. PNG, "yellow-real-png")
|
||||
local staleImg = Assets.image(PNG)
|
||||
check(staleImg ~= nil, "Assets.image prefers Yellow bytes over empty unprefixed stub")
|
||||
eq(staleImg.path, PNG, "stale-path fallback still names the logical generated path")
|
||||
|
||||
-- --- Missing generated file: no invented bytes
|
||||
GameVersion.set("yellow")
|
||||
Assets.flush()
|
||||
|
||||
@@ -4,13 +4,16 @@ 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")
|
||||
|
||||
love.filesystem._mounts = {}
|
||||
-- Imply blue/data/generated and blue/assets/generated directories via file keys.
|
||||
love.filesystem.write("blue/data/generated/maps.lua", "return {}")
|
||||
love.filesystem.write("blue/assets/generated/fonts/font.png", "x")
|
||||
love.filesystem.write("blue/data/generated/constants.lua", "return {}")
|
||||
love.filesystem.write("blue/assets/generated/fonts/font.png", "font-bytes")
|
||||
|
||||
check(CacheFs.mountVersion("blue") == true, "mountVersion(blue) returns true")
|
||||
|
||||
@@ -32,4 +35,19 @@ check(sawBlueRoot, "prepend-mounts save-dir relative blue/")
|
||||
check(sawDataGen, "prepend-mounts blue/data/generated -> data/generated")
|
||||
check(sawAssetsGen, "prepend-mounts blue/assets/generated -> assets/generated")
|
||||
|
||||
eq(love.filesystem.read("assets/generated/fonts/font.png"), "font-bytes",
|
||||
"post-mount probe can read unprefixed assets/generated canary")
|
||||
eq(love.filesystem.read("data/generated/constants.lua"), "return {}",
|
||||
"post-mount probe can read unprefixed data/generated canary")
|
||||
|
||||
-- Yellow-only: same overlay contract
|
||||
love.filesystem._mounts = {}
|
||||
GameVersion.set("yellow")
|
||||
love.filesystem.write("yellow/data/generated/constants.lua", "return {y=1}")
|
||||
love.filesystem.write("yellow/assets/generated/fonts/font.png", "yellow-font")
|
||||
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")
|
||||
|
||||
GameVersion.set("red")
|
||||
T.finish()
|
||||
|
||||
+107
-48
@@ -125,58 +125,11 @@ stub.math = {
|
||||
|
||||
stub.filesystem = {
|
||||
write = function(name, content) files[name] = content return true end,
|
||||
read = function(name) return files[name] end,
|
||||
remove = function(name) files[name] = nil return true end,
|
||||
newFileData = function(contents, name)
|
||||
return { _fileData = true, contents = contents, name = name or "" }
|
||||
end,
|
||||
createDirectory = function() return true end,
|
||||
-- directories are implied by key prefixes ("mods/x/manifest.json")
|
||||
getInfo = function(name, filter)
|
||||
if files[name] then
|
||||
if filter and filter ~= "file" then return nil end
|
||||
return { type = "file" }
|
||||
end
|
||||
local prefix = name .. "/"
|
||||
for key in pairs(files) do
|
||||
if key:sub(1, #prefix) == prefix then
|
||||
if filter and filter ~= "directory" then return nil end
|
||||
return { type = "directory" }
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
load = function(name)
|
||||
if not files[name] then return nil, "no file" end
|
||||
return load(files[name], name)
|
||||
end,
|
||||
getDirectoryItems = function(name)
|
||||
local seen, items = {}, {}
|
||||
name = name or ""
|
||||
-- "" / "/" = save-dir root (RomImporter Android ROM scan)
|
||||
if name == "" or name == "/" then
|
||||
for key in pairs(files) do
|
||||
local child = key:match("^[^/]+")
|
||||
if child and not seen[child] then
|
||||
seen[child] = true
|
||||
items[#items + 1] = child
|
||||
end
|
||||
end
|
||||
else
|
||||
local prefix = name .. "/"
|
||||
for key in pairs(files) do
|
||||
if key:sub(1, #prefix) == prefix then
|
||||
local child = key:sub(#prefix + 1):match("^[^/]+")
|
||||
if child and not seen[child] then
|
||||
seen[child] = true
|
||||
items[#items + 1] = child
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(items)
|
||||
return items
|
||||
end,
|
||||
-- Record mounts for CacheFs.mountVersion tests (NX Blue/Yellow overlay).
|
||||
_mounts = {},
|
||||
mount = function(archive, mountpoint, appendToPath)
|
||||
@@ -186,11 +139,117 @@ stub.filesystem = {
|
||||
}
|
||||
return true
|
||||
end,
|
||||
unmount = function() return true end,
|
||||
unmount = function(archive)
|
||||
local mounts = stub.filesystem._mounts
|
||||
for i = #mounts, 1, -1 do
|
||||
if mounts[i].archive == archive then
|
||||
table.remove(mounts, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
getSaveDirectory = function() return "/tmp/pokeport-stub-save" end,
|
||||
isFused = function() return false end,
|
||||
}
|
||||
|
||||
-- Resolve a PhysFS path through recorded mounts (prepend first, newest wins).
|
||||
local function resolveViaMounts(name)
|
||||
local mounts = stub.filesystem._mounts
|
||||
for i = #mounts, 1, -1 do
|
||||
local m = mounts[i]
|
||||
if not m.append then
|
||||
local mp = m.mountpoint or ""
|
||||
local key
|
||||
if mp == "" then
|
||||
key = m.archive .. "/" .. name
|
||||
elseif name == mp then
|
||||
key = m.archive
|
||||
elseif name:sub(1, #mp + 1) == mp .. "/" then
|
||||
local rel = name:sub(#mp + 2)
|
||||
key = m.archive .. "/" .. rel
|
||||
end
|
||||
if key then
|
||||
if files[key] then return key, "file" end
|
||||
local prefix = key .. "/"
|
||||
for k in pairs(files) do
|
||||
if k:sub(1, #prefix) == prefix then return key, "directory" end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function stub.filesystem.read(name)
|
||||
if files[name] then return files[name] end
|
||||
local key = resolveViaMounts(name)
|
||||
if key and files[key] then return files[key] end
|
||||
return nil
|
||||
end
|
||||
|
||||
function stub.filesystem.getInfo(name, filter)
|
||||
if files[name] then
|
||||
if filter and filter ~= "file" then return nil end
|
||||
return { type = "file" }
|
||||
end
|
||||
local prefix = name .. "/"
|
||||
for key in pairs(files) do
|
||||
if key:sub(1, #prefix) == prefix then
|
||||
if filter and filter ~= "directory" then return nil end
|
||||
return { type = "directory" }
|
||||
end
|
||||
end
|
||||
local key, kind = resolveViaMounts(name)
|
||||
if key and kind then
|
||||
if filter and filter ~= kind then return nil end
|
||||
return { type = kind }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
stub.filesystem.load = function(name)
|
||||
local data = stub.filesystem.read(name)
|
||||
if not data then return nil, "no file" end
|
||||
return load(data, name)
|
||||
end
|
||||
|
||||
stub.filesystem.getDirectoryItems = function(name)
|
||||
local seen, items = {}, {}
|
||||
name = name or ""
|
||||
local function addChild(child)
|
||||
if child and not seen[child] then
|
||||
seen[child] = true
|
||||
items[#items + 1] = child
|
||||
end
|
||||
end
|
||||
-- "" / "/" = save-dir root (RomImporter Android ROM scan)
|
||||
if name == "" or name == "/" then
|
||||
for key in pairs(files) do
|
||||
addChild(key:match("^[^/]+"))
|
||||
end
|
||||
else
|
||||
local prefix = name .. "/"
|
||||
for key in pairs(files) do
|
||||
if key:sub(1, #prefix) == prefix then
|
||||
addChild(key:sub(#prefix + 1):match("^[^/]+"))
|
||||
end
|
||||
end
|
||||
-- Also surface children exposed via mounts.
|
||||
local key = resolveViaMounts(name)
|
||||
if key then
|
||||
local mprefix = key .. "/"
|
||||
for k in pairs(files) do
|
||||
if k:sub(1, #mprefix) == mprefix then
|
||||
addChild(k:sub(#mprefix + 1):match("^[^/]+"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(items)
|
||||
return items
|
||||
end
|
||||
|
||||
-- table-backed SoundData so ChipAudio's offline render seam
|
||||
-- (_renderMusicForTest) runs headless; modkit bounce writes WAVs from it
|
||||
local SoundData = {}
|
||||
|
||||
Reference in New Issue
Block a user