From 9bd3aad5ee3d7f7adba25b05f6573367228c5d99 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Mon, 20 Apr 2026 15:34:19 +0800 Subject: [PATCH 1/6] fix(Determinism): force single-batch FastTileFinder.Query in MP to prevent quest site tile divergence --- Source/Client/Patches/Determinism.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 94b096710..bd96ed6ba 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -801,4 +801,30 @@ static IEnumerable Transpiler(IEnumerable inst } } + // FastTileFinder.ComputeQueryJob uses Interlocked.Increment to race-fill a 50-slot result array + // across parallel Unity Job batches. Thread scheduling differs between machines, so clients get + // different candidate tile sets. Force single-batch execution in MP so tiles are processed in + // tileId order, making the first 50 valid tiles consistent across all clients. + [HarmonyPatch(typeof(FastTileFinder), nameof(FastTileFinder.Query))] + static class FastTileFinderQueryDeterminismPatch + { + static IEnumerable Transpiler(IEnumerable instructions) + { + var getIdealBatchCount = AccessTools.Method(typeof(UnityData), nameof(UnityData.GetIdealBatchCount)); + var getBatchCount = AccessTools.Method(typeof(FastTileFinderQueryDeterminismPatch), nameof(GetBatchCount)); + + foreach (var instr in instructions) + { + if (instr.Calls(getIdealBatchCount)) + yield return new CodeInstruction(OpCodes.Call, getBatchCount); + else + yield return instr; + } + } + + static int GetBatchCount(int length) => + Multiplayer.Client != null ? length : UnityData.GetIdealBatchCount(length); + } + + } From 0a6010d1c94a0aa7b8309a4a88bd0e5446aaea32 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Mon, 20 Apr 2026 17:44:26 +0800 Subject: [PATCH 2/6] fix(FactionContext): handle transporters and gravship map gen faction context --- Source/Client/Factions/FactionContextSetters.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Client/Factions/FactionContextSetters.cs b/Source/Client/Factions/FactionContextSetters.cs index c2d229365..c7066a9e8 100644 --- a/Source/Client/Factions/FactionContextSetters.cs +++ b/Source/Client/Factions/FactionContextSetters.cs @@ -39,13 +39,21 @@ private static Faction GetFactionAt(PlanetTile tile) var worldObjectsHolder = Find.WorldObjects; var mapParent = worldObjectsHolder.MapParentAt(tile); - if (mapParent != null) + if (mapParent != null && mapParent.Faction is { IsPlayer: true }) return mapParent.Faction; var caravan = worldObjectsHolder.PlayerControlledCaravanAt(tile); if (caravan != null) return caravan.Faction; + var transporters = worldObjectsHolder.TravellingTransporters.Find(t => t.destinationTile == tile && t.Faction is { IsPlayer: true }); + if (transporters != null) + return transporters.Faction; + + var gravship = worldObjectsHolder.AllWorldObjects.Find(t => t is Gravship g && g.destinationTile == tile && t.Faction is { IsPlayer: true }); + if (gravship != null) + return gravship.Faction; + return TileFactionContext.GetFactionForTile(tile); } From 6e4a37ff523bc65ad27bc7e7305e9d49fb29bdf7 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Mon, 20 Apr 2026 17:53:42 +0800 Subject: [PATCH 3/6] fix(FactionContext): push gravship faction context during ArriveNewMap --- Source/Client/Factions/FactionContextSetters.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Source/Client/Factions/FactionContextSetters.cs b/Source/Client/Factions/FactionContextSetters.cs index c7066a9e8..7ba933c72 100644 --- a/Source/Client/Factions/FactionContextSetters.cs +++ b/Source/Client/Factions/FactionContextSetters.cs @@ -141,6 +141,20 @@ static void Finalizer(Map __state) } } +[HarmonyPatch(typeof(GravshipUtility), nameof(GravshipUtility.ArriveNewMap))] +static class GravshipArriveNewMapFactionPatch +{ + static void Prefix(Gravship gravship) + { + FactionContext.Push(gravship.Faction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + // Clean up after map generation is complete [HarmonyPatch(typeof(MapGenerator), nameof(MapGenerator.GenerateMap))] static class CleanupTileFactionContext From 6aa1489b20405bb0e8c912ce5c10c1fdfe893d57 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Wed, 6 May 2026 16:35:35 +0800 Subject: [PATCH 4/6] Prevent desync caused by unstable result of FindPlayerHomeWithMinTimezone --- Source/Client/AsyncTime/AsyncTimePatches.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index 10a8bb051..d5268377f 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -57,6 +57,7 @@ static void Prefix(DateNotifier __instance, ref int? __state) { if (Multiplayer.Client == null && Multiplayer.RealPlayerFaction != null) return; + Rand.PushState(); Map map = __instance.FindPlayerHomeWithMinTimezone(); if (map == null) return; @@ -67,9 +68,12 @@ static void Prefix(DateNotifier __instance, ref int? __state) static void Finalizer(int? __state) { - if (!__state.HasValue) return; - Find.TickManager.DebugSetTicksGame(__state.Value); - FactionContext.Pop(); + if (__state.HasValue) + { + Find.TickManager.DebugSetTicksGame(__state.Value); + FactionContext.Pop(); + } + Rand.PopState(); } } From a1a35171307732cebbcb3d037a5911c8e15883b5 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Wed, 6 May 2026 16:36:29 +0800 Subject: [PATCH 5/6] Make season notifier message distinct to all player factions --- Source/Client/AsyncTime/AsyncTimePatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index d5268377f..b2f40525c 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -57,12 +57,12 @@ static void Prefix(DateNotifier __instance, ref int? __state) { if (Multiplayer.Client == null && Multiplayer.RealPlayerFaction != null) return; + FactionContext.Push(Multiplayer.RealPlayerFaction); Rand.PushState(); Map map = __instance.FindPlayerHomeWithMinTimezone(); if (map == null) return; __state = Find.TickManager.TicksGame; - FactionContext.Push(Multiplayer.RealPlayerFaction); Find.TickManager.DebugSetTicksGame(map.AsyncTime().mapTicks); } @@ -71,9 +71,9 @@ static void Finalizer(int? __state) if (__state.HasValue) { Find.TickManager.DebugSetTicksGame(__state.Value); - FactionContext.Pop(); } Rand.PopState(); + FactionContext.Pop(); } } From 1e66d82e196bfd590cf1351b263a99c72a176120 Mon Sep 17 00:00:00 2001 From: Sakura-TA <52643135+Sakura-TA@users.noreply.github.com> Date: Wed, 6 May 2026 16:47:44 +0800 Subject: [PATCH 6/6] Add MP check --- Source/Client/AsyncTime/AsyncTimePatches.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index b2f40525c..241b64aea 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -68,6 +68,7 @@ static void Prefix(DateNotifier __instance, ref int? __state) static void Finalizer(int? __state) { + if (Multiplayer.Client == null && Multiplayer.RealPlayerFaction != null) return; if (__state.HasValue) { Find.TickManager.DebugSetTicksGame(__state.Value);