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
|
return mounted
|
||||||
end
|
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)
|
function CacheFs.mountVersion(version)
|
||||||
local prefix = require("src.core.GameVersion").cachePrefix(version)
|
local prefix = require("src.core.GameVersion").cachePrefix(version)
|
||||||
local sub = prefix:gsub("/+$", "")
|
local sub = prefix:gsub("/+$", "")
|
||||||
@@ -432,6 +458,27 @@ function CacheFs.mountVersion(version)
|
|||||||
|
|
||||||
-- Version-scoped generated trees → un-prefixed paths (Red prefix is "").
|
-- Version-scoped generated trees → un-prefixed paths (Red prefix is "").
|
||||||
mountGeneratedTrees(prefix)
|
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
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+15
-4
@@ -43,15 +43,26 @@ function Assets.resolve(path)
|
|||||||
return loader:derivedPath(rel) or path
|
return loader:derivedPath(rel) or path
|
||||||
end
|
end
|
||||||
|
|
||||||
-- When PhysFS hides Blue/Yellow prefixed trees (fused NX), load generated
|
-- When PhysFS hides or mis-exposes Blue/Yellow prefixed trees (fused NX),
|
||||||
-- asset bytes the same way Data:load does via CacheFs.readActive.
|
-- 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)
|
local function generatedFileData(resolved)
|
||||||
if type(resolved) ~= "string" or resolved:sub(1, #GENERATED) ~= GENERATED then
|
if type(resolved) ~= "string" or resolved:sub(1, #GENERATED) ~= GENERATED then
|
||||||
return nil
|
return nil
|
||||||
end
|
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
|
if exists(resolved) then return nil end
|
||||||
local bytes = require("src.import.CacheFs").readActive(resolved)
|
local bytes = CacheFs.readActive(resolved)
|
||||||
if type(bytes) ~= "string" then return nil end
|
if type(bytes) ~= "string" or #bytes == 0 then return nil end
|
||||||
return love.filesystem.newFileData(bytes, resolved)
|
return love.filesystem.newFileData(bytes, resolved)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ check(redImg ~= nil, "Assets.image loads Red tileset from unprefixed path")
|
|||||||
eq(love.filesystem.read(PNG), "red-png-bytes",
|
eq(love.filesystem.read(PNG), "red-png-bytes",
|
||||||
"Red still stores generated assets at the save-dir root")
|
"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
|
-- --- Missing generated file: no invented bytes
|
||||||
GameVersion.set("yellow")
|
GameVersion.set("yellow")
|
||||||
Assets.flush()
|
Assets.flush()
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ if not _G.love then _G.love = require("tests.love_stub") end
|
|||||||
|
|
||||||
local T = require("tests.harness")
|
local T = require("tests.harness")
|
||||||
local check = T.check
|
local check = T.check
|
||||||
|
local eq = T.eq
|
||||||
|
|
||||||
local CacheFs = require("src.import.CacheFs")
|
local CacheFs = require("src.import.CacheFs")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
|
||||||
love.filesystem._mounts = {}
|
love.filesystem._mounts = {}
|
||||||
-- Imply blue/data/generated and blue/assets/generated directories via file keys.
|
-- 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/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")
|
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(sawDataGen, "prepend-mounts blue/data/generated -> data/generated")
|
||||||
check(sawAssetsGen, "prepend-mounts blue/assets/generated -> assets/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()
|
T.finish()
|
||||||
|
|||||||
+90
-31
@@ -125,14 +125,70 @@ stub.math = {
|
|||||||
|
|
||||||
stub.filesystem = {
|
stub.filesystem = {
|
||||||
write = function(name, content) files[name] = content return true end,
|
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,
|
remove = function(name) files[name] = nil return true end,
|
||||||
newFileData = function(contents, name)
|
newFileData = function(contents, name)
|
||||||
return { _fileData = true, contents = contents, name = name or "" }
|
return { _fileData = true, contents = contents, name = name or "" }
|
||||||
end,
|
end,
|
||||||
createDirectory = function() return true end,
|
createDirectory = function() return true end,
|
||||||
-- directories are implied by key prefixes ("mods/x/manifest.json")
|
-- Record mounts for CacheFs.mountVersion tests (NX Blue/Yellow overlay).
|
||||||
getInfo = function(name, filter)
|
_mounts = {},
|
||||||
|
mount = function(archive, mountpoint, appendToPath)
|
||||||
|
stub.filesystem._mounts[#stub.filesystem._mounts + 1] = {
|
||||||
|
archive = archive, mountpoint = mountpoint or "",
|
||||||
|
append = appendToPath and true or false,
|
||||||
|
}
|
||||||
|
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 files[name] then
|
||||||
if filter and filter ~= "file" then return nil end
|
if filter and filter ~= "file" then return nil end
|
||||||
return { type = "file" }
|
return { type = "file" }
|
||||||
@@ -144,52 +200,55 @@ stub.filesystem = {
|
|||||||
return { type = "directory" }
|
return { type = "directory" }
|
||||||
end
|
end
|
||||||
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
|
return nil
|
||||||
end,
|
end
|
||||||
load = function(name)
|
|
||||||
if not files[name] then return nil, "no file" end
|
stub.filesystem.load = function(name)
|
||||||
return load(files[name], name)
|
local data = stub.filesystem.read(name)
|
||||||
end,
|
if not data then return nil, "no file" end
|
||||||
getDirectoryItems = function(name)
|
return load(data, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
stub.filesystem.getDirectoryItems = function(name)
|
||||||
local seen, items = {}, {}
|
local seen, items = {}, {}
|
||||||
name = name or ""
|
name = name or ""
|
||||||
-- "" / "/" = save-dir root (RomImporter Android ROM scan)
|
local function addChild(child)
|
||||||
if name == "" or name == "/" then
|
|
||||||
for key in pairs(files) do
|
|
||||||
local child = key:match("^[^/]+")
|
|
||||||
if child and not seen[child] then
|
if child and not seen[child] then
|
||||||
seen[child] = true
|
seen[child] = true
|
||||||
items[#items + 1] = child
|
items[#items + 1] = child
|
||||||
end
|
end
|
||||||
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
|
else
|
||||||
local prefix = name .. "/"
|
local prefix = name .. "/"
|
||||||
for key in pairs(files) do
|
for key in pairs(files) do
|
||||||
if key:sub(1, #prefix) == prefix then
|
if key:sub(1, #prefix) == prefix then
|
||||||
local child = key:sub(#prefix + 1):match("^[^/]+")
|
addChild(key:sub(#prefix + 1):match("^[^/]+"))
|
||||||
if child and not seen[child] then
|
end
|
||||||
seen[child] = true
|
end
|
||||||
items[#items + 1] = child
|
-- 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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(items)
|
table.sort(items)
|
||||||
return items
|
return items
|
||||||
end,
|
end
|
||||||
-- Record mounts for CacheFs.mountVersion tests (NX Blue/Yellow overlay).
|
|
||||||
_mounts = {},
|
|
||||||
mount = function(archive, mountpoint, appendToPath)
|
|
||||||
stub.filesystem._mounts[#stub.filesystem._mounts + 1] = {
|
|
||||||
archive = archive, mountpoint = mountpoint or "",
|
|
||||||
append = appendToPath and true or false,
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
unmount = function() return true end,
|
|
||||||
getSaveDirectory = function() return "/tmp/pokeport-stub-save" end,
|
|
||||||
isFused = function() return false end,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- table-backed SoundData so ChipAudio's offline render seam
|
-- table-backed SoundData so ChipAudio's offline render seam
|
||||||
-- (_renderMusicForTest) runs headless; modkit bounce writes WAVs from it
|
-- (_renderMusicForTest) runs headless; modkit bounce writes WAVs from it
|
||||||
|
|||||||
Reference in New Issue
Block a user