mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
fix(import): mount Blue/Yellow save-dir cache without FFI
NX Play for Blue failed because mountVersion relied on absolute PHYSFS_mount first. Prefer love.filesystem.mount of blue|yellow, overlay generated trees by version prefix, and align CacheFs.prefix in bootGame. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -79,28 +79,26 @@ T19 hardware gate: **closed**. No stuck input, duplicate audio, or crash reporte
|
||||
|
||||
---
|
||||
|
||||
## T24 — fused NRO alone + NRO-only update — PARTIAL / FAIL (Play)
|
||||
## T24 — fused NRO — Red PASS; Blue Play FAIL (open)
|
||||
|
||||
| Field | Value |
|
||||
| ----- | ----- |
|
||||
| Commit tested | `6fb5602` |
|
||||
| Artifact | `dist/switch/gen1recomp-6fb5602-switch.nro` |
|
||||
| Commit (first fused attempt) | `6fb5602` |
|
||||
| Artifact | `gen1recomp-6fb5602-switch.nro` |
|
||||
| SHA-256 | `b019e2e82c7fe6ec3cf4339e1bc71e8752c8140b6242028bb0b662fcf20daac2` |
|
||||
| Deploy | isolated folder, **no** adjacent `game.love` |
|
||||
| MTP round-trip | skipped |
|
||||
| Deploy | isolated folder, no adjacent `game.love` |
|
||||
| Boot fused | **pass** |
|
||||
| ROM import (inbox) | **pass** |
|
||||
| Play after import | **fail** — app closed immediately |
|
||||
| ROM import | **pass** |
|
||||
| Play **Red** (operator follow-up) | **pass** — same import flow as loose; fused not the differentiator |
|
||||
| Play **Blue** (operator follow-up) | **fail** — app closed on Play (was mis-attributed to fused-only) |
|
||||
| NRO-only replace / save survive | **not tested** |
|
||||
|
||||
### lua-error.log (operator)
|
||||
### Revised root cause
|
||||
|
||||
Two events ~1 min apart; body was fully `<redacted>` because `redactString` treated newlines in stack traces as binary. Fix: allow TAB/LF/CR in diagnostics (`SwitchDiagnostics`).
|
||||
Blue/Yellow caches live under `blue/` / `yellow/`. `CacheFs.mountVersion` preferred absolute `PHYSFS_mount(save/blue)` (FFI), which fails on NX; the save-dir-relative `love.filesystem.mount("blue", …)` was only a fallback after that path assumed a usable `base`. Red (`cachePrefix == ""`) never needed that mount → Play OK.
|
||||
|
||||
### Suspected root cause (fix in flight)
|
||||
### Fix follow-up
|
||||
|
||||
Fused `game.love` contains a `data/` tree (scripts). PhysFS does not merge that with save-dir `data/generated/` after import → `Data:load` / `require("data.generated.*")` fails on Play while launcher `isReady` still sees CacheFs files. Loose mode search order differed enough that Play worked earlier.
|
||||
Mount save-dir-relative version folder first; always `mountGeneratedTrees(prefix)` for `blue/data/generated` → `data/generated`; set `CacheFs.prefix` in `bootGame`; Data:load reads version-prefixed cache on require miss.
|
||||
|
||||
Mitigations: `CacheFs.mountVersion` prepend-mounts `data/generated` + `assets/generated`; `Data.loadModule` falls back to `CacheFs.read`+`loadstring`; Boot.run skips on NX (`networkValidated == false`).
|
||||
|
||||
**T24 remains OPEN** until fused Play re-verify + NRO-only save test.
|
||||
**T24**: fused Red OK is evidence for fused boot/import/play(Red). Full T24 (NRO-only update) + Blue Play still need re-verify.
|
||||
|
||||
@@ -177,7 +177,11 @@ local function bootGame(version)
|
||||
-- data, so data/generated + assets/generated resolve to that version's files.
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set(version or os.getenv("POKEPORT_VERSION") or "red")
|
||||
require("src.import.CacheFs").mountVersion(GameVersion.get())
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
-- Keep CacheFs.prefix aligned for any CacheFs.read fallback during Data:load
|
||||
-- (Blue/Yellow caches live under blue/ / yellow/).
|
||||
CacheFs.prefix = GameVersion.cachePrefix()
|
||||
CacheFs.mountVersion(GameVersion.get())
|
||||
if love.window and love.window.setTitle then
|
||||
local Version = require("src.core.Version")
|
||||
love.window.setTitle(Version.title(
|
||||
|
||||
+12
-3
@@ -198,11 +198,20 @@ local function loadModule(dir, name)
|
||||
end
|
||||
local ok, mod = pcall(require, "data.generated." .. name)
|
||||
if ok then return true, mod end
|
||||
-- Fused PhysFS may hide save-dir data/generated behind the archive's
|
||||
-- data/ tree even after mountVersion; load bytes explicitly as fallback.
|
||||
-- Fused PhysFS / Blue|Yellow prefix: load bytes from the active version's
|
||||
-- cache explicitly when require cannot see the mounted tree.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local path = "data/generated/" .. name .. ".lua"
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local prefix = GameVersion.cachePrefix()
|
||||
local path = prefix .. "data/generated/" .. name .. ".lua"
|
||||
local saved = CacheFs.prefix
|
||||
CacheFs.prefix = ""
|
||||
local bytes = CacheFs.read(path)
|
||||
CacheFs.prefix = saved
|
||||
if type(bytes) ~= "string" then
|
||||
-- Also try with CacheFs.prefix if the caller set it for this version.
|
||||
bytes = CacheFs.read("data/generated/" .. name .. ".lua")
|
||||
end
|
||||
if type(bytes) == "string" then
|
||||
local chunk, err = loadstring(bytes, "@" .. path)
|
||||
if not chunk then return false, err or mod end
|
||||
|
||||
+24
-29
@@ -361,18 +361,15 @@ end
|
||||
|
||||
-- Overlay the active version's extracted cache onto the un-prefixed read
|
||||
-- paths, so require("data.generated.*") and love.graphics.newImage(
|
||||
-- "assets/generated/*") resolve to that version's files. Red lives at the
|
||||
-- cache root and needs nothing; non-Red versions (blue/, yellow/, …) are
|
||||
-- *prepended* so they win over any Red copy at the root and over the game
|
||||
-- source. Called once at boot, before Game:load (main.lua). Returns true
|
||||
-- when nothing was needed or the mount succeeded.
|
||||
-- "assets/generated/*") resolve to that version's files.
|
||||
--
|
||||
-- Fused builds also ship a `data/` tree (scripts, palettes) inside game.love.
|
||||
-- PhysFS does not merge directories across archives: that `data/` can hide
|
||||
-- `data/generated/` written to the save directory after ROM import. Loose
|
||||
-- play often still works (search order / mount layout differs); fused NX
|
||||
-- Play-after-import then fails Data:load with a missing-module error.
|
||||
-- Explicitly prepend-mount the generated subtrees so they win.
|
||||
-- Non-Red versions live under blue/ / yellow/ in the save directory. On
|
||||
-- desktop fused+portable we PHYSFS_mount that folder by absolute path. On
|
||||
-- NX (and any host without a working FFI mount) love.filesystem.mount of
|
||||
-- the save-dir-relative name must succeed, or Play boots with Red's paths
|
||||
-- and Data:load dies. Always also prepend-mount the version's
|
||||
-- data/generated + assets/generated onto the un-prefixed paths so PhysFS
|
||||
-- directory non-merge (archive data/ vs save generated) cannot hide them.
|
||||
local function mountGeneratedTrees(prefix)
|
||||
prefix = prefix or ""
|
||||
if not (love and love.filesystem and love.filesystem.mount) then
|
||||
@@ -396,31 +393,29 @@ end
|
||||
|
||||
function CacheFs.mountVersion(version)
|
||||
local prefix = require("src.core.GameVersion").cachePrefix(version)
|
||||
if prefix == "" then
|
||||
mountGeneratedTrees("")
|
||||
return true
|
||||
local sub = prefix:gsub("/+$", "")
|
||||
|
||||
-- Save-dir relative mount first (NX / no-FFI). Prepend so blue|yellow win.
|
||||
if sub ~= "" and love.filesystem.mount
|
||||
and love.filesystem.getInfo(sub, "directory") then
|
||||
love.filesystem.mount(sub, "", false)
|
||||
end
|
||||
local sub = prefix:gsub("/+$", "") -- "blue/" / "yellow/" -> bare dir
|
||||
-- The cache root is the portable game folder when active, else LÖVE's OS
|
||||
-- save directory (where love.filesystem wrote blue/... or yellow/...).
|
||||
|
||||
-- Portable / desktop fused: absolute PHYSFS_mount of the version folder.
|
||||
if sub ~= "" then
|
||||
local base = CacheFs.root()
|
||||
if not base and love.filesystem.getSaveDirectory then
|
||||
base = love.filesystem.getSaveDirectory()
|
||||
end
|
||||
if not base then return false end
|
||||
if mountReadable(base .. SEP .. sub, false) then
|
||||
mountGeneratedTrees("")
|
||||
if base then
|
||||
mountReadable(base .. SEP .. sub, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Version-scoped generated trees → un-prefixed paths (Red prefix is "").
|
||||
mountGeneratedTrees(prefix)
|
||||
return true
|
||||
end
|
||||
-- Fallback when FFI/PHYSFS_mount is unavailable: LÖVE can mount a folder
|
||||
-- that lives in the save directory by name (prepended: appendToPath=false).
|
||||
if love.filesystem.mount then
|
||||
local ok = love.filesystem.mount(sub, "", false)
|
||||
mountGeneratedTrees("")
|
||||
return ok
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Undo mountVersion. A process normally mounts exactly one version and then
|
||||
-- boots it, but the launcher can open the save editor on a Blue/Yellow save,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- Blue/Yellow mountVersion must overlay save-dir caches without FFI (NX).
|
||||
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 CacheFs = require("src.import.CacheFs")
|
||||
|
||||
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")
|
||||
|
||||
check(CacheFs.mountVersion("blue") == true, "mountVersion(blue) returns true")
|
||||
|
||||
local sawBlueRoot, sawDataGen, sawAssetsGen = false, false, false
|
||||
for _, m in ipairs(love.filesystem._mounts) do
|
||||
if m.archive == "blue" and m.mountpoint == "" and m.append == false then
|
||||
sawBlueRoot = true
|
||||
end
|
||||
if m.archive == "blue/data/generated" and m.mountpoint == "data/generated"
|
||||
and m.append == false then
|
||||
sawDataGen = true
|
||||
end
|
||||
if m.archive == "blue/assets/generated" and m.mountpoint == "assets/generated"
|
||||
and m.append == false then
|
||||
sawAssetsGen = true
|
||||
end
|
||||
end
|
||||
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")
|
||||
|
||||
T.finish()
|
||||
@@ -160,6 +160,18 @@ stub.filesystem = {
|
||||
table.sort(items)
|
||||
return items
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user