mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
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:
@@ -352,3 +352,36 @@ or text-box state.
|
|||||||
|
|
||||||
Developer mode also arms the mod loader's dev tripwire, which flags mods
|
Developer mode also arms the mod loader's dev tripwire, which flags mods
|
||||||
that reach outside their permission set.
|
that reach outside their permission set.
|
||||||
|
|
||||||
|
## Process-lifecycle hooks
|
||||||
|
|
||||||
|
These exist so a platform-specific launcher integration (a native shell
|
||||||
|
that embeds this engine and wraps its window in platform UI) can live
|
||||||
|
entirely in a mod instead of hand-patching `main.lua`, which every other
|
||||||
|
engine change also touches.
|
||||||
|
|
||||||
|
`core.update` receives `(next, game, dt)` once per frame from
|
||||||
|
`love.update`. Vanilla behavior is `game:update(dt)`, unconditionally. A
|
||||||
|
mod may skip calling `next(game, dt)` to pause the simulation for that
|
||||||
|
frame (e.g. while a native settings sheet is on top), and may run
|
||||||
|
additional per-frame polling before or after that call regardless of
|
||||||
|
whether it calls `next` -- useful for one-shot flags that must be observed
|
||||||
|
every frame even while paused.
|
||||||
|
|
||||||
|
`core.quit_to_launcher` receives `(next)` once from `love.quit()`. `next()`
|
||||||
|
returns the engine's own decision for whether closing the window should
|
||||||
|
return to the Lua launcher instead of exiting; a mod may return `false`
|
||||||
|
outright, without ever calling `next`, to veto that and let the process
|
||||||
|
really quit -- for a platform host that owns its own "return to launcher"
|
||||||
|
UI and would otherwise get looped straight back into the game it just
|
||||||
|
quit.
|
||||||
|
|
||||||
|
A manifest may also declare `force_enable_env`, an environment variable
|
||||||
|
name that re-enables the mod regardless of a saved disable in
|
||||||
|
`options.mods` when that variable is set to `"1"`. This is for a mod that
|
||||||
|
cannot function disabled on the one build where its env var is set (a
|
||||||
|
platform-bridge mod bundled only with that build's launcher, for example).
|
||||||
|
|
||||||
|
Neither hook needs a `Runtime.wantsHook` guard before calling it: `Hooks:call`
|
||||||
|
already falls straight through to the vanilla function when no mod has
|
||||||
|
wrapped the name, at negligible cost.
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# RFC 0006 — Generic process-lifecycle hooks for platform launcher integrations
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed. Engine: `PlatformHooks.lua` (new), `main.lua`, `Manifest.lua`,
|
||||||
|
`Loader.lua`. Tests: `tests/modkit/cases/platform_lifecycle_hooks.lua`,
|
||||||
|
`tests/mod_loader_tests.lua`, `tests/mod_manifest_tests.lua`.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
A platform-specific launcher wrapper -- a native shell that embeds this
|
||||||
|
engine and owns its own UI around the game window (a mobile app shell,
|
||||||
|
say, presenting its own settings/import/save screens and only handing
|
||||||
|
control to the LÖVE window once play starts) needs three things no
|
||||||
|
current hook covers:
|
||||||
|
|
||||||
|
1. Pause the simulation while its own UI is on top of the game window.
|
||||||
|
2. Live-reload options it wrote from outside any Lua UI.
|
||||||
|
3. Veto `main.lua`'s "closing the window returns to the Lua launcher"
|
||||||
|
behavior when the platform shell owns that job itself -- without this,
|
||||||
|
a shell that re-fronts its own launcher UI on quit gets looped straight
|
||||||
|
back into `HostShell.restart()`'s in-process reboot instead.
|
||||||
|
|
||||||
|
Implementing this by hand-patching `main.lua`'s `love.update`/`love.quit`
|
||||||
|
directly ties every such integration to editing the one file every other
|
||||||
|
engine change also touches, guaranteeing merge conflicts for any second
|
||||||
|
platform integration (or any unrelated engine PR landing around the same
|
||||||
|
time). No existing hook covers "should the per-frame simulation step run"
|
||||||
|
or "should closing the window return to the Lua launcher."
|
||||||
|
|
||||||
|
## The decision it extends
|
||||||
|
|
||||||
|
No prior D-number. Extends the hook-contract section of `docs/modding.md`
|
||||||
|
alongside `input.step`, `render.hud`, `screen.render_visible`, etc.
|
||||||
|
|
||||||
|
## The exact API delta
|
||||||
|
|
||||||
|
Backward-compatible, additive-only.
|
||||||
|
|
||||||
|
### `core.update`
|
||||||
|
|
||||||
|
New hook, `(game, dt) -> nil` through the public wrapper signature
|
||||||
|
`(next, game, dt)`, called once per frame from `love.update` via
|
||||||
|
`src/core/PlatformHooks.lua`'s `PlatformHooks.update(game, dt)`. Vanilla
|
||||||
|
behavior (used when no mod claims the hook) is `game:update(dt)`,
|
||||||
|
unconditionally -- identical to `love.update`'s behavior before this hook
|
||||||
|
existed. A subscriber may skip calling `next(game, dt)` to pause the
|
||||||
|
simulation for that frame, or do additional per-frame work before/after
|
||||||
|
calling it regardless of whether it calls `next`.
|
||||||
|
|
||||||
|
### `core.quit_to_launcher`
|
||||||
|
|
||||||
|
New hook, `() -> boolean` through the public wrapper signature `(next)`,
|
||||||
|
called once from `love.quit()` via
|
||||||
|
`PlatformHooks.quitToLauncher(vanilla)`. `vanilla` is the pre-existing
|
||||||
|
non-platform-specific decision (`Game and not Importer and not
|
||||||
|
quitToLauncher and not scripted and not launchedIntoGame`). A subscriber
|
||||||
|
may return `false` outright to veto returning to the Lua launcher (without
|
||||||
|
ever calling `next`, so the vanilla condition is never evaluated), or call
|
||||||
|
`next()` and return its result to pass the vanilla decision through
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
Neither hook is guarded by `Runtime.wantsHook` -- both fire unconditionally
|
||||||
|
every call, matching the existing `input.step` precedent
|
||||||
|
(`src/core/Game.lua`), since `Hooks:call` already fast-paths to a bare
|
||||||
|
`vanilla(...)` call when no mod has wrapped the name.
|
||||||
|
|
||||||
|
### `Manifest.force_enable_env`
|
||||||
|
|
||||||
|
New optional manifest field, a bare env-var name. `Loader:load` re-enables
|
||||||
|
a mod carrying this field whenever that variable is set to `"1"`,
|
||||||
|
regardless of a saved disable in `options.mods`. This exists for exactly
|
||||||
|
the mod class this RFC is for: a platform-bridge mod that ships only with
|
||||||
|
one build and cannot function disabled there, but must still behave like
|
||||||
|
every other mod (a manifest opt-in, not an engine special case) on every
|
||||||
|
build that doesn't set its variable.
|
||||||
|
|
||||||
|
## Migration note for existing mods
|
||||||
|
|
||||||
|
**Nothing.** With no subscriber, `love.update` still calls `Game:update(dt)`
|
||||||
|
unconditionally every frame and `love.quit()`'s restart-to-launcher
|
||||||
|
decision is exactly the pre-existing condition -- bit-identical to today's
|
||||||
|
behavior on every platform where no mod wraps either hook. A manifest with
|
||||||
|
no `force_enable_env` field behaves exactly as before.
|
||||||
|
|
||||||
|
## Parity tests
|
||||||
|
|
||||||
|
- **No-mod:** `core.update`'s vanilla runs exactly once per call with the
|
||||||
|
hook chain empty; `core.quit_to_launcher`'s vanilla return value passes
|
||||||
|
through unchanged. Both hooks are picked up automatically by the
|
||||||
|
catalog-driven no-mod gate (`tests/engine/gate_hooks.lua`, which scans
|
||||||
|
for `Runtime.call("...")` call sites), so neither needs a dedicated
|
||||||
|
no-mod test file.
|
||||||
|
- **Mod-API:** `tests/modkit/cases/platform_lifecycle_hooks.lua` proves,
|
||||||
|
through a fixture mod loaded via the public loader (not the engine's
|
||||||
|
internals), that a subscriber can skip the vanilla update call (pause),
|
||||||
|
run extra per-frame polling regardless of pause state, and veto the
|
||||||
|
quit-to-launcher decision without the vanilla condition ever running.
|
||||||
|
- `tests/mod_loader_tests.lua` and `tests/mod_manifest_tests.lua` cover
|
||||||
|
`force_enable_env`: a matching env var re-enables a mod saved as
|
||||||
|
disabled, and an unset one leaves the saved disable alone.
|
||||||
|
|
||||||
|
## Deprecation etiquette
|
||||||
|
|
||||||
|
Nothing deprecated. These are two additive hooks and one additive manifest
|
||||||
|
field; `main.lua`'s only footprint is one `require` and two call sites
|
||||||
|
into `src/core/PlatformHooks.lua`.
|
||||||
@@ -13,6 +13,7 @@ local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE =
|
|||||||
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||||
local LaunchOptions = require("src.core.LaunchOptions")
|
local LaunchOptions = require("src.core.LaunchOptions")
|
||||||
local NxDisplay = require("src.core.NxDisplay")
|
local NxDisplay = require("src.core.NxDisplay")
|
||||||
|
local PlatformHooks = require("src.core.PlatformHooks")
|
||||||
|
|
||||||
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
|
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
|
||||||
do
|
do
|
||||||
@@ -426,7 +427,11 @@ function love.update(dt)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
Game:update(dt)
|
-- Mods may wrap or veto the per-frame simulation step (pause it, react
|
||||||
|
-- to external platform state, etc.) -- see docs/modding.md's core.update
|
||||||
|
-- entry. Vanilla behavior (used when no mod claims the hook) is just
|
||||||
|
-- Game:update(dt), unconditionally, exactly as before this hook existed.
|
||||||
|
PlatformHooks.update(Game, dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.draw()
|
function love.draw()
|
||||||
@@ -819,8 +824,16 @@ function love.quit()
|
|||||||
or os.getenv("POKEPORT_IMPORT_ONLY") == "1" or os.getenv("POKEPORT_IMPORT_ROM")
|
or os.getenv("POKEPORT_IMPORT_ONLY") == "1" or os.getenv("POKEPORT_IMPORT_ROM")
|
||||||
-- #887: a shortcut session (--game / POKEPORT_GAME) has no launcher to go
|
-- #887: a shortcut session (--game / POKEPORT_GAME) has no launcher to go
|
||||||
-- back to and the restart would re-read the shortcut, so it exits instead.
|
-- back to and the restart would re-read the shortcut, so it exits instead.
|
||||||
if Game and not Importer and not quitToLauncher and not scripted
|
--
|
||||||
and not launchedIntoGame then
|
-- A platform launcher that owns "return to launcher" itself (see
|
||||||
|
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
||||||
|
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
||||||
|
-- claims the hook) is exactly the condition below.
|
||||||
|
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
||||||
|
return Game and not Importer and not quitToLauncher and not scripted
|
||||||
|
and not launchedIntoGame
|
||||||
|
end)
|
||||||
|
if wouldReturnToLauncher then
|
||||||
quitToLauncher = true
|
quitToLauncher = true
|
||||||
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
||||||
-- once, so the restart really does land in the launcher (#887). A failed
|
-- once, so the restart really does land in the launcher (#887). A failed
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Generic process-lifecycle mod hooks so a platform-specific launcher
|
||||||
|
-- integration (a native shell embedding this engine, e.g. wrapping the
|
||||||
|
-- window in a platform UI) can live entirely in a mod instead of
|
||||||
|
-- hand-patching main.lua, which every other engine change also touches.
|
||||||
|
-- See docs/modding.md's "Process-lifecycle hooks" section.
|
||||||
|
local ModRuntime = require("src.mods.Runtime")
|
||||||
|
|
||||||
|
local PlatformHooks = {}
|
||||||
|
|
||||||
|
function PlatformHooks.update(game, dt)
|
||||||
|
return ModRuntime.call("core.update", function(g, d) g:update(d) end, game, dt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PlatformHooks.quitToLauncher(vanilla)
|
||||||
|
return ModRuntime.call("core.quit_to_launcher", vanilla)
|
||||||
|
end
|
||||||
|
|
||||||
|
return PlatformHooks
|
||||||
@@ -951,6 +951,16 @@ function Loader:load(data)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- A manifest may name an env var that force-enables it regardless of a
|
||||||
|
-- saved disable in options.mods -- generic, not tied to any mod id, for
|
||||||
|
-- a mod (e.g. a native-launcher bridge) that cannot function disabled on
|
||||||
|
-- the one build where its env var is set.
|
||||||
|
for id, mod in pairs(self.mods) do
|
||||||
|
local envName = mod.manifest.force_enable_env
|
||||||
|
if envName and os.getenv(envName) == "1" then
|
||||||
|
self.disabled[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
for id, mod in pairs(self.mods) do
|
for id, mod in pairs(self.mods) do
|
||||||
mod.enabled = not self.disabled[id]
|
mod.enabled = not self.disabled[id]
|
||||||
mod.state = mod.enabled and "pending" or "disabled"
|
mod.state = mod.enabled and "pending" or "disabled"
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ function Manifest.validate(raw, path)
|
|||||||
permissionSet = permissionSet,
|
permissionSet = permissionSet,
|
||||||
options_schema = optionalFile(raw.options_schema, "options_schema"),
|
options_schema = optionalFile(raw.options_schema, "options_schema"),
|
||||||
assets_transforms = optionalFile(raw.assets_transforms, "assets_transforms"),
|
assets_transforms = optionalFile(raw.assets_transforms, "assets_transforms"),
|
||||||
|
force_enable_env = optionalFile(raw.force_enable_env, "force_enable_env"),
|
||||||
path = path,
|
path = path,
|
||||||
raw = raw,
|
raw = raw,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,6 +271,38 @@ do
|
|||||||
"and nothing is disabled off a failed migration")
|
"and nothing is disabled off a failed migration")
|
||||||
end
|
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
|
-- leave shared singletons the way we found them for later chained tests
|
||||||
local StateStack = require("src.core.StateStack")
|
local StateStack = require("src.core.StateStack")
|
||||||
while StateStack:top() do StateStack:pop() end
|
while StateStack:top() do StateStack:pop() end
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ local full = Manifest.validate({
|
|||||||
api = 2, profile = "overhaul", permissions = { "network" },
|
api = 2, profile = "overhaul", permissions = { "network" },
|
||||||
dependencies = { "colorlib@^1.2" }, conflicts = { "always_noon" },
|
dependencies = { "colorlib@^1.2" }, conflicts = { "always_noon" },
|
||||||
options_schema = "options.lua", assets_transforms = "transforms.lua",
|
options_schema = "options.lua", assets_transforms = "transforms.lua",
|
||||||
|
force_enable_env = "SOME_ENV",
|
||||||
}, "mods/full")
|
}, "mods/full")
|
||||||
check(full.api == 2 and full.profile == "overhaul", "api and profile parse")
|
check(full.api == 2 and full.profile == "overhaul", "api and profile parse")
|
||||||
check(full.affects_link == true, "overhaul defaults to affecting link play")
|
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")
|
"a bare conflict entry has no range")
|
||||||
check(full.options_schema == "options.lua"
|
check(full.options_schema == "options.lua"
|
||||||
and full.assets_transforms == "transforms.lua", "declared files are kept")
|
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
|
-- ------- github / experimental / incompatible
|
||||||
local gh = Manifest.validate({
|
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")
|
||||||
Reference in New Issue
Block a user