From 9bd3aad5ee3d7f7adba25b05f6573367228c5d99 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Mon, 20 Apr 2026 15:34:19 +0800 Subject: [PATCH 1/4] 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 94b09671..bd96ed6b 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/4] 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 c2d22936..c7066a9e 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/4] 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 c7066a9e..7ba933c7 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 5fbd8faf68dcea3de4bff5d291077949ed1ff6ff Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Thu, 7 May 2026 00:01:26 +0800 Subject: [PATCH 4/4] Cache the gravship search result for player factions --- Source/Client/Comp/Map/FactionMapData.cs | 8 +++ Source/Client/Comp/Map/MultiplayerMapComp.cs | 2 + Source/Client/Comp/World/FactionWorldData.cs | 10 +++- .../Client/Comp/World/MultiplayerWorldComp.cs | 3 + Source/Client/Patches/GravshipCache.cs | 60 +++++++++++++++++++ Source/Client/Persistent/GravshipCache.cs | 32 ++++++++++ 6 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 Source/Client/Patches/GravshipCache.cs create mode 100644 Source/Client/Persistent/GravshipCache.cs diff --git a/Source/Client/Comp/Map/FactionMapData.cs b/Source/Client/Comp/Map/FactionMapData.cs index 71d6c210..f3bfa916 100644 --- a/Source/Client/Comp/Map/FactionMapData.cs +++ b/Source/Client/Comp/Map/FactionMapData.cs @@ -1,4 +1,5 @@ using Multiplayer.Client.Factions; +using Multiplayer.Client.Persistent; using RimWorld; using Verse; @@ -23,6 +24,7 @@ public class FactionMapData : IExposable public ListerFilthInHomeArea listerFilthInHomeArea; public ListerMergeables listerMergeables; + public GravshipCache gravshipCache; private FactionMapData() { } // Loading ctor @@ -35,6 +37,8 @@ public FactionMapData(Map map) resourceCounter = new ResourceCounter(map); listerFilthInHomeArea = new ListerFilthInHomeArea(map); listerMergeables = new ListerMergeables(map); + + gravshipCache = new GravshipCache(map); } private FactionMapData(int factionId, Map map) : this(map) @@ -65,6 +69,8 @@ public void ExposeData() planManager ??= new PlanManager(map); } + gravshipCache = new GravshipCache(map); + ExposeActor.OnPostInit(() => map.PopFaction()); } @@ -89,6 +95,8 @@ public static FactionMapData NewFromMap(Map map, int factionId) resourceCounter = map.resourceCounter, listerFilthInHomeArea = map.listerFilthInHomeArea, listerMergeables = map.listerMergeables, + + gravshipCache = new GravshipCache(map) }; } } diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 541a9233..2373f061 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -136,6 +136,8 @@ public void SetFaction(Faction faction) map.resourceCounter = data.resourceCounter; map.listerFilthInHomeArea = data.listerFilthInHomeArea; map.listerMergeables = data.listerMergeables; + + data?.gravshipCache.Apply(); } public CustomFactionMapData GetCurrentCustomFactionData() diff --git a/Source/Client/Comp/World/FactionWorldData.cs b/Source/Client/Comp/World/FactionWorldData.cs index 20b43fe7..fd3ed16f 100644 --- a/Source/Client/Comp/World/FactionWorldData.cs +++ b/Source/Client/Comp/World/FactionWorldData.cs @@ -1,3 +1,4 @@ +using Multiplayer.Client.Persistent; using RimWorld; using Verse; @@ -17,6 +18,7 @@ public class FactionWorldData : IExposable public Storyteller storyteller; public StoryWatcher storyWatcher; + public GravshipCache gravshipCache; public FactionWorldData() { } public void ExposeData() @@ -74,7 +76,9 @@ public static FactionWorldData New(int factionId) history = new History(), storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty), - storyWatcher = new StoryWatcher() + storyWatcher = new StoryWatcher(), + + gravshipCache = new GravshipCache(), }; } @@ -92,7 +96,9 @@ public static FactionWorldData FromCurrent(int factionId) history = Find.History, storyteller = Find.Storyteller, - storyWatcher = Find.StoryWatcher + storyWatcher = Find.StoryWatcher, + + gravshipCache = new GravshipCache() }; } } diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index a505856a..9ffd2204 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -126,6 +126,7 @@ private void ExposeFactionData() // Game manager order? factionData[currentFactionId] = FactionWorldData.FromCurrent(currentFactionId); } + } public void WriteSessionData(ByteWriter writer) @@ -164,6 +165,8 @@ public void SetFaction(Faction faction) game.history = data.history; game.storyteller = data.storyteller; game.storyWatcher = data.storyWatcher; + + //data?.gravshipCache.Apply(); } public void DirtyColonyTradeForMap(Map map) diff --git a/Source/Client/Patches/GravshipCache.cs b/Source/Client/Patches/GravshipCache.cs new file mode 100644 index 00000000..2eb54f02 --- /dev/null +++ b/Source/Client/Patches/GravshipCache.cs @@ -0,0 +1,60 @@ +using HarmonyLib; +using Multiplayer.Client.Util; +using RimWorld; +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.Client.Persistent +{ + [HarmonyPatch(typeof(GravshipUtility), nameof(GravshipUtility.GetPlayerGravEngine_NewTemp))] + public static class PatchGravshipUtilityGetPlayerGravEngine_NewTemp + { + static bool Prefix(Map map, Building_GravEngine __result, Building_GravEngine __state) + { + if (!ModsConfig.OdysseyActive || Faction.OfPlayer.loadID < 0) + { + return false; + } + GravshipCache cache = map.MpComp().factionData.GetValueOrDefault(Faction.OfPlayer.loadID).gravshipCache; + if (cache != null) + { + Building_GravEngine cachedGravEngine = __state = cache.cachedGravEngine; + + // uptime cache + if (Find.TickManager.TicksGame == cache.lastCachedEngineTick) + { + __result = cachedGravEngine; + return false; + } + // no cache, default to search for one + if (cachedGravEngine == null) + { + //MpLog.Debug($"Allowed searching for gravengine of {Faction.OfPlayer} at {map.GetUniqueLoadID()} tick.{Find.TickManager.TicksGame}"); + return true; + } + // cached but old, validate cache available + if (cachedGravEngine.Map == map && !cachedGravEngine.Destroyed) + { + return false; + } + } + return true; + } + static void Finalizer(Building_GravEngine __result, Building_GravEngine __state, Map map) + { + if (Faction.OfPlayer.loadID > 0) + { + GravshipCache cache = map.MpComp().factionData.GetValueOrDefault(Faction.OfPlayer.loadID).gravshipCache; + if(cache != null) + { + // if engine updated + if (__result != __state) + cache.cachedGravEngine = __result; + + cache.lastCachedEngineTick = Find.TickManager.TicksGame; + } + } + + } + } +} diff --git a/Source/Client/Persistent/GravshipCache.cs b/Source/Client/Persistent/GravshipCache.cs new file mode 100644 index 00000000..0a11320c --- /dev/null +++ b/Source/Client/Persistent/GravshipCache.cs @@ -0,0 +1,32 @@ +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Persistent +{ + public class GravshipCache + { + public Map parent; + public Building_GravEngine cachedGravEngine; + public int lastCachedEngineTick; + public GravshipCache(Map map = null) + { + parent = map; + } + public void Apply() + { + if (cachedGravEngine != null) + { + GravshipUtility.lastCachedEngineTick = lastCachedEngineTick; + GravshipUtility.cachedGravEngine = cachedGravEngine; + GravshipUtility.lastCachedEngineMapID = parent.uniqueID; + } + else + { + //comment out so default fall to world status + GravshipUtility.lastCachedEngineTick = -1; + GravshipUtility.cachedGravEngine = null; + GravshipUtility.lastCachedEngineMapID = -1; + } + } + } +}