From 3c74aa9c9416fa893217192b8f6a06f3a10698a8 Mon Sep 17 00:00:00 2001 From: chimook Date: Fri, 24 Jul 2026 16:27:30 +0900 Subject: [PATCH] Isolate RNG in the gravship launch ritual's reachability check Building the gravship launch confirmation dialog decides which pawns can board in Dialog_BeginRitual's constructor: CreateRitualRoleAssignments -> RitualRoleAssignments.PawnNotAssignableReason -> RitualBehaviorWorker_GravshipLaunch.PawnCanFillRole -> CanReachGravship -> GravshipUtility.TryFindSpotOnGravship -> Region.RandomCell, which consumes RNG. Building the dialog is UI work opened only by the issuing peer (the currentExecutingCmdIssuedBySelf gate in CancelDialogBeginRitual), so that RNG runs on just one peer and advances the shared stream on that peer only. Spamming the launch button repeats it and the divergence accumulates into a desync ("Random state from commands doesn't match"). Wrap CanReachGravship in Rand.PushState/PopState, matching the existing SeedPreceptComp_UnwillingToDo_Chance isolation for RNG that "can be called in interface". CanReachGravship returns a bool that does not depend on the RNG (it only searches for any allowed cell), and the real boarding calls TryFindSpotOnGravship outside this check, so isolating here leaves both the check result and determinism intact. Co-Authored-By: Claude Opus 4.8 (1M context) --- Source/Client/Patches/Seeds.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index 373af28f8..31e4368a7 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -235,4 +235,34 @@ public static void Finalizer(bool __state) } } + [HarmonyPatch(typeof(RitualBehaviorWorker_GravshipLaunch), nameof(RitualBehaviorWorker_GravshipLaunch.CanReachGravship))] + static class SeedGravshipCanReachGravship + { + static void Prefix(ref bool __state) + { + if (Multiplayer.Client == null) return; + + // The gravship launch confirmation dialog works out which pawns can board while it is being + // built: Dialog_BeginRitual's constructor calls CreateRitualRoleAssignments -> + // RitualRoleAssignments.PawnNotAssignableReason -> RitualBehaviorWorker_GravshipLaunch + // .PawnCanFillRole -> CanReachGravship -> GravshipUtility.TryFindSpotOnGravship -> + // Region.RandomCell, which consumes RNG. Building that dialog is UI work and, because the + // dialog is opened only by the issuing peer (see CancelDialogBeginRitual's + // currentExecutingCmdIssuedBySelf gate), it runs on just one peer - so this RNG must not + // advance the shared stream. Spamming the launch button repeats it and the divergence + // accumulates into a desync ("Random state from commands doesn't match"). + // CanReachGravship returns a bool that does not depend on the RNG (it only searches for any + // allowed cell), and the real boarding calls TryFindSpotOnGravship outside this check, so + // isolating the RNG here leaves both the check result and determinism intact. + Rand.PushState(); + __state = true; + } + + static void Finalizer(bool __state) + { + if (__state) + Rand.PopState(); + } + } + }