From 1cc663f71d59d1eb03f8fd687a09056981ff88ec Mon Sep 17 00:00:00 2001 From: Zetrith Date: Sun, 14 Jul 2024 17:32:38 +0200 Subject: [PATCH 01/11] Add ModIcon.png --- About/ModIcon.png | Bin 0 -> 11624 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 About/ModIcon.png diff --git a/About/ModIcon.png b/About/ModIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..a2d0ad41d7a1b210cdd3a03b36e45e2abb36ffc6 GIT binary patch literal 11624 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 Date: Sun, 14 Jul 2024 17:33:48 +0200 Subject: [PATCH 02/11] Move ritual code to separate files --- Source/Client/Persistent/RitualBeginProxy.cs | 87 +++++++++ Source/Client/Persistent/RitualData.cs | 5 + .../{Rituals.cs => RitualPatches.cs} | 175 +----------------- Source/Client/Persistent/RitualSession.cs | 87 +++++++++ Source/MultiplayerLoader/SyncRituals.cs | 6 +- 5 files changed, 186 insertions(+), 174 deletions(-) create mode 100644 Source/Client/Persistent/RitualBeginProxy.cs rename Source/Client/Persistent/{Rituals.cs => RitualPatches.cs} (56%) create mode 100644 Source/Client/Persistent/RitualSession.cs diff --git a/Source/Client/Persistent/RitualBeginProxy.cs b/Source/Client/Persistent/RitualBeginProxy.cs new file mode 100644 index 00000000..7f3b015f --- /dev/null +++ b/Source/Client/Persistent/RitualBeginProxy.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Persistent; + +public class RitualBeginProxy : Dialog_BeginRitual, ISwitchToMap +{ + public static RitualBeginProxy drawing; + + public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); + + // In the base type, the unused fields are used to create RitualRoleAssignments which already exist in an MP session + // They are here to help keep the base constructor in sync with this one + public RitualBeginProxy( + RitualRoleAssignments assignments, + string ritualLabel, + Precept_Ritual ritual, + TargetInfo target, + Map map, + ActionCallback action, + Pawn organizer, + RitualObligation obligation, + PawnFilter filter = null, + string okButtonText = null, + // ReSharper disable once UnusedParameter.Local + List requiredPawns = null, + // ReSharper disable once UnusedParameter.Local + Dictionary forcedForRole = null, + RitualOutcomeEffectDef outcome = null, + List extraInfoText = null, + // ReSharper disable once UnusedParameter.Local + Pawn selectedPawn = null) : + base(assignments, ritual, target, ritual?.outcomeEffect?.def ?? outcome) + { + this.obligation = obligation; + this.filter = filter; + this.organizer = organizer; + this.map = map; + this.action = action; + ritualExplanation = ritual?.ritualExplanation; + this.ritualLabel = ritualLabel; + this.okButtonText = okButtonText ?? "OK".Translate(); + extraInfos = extraInfoText; + + soundClose = SoundDefOf.TabClose; + + // This gets cancelled in the base constructor if called from ticking/cmd in DontClearDialogBeginRitualCache + // Note that: cachedRoles is a static field, cachedRoles is only used for UI drawing + cachedRoles.Clear(); + if (ritual is { ideo: not null }) + { + cachedRoles.AddRange(ritual.ideo.RolesListForReading.Where(r => !r.def.leaderRole)); + Precept_Role preceptRole = Faction.OfPlayer.ideos.PrimaryIdeo.RolesListForReading.FirstOrDefault(p => p.def.leaderRole); + if (preceptRole != null) + cachedRoles.Add(preceptRole); + cachedRoles.SortBy(x => x.def.displayOrderInImpact); + } + } + + public override void DoWindowContents(Rect inRect) + { + drawing = this; + + try + { + var session = Session; + + if (session == null) + { + soundClose = SoundDefOf.Click; + Close(); + } + + // Make space for the "Switch to map" button + inRect.yMin += 20f; + + base.DoWindowContents(inRect); + } + finally + { + drawing = null; + } + } +} diff --git a/Source/Client/Persistent/RitualData.cs b/Source/Client/Persistent/RitualData.cs index 2827e942..9028a8b0 100644 --- a/Source/Client/Persistent/RitualData.cs +++ b/Source/Client/Persistent/RitualData.cs @@ -6,6 +6,11 @@ namespace Multiplayer.Client.Persistent { + public class MpRitualAssignments : RitualRoleAssignments + { + public RitualSession session; + } + public class RitualData : ISynchronizable { public Precept_Ritual ritual; diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/RitualPatches.cs similarity index 56% rename from Source/Client/Persistent/Rituals.cs rename to Source/Client/Persistent/RitualPatches.cs index c5ebcc91..e8cff093 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/RitualPatches.cs @@ -11,179 +11,12 @@ namespace Multiplayer.Client.Persistent { - public class RitualSession : SemiPersistentSession - { - public Map map; - public RitualData data; - - public override Map Map => map; - - public RitualSession(Map map) : base(map) - { - this.map = map; - } - - public RitualSession(Map map, RitualData data) : this(map) - { - this.data = data; - this.data.assignments.session = this; - } - - [SyncMethod] - public void Remove() - { - map.MpComp().sessionManager.RemoveSession(this); - } - - [SyncMethod] - public void Start() - { - if (data.action != null && data.action(data.assignments)) - Remove(); - } - - public void OpenWindow(bool sound = true) - { - var dialog = new BeginRitualProxy( - data.assignments, - data.ritualLabel, - data.ritual, - data.target, - map, - data.action, - data.organizer, - data.obligation, - null, - data.confirmText, - null, - null, - data.outcome, - data.extraInfos, - null - ); - - if (!sound) - dialog.soundAppear = null; - - Find.WindowStack.Add(dialog); - } - - public override void Sync(SyncWorker sync) - { - if (sync.isWriting) - { - sync.Write(data); - } - else - { - data = sync.Read(); - data.assignments.session = this; - } - } - - public override bool IsCurrentlyPausing(Map map) => map == this.map; - - public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) - { - return new FloatMenuOption("MpRitualSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - OpenWindow(); - }); - } - } - - public class MpRitualAssignments : RitualRoleAssignments - { - public RitualSession session; - } - - public class BeginRitualProxy : Dialog_BeginRitual, ISwitchToMap - { - public static BeginRitualProxy drawing; - - public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); - - // In the base type, the unused fields are used to create RitualRoleAssignments which already exist in an MP session - // They are here to help keep the base constructor in sync with this one - public BeginRitualProxy( - RitualRoleAssignments assignments, - string ritualLabel, - Precept_Ritual ritual, - TargetInfo target, - Map map, - ActionCallback action, - Pawn organizer, - RitualObligation obligation, - PawnFilter filter = null, - string okButtonText = null, - // ReSharper disable once UnusedParameter.Local - List requiredPawns = null, - // ReSharper disable once UnusedParameter.Local - Dictionary forcedForRole = null, - RitualOutcomeEffectDef outcome = null, - List extraInfoText = null, - // ReSharper disable once UnusedParameter.Local - Pawn selectedPawn = null) : - base(assignments, ritual, target, ritual?.outcomeEffect?.def ?? outcome) - { - this.obligation = obligation; - this.filter = filter; - this.organizer = organizer; - this.map = map; - this.action = action; - ritualExplanation = ritual?.ritualExplanation; - this.ritualLabel = ritualLabel; - this.okButtonText = okButtonText ?? "OK".Translate(); - extraInfos = extraInfoText; - - soundClose = SoundDefOf.TabClose; - - // This gets cancelled in the base constructor if called from ticking/cmd in DontClearDialogBeginRitualCache - // Note that: cachedRoles is a static field, cachedRoles is only used for UI drawing - cachedRoles.Clear(); - if (ritual is { ideo: not null }) - { - cachedRoles.AddRange(ritual.ideo.RolesListForReading.Where(r => !r.def.leaderRole)); - Precept_Role preceptRole = Faction.OfPlayer.ideos.PrimaryIdeo.RolesListForReading.FirstOrDefault(p => p.def.leaderRole); - if (preceptRole != null) - cachedRoles.Add(preceptRole); - cachedRoles.SortBy(x => x.def.displayOrderInImpact); - } - } - - public override void DoWindowContents(Rect inRect) - { - drawing = this; - - try - { - var session = Session; - - if (session == null) - { - soundClose = SoundDefOf.Click; - Close(); - } - - // Make space for the "Switch to map" button - inRect.yMin += 20f; - - base.DoWindowContents(inRect); - } - finally - { - drawing = null; - } - } - } - [HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonTextWorker))] static class MakeCancelRitualButtonRed { static void Prefix(string label, ref bool __state) { - if (BeginRitualProxy.drawing == null) return; + if (RitualBeginProxy.drawing == null) return; if (label != "CancelButton".Translate()) return; GUI.color = new Color(1f, 0.3f, 0.35f); @@ -197,7 +30,7 @@ static void Postfix(bool __state, ref DraggableResult __result) GUI.color = Color.white; if (__result.AnyPressed()) { - BeginRitualProxy.drawing.Session?.Remove(); + RitualBeginProxy.drawing.Session?.Remove(); __result = DraggableResult.Idle; } } @@ -208,7 +41,7 @@ static class HandleStartRitual { static bool Prefix(Dialog_BeginRitual __instance) { - if (__instance is BeginRitualProxy proxy) + if (__instance is RitualBeginProxy proxy) { proxy.Session.Start(); return false; @@ -224,7 +57,7 @@ static class CancelDialogBeginRitual static bool Prefix(Window window) { if (Multiplayer.Client != null - && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let BeginRitualProxy through + && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let RitualBeginProxy through && (Multiplayer.ExecutingCmds || Multiplayer.Ticking)) { var tempDialog = (Dialog_BeginRitual)window; diff --git a/Source/Client/Persistent/RitualSession.cs b/Source/Client/Persistent/RitualSession.cs new file mode 100644 index 00000000..6010eed0 --- /dev/null +++ b/Source/Client/Persistent/RitualSession.cs @@ -0,0 +1,87 @@ +using Multiplayer.API; +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Persistent; + +public class RitualSession : SemiPersistentSession +{ + public Map map; + public RitualData data; + + public override Map Map => map; + + public RitualSession(Map map) : base(map) + { + this.map = map; + } + + public RitualSession(Map map, RitualData data) : this(map) + { + this.data = data; + this.data.assignments.session = this; + } + + [SyncMethod] + public void Remove() + { + map.MpComp().sessionManager.RemoveSession(this); + } + + [SyncMethod] + public void Start() + { + if (data.action != null && data.action(data.assignments)) + Remove(); + } + + public void OpenWindow(bool sound = true) + { + var dialog = new RitualBeginProxy( + data.assignments, + data.ritualLabel, + data.ritual, + data.target, + map, + data.action, + data.organizer, + data.obligation, + null, + data.confirmText, + null, + null, + data.outcome, + data.extraInfos, + null + ); + + if (!sound) + dialog.soundAppear = null; + + Find.WindowStack.Add(dialog); + } + + public override void Sync(SyncWorker sync) + { + if (sync.isWriting) + { + sync.Write(data); + } + else + { + data = sync.Read(); + data.assignments.session = this; + } + } + + public override bool IsCurrentlyPausing(Map map) => map == this.map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return new FloatMenuOption("MpRitualSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + OpenWindow(); + }); + } +} diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs index b927f4bd..759b4cb3 100644 --- a/Source/MultiplayerLoader/SyncRituals.cs +++ b/Source/MultiplayerLoader/SyncRituals.cs @@ -47,9 +47,9 @@ void Register(Type baseType, string method, Type derivedType, Action Date: Sun, 14 Jul 2024 17:34:03 +0200 Subject: [PATCH 03/11] Fix caravans --- Source/Client/Patches/UniqueIds.cs | 11 ++-- .../Persistent/CaravanFormingPatches.cs | 65 +++++++++++++++++-- .../Client/Persistent/CaravanFormingProxy.cs | 5 +- .../Persistent/CaravanFormingSession.cs | 14 ++-- Source/Client/Syncing/Game/SyncDelegates.cs | 48 -------------- 5 files changed, 80 insertions(+), 63 deletions(-) diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs index c2591c1d..cc96246e 100644 --- a/Source/Client/Patches/UniqueIds.cs +++ b/Source/Client/Patches/UniqueIds.cs @@ -13,16 +13,19 @@ public static class UniqueIdsPatch // Start at -2 because -1 is sometimes used as the uninitialized marker private static int localIds = -2; + public static bool useLocalIdsOverride; + + private static bool UseLocalIds => + Multiplayer.Client != null && (useLocalIdsOverride || Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry); + static bool Prefix() { - return Multiplayer.Client == null || (!Multiplayer.InInterface && Current.ProgramState != ProgramState.Entry); + return !UseLocalIds; } static void Postfix(ref int __result) { - if (Multiplayer.Client == null) return; - - if (Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry) + if (UseLocalIds) __result = localIds--; } } diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs index 18922d96..9dab7444 100644 --- a/Source/Client/Persistent/CaravanFormingPatches.cs +++ b/Source/Client/Persistent/CaravanFormingPatches.cs @@ -2,6 +2,8 @@ using RimWorld; using RimWorld.Planet; using System; +using Multiplayer.API; +using Multiplayer.Client.Patches; using UnityEngine; using Verse; using static Verse.Widgets; @@ -147,7 +149,7 @@ static class CancelDialogFormCaravan { static bool Prefix(Window window) { - if (Multiplayer.MapContext != null && window.GetType() == typeof(Dialog_FormCaravan)) + if (Multiplayer.Client != null && window.GetType() == typeof(Dialog_FormCaravan)) return false; return true; @@ -155,21 +157,76 @@ static bool Prefix(Window window) } [HarmonyPatch(typeof(Dialog_FormCaravan), MethodType.Constructor)] - [HarmonyPatch(new[] { typeof(Map), typeof(bool), typeof(Action), typeof(bool), typeof(IntVec3) })] + [HarmonyPatch(new[] { typeof(Map), typeof(bool), typeof(Action), typeof(bool), typeof(IntVec3?) })] static class DialogFormCaravanCtorPatch { - static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved) + static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? designatedMeetingPoint) { + if (Multiplayer.Client == null) + return; + if (__instance.GetType() != typeof(Dialog_FormCaravan)) return; // Handles showing the dialog from TimedForcedExit.CompTick -> TimedForcedExit.ForceReform + // (note TimedForcedExit is obsolete) if (Multiplayer.ExecutingCmds || Multiplayer.Ticking) { var comp = map.MpComp(); if (comp.sessionManager.GetFirstOfType() == null) - comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved); + comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved, designatedMeetingPoint); } + else // Handles opening from the interface: forming gizmos, reforming gizmos and caravan hitching spots + { + StartFormingCaravan(map, reform, designatedMeetingPoint); + } + } + + [SyncMethod] + internal static void StartFormingCaravan(Map map, bool reform = false, IntVec3? designatedMeetingPoint = null, int? routePlannerWaypoint = null) + { + var comp = map.MpComp(); + var session = comp.CreateCaravanFormingSession(reform, null, false, designatedMeetingPoint); + + if (TickPatch.currentExecutingCmdIssuedBySelf) + { + var dialog = session.OpenWindow(); + if (routePlannerWaypoint is { } tile) + { + try + { + UniqueIdsPatch.useLocalIdsOverride = true; + + // Just to be safe + // RNG shouldn't be invoked but TryAddWaypoint is quite complex and calls pathfinding + Rand.PushState(); + + var worldRoutePlanner = Find.WorldRoutePlanner; + worldRoutePlanner.Start(dialog); + worldRoutePlanner.TryAddWaypoint(tile); + } + finally + { + Rand.PopState(); + UniqueIdsPatch.useLocalIdsOverride = false; + } + } + } + } + } + + [HarmonyPatch(typeof(FormCaravanGizmoUtility), nameof(FormCaravanGizmoUtility.DialogFromToSettlement))] + static class HandleFormCaravanShowRoutePlanner + { + static bool Prefix(Map origin, int tile) + { + if (Multiplayer.Client == null) + return true; + + // Override behavior in multiplayer + DialogFormCaravanCtorPatch.StartFormingCaravan(origin, routePlannerWaypoint: tile); + + return false; } } diff --git a/Source/Client/Persistent/CaravanFormingProxy.cs b/Source/Client/Persistent/CaravanFormingProxy.cs index c8886833..8536b51b 100644 --- a/Source/Client/Persistent/CaravanFormingProxy.cs +++ b/Source/Client/Persistent/CaravanFormingProxy.cs @@ -11,8 +11,11 @@ public class CaravanFormingProxy : Dialog_FormCaravan, ISwitchToMap public CaravanFormingSession Session => map.MpComp().sessionManager.GetFirstOfType(); - public CaravanFormingProxy(Map map, bool reform = false, Action onClosed = null, bool mapAboutToBeRemoved = false, IntVec3? meetingSpot = null) : base(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot) + public int originalSessionId; + + public CaravanFormingProxy(int originalSessionId, Map map, bool reform = false, Action onClosed = null, bool mapAboutToBeRemoved = false, IntVec3? meetingSpot = null) : base(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot) { + this.originalSessionId = originalSessionId; } public override void DoWindowContents(Rect inRect) diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 7f2639af..fd1fc32a 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -44,7 +44,7 @@ public CaravanFormingSession(Map map, bool reform, Action onClosed, bool mapAbou private void AddItems() { - var dialog = new CaravanFormingProxy(map, reform, null, mapAboutToBeRemoved, meetingSpot) + var dialog = new CaravanFormingProxy(sessionId, map, reform, null, mapAboutToBeRemoved, meetingSpot) { autoSelectTravelSupplies = autoSelectTravelSupplies }; @@ -52,7 +52,7 @@ private void AddItems() transferables = dialog.transferables; } - public void OpenWindow(bool sound = true) + public CaravanFormingProxy OpenWindow(bool sound = true) { var dialog = PrepareDummyDialog(); if (!sound) @@ -72,13 +72,14 @@ public void OpenWindow(bool sound = true) ); dialog.Notify_TransferablesChanged(); - Find.WindowStack.Add(dialog); + + return dialog; } private CaravanFormingProxy PrepareDummyDialog() { - var dialog = new CaravanFormingProxy(map, reform, null, mapAboutToBeRemoved, meetingSpot) + var dialog = new CaravanFormingProxy(sessionId, map, reform, null, mapAboutToBeRemoved, meetingSpot) { transferables = transferables, startingTile = startingTile, @@ -142,7 +143,9 @@ public void Cancel() private void Remove() { map.MpComp().sessionManager.RemoveSession(this); - Find.WorldRoutePlanner.Stop(); + + if (Find.WorldRoutePlanner.currentFormCaravanDialog is CaravanFormingProxy proxy && proxy.originalSessionId == sessionId) + Find.WorldRoutePlanner.Stop(); } [SyncMethod] @@ -186,7 +189,6 @@ public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry { return new FloatMenuOption("MpCaravanFormingSession".Translate(), () => { - SwitchToMapOrWorld(entry.map); OpenWindow(); }); } diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index b145fc5b..cb2ef6ee 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -468,54 +468,6 @@ static void GeneUIUtilityTarget(Thing target) geneUIUtilityTarget = target; } - [MpPrefix(typeof(FormCaravanComp), nameof(FormCaravanComp.GetGizmos), lambdaOrdinal: 0)] - static bool GizmoFormCaravan(MapParent ___mapParent) - { - if (Multiplayer.Client == null) return true; - GizmoFormCaravan(___mapParent.Map, false); - return false; - } - - [MpPrefix(typeof(FormCaravanComp), nameof(FormCaravanComp.GetGizmos), lambdaOrdinal: 1)] - static bool GizmoReformCaravan(MapParent ___mapParent) - { - if (Multiplayer.Client == null) return true; - GizmoFormCaravan(___mapParent.Map, true); - return false; - } - - [MpPrefix(typeof(CompHitchingSpot), nameof(CompHitchingSpot.CompGetGizmosExtra), 0)] - static bool GizmoFormCaravan(CompHitchingSpot __instance) - { - if (Multiplayer.Client == null) return true; - GizmoFormCaravan(__instance.parent.Map, false, __instance.parent.Position); - return false; - } - - private static void GizmoFormCaravan(Map map, bool reform, IntVec3? meetingSpot = null) - { - var comp = map.MpComp(); - - if (comp.sessionManager.GetFirstOfType() is { } session) - session.OpenWindow(); - else - CreateCaravanFormingSession(comp, reform, meetingSpot); - } - - [SyncMethod] - private static void CreateCaravanFormingSession(MultiplayerMapComp comp, bool reform, IntVec3? meetingSpot = null) - { - var session = comp.CreateCaravanFormingSession(reform, null, false, meetingSpot); - - if (TickPatch.currentExecutingCmdIssuedBySelf) - { - session.OpenWindow(); - AsyncTimeComp.keepTheMap = true; - Current.Game.CurrentMap = comp.map; - Find.World.renderer.wantedMode = WorldRenderMode.None; - } - } - [MpPostfix(typeof(CaravanVisitUtility), nameof(CaravanVisitUtility.TradeCommand))] static void ReopenTradingWindowLocally(Caravan caravan, Command __result) { From 50e159ce1e2febfc467ec51322984f427b124921 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:37:30 +0200 Subject: [PATCH 04/11] Patch for UndercaveMapComponent (pit gate) (#470) Info on `UndercaveMapComponent:MapComponentTick` and why it needs patching: - RNG calls after current map/visible camera area checks - Majority of the RNG calls are visual/audio effects only - `TriggerCollapseFX` does have an effect on simulation by spawning collapsed mountain roof - The collapsing rocks have minimal impact on the game, as they avoid player pawns Fixing RNG is easy by just pushing/popping the state. Fixing the collapsed mountain roof is more complex. Possible solutions: - Disable rock collapse completely - Disable current map checks (suboptimal for performance) - Disable the existing calls to `TriggerCollapseFX` and call it in a deterministic way (used in this PR) Information about the patch: - The transpiler will make the call to `Rand.MTBEventOccurs` always fail, making so `TriggerCollapseFX` is never called - This could be further improved by removing the RNG call and the current map check altogether to improve performance, but would be more complex - The prefix and postfix push/pop RNG state, making sure the ticking code doesn't mess with the RNG state - The postfix re-implements the call to `TriggerCollapseFX` in a deterministic way - If the current player is not looking at the current map, it will be called with `0` as both arguments to prevent additional effects from triggering - The call to the method is surrounded by RNG push/pop state, as the amount of RNG calls will differ if the player is not looking at the map - It's safe as the simulation-affecting RNG calls happen first - The call is currently unseeded, but could be easily seeded with `Gen.HashCombineInt(Find.TickManager.TicksGame, __instance.map.uniqueID)` if we care about having a seed here Remaining issues with `UndercaveMapComponent`/Pit Gate: - The rock collapse has additional check to not drop rocks on player faction pawns. However, I assume this won't work properly with Multifaction, which could end up crushing pawns of some players. --- Source/Client/Patches/Determinism.cs | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 1eac7ec5..b6b8a7b2 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -524,4 +524,64 @@ static IEnumerable Transpiler(IEnumerable inst } } + [HarmonyPatch(typeof(UndercaveMapComponent), nameof(UndercaveMapComponent.MapComponentTick))] + static class DeterministicUndercaveRockCollapse + { + static IEnumerable Transpiler(IEnumerable instr) + { + var target = MethodOf.Lambda(Rand.MTBEventOccurs); + + foreach (var ci in instr) + { + yield return ci; + + // Add "& false" to any call to Rand.MTBEventOccurs. + // We'll handle those calls in our postfix. + if (ci.Calls(target)) + { + yield return new CodeInstruction(OpCodes.Ldc_I4_0); + yield return new CodeInstruction(OpCodes.And); + } + } + } + + static void Prefix() => Rand.PushState(); + + static void Postfix(UndercaveMapComponent __instance) + { + // Pop the RNG state from the prefix + Rand.PopState(); + + // Make sure the pit gate is collapsing + if (__instance.pitGate is not { IsCollapsing: true }) + return; + + // Check if the rocks should collapse + var mtb = UndercaveMapComponent.HoursToShakeMTBTicksCurve.Evaluate(__instance.pitGate.TicksUntilCollapse / 2500f); + if (!Rand.MTBEventOccurs(mtb, 1, 1)) + return; + + // Since the number of RNG calls will depend on numDustEffecters argument, we need to push/pop the RNG state. + // The RNG calls related to simulation will happen first, followed by the one determined by amount of + // effecters - it would not be MP safe, but since it happens last it will be fine once we pop the state. + Rand.PushState(); + + // If not looking at the map, trigger the collapse without shake/effecters (since it's not needed for current player). + // The call to play a sound is handled by RW itself, since it targets a specific map already. + if (Find.CurrentMap != __instance.map) + { + // Progress the RNG state, matching the RandomInRange call in other two cases + Rand.RangeInclusive(0, 100); + __instance.TriggerCollapseFX(0, 0); + } + // Else, follow vanilla shake/effecter rules + else if (__instance.pitGate.CollapseStage == 1) + __instance.TriggerCollapseFX(UndercaveMapComponent.StageOneShakeAmount, UndercaveMapComponent.StageOneNumCollapseEffects.RandomInRange); + else + __instance.TriggerCollapseFX(UndercaveMapComponent.StageTwoShakeAmount, UndercaveMapComponent.StageTwoNumCollapseEffects.RandomInRange); + + Rand.PopState(); + } + } + } From b23cbfaa3fbae1ceb252e3948b325ea795aaed1c Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:37:57 +0200 Subject: [PATCH 05/11] Add EarlyPatch attribute to PawnTweener.TweenedPos patch (#471) Zetrith provided the fix on Discord and pointed out that it seems to be inlining issue. It was the first time I've dealt with an issue like this, so I greatly appreciate it. I've spent several day investigating the issue and have just reached a dead end. This should fix the issue with Vanilla Expanded Framework. Using this fix I was not able to reproduce the issue anymore. --- Source/Client/Patches/Determinism.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index b6b8a7b2..5412aaf0 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -15,6 +15,7 @@ namespace Multiplayer.Client.Patches { + [EarlyPatch] [HarmonyPatch(typeof(PawnTweener))] [HarmonyPatch(nameof(PawnTweener.TweenedPos), MethodType.Getter)] static class DrawPosPatch From c0a7ef83c7888f27553d3f69379917723848d755 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:38:36 +0200 Subject: [PATCH 06/11] Fix issues due to _NewTemp methods, fixes heater/cooler temperature changes (#472) This should fix changing heater/cooler temperature settings as well as giving the players time to rename stillborn pawns (normally would be 1 tick in this situation). --- Source/Client/Syncing/Game/SyncDelegates.cs | 2 +- Source/Client/Syncing/Game/SyncMethods.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index cb2ef6ee..4772f0af 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -364,7 +364,7 @@ static void SetBabyName(ChoiceLetter_BabyBirth letter) } // If the baby ended up being stillborn, the timer to name them is 1 tick. This patch is here to allow players in MP to actually change their name. - [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome))] + [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome_NewTemp))] static void GiveTimeToNameStillborn(Thing __result) { if (Multiplayer.Client != null && __result is Pawn pawn && pawn.health.hediffSet.HasHediff(HediffDefOf.Stillborn)) diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs index d2351e09..dc63361b 100644 --- a/Source/Client/Syncing/Game/SyncMethods.cs +++ b/Source/Client/Syncing/Game/SyncMethods.cs @@ -105,7 +105,7 @@ public static void Init() SyncMethod.Register(typeof(Building_SunLamp), nameof(Building_SunLamp.MakeMatchingGrowZone)); SyncMethod.Register(typeof(Building_ShipComputerCore), nameof(Building_ShipComputerCore.TryLaunch)); SyncMethod.Register(typeof(CompPower), nameof(CompPower.TryManualReconnect)); - SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature)); + SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature_NewTemp)); SyncMethod.Register(typeof(CompTransporter), nameof(CompTransporter.CancelLoad), Array.Empty()); SyncMethod.Register(typeof(MapPortal), nameof(MapPortal.CancelLoad)); SyncMethod.Register(typeof(StorageSettings), nameof(StorageSettings.CopyFrom)).ExposeParameter(0); From 8011e49b6dea8b0101b8fe5304d84f526fdb0f75 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:40:01 +0200 Subject: [PATCH 07/11] Update position of attached motes based on real position (#473) This should fix the issue with Noctol eyes, along with other attached motes being drawn away from the pawns they are attached to. This was likely never really noticed before due to no other vanilla attached motes having their position as important as here. Getting a DrawPos of a pawn during ticking will cause `PawnTweener.TweenedPos` to return `TweenedPosRoot` in MP to make the method deterministic. However, `MoteAttached` updated during simulation will draw it in an incorrect position due to the pawn position not being where it is visually. The fix here is to cause `PawnTweener.TweenedPos` patch not to run while calculating the position of the attached mote. --- Source/Client/Patches/Determinism.cs | 6 ++++-- Source/Client/Patches/Patches.cs | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 5412aaf0..388db51f 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -20,12 +20,14 @@ namespace Multiplayer.Client.Patches [HarmonyPatch(nameof(PawnTweener.TweenedPos), MethodType.Getter)] static class DrawPosPatch { - static bool Prefix() => Multiplayer.Client == null || Multiplayer.InInterface; + public static bool returnTruePosition = false; + + static bool Prefix() => Multiplayer.Client == null || Multiplayer.InInterface || returnTruePosition; // Give the root position during ticking static void Postfix(PawnTweener __instance, ref Vector3 __result) { - if (Multiplayer.Client == null || Multiplayer.InInterface) return; + if (Multiplayer.Client == null || Multiplayer.InInterface || returnTruePosition) return; __result = __instance.TweenedPosRoot(); } } diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index a0c35a87..37e4e25c 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -9,6 +9,7 @@ using System.Reflection.Emit; using System.Text.RegularExpressions; using System.Xml.Linq; +using Multiplayer.Client.Patches; using UnityEngine; using Verse; using Verse.AI; @@ -581,4 +582,12 @@ static void FixStorage(IStoreSettingsParent __instance, StorageSettings ___allow ___allowedNutritionSettings.owner ??= __instance; } } + + [HarmonyPatch(typeof(MoteAttachLink), nameof(MoteAttachLink.UpdateDrawPos))] + static class MoteAttachLinkUsesTruePosition + { + static void Prefix() => DrawPosPatch.returnTruePosition = true; + + static void Finalizer() => DrawPosPatch.returnTruePosition = false; + } } From 8b74f832c83121bd6d77f47f939e7ee201e62c8b Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:40:16 +0200 Subject: [PATCH 08/11] Use a randomized seed for long events (#474) Previously, we've used a constant seed for long events. This change will replace it with a randomized seed which is selected whenever a long event is queued. This should allow for more randomness in long events, and is required by the labyrinth map generation to produce unique maps, instead of only having a single possible layout. --- Source/Client/Patches/Seeds.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index d4301500..cef7d7d3 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -98,11 +98,12 @@ static void Prefix(ref Action action) { if (Multiplayer.Client != null && (Multiplayer.Ticking || Multiplayer.ExecutingCmds)) { - action = PushState + action + Rand.PopState; + var seed = Rand.Int; + action = (() => PushState(seed)) + action + Rand.PopState; } } - static void PushState() => Rand.PushState(4); + static void PushState(int seed) => Rand.PushState(seed); } // Seed the rotation random From ffa6080dad3771371005d48ed166ffb1f7ee6f88 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:45:04 +0200 Subject: [PATCH 09/11] Synced ChoiceLetter_AcceptCreepJoiner (#477) It was synced by `PersistentDialog` syncing if the dialog was opened by interacting with the joiner pawn. However, it is not synced when interacted using the choice letter (which this PR fixes). Preferably, we'd disable the dialog opening when interacted (and only allow through choice letter) since it opens it for all players, and is rather disruptive. However, I'm leaving it for another PR. --- Source/Client/Syncing/Game/SyncDelegates.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index 4772f0af..caf07fa3 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -320,6 +320,13 @@ private static void InitChoiceLetters() // Growth moment for a child CloseDialogsForExpiredLetters.RegisterDefaultLetterChoice(AccessTools.Method(typeof(SyncDelegates), nameof(PickRandomTraitAndPassions)), typeof(ChoiceLetter_GrowthMoment)); SyncMethod.Register(typeof(ChoiceLetter_GrowthMoment), nameof(ChoiceLetter_GrowthMoment.MakeChoices)).ExposeParameter(1); + + // Creep joiner + SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 0); // Accept joiner + SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 1); // Arrest joiner + CloseDialogsForExpiredLetters.RegisterDefaultLetterChoice( + SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 2) + .method); // Reject joiner } static void SyncBabyToChildLetter(ChoiceLetter_BabyToChild letter) From cd7504e68904048bfad7840901efa4f3c12db716 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:46:25 +0200 Subject: [PATCH 10/11] Fix desyncs due to CompCableConnection motes, fixes #447 (#481) The method calls random whenever the mote is null or destroyed. There may be situations where the mote ends up being created late or destroyed for some players, but not the others. This is guaranteed to happen when playing with multiple maps, and seems like it may happen when the game is not paused when someone is joining. Since the desync happens due to RNG call to randomize the mote position, this change should fix it by pushing/popping the RNG state before/after the method is called. --- Source/Client/MultiplayerStatic.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index df943b0b..34ce2342 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -353,7 +353,8 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po var canEverSpectate = typeof(RitualRoleAssignments).GetMethod(nameof(RitualRoleAssignments.CanEverSpectate)); var effectMethods = new MethodBase[] { subSustainerStart, sampleCtor, subSoundPlay, effecterTick, effecterTrigger, effecterCleanup, randomBoltMesh, drawTrackerCtor, randomHair }; - var moteMethods = typeof(MoteMaker).GetMethods(BindingFlags.Static | BindingFlags.Public); + var moteMethods = typeof(MoteMaker).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Concat(typeof(CompCableConnection.Cable).GetMethod(nameof(CompCableConnection.Cable.Tick))); var fleckMethods = typeof(FleckMaker).GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(m => m.ReturnType == typeof(void)) .Concat(typeof(FleckManager).GetMethods() // FleckStatic uses Rand in Setup method, FleckThrown uses RandomInRange in TimeInterval. May as well catch all in case mods do the same. From 49b6f5aecd142cd9a96ee23b8ade52d143dfd91b Mon Sep 17 00:00:00 2001 From: Zetrith Date: Tue, 16 Jul 2024 20:06:05 +0200 Subject: [PATCH 11/11] Version 0.10.4 --- Source/Client/Multiplayer.csproj | 2 +- Source/Common/Common.csproj | 2 +- Source/Common/Version.cs | 4 ++-- Source/MultiplayerLoader/MultiplayerLoader.csproj | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index a38b0088..0c443666 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -32,7 +32,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index 039415ad..7f39a9d6 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -12,7 +12,7 @@ - + diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index db91a7f4..b43cf4a9 100644 --- a/Source/Common/Version.cs +++ b/Source/Common/Version.cs @@ -2,8 +2,8 @@ namespace Multiplayer.Common { public static class MpVersion { - public const string Version = "0.10.3"; - public const int Protocol = 45; + public const string Version = "0.10.4"; + public const int Protocol = 46; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index 195aae26..5572bda9 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive