fix(android): keep Gen1 Game.load after in-process EXIT GAME

Stop fanning out arbitrary field:release() during Game/Game2 reset — shared
modules use :release as a handle API, and that teardown left the Gen1
singleton unbootable (Game:load nil) on Play-again. Harden bootGame and
endGameSession to rebuild the module if load is missing.
This commit is contained in:
1jamie
2026-08-24 14:33:43 -05:00
parent 0142468125
commit d76d173b60
6 changed files with 79 additions and 17 deletions
@@ -99,4 +99,19 @@ do
check(type(Game.load) == "function", "Game:reset keeps methods")
end
-- 6. Play-again after endGameSession: Game.load must still be callable
-- (Android crash: main.lua bootGame → Game:load with load == nil)
do
local Game = require("src.core.Game")
local SessionLifecycle = require("src.core.SessionLifecycle")
Game.save = {}
Game.stack = { clear = function() end }
-- Mimic a shared net module parked on the singleton (handle-style release).
Game.net = { release = function(id) end }
SessionLifecycle.endGameSession(Game)
local again = require("src.core.Game")
check(type(again.load) == "function",
"Play-again can call Game:load after endGameSession")
end
T.finish("android_exit_to_launcher_test")
@@ -28,8 +28,15 @@ do
Game.network = { live = true } -- future field: must not need a whitelist
Game.stack = StateStack
Game.renderer = Renderer
Game.SKIN_FAST_FORWARD = 4
Renderer.canvas = love.graphics.newCanvas(8, 8)
-- Handle-style :release (Fetch/SyncClient) must not run as instance teardown.
local shared = {
release = function(self) self.killed = true end,
}
Game.sharedNet = shared
Game:reset()
check(type(Game.load) == "function", "Game:reset keeps methods")
@@ -40,6 +47,29 @@ do
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")
check(shared.killed ~= true,
"Game:reset does not call handle-style :release on session fields")
check(Game.SKIN_FAST_FORWARD == 4,
"Game:reset preserves module scalars like SKIN_FAST_FORWARD")
end
-- ---- endGameSession must leave Gen1 Game.load callable for Play-again ------
do
Game.save = { money = 1 }
Game.stack = { clear = function() end }
-- Poison pattern from the Android crash: a field whose :release is a
-- job-handle API. Old reset called it as value:release() and could leave
-- the singleton unbootable (Game.load nil → main.lua bootGame crash).
local jobs = {}
Game.linkFetch = {
release = function(id) jobs[id] = nil end,
}
SessionLifecycle.endGameSession(Game)
check(type(Game.load) == "function",
"endGameSession leaves Game.load intact for the next bootGame")
check(package.loaded["src.core.Game"] == Game
or type((package.loaded["src.core.Game"] or {}).load) == "function",
"Gen1 Game module remains require-able after endGameSession")
end
-- ---- Game2:reset releases world GPU and present canvases ------------------