Merge pull request #1037 from dlloa/upstream-pr/platform-lifecycle-hooks

Add core.update / core.quit_to_launcher platform lifecycle hooks
This commit is contained in:
bryanthaboi
2026-08-10 14:14:17 -04:00
committed by GitHub
9 changed files with 312 additions and 3 deletions
+32
View File
@@ -271,6 +271,38 @@ do
"and nothing is disabled off a failed migration")
end
-- ------- force_enable_env: an env var can override a saved disable
-- (src/mods/Loader.lua's enable-resolution block, added for a mod that
-- cannot function disabled on the one build where its env var is set --
-- e.g. a platform-launcher bridge mod).
do
local forceFiles = {
["options.lua"] = "return { mods = { forced = false } }",
["mods/forced/manifest.json"] =
[[{"id":"forced","name":"forced","version":"1.0.0","entry":"main.lua",]]
.. [["force_enable_env":"SOME_TEST_ENV"}]],
["mods/forced/main.lua"] = "return function(mod) end",
}
local realGetenv = os.getenv
os.getenv = function(name)
if name == "SOME_TEST_ENV" then return "1" end
return realGetenv(name)
end
local onLoader = Loader.new({ fs = memfs(forceFiles) })
check(onLoader:load({ pokemon = {} }) == true,
"force_enable_env: load succeeds with the env var set")
check(onLoader.mods.forced.enabled == true,
"a matching force_enable_env re-enables a mod saved as disabled")
os.getenv = realGetenv
local offLoader = Loader.new({ fs = memfs(forceFiles) })
check(offLoader:load({ pokemon = {} }) == true,
"force_enable_env: load succeeds with the env var unset")
check(offLoader.mods.forced.enabled == false,
"with the env var unset, the saved disable is left alone")
end
-- leave shared singletons the way we found them for later chained tests
local StateStack = require("src.core.StateStack")
while StateStack:top() do StateStack:pop() end
+2
View File
@@ -105,6 +105,7 @@ local full = Manifest.validate({
api = 2, profile = "overhaul", permissions = { "network" },
dependencies = { "colorlib@^1.2" }, conflicts = { "always_noon" },
options_schema = "options.lua", assets_transforms = "transforms.lua",
force_enable_env = "SOME_ENV",
}, "mods/full")
check(full.api == 2 and full.profile == "overhaul", "api and profile parse")
check(full.affects_link == true, "overhaul defaults to affecting link play")
@@ -115,6 +116,7 @@ check(full.conflictSpecs[1].id == "always_noon" and full.conflictSpecs[1].range
"a bare conflict entry has no range")
check(full.options_schema == "options.lua"
and full.assets_transforms == "transforms.lua", "declared files are kept")
check(full.force_enable_env == "SOME_ENV", "force_enable_env is kept")
-- ------- github / experimental / incompatible
local gh = Manifest.validate({
@@ -0,0 +1,93 @@
-- core.update / core.quit_to_launcher through the public mod API: a
-- platform-launcher integration can pause the simulation and veto the
-- return-to-launcher decision from a mod, with no main.lua patch.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local PlatformHooks = require("src.core.PlatformHooks")
local FIXTURE = {
["mods/fix_platform_bridge/manifest.json"] = [[{
"id": "fix_platform_bridge",
"name": "Fixture Platform Bridge",
"version": "1.0.0",
"entry": "main.lua",
"api": 2
}]],
["mods/fix_platform_bridge/main.lua"] = [[
local mod = ...
local paused = false
local extraPolls = 0
mod.hooks:wrap("core.update", function(nextFn, game, dt)
extraPolls = extraPolls + 1
if not paused then nextFn(game, dt) end
end)
mod.hooks:wrap("core.quit_to_launcher", function(nextFn)
if os.getenv("FIXTURE_VETO_QUIT") == "1" then return false end
return nextFn()
end)
-- test-only knobs, read back through mod.storage-free globals since
-- this fixture never leaves the process
_G.__fixturePlatformBridge = {
setPaused = function(v) paused = v end,
extraPolls = function() return extraPolls end,
}
]],
}
-- core.update: a subscriber can pause (skip vanilla) and still run every frame
do
local run = T.sdk.loadMods({ "mods/fix_platform_bridge" },
{ fs = T.sdk.memfs(FIXTURE) })
T.eq(#run.errors, 0,
"the fixture mod loads clean (" .. tostring(run.errors[1]) .. ")")
local calls = 0
local fakeGame = { update = function(self, dt) calls = calls + 1 end }
_G.__fixturePlatformBridge.setPaused(false)
PlatformHooks.update(fakeGame, 1 / 60)
T.eq(calls, 1, "unpaused: vanilla Game:update runs")
T.eq(_G.__fixturePlatformBridge.extraPolls(), 1,
"the subscriber's wrapper runs every frame")
_G.__fixturePlatformBridge.setPaused(true)
PlatformHooks.update(fakeGame, 1 / 60)
T.eq(calls, 1, "paused: vanilla Game:update is skipped")
T.eq(_G.__fixturePlatformBridge.extraPolls(), 2,
"the subscriber keeps polling every frame while paused")
run.release()
_G.__fixturePlatformBridge = nil
end
-- core.quit_to_launcher: a subscriber can veto without the vanilla
-- condition ever running, or pass it through unchanged
do
local run = T.sdk.loadMods({ "mods/fix_platform_bridge" },
{ fs = T.sdk.memfs(FIXTURE) })
T.eq(#run.errors, 0,
"the fixture mod loads clean (" .. tostring(run.errors[1]) .. ")")
local realGetenv = os.getenv
os.getenv = function(name)
if name == "FIXTURE_VETO_QUIT" then return "1" end
return realGetenv(name)
end
local vanillaCalls = 0
local vetoed = PlatformHooks.quitToLauncher(function()
vanillaCalls = vanillaCalls + 1
return true
end)
T.eq(vetoed, false, "a subscriber can veto the return-to-launcher decision")
T.eq(vanillaCalls, 0, "a veto never evaluates the vanilla condition")
os.getenv = realGetenv
local passed = PlatformHooks.quitToLauncher(function() return true end)
T.eq(passed, true, "with no veto, the vanilla decision passes through unchanged")
run.release()
end
T.finish("platform_lifecycle_hooks")