From ad354d1828a23d8a41a6e06174ee543d08949a01 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Tue, 24 Mar 2026 11:44:21 +0800 Subject: [PATCH 1/4] fix multithread race condition about pathfinder --- Source/Client/AsyncTime/AsyncTimeComp.cs | 3 ++ Source/Client/Comp/Map/MultiplayerMapComp.cs | 5 +++ Source/Client/Factions/Forbiddables.cs | 34 ++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index d9258cc40..7d626556b 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -118,6 +118,9 @@ public void Tick() try { map.MapPreTick(); + + map.pathFinder.ForceCompleteScheduledJobs(); + mapTicks++; Find.TickManager.ticksGameInt = mapTicks; diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 16e4ad956..7b3df3627 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -143,6 +143,11 @@ public CustomFactionMapData GetCurrentCustomFactionData() return customFactionData[Faction.OfPlayer.loadID]; } + public CustomFactionMapData GetCustomFactionData(Faction faction) + { + return customFactionData[faction.loadID]; + } + public void Notify_ThingDespawned(Thing t) { foreach (var data in customFactionData.Values) diff --git a/Source/Client/Factions/Forbiddables.cs b/Source/Client/Factions/Forbiddables.cs index fae69e2ef..7326ae4f6 100644 --- a/Source/Client/Factions/Forbiddables.cs +++ b/Source/Client/Factions/Forbiddables.cs @@ -7,6 +7,40 @@ namespace Multiplayer.Client { // todo handle conversion to singleplayer and PostSplitOff + [HarmonyPatch(typeof(ForbidUtility), nameof(ForbidUtility.IsForbidden), + typeof(Thing), typeof(Faction))] + static class IsForbiddenPatch + { + public static bool IsForbiddenByFaction(Thing t, Faction faction) + { + if (Multiplayer.Client == null) + return t.TryGetComp()?.Forbidden ?? false; // singleplayer: use vanilla + + if (!t.Spawned) return false; + + return !t.Map.MpComp().GetCustomFactionData(faction).unforbidden.Contains(t); + } + static bool Prefix(Thing t, Faction faction, ref bool __result) + { + if (Multiplayer.Client == null) return true; // singleplayer: run vanilla + + if (faction == null) { __result = false; return false; } + if (!faction.IsPlayer) { __result = false; return false; } // guests/enemies unblocked + + ThingWithComps thingWithComps = t as ThingWithComps; + if (thingWithComps == null) + { + __result = false; + return false; + } + CompForbiddable compForbiddable = thingWithComps.compForbiddable; + + __result = compForbiddable != null && IsForbiddenByFaction(t, faction); // use faction-specific data directly + return false; // skip vanilla + } + + } + [HarmonyPatch(typeof(CompForbiddable), nameof(CompForbiddable.Forbidden), MethodType.Getter)] static class GetForbidPatch { From 26559fa23586580dd30b3452fcbd0549a72aeb8e Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Tue, 24 Mar 2026 23:09:54 +0800 Subject: [PATCH 2/4] remove un-necessary call to map.pathFinder.ForceCompleteScheduledJobs() --- Source/Client/AsyncTime/AsyncTimeComp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 7d626556b..2df943140 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -119,7 +119,7 @@ public void Tick() { map.MapPreTick(); - map.pathFinder.ForceCompleteScheduledJobs(); + //map.pathFinder.ForceCompleteScheduledJobs(); mapTicks++; Find.TickManager.ticksGameInt = mapTicks; From 8868b0b410901b10121a71dd87e5bc6c043b60ed Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Wed, 25 Mar 2026 02:18:53 +0800 Subject: [PATCH 3/4] remove unnecessary comment --- Source/Client/AsyncTime/AsyncTimeComp.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 2df943140..d9258cc40 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -118,9 +118,6 @@ public void Tick() try { map.MapPreTick(); - - //map.pathFinder.ForceCompleteScheduledJobs(); - mapTicks++; Find.TickManager.ticksGameInt = mapTicks; From da12a1555418df18852b143e93d5296910f19848 Mon Sep 17 00:00:00 2001 From: Sakura-TA Date: Wed, 25 Mar 2026 03:41:01 +0800 Subject: [PATCH 4/4] remove method IsForbiddenByFaction and drop more situation to original method --- Source/Client/Factions/Forbiddables.cs | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Source/Client/Factions/Forbiddables.cs b/Source/Client/Factions/Forbiddables.cs index 7326ae4f6..590e9d58e 100644 --- a/Source/Client/Factions/Forbiddables.cs +++ b/Source/Client/Factions/Forbiddables.cs @@ -11,21 +11,9 @@ namespace Multiplayer.Client typeof(Thing), typeof(Faction))] static class IsForbiddenPatch { - public static bool IsForbiddenByFaction(Thing t, Faction faction) - { - if (Multiplayer.Client == null) - return t.TryGetComp()?.Forbidden ?? false; // singleplayer: use vanilla - - if (!t.Spawned) return false; - - return !t.Map.MpComp().GetCustomFactionData(faction).unforbidden.Contains(t); - } static bool Prefix(Thing t, Faction faction, ref bool __result) { - if (Multiplayer.Client == null) return true; // singleplayer: run vanilla - - if (faction == null) { __result = false; return false; } - if (!faction.IsPlayer) { __result = false; return false; } // guests/enemies unblocked + if (Multiplayer.Client == null || faction == null || !faction.IsPlayer) return true; // singleplayer: run vanilla ThingWithComps thingWithComps = t as ThingWithComps; if (thingWithComps == null) @@ -35,7 +23,18 @@ static bool Prefix(Thing t, Faction faction, ref bool __result) } CompForbiddable compForbiddable = thingWithComps.compForbiddable; - __result = compForbiddable != null && IsForbiddenByFaction(t, faction); // use faction-specific data directly + if(compForbiddable == null) { + __result = false; + return false; + } + + if(!t.Spawned) + { + __result = false; + return false; + } + + __result = !t.Map.MpComp().GetCustomFactionData(faction).unforbidden.Contains(t); // use faction-specific data directly return false; // skip vanilla }