From e4ce1063a1ad98c7fc3d55f5c135cc28ce960a1d Mon Sep 17 00:00:00 2001 From: Andrew Quenehen Date: Mon, 3 Aug 2026 15:19:15 -0300 Subject: [PATCH] fix(switch): extend NX-only asset prefix to audio and title/intro art The previous NX gate only rewrote image paths that go through Assets.resolve. Pokemon Yellow still had no sound and a blank title screen because: - ChipSynth reads programs.bin directly via love.filesystem.read, bypassing Assets. On NX the unprefixed path is missing when the mount overlay fails, so the engine never built and every song/SFX was silent. - Sound.playPikaCry loads pika_cries WAVs with love.audio.newSource, also bypassing Assets.resolve. - TitleState, YellowIntro, and IntroMovie call love.graphics.newImage directly on unprefixed assets/generated paths, so the Pikachu title and intro atlases failed to load. Fix: apply the same NX-only prefix rewrite in those four places. Desktop/Android keep the existing mountVersion overlay behavior. Also add ChipSynth._loadBanksForTest and tests covering the new paths. Co-authored-by: Cursor --- src/core/ChipSynth.lua | 18 +++++- src/core/Sound.lua | 7 +++ src/ui/IntroMovie.lua | 2 +- src/ui/TitleState.lua | 2 +- src/ui/YellowIntro.lua | 2 +- tests/engine/assets_version_fallback_test.lua | 61 +++++++++++++++++++ 6 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/core/ChipSynth.lua b/src/core/ChipSynth.lua index fb29c218..1115cc36 100644 --- a/src/core/ChipSynth.lua +++ b/src/core/ChipSynth.lua @@ -123,7 +123,18 @@ local function loadBanks(data) if cachedProgramFile == audio.programFile and cachedBanks then return cachedBanks end - local raw, readError = love.filesystem.read(audio.programFile) + local raw, readError + -- NX-only: Blue/Yellow live under a versioned save-dir prefix; desktop + -- relies on mountVersion overlay. Read the prefixed path when present. + if require("src.core.Platform").isNX() then + local prefix = require("src.core.GameVersion").cachePrefix() + if prefix ~= "" then + raw, readError = love.filesystem.read(prefix .. audio.programFile) + end + end + if not raw then + raw, readError = love.filesystem.read(audio.programFile) + end if not raw then error("could not read sound programs: " .. tostring(readError)) end local banks = {} for index, bank in ipairs(audio.bankOrder) do @@ -140,6 +151,11 @@ function ChipSynth.invalidateBanks() cachedProgramFile, cachedBanks = nil, nil end +-- test-only: exercise loadBanks without building a full engine +function ChipSynth._loadBanksForTest(data) + return loadBanks(data) +end + -- A def-local program (ChipAsm output) is mounted as pseudo-bank 0 next to -- the ROM banks, so the 0x4000-window byte reader and every call/loop -- target work unchanged. The ROM's own cached bank table is never touched diff --git a/src/core/Sound.lua b/src/core/Sound.lua index 2c5deb14..46cdc138 100644 --- a/src/core/Sound.lua +++ b/src/core/Sound.lua @@ -269,6 +269,13 @@ function Sound.playPikaCry(data, n) if src == false then return nil end if not src then local path = ("assets/generated/audio/pika_cries/cry_%02d.wav"):format(n) + -- NX-only: Blue/Yellow live under a versioned save-dir prefix. + if require("src.core.Platform").isNX() then + local prefix = require("src.core.GameVersion").cachePrefix() + if prefix ~= "" and love.filesystem.getInfo(prefix .. path) then + path = prefix .. path + end + end local ok, s = pcall(love.audio.newSource, path, "static") if not ok or not s then cache[key] = false diff --git a/src/ui/IntroMovie.lua b/src/ui/IntroMovie.lua index 15f14be2..3445a3c4 100644 --- a/src/ui/IntroMovie.lua +++ b/src/ui/IntroMovie.lua @@ -136,7 +136,7 @@ local FIGHT_SCRIPT = { local function tryImage(path) if not path then return nil end - local ok, img = pcall(love.graphics.newImage, path) + local ok, img = pcall(love.graphics.newImage, require("src.render.Assets").resolve(path)) return ok and img or nil end diff --git a/src/ui/TitleState.lua b/src/ui/TitleState.lua index 9f293f42..2a71845e 100644 --- a/src/ui/TitleState.lua +++ b/src/ui/TitleState.lua @@ -100,7 +100,7 @@ local CYCLE_FRAMES = 240 -- the original waits ~4s between picks local function tryImage(path) if not path then return nil end - local ok, img = pcall(love.graphics.newImage, path) + local ok, img = pcall(love.graphics.newImage, require("src.render.Assets").resolve(path)) return ok and img or nil end diff --git a/src/ui/YellowIntro.lua b/src/ui/YellowIntro.lua index 0cfc1446..30e5cd80 100644 --- a/src/ui/YellowIntro.lua +++ b/src/ui/YellowIntro.lua @@ -220,7 +220,7 @@ local function bobOffset(phase) end local function tryImage(path) - local ok, img = pcall(love.graphics.newImage, path) + local ok, img = pcall(love.graphics.newImage, require("src.render.Assets").resolve(path)) return ok and img or nil end diff --git a/tests/engine/assets_version_fallback_test.lua b/tests/engine/assets_version_fallback_test.lua index 64b7d8b5..6f777205 100644 --- a/tests/engine/assets_version_fallback_test.lua +++ b/tests/engine/assets_version_fallback_test.lua @@ -95,6 +95,67 @@ local luaBytes = CacheFs.readActive("data/generated/maps.lua") check(type(luaBytes) == "string" and luaBytes:find("ok", 1, true), "readActive still finds yellow/data/generated when CacheFs.prefix is set") +-- ChipSynth.loadBanks: NX prefers the versioned prefix; desktop untouched +setOS("NX") +GameVersion.set("yellow") +local PROG = "assets/generated/audio/programs.bin" +local PROG_BYTES = string.rep("\0", 0x4000 * 2) +love.filesystem.write("yellow/" .. PROG, PROG_BYTES) +clearPath(PROG) +local ChipSynth = require("src.core.ChipSynth") +ChipSynth.invalidateBanks() +local progData = { audio = { programFile = PROG, bankOrder = { 1, 2 } } } +local okB, banks = pcall(ChipSynth._loadBanksForTest, progData) +check(okB and banks ~= nil, "NX loadBanks reads yellow/programs.bin") +if okB and banks then + eq(banks[1], PROG_BYTES:sub(1, 0x4000), "loadBanks returns the bank 1 bytes") +end + +-- Sound.playPikaCry: NX rewrites the pika-cry path before newSource +setOS("NX") +GameVersion.set("yellow") +local Sound = require("src.core.Sound") +local CRY = "assets/generated/audio/pika_cries/cry_01.wav" +love.filesystem.write("yellow/" .. CRY, "RIFF\x24\x00\x00\x00WAVEfmt ") +clearPath(CRY) +local lastNewSource +local savedAudio = love.audio +love.audio = { + newSource = function(path, mode) + lastNewSource = path + return setmetatable({ + stop = function() end, + play = function() end, + setVolume = function() end, + }, { __index = function() return function() end end }) + end, +} +Sound.invalidate("pikacry:1") +local cryData = { audio = { pikaCries = 1 } } +local src = Sound.playPikaCry(cryData, 1) +love.audio = savedAudio +eq(lastNewSource, "yellow/" .. CRY, "NX playPikaCry loads yellow/pika_cries") + +-- TitleState/YellowIntro/IntroMovie use Assets.resolve (NX prefix); a static +-- source check keeps them from regressing to raw newImage(path). +local function srcHasResolve(path) + local f = io.open(path, "r") + if not f then return false end + local body = f:read("*a") + f:close() + return body:find("Assets%.resolve", 1, false) ~= nil + or body:find('require%("src%.render%.Assets"%)%.resolve', 1, false) ~= nil +end +check(srcHasResolve("src/ui/TitleState.lua"), + "TitleState loads art via Assets.resolve") +check(srcHasResolve("src/ui/YellowIntro.lua"), + "YellowIntro loads art via Assets.resolve") +check(srcHasResolve("src/ui/IntroMovie.lua"), + "IntroMovie loads art via Assets.resolve") + +clearPath("yellow/" .. PROG) +clearPath("yellow/" .. CRY) + love.system = savedSystem Platform._resetForTests() CacheFs.prefix = savedPrefix