Files
gen1recomp/tests/engine/assets_version_fallback_test.lua
T
Andrew Quenehen 67e6b1fb04 fix(switch): centralize the NX asset fallback in a boot-time loader overlay
The scattered per-call-site prefix rewrites were a parallel track that any
future newImage("assets/generated/...") would silently bypass.  Replace
them with NxAssetOverlay: installed once from love.load on NX only, it
wraps newImage / newImageData / newSource / filesystem.read / getInfo so a
missing assets/generated path falls back to the active version's
blue|yellow copy.  Call sites return to plain love loader calls, and
Assets.resolve goes back to being the platform-free mod-override point.

Two deliberate exceptions remain: the chip-audio worker (separate Lua
state) keeps receiving the prefix explicitly via audio.programPrefix, and
data/generated module loads keep using CacheFs.readActive.

A new guard test (tests/engine/nx_generated_guard_test.lua) fails CI on
any direct love loader call with a literal assets/generated path, so the
class of bug cannot regress by accident.  scripts/test.sh --quick is
green across all tiers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:42:55 -03:00

144 lines
5.1 KiB
Lua

-- NxAssetOverlay: fused love-nx often cannot mount blue|yellow onto
-- assets/generated, so on NX the love loaders are wrapped once at boot and
-- fall back to the versioned save-dir path. Desktop/Android never install
-- the overlay; the chip worker gets the prefix explicitly via the audio
-- payload. Self-contained: luajit tests/engine/assets_version_fallback_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 GameVersion = require("src.core.GameVersion")
local Platform = require("src.core.Platform")
local Assets = require("src.render.Assets")
local Overlay = require("src.core.NxAssetOverlay")
local PNG = "assets/generated/tilesets/reds_house.png"
local savedVersion = GameVersion.get()
local savedSystem = love.system
local function clearPath(path)
love.filesystem.remove(path)
end
local function setOS(osName)
love.system = {
getOS = function() return osName end,
}
Platform._resetForTests()
end
-- --- Assets.resolve stays platform-free: no rewrite even for NX Yellow
setOS("NX")
GameVersion.set("yellow")
love.filesystem.write("yellow/" .. PNG, "yellow-png-bytes")
clearPath(PNG)
eq(Assets.resolve(PNG), PNG,
"resolve is the identity without a mod loader (overlay owns NX fallback)")
-- --- Overlay installed: every loader falls back to the versioned path
Overlay.install()
check(Overlay.isInstalled(), "overlay installs")
local img = love.graphics.newImage(PNG)
eq(img.path, "yellow/" .. PNG, "wrapped newImage receives the yellow/ path")
local id = love.image.newImageData(PNG)
eq(id.path, "yellow/" .. PNG, "wrapped newImageData receives the yellow/ path")
eq(love.filesystem.read(PNG), "yellow-png-bytes",
"wrapped filesystem.read returns the versioned bytes")
check(love.filesystem.getInfo(PNG) ~= nil,
"wrapped getInfo sees the versioned file at the un-prefixed path")
-- Assets.image/imageData benefit transparently (no call-site changes)
Assets.flush()
local aimg = Assets.image(PNG)
eq(aimg.path, "yellow/" .. PNG, "Assets.image loads via the overlay")
-- Non-string arguments pass through untouched
local fromData = love.graphics.newImage(id)
check(fromData ~= nil, "newImage(ImageData) is not rewritten")
-- Non-generated paths pass through untouched
local launcher = love.graphics.newImage("assets/launcher/gear.png")
eq(launcher.path, "assets/launcher/gear.png",
"overlay leaves non-generated paths alone")
-- The real un-prefixed file wins when it exists
love.filesystem.write(PNG, "root-png-bytes")
eq(love.filesystem.read(PNG), "root-png-bytes",
"overlay prefers the real un-prefixed file over the versioned copy")
clearPath(PNG)
-- Blue gets the same treatment
GameVersion.set("blue")
love.filesystem.write("blue/" .. PNG, "blue-png-bytes")
clearPath("yellow/" .. PNG)
eq(love.filesystem.read(PNG), "blue-png-bytes",
"overlay maps generated reads to blue/ for Blue")
-- Red has no prefix: nothing is rewritten
GameVersion.set("red")
clearPath("blue/" .. PNG)
eq(love.filesystem.read(PNG), nil, "Red keeps the stock miss behavior")
-- Uninstall restores the stock loaders byte for byte
GameVersion.set("yellow")
love.filesystem.write("yellow/" .. PNG, "yellow-png-bytes")
Overlay.uninstall()
check(not Overlay.isInstalled(), "overlay uninstalls")
eq(love.filesystem.read(PNG), nil,
"after uninstall the stock loader no longer sees the versioned path")
-- --- ChipSynth honors audio.programPrefix (the worker exception)
local ChipSynth = require("src.core.ChipSynth")
ChipSynth.invalidateBanks()
local PROG = "assets/generated/audio/programs.bin"
local PROG_BYTES = string.rep("\0", 0x4000 * 2)
clearPath(PROG)
love.filesystem.write("yellow/" .. PROG, PROG_BYTES)
local workerData = { audio = {
programFile = PROG,
programPrefix = "yellow/",
bankOrder = { 1, 2 },
} }
local okW, wbanks = pcall(ChipSynth._loadBanksForTest, workerData)
check(okW and wbanks ~= nil, "loadBanks uses audio.programPrefix when set")
if okW and wbanks then
eq(wbanks[1], PROG_BYTES:sub(1, 0x4000),
"programPrefix loads the bank 1 bytes from the versioned file")
end
-- Without programPrefix the sync path relies on the overlay/mount: with the
-- overlay uninstalled (this test process), the plain read misses.
ChipSynth.invalidateBanks()
local plainData = { audio = { programFile = PROG, bankOrder = { 1, 2 } } }
local okP = pcall(ChipSynth._loadBanksForTest, plainData)
check(not okP, "without programPrefix or overlay, programs.bin is a clean miss")
-- --- ChipAudio.slimAudio hands the NX prefix to the worker payload
setOS("NX")
GameVersion.set("yellow")
local ChipAudio = require("src.core.ChipAudio")
local slim = ChipAudio._slimAudioForTest(plainData)
eq(slim.programPrefix, "yellow/",
"slimAudio passes the NX cache prefix to the worker")
setOS("OS X")
local slimDesktop = ChipAudio._slimAudioForTest(plainData)
eq(slimDesktop.programPrefix, nil,
"desktop worker payloads carry no prefix (mount owns the overlay)")
clearPath("yellow/" .. PNG)
clearPath("yellow/" .. PROG)
love.system = savedSystem
Platform._resetForTests()
GameVersion.set(savedVersion)
Assets.flush()
T.finish()