From f32293eac08e1e26fcdcd22f2c0a13f313fe7786 Mon Sep 17 00:00:00 2001 From: chimook Date: Fri, 24 Jul 2026 03:14:55 +0900 Subject: [PATCH] Fix desync when cancelling a gravship launch The prelaunch-cancel handler closed the GravshipTravelSession with CloseSessionAt(Find.CurrentMap.Tile). Cancelling runs as a synced command on every peer, but Find.CurrentMap is the map each peer's camera is on - local UI state, not synchronized simulation state. A peer whose camera is on a different map closes the wrong tile and never closes the gravship session, so that map stays paused only for them. The per-map tick counts then diverge and the game desyncs ("Map instances don't match" / "Wrong random state on map N"). Close the session by looking it up instead of by the local camera. Of the four CloseSessionAt call sites, this was the only one using Find.CurrentMap; the others already pass deterministic tiles (curTile/landingTile/takeoffTile). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Patches/GravshipTravelSessionPatches.cs | 8 +++++++- .../Client/Persistent/GravshipTravelSession.cs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Source/Client/Patches/GravshipTravelSessionPatches.cs b/Source/Client/Patches/GravshipTravelSessionPatches.cs index 4939f821c..5543893c1 100644 --- a/Source/Client/Patches/GravshipTravelSessionPatches.cs +++ b/Source/Client/Patches/GravshipTravelSessionPatches.cs @@ -38,7 +38,13 @@ static void Postfix(Dialog_MessageBox __instance) if (Multiplayer.Client == null) return; if (!Multiplayer.ExecutingCmds) return; - GravshipTravelUtils.CloseSessionAt(Find.CurrentMap.Tile); + // Cancelling is a synced command, so this runs on every peer. Using Find.CurrentMap here is + // non-deterministic: it's the map each peer's camera happens to be on, not synchronized state. + // A peer whose camera is on another map would call CloseSessionAt with the wrong tile and never + // close the gravship session, leaving that map paused only for them -> the per-map tick counts + // diverge and the game desyncs. There is exactly one open GravshipTravelSession at prelaunch time, + // so close it by looking it up rather than by the local camera. + GravshipTravelUtils.CloseAllSessions(); GravshipTravelUtils.CloseGravshipPrelaunchDialog(); } } diff --git a/Source/Client/Persistent/GravshipTravelSession.cs b/Source/Client/Persistent/GravshipTravelSession.cs index 92461b7b8..8e8de83e4 100644 --- a/Source/Client/Persistent/GravshipTravelSession.cs +++ b/Source/Client/Persistent/GravshipTravelSession.cs @@ -57,6 +57,22 @@ public static void CloseSessionAt(PlanetTile tile) } } + // Closes every open GravshipTravelSession, independent of the local camera (Find.CurrentMap). + // Used by the prelaunch-cancel path, which runs as a synced command on all peers: closing by the + // local camera's tile is non-deterministic and leaves the session (and thus the map's pause) alive + // on peers looking at another map. There is only ever one such session while a launch is pending. + public static void CloseAllSessions() + { + foreach (var sessionManager in Multiplayer.game.mapComps.Select(mp => mp.sessionManager)) + { + foreach (var session in sessionManager.AllSessions.OfType().ToList()) + { + session.Map.MpComp()?.sessionManager?.RemoveSession(session); + session.StopPausing(); + } + } + } + public static bool TryGetSessionAt(PlanetTile takeoffTile, out GravshipTravelSession session) { session = null;