Merge pull request #1523 from bryanthaboi/dev

some bugs and some switch stuff
This commit is contained in:
bryanthaboi
2026-08-18 10:18:58 -04:00
committed by GitHub
37 changed files with 1025 additions and 150 deletions
+27 -4
View File
@@ -97,10 +97,11 @@ 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
-- Leftover un-prefixed Red cache must not shadow the versioned copy
-- (pre-#899 assets/generated at the save-dir root).
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")
eq(love.filesystem.read(PNG), "yellow-png-bytes",
"overlay prefers the versioned copy over leftover unprefixed")
clearPath(PNG)
-- Blue gets the same treatment
@@ -110,7 +111,29 @@ 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
-- Leftover unprefixed font (old Red root) must not beat Gold's copy.
GameVersion.set("gold")
local FONT = "assets/generated/fonts/font.png"
love.filesystem.write(FONT, "stale-red-font")
love.filesystem.write("gold/" .. FONT, "gold-font")
eq(love.filesystem.read(FONT), "gold-font",
"Gold versioned font wins over leftover unprefixed Red font")
clearPath(FONT)
clearPath("gold/" .. FONT)
-- Gold data/generated: fused NX hides gold/maps.lua at the unprefixed path,
-- which is the "Gold cache incomplete / maps.lua Does not exist" crash.
local MAPS = "data/generated/maps.lua"
love.filesystem.write("gold/" .. MAPS, "return { NEW_BARK_TOWN = true }")
local mapsChunk = love.filesystem.load(MAPS)
eq(type(mapsChunk) == "function" and mapsChunk().NEW_BARK_TOWN or nil, true,
"wrapped filesystem.load resolves gold/data/generated/maps.lua")
eq(love.filesystem.read(MAPS), "return { NEW_BARK_TOWN = true }",
"wrapped filesystem.read returns gold maps.lua bytes")
clearPath("gold/" .. MAPS)
-- Red has no empty prefix anymore (cachePrefix is red/), but with no
-- red/ copy the unprefixed miss stays a miss.
GameVersion.set("red")
clearPath("blue/" .. PNG)
eq(love.filesystem.read(PNG), nil, "Red keeps the stock miss behavior")
+11
View File
@@ -49,5 +49,16 @@ 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")
-- Gold-only: same overlay contract (the Switch intro/maps.lua hole)
love.filesystem._mounts = {}
GameVersion.set("gold")
love.filesystem.write("gold/data/generated/maps.lua", "return { ok = true }")
love.filesystem.write("gold/assets/generated/fonts/font.png", "gold-font")
check(CacheFs.mountVersion("gold") == true, "mountVersion(gold) returns true")
eq(love.filesystem.read("assets/generated/fonts/font.png"), "gold-font",
"Gold mount exposes fonts/font.png at the unprefixed path")
eq(love.filesystem.read("data/generated/maps.lua"), "return { ok = true }",
"Gold mount exposes maps.lua at the unprefixed path")
GameVersion.set("red")
T.finish()
@@ -0,0 +1,57 @@
-- Gold on fused NX: gold/data/generated exists, but the overlay mount
-- onto data/generated often fails. Game2/World used to love.filesystem.load
-- the unprefixed path and crash with "Gold cache incomplete / maps.lua
-- Does not exist" after a textless intro. CacheFs.loadActive must read the
-- versioned file without any mount.
-- Self-contained: luajit tests/engine/cache_fs_gold_nx_load_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 CacheFs = require("src.import.CacheFs")
local GameVersion = require("src.core.GameVersion")
local savedVersion = GameVersion.get()
local savedPrefix = CacheFs.prefix
love.filesystem._mounts = {}
GameVersion.set("gold")
CacheFs.prefix = GameVersion.cachePrefix()
love.filesystem.write("gold/data/generated/maps.lua",
"return { NEW_BARK_TOWN = { id = 1 } }")
love.filesystem.write("gold/data/generated/oak_speech.lua",
"return { text = { _OakText1 = 'Hello!' } }")
love.filesystem.write("gold/data/generated/font.lua",
"return { width = 8 }")
-- Unprefixed path is a miss: the NX mount hole.
eq(love.filesystem.read("data/generated/maps.lua"), nil,
"unprefixed maps.lua is missing (mount hole)")
eq(love.filesystem.load("data/generated/maps.lua"), nil,
"love.filesystem.load misses unprefixed maps.lua")
local maps, mapsErr = CacheFs.loadActive("data/generated/maps.lua")
check(maps ~= nil, "loadActive finds gold/data/generated/maps.lua ("
.. tostring(mapsErr) .. ")")
eq(maps and maps.NEW_BARK_TOWN and maps.NEW_BARK_TOWN.id, 1,
"loadActive returns the Gold maps table")
local oak = CacheFs.loadActive("data/generated/oak_speech.lua")
eq(oak and oak.text and oak.text._OakText1, "Hello!",
"loadActive returns oak_speech.lua from gold/")
local font = CacheFs.loadActive("data/generated/font.lua")
eq(font and font.width, 8,
"loadActive returns font.lua from gold/")
love.filesystem.remove("gold/data/generated/maps.lua")
love.filesystem.remove("gold/data/generated/oak_speech.lua")
love.filesystem.remove("gold/data/generated/font.lua")
CacheFs.prefix = savedPrefix
GameVersion.set(savedVersion)
T.finish()
+3
View File
@@ -16,5 +16,8 @@ check(CacheFs.read("data/generated/audio.lua") == nil,
"read is a nil miss headless, not a crash")
check(CacheFs.readActive("data/generated/audio.lua") == nil,
"readActive is a nil miss headless, not a crash")
local loaded, loadErr = CacheFs.loadActive("data/generated/audio.lua")
check(loaded == nil,
"loadActive is a nil miss headless, not a crash (" .. tostring(loadErr) .. ")")
T.finish()
+17
View File
@@ -66,4 +66,21 @@ check(mainSrc:find("isNX", 1, true) ~= nil
and mainSrc:find('require("src.core.NxAssetOverlay").install()', 1, true) ~= nil,
"the overlay install stays gated on Platform.isNX()")
-- Gold Game2 / World must compile data/generated via CacheFs.loadActive so
-- a fused NX boot does not depend on mounting gold/ onto data/generated.
local function readSrc(path)
local fh = io.open(path, "r")
local body = fh and fh:read("*a") or ""
if fh then fh:close() end
return body
end
local game2Src = readSrc("src/core/Game2.lua")
check(game2Src:find("loadActive", 1, true) ~= nil,
"Game2 compiles generated modules through CacheFs.loadActive")
check(game2Src:find("loadActive(path)", 1, true) ~= nil,
"Game2.loadGenerated calls loadActive")
local worldSrc = readSrc("src/world/gen2/World.lua")
check(worldSrc:find("loadActive", 1, true) ~= nil,
"World's on-disk fallback also uses CacheFs.loadActive")
T.finish()
@@ -59,7 +59,7 @@ package.loaded["src.import.RomImporter"] = nil
RomImporter = require("src.import.RomImporter")
local function clearSavesInbox()
for _, ver in ipairs({ "red", "blue", "yellow" }) do
for _, ver in ipairs({ "red", "blue", "yellow", "gold" }) do
local dir = "imports/saves/" .. ver
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
love.filesystem.remove(dir .. "/" .. name)
@@ -135,6 +135,8 @@ check(createdDirs["imports/saves/blue"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/blue/")
check(createdDirs["imports/saves/yellow"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/yellow/")
check(createdDirs["imports/saves/gold"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/gold/")
-- NXSAV-02: notice/hint includes save dir + per-game imports/saves/<version>/ MTP path
ri = freshImporter()
@@ -0,0 +1,30 @@
-- sourceTreeHasData must use each version's required-file list. Gold's
-- cache has no Gen 1 trade art / pikachu.png; validating it against
-- REQUIRED_FILES made a Gold source tree look incomplete forever.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check = T.check
local f = assert(io.open("src/import/RomImporter.lua", "r"))
local src = f:read("*a")
f:close()
local start = src:find("local function sourceTreeHasData", 1, true)
check(start ~= nil, "sourceTreeHasData is defined")
local finish = src:find("\nfunction RomImporter.isReady", start, true)
check(finish ~= nil, "sourceTreeHasData ends before isReady")
local body = src:sub(start, finish)
check(body:find("requiredFilesFor", 1, true) ~= nil,
"sourceTreeHasData uses requiredFilesFor (Gold override, not Gen 1 only)")
check(body:find("ipairs(REQUIRED_FILES)", 1, true) == nil,
"sourceTreeHasData does not iterate the Gen 1 REQUIRED_FILES list raw")
local helperStart = src:find("local function requiredFilesFor", 1, true)
check(helperStart ~= nil, "requiredFilesFor helper exists")
local helper = src:sub(helperStart, start)
check(helper:find("VERSION_REQUIRED_FILES_OVERRIDE", 1, true) ~= nil,
"requiredFilesFor consults VERSION_REQUIRED_FILES_OVERRIDE")
T.finish()
+49
View File
@@ -195,6 +195,55 @@ Studio.addControl()
eq(#Studio.skin.pages[2].controls, 1, "controls land on the active page")
eq(#Studio.skin.pages[1].controls, 0, "and not on the other one")
-- page orientation lock follows the canvas when Match canvas is on (#1503)
session()
Studio.canvasIndex = 1
Studio.matchOrient = true
eq(Studio.cyclePageOrient(1), "portrait", "cycle starts at portrait from unlocked")
eq(Studio.page().orient, "portrait", "and stores the lock on the page")
eq(Studio.page().name, "portrait", "a generic page is renamed so play can auto-rotate")
eq(Studio.canvas().id, "phone_portrait", "and the canvas stays portrait")
Studio.addPage()
eq(Studio.cyclePageOrient(1), "portrait", "the new page starts unlocked, first lock is portrait")
Studio.cyclePageOrient(1)
eq(Studio.page().orient, "landscape", "second cycle is landscape")
eq(Studio.canvas().id, "phone_landscape", "Match canvas flips the mock device with the page")
Studio.pageIndex = 1
Studio.syncCanvasToPage()
eq(Studio.canvas().id, "phone_portrait", "switching back to the portrait page restores portrait canvas")
Studio.setCanvas(2)
eq(Studio.pageIndex, 2, "picking a landscape canvas selects the landscape page")
Studio.matchOrient = false
Studio.pageIndex = 1
Studio.setCanvas(2)
eq(Studio.pageIndex, 1, "Match canvas off leaves the page when the device changes")
eq(Studio.canvas().id, "phone_landscape", "and still honours the canvas click")
-- a RetroArch overlay that already auto-rotates locks itself on open (#1503)
session()
Studio.matchOrient = false
Studio.canvasIndex = 2
Studio.skin = assert(TouchSkin.parse([[
overlays = 2
overlay0_name = "portrait"
overlay0_full_screen = true
overlay0_descs = 1
overlay0_desc0 = "a,0.5,0.5,radial,0.05,0.05"
overlay1_name = "landscape"
overlay1_full_screen = true
overlay1_descs = 1
overlay1_desc0 = "a,0.5,0.5,radial,0.05,0.05"
]]))
Studio.pageIndex = 1
check(Studio.applyImportedOrient(), "import of a portrait/landscape pair is automatic")
check(Studio.matchOrient, "and turns Match canvas on")
eq(Studio.page().name, "landscape", "keeping the landscape canvas already on screen")
eq(Studio.page().orient, "landscape", "with the page already locked")
-- --------------------------------------------------------------- clone
local source = TouchSkin.load("assets/skins/gb_anim", "gb_anim")
+19
View File
@@ -89,6 +89,23 @@ check(probe:find("resolve=yellow/assets/generated/fonts/font.png", 1, true) ~= n
"probe records versioned font path visibility")
check(not probe:find(string.char(0xEA, 0x9B), 1, true),
"probe log contains no ROM-like binary")
check(probe:find("data/generated/maps.lua", 1, true) ~= nil,
"probe records Gold/Gen2 maps.lua visibility")
check(probe:find("loadActive=", 1, true) ~= nil,
"probe uses loadActive for generated Lua instead of newImage")
-- Gold maps.lua / gold/ folders: fused NX intro/naming/overworld crash.
GameVersion.set("gold")
love.filesystem.write("gold/data/generated/maps.lua", "return { NEW_BARK_TOWN = true }")
love.filesystem.write("gold/data/generated/oak_speech.lua", "return {}")
SwitchDiagnostics.probeAssets("gold")
probe = love.filesystem.read("nx-asset-probe.log") or ""
check(probe:find("cachePrefix=gold/", 1, true) ~= nil, "probe records gold prefix")
check(probe:find("data/generated/maps.lua", 1, true) ~= nil,
"probe lists maps.lua (the Gold cache incomplete path)")
check(probe:find("list gold", 1, true) ~= nil, "probe lists gold/ folder")
check(probe:find("list gold/data/generated", 1, true) ~= nil,
"probe lists gold/data/generated/")
love.system = { getOS = function() return "OS X" end }
Platform._resetForTests()
@@ -104,5 +121,7 @@ love.filesystem.remove("nx-asset-probe.log")
love.filesystem.remove("yellow/assets/generated/fonts/font.png")
love.filesystem.remove("yellow/assets/generated/tilesets/reds_house.png")
love.filesystem.remove("yellow/assets/generated/sprites/red.png")
love.filesystem.remove("gold/data/generated/maps.lua")
love.filesystem.remove("gold/data/generated/oak_speech.lua")
T.finish()
+104 -1
View File
@@ -51,6 +51,7 @@ local page = skin.pages[1]
eq(page.name, "shell", "page name")
eq(page.imagePath, "img/back.png", "page background path")
check(page.fullScreen, "full_screen parsed")
check(not page.aspectFromCfg, "a cfg without aspect_ratio does not lock aspect")
eq(page.alphaMod, 0.001, "overlay alpha_mod parsed")
eq(#page.controls, 7, "seven descs parsed")
@@ -109,7 +110,7 @@ local sx, sy, sw, sh, fill = TouchSkin.viewport(W, H)
eq(sx, 0, "viewport x") eq(sy, 0, "viewport y")
eq(sw, 400, "viewport w") eq(sh, 400, "viewport h")
check(fill, "viewport fill flag returned")
eq(TouchSkin.viewport(W, H), 0, "viewport is window-relative, not page-relative")
eq(TouchSkin.viewport(W, H), 0, "without a locked aspect, viewport tracks the window")
check(TouchControls:touchpressed("f1", at(0.80, 0.75)), "press on A is captured")
check(Input:isDown("a"), "skin A presses GB a")
@@ -195,6 +196,8 @@ if bundled then
for _, btn in ipairs({ "a", "b", "start", "select", "up", "down", "left", "right" }) do
check(named[btn], "gb_anim binds GB " .. btn)
end
check(not TouchSkin.hasOrientPair(bundled),
"gb_anim is not a portrait/landscape auto-rotate overlay")
end
local tv = TouchSkin.load("assets/skins/tv_crt", "tv_crt")
@@ -322,4 +325,104 @@ end
TouchSkin.setActive(nil)
TouchControls:setHotkeyHandler(nil)
-- ------------------------------------------ auto-rotate (#1503)
-- RetroArch overlays name a portrait page and a landscape page and wire
-- overlay_next between them. Play has to pick the one that matches the
-- window, or landscape stretches the portrait ranges into wide ovals.
local ORIENT_CFG = [[
overlays = 2
overlay0_name = "portrait"
overlay0_full_screen = true
overlay0_normalized = true
overlay0_aspect_ratio = 0.45
overlay0_descs = 1
overlay0_desc0 = "a,0.84382,0.69563,radial,0.09722,0.04375"
overlay1_name = "landscape"
overlay1_full_screen = true
overlay1_normalized = true
overlay1_aspect_ratio = 2.22222222222222
overlay1_descs = 1
overlay1_desc0 = "a,0.8307031248,0.8614400000,radial,0.0364168752,0.0813300000"
]]
local orient = assert(TouchSkin.parse(ORIENT_CFG))
check(orient.pages[1].aspectFromCfg, "portrait page locks the cfg aspect_ratio")
check(orient.pages[2].aspectFromCfg, "landscape page locks the cfg aspect_ratio")
eq(orient.pages[1].orient, "portrait", "a portrait page name locks on import")
eq(orient.pages[2].orient, "landscape", "a landscape page name locks on import")
check(TouchSkin.hasOrientPair(orient), "the pair is an auto-rotate overlay")
TouchSkin.setActive(orient)
TouchSkin.autoOrient = true
local PW, PH = 720, 1600
TouchSkin.setSurface(0, 0, PW, PH)
eq(TouchSkin.page().name, "portrait", "a tall window picks the portrait page")
local _, _, pHalfW, pHalfH =
TouchSkin.controlGeometry(TouchSkin.page(), TouchSkin.page().controls[1], PW, PH)
eq(math.floor(pHalfW + 0.5), 70, "portrait A half-width follows range_x")
eq(math.floor(pHalfH + 0.5), 70, "portrait A half-height follows range_y")
local LW, LH = 1600, 720
TouchSkin.setSurface(0, 0, LW, LH)
eq(TouchSkin.page().name, "landscape", "a wide window picks the landscape page")
local _, _, lHalfW, lHalfH =
TouchSkin.controlGeometry(TouchSkin.page(), TouchSkin.page().controls[1], LW, LH)
eq(math.floor(lHalfW + 0.5), 58, "landscape A half-width follows the landscape range")
eq(math.floor(lHalfH + 0.5), 59, "landscape A half-height follows the landscape range")
check(math.abs(lHalfW - lHalfH) < 2, "landscape A stays round instead of stretching")
-- 16:9 is not the overlay's 20:9. Letterbox to aspect_ratio so A stays round
-- instead of filling the window and turning into a tall oval (#1503).
TouchSkin.setSurface(0, 0, 1920, 1080)
eq(TouchSkin.page().name, "landscape", "16:9 still picks landscape")
local _, _, boxW, boxH = TouchSkin.pageBox(TouchSkin.page(), 1920, 1080)
eq(math.floor(boxW + 0.5), 1920, "letterbox keeps the 16:9 width")
eq(math.floor(boxH + 0.5), 864, "and the overlay's 20:9 height")
local _, _, sHalfW, sHalfH =
TouchSkin.controlGeometry(TouchSkin.page(), TouchSkin.page().controls[1], 1920, 1080)
check(math.abs(sHalfW - sHalfH) < 2, "A stays round on a 16:9 window")
local nativeOrient = TouchSkin.parseNative(TouchSkin.serialize(orient))
check(nativeOrient and nativeOrient.pages[2].aspectFromCfg,
"native export keeps the aspect lock")
-- an explicit lock wins over the page name, so a studio-authored "main"
-- page can still auto-rotate in play
local LOCK_CFG = [[
overlays = 2
overlay0_name = "main"
overlay0_full_screen = true
overlay0_descs = 1
overlay0_desc0 = "a,0.5,0.5,radial,0.05,0.05"
overlay1_name = "wide"
overlay1_full_screen = true
overlay1_descs = 1
overlay1_desc0 = "a,0.5,0.5,radial,0.05,0.05"
]]
local locked = assert(TouchSkin.parse(LOCK_CFG))
locked.pages[1].orient = "portrait"
locked.pages[2].orient = "landscape"
TouchSkin.setActive(locked)
TouchSkin.setSurface(0, 0, 1600, 720)
eq(TouchSkin.page().name, "wide", "orient lock auto-rotates a page not named landscape")
local roundTrip = TouchSkin.parseNative(TouchSkin.serialize(locked))
eq(roundTrip.pages[1].orient, "portrait", "native export keeps a portrait lock")
eq(roundTrip.pages[2].orient, "landscape", "and a landscape lock")
TouchSkin.setActive(orient)
TouchSkin.autoOrient = false
TouchSkin.pageIndex = 1
eq(TouchSkin.page().name, "portrait",
"the studio can keep a portrait page on a landscape canvas")
TouchSkin.autoOrient = true
TouchSkin.setSurface(nil)
-- pages that are not a portrait/landscape pair (gb_anim) stay put
TouchSkin.setActive(skin)
TouchSkin.setSurface(0, 0, LW, LH)
eq(TouchSkin.page().name, "shell", "a non-oriented skin does not auto-rotate")
TouchSkin.setSurface(nil)
TouchSkin.setActive(nil)
T.finish("touch_skin")