mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
Implement session teardown and resource management for in-process transitions
- Introduced `teardownMountedSession` to handle cleanup of mounted versions, generated data, and mod states during transitions between the editor and launcher. - Added `flushEditorPackageLoaded` to evict save-editor modules from `package.loaded` dynamically, ensuring a clean state for subsequent sessions. - Implemented `Game:reset` and `Game2:reset` methods to clear session-specific fields, allowing for a fresh start when returning to the launcher. - Enhanced `Renderer` and `TileRenderer` to release GPU resources immediately, preventing memory leaks during rapid transitions. - Updated `MagnetTrainRide` to support OAM priority overlays and manage background rendering with new shader functionality. This commit improves the stability and performance of the application during in-process transitions, particularly on Android. fixes Yellow color palette for title screen fixes #1662 #1643 #1536 and finishes fixing #1597
This commit is contained in:
@@ -77,4 +77,26 @@ do
|
||||
check(Runtime.modRequire == nil, "Runtime.reset clears modRequire")
|
||||
end
|
||||
|
||||
-- 4. returnToLauncher / closeEditor also clear Assets.loader (orphaned Loader)
|
||||
do
|
||||
local Assets = require("src.render.Assets")
|
||||
Assets.installLoader({
|
||||
overrideOrder = function() return { { id = "x", path = "mods/x" } } end,
|
||||
derivedPath = function() return nil end,
|
||||
})
|
||||
check(Assets.loader ~= nil, "Assets.loader installed for the session")
|
||||
Assets.installLoader(nil)
|
||||
check(Assets.loader == nil, "session teardown clears Assets.loader")
|
||||
end
|
||||
|
||||
-- 5. Gen1 Game:reset exists so main.lua need not guess field names
|
||||
do
|
||||
local Game = require("src.core.Game")
|
||||
check(type(Game.reset) == "function", "Game:reset is defined for in-process return")
|
||||
Game.mods = { stale = true }
|
||||
Game:reset()
|
||||
check(Game.mods == nil, "Game:reset clears session fields")
|
||||
check(type(Game.load) == "function", "Game:reset keeps methods")
|
||||
end
|
||||
|
||||
T.finish("android_exit_to_launcher_test")
|
||||
|
||||
@@ -57,6 +57,36 @@ eq(love.filesystem.read("assets/generated/fonts/font.png"), "font-bytes",
|
||||
eq(love.filesystem.read("data/generated/constants.lua"), "return {}",
|
||||
"post-mount probe reads red data at the un-prefixed path")
|
||||
|
||||
-- unmountVersion peels generated overlays (LIFO) then the version folder, so
|
||||
-- a later Play/Edit on another game cannot resolve this version's files.
|
||||
do
|
||||
local mounts = love.filesystem._mounts
|
||||
local archives = {}
|
||||
for _, m in ipairs(mounts) do archives[#archives + 1] = m.archive end
|
||||
local function indexOf(name)
|
||||
for i, a in ipairs(archives) do if a == name then return i end end
|
||||
end
|
||||
local dataIdx = indexOf("red/data/generated")
|
||||
local assetsIdx = indexOf("red/assets/generated")
|
||||
check(dataIdx and assetsIdx, "mountVersion recorded generated-tree overlays")
|
||||
check(assetsIdx > dataIdx,
|
||||
"assets/generated was mounted after data/generated (LIFO unmount peels it first)")
|
||||
end
|
||||
check(CacheFs.unmountVersion("red") == true, "unmountVersion(red) returns true")
|
||||
check(love.filesystem.read("assets/generated/fonts/font.png") == nil,
|
||||
"post-unmount probe no longer sees red assets at the un-prefixed path")
|
||||
check(love.filesystem.read("data/generated/constants.lua") == nil,
|
||||
"post-unmount probe no longer sees red data at the un-prefixed path")
|
||||
do
|
||||
local left = {}
|
||||
for _, m in ipairs(love.filesystem._mounts) do
|
||||
if tostring(m.archive):find("red", 1, true) then
|
||||
left[#left + 1] = m.archive
|
||||
end
|
||||
end
|
||||
eq(#left, 0, "no red/ mounts remain after unmountVersion")
|
||||
end
|
||||
|
||||
-- no legacy cache at all: migration is a no-op, not an error
|
||||
love.filesystem.remove("red/rom-cache.complete")
|
||||
love.filesystem.remove("red/data/generated/maps.lua")
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
-- In-process launcher session teardown: Game:reset, Renderer canvas release,
|
||||
-- Runtime/Assets/LegacyCompat cleanup, and editor package.loaded discovery flush.
|
||||
-- luajit tests/engine/launcher_session_teardown_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Assets = require("src.render.Assets")
|
||||
local LegacyCompat = require("src.mods.LegacyCompat")
|
||||
local Game = require("src.core.Game")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
|
||||
-- ---- Game:reset drops instance state, keeps methods ----------------------
|
||||
do
|
||||
StateStack:init()
|
||||
StateStack:push({ name = "stale" })
|
||||
Game.mods = { id = "orphan" }
|
||||
Game.save = { money = 1 }
|
||||
Game.network = { live = true } -- future field: must not need a whitelist
|
||||
Game.stack = StateStack
|
||||
Game.renderer = Renderer
|
||||
Renderer.canvas = love.graphics.newCanvas(8, 8)
|
||||
|
||||
Game:reset()
|
||||
|
||||
check(type(Game.load) == "function", "Game:reset keeps methods")
|
||||
check(type(Game.reset) == "function", "Game:reset keeps itself")
|
||||
check(Game.mods == nil, "Game:reset clears mods")
|
||||
check(Game.save == nil, "Game:reset clears save")
|
||||
check(Game.network == nil, "Game:reset clears arbitrary future fields")
|
||||
check(Game.stack == nil, "Game:reset clears stack reference")
|
||||
check(Game.renderer == nil, "Game:reset clears renderer reference")
|
||||
check(StateStack:top() == nil, "Game:reset cleared the shared StateStack")
|
||||
end
|
||||
|
||||
-- ---- Renderer:init releases prior canvases before realloc ----------------
|
||||
do
|
||||
local first = love.graphics.newCanvas(16, 16)
|
||||
Renderer.canvas = first
|
||||
Renderer.battleHUDCanvas = love.graphics.newCanvas(16, 16)
|
||||
Renderer.worldCanvas = love.graphics.newCanvas(16, 16)
|
||||
Renderer.uprightCanvas = love.graphics.newCanvas(16, 16)
|
||||
Renderer:init()
|
||||
check(first.released == true,
|
||||
"Renderer:init Object:release()s the previous primary canvas")
|
||||
check(Renderer.canvas ~= nil and Renderer.canvas ~= first,
|
||||
"Renderer:init allocates a fresh primary canvas")
|
||||
check(Renderer.canvas.released ~= true,
|
||||
"the new primary canvas is not released")
|
||||
-- second init also releases the one just created
|
||||
local second = Renderer.canvas
|
||||
Renderer:init()
|
||||
check(second.released == true,
|
||||
"a second Renderer:init releases the canvas from the prior init")
|
||||
end
|
||||
|
||||
-- ---- Shared singleton teardown contract (closeEditor / returnToLauncher)
|
||||
do
|
||||
Runtime.install({ emit = function() end }, { call = function() end }, { "e" })
|
||||
Assets.installLoader({
|
||||
overrideOrder = function() return {} end,
|
||||
derivedPath = function() return nil end,
|
||||
})
|
||||
LegacyCompat.reports = { some_mod = { order = {} } }
|
||||
|
||||
-- Mirrors main.lua teardownMountedSession without mounting CacheFs.
|
||||
require("src.core.Data"):unloadGenerated()
|
||||
Runtime.reset()
|
||||
Assets.installLoader(nil)
|
||||
LegacyCompat.reset()
|
||||
|
||||
check(Runtime.errors == nil, "teardown clears Runtime.errors")
|
||||
check(Assets.loader == nil, "teardown clears Assets.loader")
|
||||
eq(next(LegacyCompat.reports), nil, "teardown clears LegacyCompat.reports")
|
||||
end
|
||||
|
||||
-- ---- Editor package.loaded discovery flush (no panel whitelist) ---------
|
||||
do
|
||||
love.filesystem.write("tools/save-editor/App.lua", "return {}")
|
||||
love.filesystem.write("tools/save-editor/panels/NewPanel.lua", "return {}")
|
||||
package.loaded["App"] = { stale = true }
|
||||
package.loaded["NewPanel"] = { stale = true }
|
||||
package.loaded["src.core.Data"] = package.loaded["src.core.Data"] -- keep
|
||||
|
||||
local function isEditorFlat(name)
|
||||
if name:find("[./]") then return false end
|
||||
return love.filesystem.getInfo("tools/save-editor/" .. name .. ".lua") ~= nil
|
||||
or love.filesystem.getInfo("tools/save-editor/panels/" .. name .. ".lua") ~= nil
|
||||
end
|
||||
for k in pairs(package.loaded) do
|
||||
if type(k) == "string"
|
||||
and (k:find("save%-editor", 1, false) or isEditorFlat(k)) then
|
||||
package.loaded[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
check(package.loaded["App"] == nil, "discovery flush drops flat App")
|
||||
check(package.loaded["NewPanel"] == nil,
|
||||
"discovery flush drops a new panel without a hardcoded list")
|
||||
check(package.loaded["src.core.Data"] ~= nil,
|
||||
"discovery flush leaves engine modules alone")
|
||||
love.filesystem.remove("tools/save-editor/App.lua")
|
||||
love.filesystem.remove("tools/save-editor/panels/NewPanel.lua")
|
||||
end
|
||||
|
||||
T.finish("launcher_session_teardown_test")
|
||||
@@ -266,6 +266,18 @@ do
|
||||
"and with no hook at all the special is a no-op, not a clobber")
|
||||
end
|
||||
|
||||
-- ---- MagnetTrainRide UI screen --------------------------------------------
|
||||
do
|
||||
local MagnetTrainRide = require("src.ui.gen2.MagnetTrainRide")
|
||||
check(MagnetTrainRide.isOpaque, "MagnetTrainRide is opaque")
|
||||
local ride = MagnetTrainRide.new(nil, { toGoldenrod = false })
|
||||
check(ride:wantsFillScale(), "wantsFillScale returns true")
|
||||
check(ride:drawsWidescreen(), "drawsWidescreen returns true")
|
||||
check(type(ride.drawBands) == "function", "drawBands exists")
|
||||
check(type(ride.backgrounds) == "function", "backgrounds exists for OAM_PRIO")
|
||||
end
|
||||
|
||||
|
||||
-- ---- the arrival, from `warpcheck` to the officer's line -------------------
|
||||
--
|
||||
-- The ride is only half the feature: the two station scripts end
|
||||
|
||||
+3
-1
@@ -56,7 +56,9 @@ stub.graphics = {
|
||||
end,
|
||||
newQuad = function(x, y, w, h) return { x = x, y = y, w = w, h = h } end,
|
||||
newCanvas = function(w, h)
|
||||
return setmetatable({ w = w, h = h, setFilter = noop }, Image)
|
||||
local canvas = setmetatable({ w = w, h = h, setFilter = noop, released = false }, Image)
|
||||
function canvas:release() self.released = true end
|
||||
return canvas
|
||||
end,
|
||||
newSpriteBatch = function(image, size)
|
||||
local batch = { image = image, sprites = {} }
|
||||
|
||||
@@ -143,6 +143,28 @@ local ok, err = pcall(function()
|
||||
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
||||
"validate keeps the modded mon while the mod is enabled")
|
||||
|
||||
-- Second editor session in the same process (launcher Close → Edit again):
|
||||
-- host teardown must leave Runtime/Assets clean and Data unloaded so the
|
||||
-- next App.load remounts mods instead of serving vanilla catalogs.
|
||||
App.unload()
|
||||
if Data._pristineKeys or Data.pokemon then Data:unloadGenerated() end
|
||||
Runtime.reset()
|
||||
Assets.installLoader(nil)
|
||||
package.loaded["App"] = nil
|
||||
package.loaded["Catalog"] = nil
|
||||
App = require("App")
|
||||
App.load(tmpPath)
|
||||
S = App.getState()
|
||||
loaded = S.mods:status().loaded
|
||||
check(#loaded == 1 and loaded[1].id == "zz_editor_fixture",
|
||||
"fixture mod loads again after host-style teardown")
|
||||
hasSpecies = false
|
||||
for _, id in ipairs(S.cat.species) do
|
||||
if id == "EDITMON" then hasSpecies = true end
|
||||
end
|
||||
check(hasSpecies, "second App.load catalog still carries the mod's mon")
|
||||
check(Data.pokemon.EDITMON ~= nil, "second merge lands EDITMON in Data again")
|
||||
|
||||
-- Gold-targeted mods: spa/spd records are usable, and a Gen 1-only
|
||||
-- manifest stays out of Gold (no ROM cache / App.load("gold") required).
|
||||
local ModTargets = require("src.mods.ModTargets")
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local S = require("tests.harness").suite("yellow title palette")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local TitleState = require("src.ui.TitleState")
|
||||
|
||||
GameVersion.set("yellow")
|
||||
local prevMode = PaletteFX.mode
|
||||
|
||||
-- Test 1: SGB mode
|
||||
PaletteFX.setMode("gbc")
|
||||
local mewmonSgb = PaletteFX.pal(nil, "MEWMON")
|
||||
check(mewmonSgb ~= nil, "MEWMON palette exists in SGB mode on Yellow")
|
||||
-- In Yellow SGB SuperPalettes: MEWMON color 1 is Yellow {255, 247, 181}, color 2 is Red {222, 132, 132}
|
||||
eq(mewmonSgb[1][1], 255, "SGB: Color 0 is white R=255")
|
||||
eq(mewmonSgb[2][1], 255, "SGB: Color 1 is yellow R=255")
|
||||
eq(mewmonSgb[3][1], 222, "SGB: Color 2 is red R=222")
|
||||
|
||||
-- Test 2: ADVANCED mode (redpp)
|
||||
PaletteFX.setMode("redpp")
|
||||
local mewmonAdv = PaletteFX.pal(nil, "MEWMON")
|
||||
check(mewmonAdv ~= nil, "MEWMON palette exists in ADVANCED mode on Yellow")
|
||||
-- Must NOT be Red++'s Mew purple {115, 33, 165}! It must be Yellow's Pikachu palette!
|
||||
eq(mewmonAdv[1][1], 255, "ADVANCED: Color 0 is white R=255 (Pikachu eye sclera)")
|
||||
eq(mewmonAdv[2][1], 255, "ADVANCED: Color 1 is yellow R=255 (Pikachu body)")
|
||||
check(mewmonAdv[3][3] < 150, "ADVANCED: Color 2 is not purple (B < 150, red cheeks)")
|
||||
|
||||
-- Test 3: OG YELLOW mode (ogred on Yellow)
|
||||
PaletteFX.setMode("ogred")
|
||||
local mewmonOg = PaletteFX.pal(nil, "MEWMON")
|
||||
check(mewmonOg ~= nil, "MEWMON palette exists in OG YELLOW mode")
|
||||
eq(mewmonOg[1][1], 255, "OG YELLOW: Color 0 is white")
|
||||
eq(mewmonOg[2][1], 255, "OG YELLOW: Color 1 is yellow")
|
||||
|
||||
-- Test 4: TitleState sgbPalettes
|
||||
local fakeGame = { data = { palettes = PaletteFX.yellowPack() } }
|
||||
local ts = { yellowLayout = true }
|
||||
local zones = TitleState.sgbPalettes(ts, fakeGame)
|
||||
check(zones ~= nil and #zones >= 3, "TitleState produces zones for Yellow")
|
||||
eq(zones[1].colors, PaletteFX.pal(fakeGame.data, "LOGO2"), "Zone 1 uses LOGO2")
|
||||
eq(zones[2].colors, PaletteFX.pal(fakeGame.data, "MEWMON"), "Zone 2 uses MEWMON (Pikachu yellow/red)")
|
||||
|
||||
PaletteFX.setMode(prevMode)
|
||||
GameVersion.set("red")
|
||||
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user