Merge pull request #1786 from 1Jamie/more-android-lc

fix(android): keep Gen1 Game.load after in-process EXIT GAME
This commit is contained in:
bryanthaboi
2026-08-25 08:10:21 -04:00
committed by GitHub
6 changed files with 79 additions and 17 deletions
+8 -1
View File
@@ -384,7 +384,14 @@ function bootGame(version, cartId)
Game = require("src.core.Game2").new()
Game:load()
else
Game = require("src.core.Game")
-- Gen1 Game is a module singleton. In-process EXIT GAME resets it in
-- place; if a prior teardown left load missing, rebuild from source.
local gameMod = require("src.core.Game")
if type(gameMod.load) ~= "function" then
package.loaded["src.core.Game"] = nil
gameMod = require("src.core.Game")
end
Game = gameMod
Game:load()
if os.getenv("POKEPORT_AUTOPILOT") then
autopilot = require("tests.autopilot")
+14 -9
View File
@@ -1387,8 +1387,13 @@ end
-- Drop every session-owned field so the next Game:load() starts clean when
-- the process returns to the launcher in-place (Android / intent_game).
-- main.lua must not guess field names: new systems (Game.network, …) are
-- cleared automatically because only functions (methods) are kept.
--
-- Gen1 Game is a MODULE SINGLETON (methods live on this table). Never fan
-- out arbitrary field:release() here: session fields can hold shared modules
-- (Fetch, SyncClient, …) whose :release is a job-handle API, not instance
-- teardown -- calling them as value:release() corrupts process state and has
-- been observed to leave Game.load nil after EXIT GAME on Android.
-- Explicit GPU owners are released below; everything else is just dropped.
function Game:reset()
if self.stack and self.stack.clear then
pcall(function() self.stack:clear() end)
@@ -1401,23 +1406,23 @@ function Game:reset()
if canvas and canvas.release then pcall(canvas.release, canvas) end
end
end
if self.renderer and self.renderer.releaseCanvases then
pcall(function() self.renderer:releaseCanvases() end)
if self.renderer then
local release = self.renderer.releaseCanvases or self.renderer.release
if release then pcall(release, self.renderer) end
end
-- Keep methods; clear every other field (including session scalars like
-- speedOverride). Re-seed module constants afterward.
local skinFast = self.SKIN_FAST_FORWARD
local keys = {}
for key, value in pairs(self) do
if type(value) ~= "function" then
if key ~= "world" and key ~= "renderer" and key ~= "_canvases" then
if type(value) == "table" and value.release then
pcall(value.release, value)
end
end
keys[#keys + 1] = key
end
end
for _, key in ipairs(keys) do
self[key] = nil
end
self.SKIN_FAST_FORWARD = skinFast or 4
end
return Game
+5 -7
View File
@@ -2208,6 +2208,8 @@ end
-- In-process return-to-launcher (Android / intent_game): drop session fields
-- so a later Game2.new() + load is not sharing a live stack or mod loader.
-- Methods live on the class table; pairs(self) only sees instance state.
-- Same rule as Gen1: only release known GPU owners -- never fan out
-- arbitrary field:release() (shared modules use :release as a handle API).
function Game2:reset()
if self.stack and self.stack.clear then
pcall(function() self.stack:clear() end)
@@ -2220,17 +2222,13 @@ function Game2:reset()
if canvas and canvas.release then pcall(canvas.release, canvas) end
end
end
if self.renderer and self.renderer.releaseCanvases then
pcall(function() self.renderer:releaseCanvases() end)
if self.renderer then
local release = self.renderer.releaseCanvases or self.renderer.release
if release then pcall(release, self.renderer) end
end
local keys = {}
for key, value in pairs(self) do
if type(value) ~= "function" then
if key ~= "world" and key ~= "renderer" and key ~= "_canvases" then
if type(value) == "table" and value.release then
pcall(value.release, value)
end
end
keys[#keys + 1] = key
end
end
+7
View File
@@ -87,6 +87,13 @@ function SessionLifecycle.endGameSession(game)
if game and game.reset then
pcall(function() game:reset() end)
end
-- Gen1 Game is the module singleton. If teardown left it without load,
-- drop the cached module so the next require rebuilds a clean table
-- (bootGame also guards this; doing it here keeps Play-again reliable).
if game and type(game.load) ~= "function"
and package.loaded["src.core.Game"] == game then
package.loaded["src.core.Game"] = nil
end
local Input = require("src.core.Input")
local TouchControls = require("src.core.TouchControls")
@@ -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 ------------------