Repository navigation
Conversation
|
Unity PR artifacts are ready.
Workflow run: Unity Source Validation #669 Artifacts are temporary and expire according to the workflow retention policy. |
Furroxide
force-pushed
the
fix/launcher-graceful-restart
branch
from
October 19, 2026 02:18
dfcc3d4 to
a8ee58a
Compare
5 tasks done
A launcher-quit-<request key>.json request in manager staging asks the game the launcher started to quit. It names that process's launch request, which no other game knows. The manager looks for its own request twice a second from the plugin's Update, takes it once, as a join is taken, and calls Application.Quit, so the quit handler lets the game's own quit steps run and holds the quit while mods unload: the clean exit is recorded, and the next start is not safe mode. A foreign or malformed request is removed unread, and stale claims of it are swept with the join claims. TopiaForge's own channel, not the quit-request file the game watches in its persistent-data folder: that folder belongs to every copy of the game on the machine, and the file is an undocumented game behaviour. The request reads and writes the same in both languages, from new shared fixtures in channels/cases.json. Signed-off-by: Furroxide <221987073+Furroxide@users.noreply.github.com>
A restart stopped the game it owned with TerminateProcess, so no clean exit was recorded and the replacement opened in safe mode with every mod off. It now asks the game to quit through the launcher-quit request, waits up to 10 s for the game to take it and 30 s in all for it to exit, and stops it only then; a game that already exited is left alone. The request is withdrawn whenever the game did not take it, including when the launcher closes while it waits, and then nothing is stopped. Ownership is unchanged: only a process this launcher started and has just verified is ever asked, under the launch request it was started with. LaunchResult says how the replaced game ended (quit, stopped or exited), and the CLI and the launcher's status line say so, with what to do when a stop leaves this start in safe mode. Signed-off-by: Furroxide <221987073+Furroxide@users.noreply.github.com>
Furroxide
force-pushed
the
fix/launcher-graceful-restart
branch
from
October 19, 2026 03:46
a8ee58a to
dc76b72
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Outcome
When the launcher restarted a game it owned, it stopped the process with
TerminateProcess. No clean exit wasrecorded, so the restarted game opened in safe mode with every mod off, and a session launch from safe mode is refused.
#234 made a normal quit record the clean exit; this makes the launcher's restart use one. It is what #239's join needs
before a join that closes the player's own game can start its session.
How it asks: TopiaForge's own channel. The launcher writes
launcher-quit-<request key>.jsoninto the install'smanager staging. The manager in the game looks for its own request twice a second from the plugin's
Update, takes itonce, and calls
Application.Quit. From there it is exactly the QUIT button's path (#234, #237): the game's ownquit steps run with every mod and patch in place, the quit is held while mods unload, and the clean exit is recorded.
quit-requestfile. Robotopia quits when aquit-requestfile appears in itspersistent-data folder (the string is in
GameCode.dll). That folder belongs to every copy of the game on themachine, so a request there would reach a game the launcher did not start, which the ownership rules forbid. It is
also an undocumented game behaviour that no surface check can prove, so it would be a GameCompat binding to keep.
manager does not answer (it failed to load, or the game hangs) is stopped once the answer window passes.
Ownership stays exactly as strict (#216). A request is written only for a process
_ownedLaunchProcessesholds and_gameProcessLivenesshas just verified, the same checks that guarded the stop. It names that process's own launchrequest, which only that process knows. The game takes only a request naming its own launch request; a foreign or
malformed one is removed unread. A game started any other way has no request id, so nothing can ask it to quit.
The wait, and the fallback (
OwnedGameQuitPolicy, defaults):An unknown liveness answer is never taken as a quit. The wait counts its own pauses, so tests never wait on the wall
clock.
It reports which happened.
LaunchResult.replacedGameisquit,stoppedorexited, and its message leads withone sentence for the player:
restart it from the launcher to load them." (A restart of that safe-mode game is graceful, so it loads the mods.)
The CLI prints it before "Game process started", and the launcher keeps it on the status line for that launch, as a
warning after a stop. The launcher log records the timings and the reason for a stop.
For #239
_closeOwnedGameForInstallshould replaceif (alive == true) await _gameProcessStopper(process);withif (alive == true) await _closeOwnedGame(install, process);, a private method of the same library. It returns theReplacedGameExit, or null when the repository closed while it waited.game's own doing, so "progress not saved there is lost" becomes, for example, "Joining quits the game, as its Quit
button does".
JoinConsentGiven(closeOwnedGameAcknowledged:)is still needed, since the game still closes.stopped. The server profile's game then opens in safe mode, where the session is refused.The join should report that and offer to restart, which is graceful from safe mode.
Deviations from the brief
read the startup stopwatch, which
WriteLastRunReportstops once startup is reported, so its 10 s teardown limitcould never pass: a stuck teardown would have held the game open for good. The new watcher, on the same clock,
looked once, on the first frame, which is how run 1 found it. Both now read a quit clock that never stops; a source
check in
PluginQuitCoordinatorTests, whichLauncherQuitTestsshares, fails if either goes back to the startupstopwatch.
for this review. Once fix(mod-manager): time the quit teardown on a clock that never stops #254 squash-merges, rebasing this branch onto
devdrops the first commit as alreadyapplied.
progress record on stderr made the pre-launch running probe answer unknown, and every launch was refused as Busy.
restartcommand still refuses to touch a game. It runs in a new process, which owns no game, so itrefuses as before ("not owned by this launcher request"). The graceful path covers every restart a repository makes
of a game it started: the launcher, and any CLI flow that launches and restarts in one process.
stopAcceptanceProcess) still stops its isolated run outright; it is not a restart.Not covered here
shutdown feedback page unless the No Feedback URL mod is on.
Troubleshooting.mdsays so. Suppressing it forlauncher-requested quits would need a game binding in the manager.
it opens the game's feedback page, as its QUIT button does. The "restart it from the launcher to load them" advice
after a stop therefore opens that page once, when No Feedback URL is in the profile.
Compatibility and security
launcher-quit-<request key>.json(schemaVersion 1:
quitId,requestId), in both languages, with shared fixtures.LaunchResultgainsreplacedGameandreplacing;LocalLauncherRepositorygains the optionalownedGameQuitpolicy;ReplacedGameExitand
OwnedGameQuitPolicyare new. No SDK surface changes; the manager's quit handling and journal format areunchanged.
owned install's manager staging; the game claims and deletes it. A restart now waits up to 30 s before it stops a game
that does not quit. Nothing new is logged that names a path or an address.
ignores the request, and the launcher stops it after 10 s.
Verification
On a Windows 11 x64 machine, rebased onto
devat 680d7a9, with the pinned Flutter 3.44.6 and its Dart 3.12.2:C#.
dotnet build TopiaForge.slnx -c Release -warnaserror: 0 warnings, 0 errors. ModManager, ModRuntime, Mods.Analyzers,Mods.Multiplayer.Generators and Mods.Multiplayer harnesses: all pass. New:
LauncherQuitTests(11 checks:taken once; a foreign request removed unread and another process's request untouched; malformed requests removed; a
request written during a take waits at its name; stale quit claims swept and live ones kept; the watcher looks once
an interval, quits once, keeps looking past a foreign request and after a failed look, rejects bad arguments, and
reads the never-stopped quit clock), one more
PluginQuitCoordinatorTestscheck (the teardown limit reads thatclock), and the quit fixtures in
LaunchSessionFixtureTests(240 of 240).dotnet format --verify-no-changes: no finding inan edited file.
tools/verify-csharp-release-surface.sh: validated 11 SDK packages and all 7 C# release harnesses.Dart and Flutter.
packages/launcher_domain: 2,279 pass; the quit fixtures add 13 cases.packages/launcher_data: 1,054, 9 skipped pass. New: "restarting a game this launcher started" (8: asks and stops nothingwhen the game quits; stops one that never takes the request once the answer window has passed; stops one that takes
it but hangs once the exit window has passed; never asks a game it did not start; a launcher closed while it waits
withdraws the request and stops nothing; a game that exits before it is asked; a request that cannot be written
falls back to stopping; an unknown liveness answer is never a quit). The fake game takes only the request at its
own launch request's channel name.
apps/topiaforge_cli: 517, 4 skipped pass; the workflow tests add 3 (quit, stopped with what to do, and a launch thatreplaced none).
apps/topiaforge_launcher_flutter: 146, 5 skipped;packages/launcher_ui3 pass; a new widget test shows thesafe-mode notice after a stop. These two ran on bdf5dd6, before the rebase; the Dart tree is unchanged since, and
dev's feat(multiplayer): track the player intent mod code acts on #238 changed no Dart file.
dart format,dart analyze --fatal-infosandflutter analyzeon each: clean.Mutations, 18 of 18 caught, each restored:
request never noticed; a closing repository that keeps waiting; a request naming another launch; an owned
process's request id not recorded; a game that already exited asked anyway; an unknown answer counted as a quit;
the restart stopping outright, as before; the result never saying how the game ended.
frame; every failed look reported; stale quit claims never swept; the watcher's clock, and the quit coordinator's,
each put back on the startup stopwatch.
In the game: Robotopia 0.0.2545, on a scratch copy of the game with its own
BepInExand windowedat 2560x1440, driven through this branch's own
LocalLauncherRepositorywith its own data root, and with fix(launcher-data): keep PowerShell progress out of the running-game probe #251'sprobe fix applied, without which this machine refuses every launch:
The first game's
manager.login run 3: "The launcher that started this game asked it to quit.", then "Quitrequested. Mods unload once the game has finished its own quit steps.", then No Feedback URL's "Suppressed shutdown
feedback page launch". The replacement's has no safe-mode line. Run 1 found the frozen quit clock (the first
commit). Run 2's safe-mode game had every mod off, so the game's own quit steps opened its feedback page, as
QUIT would. No input was needed: the restart is the launcher's.
python -I .github/scripts/check_topiaforge_residue.pyandcheck_readme_counts.py: pass. Every touched Dart file isunder 500 lines.
Checklist
AGENTS.mdand preserved CLEAN/Bloc/Unity/TopiaForge UI boundaries.