From 454829e8c9e89c046e21b935156877b010435a10 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Wed, 19 Jun 2024 01:32:42 +0200 Subject: [PATCH 1/2] Fix/sync CompObelisk_Abductor (Warped Obelisk/Labyrinth) The obelisk currently causes desyncs due to the map being generated in a separate thread. We currently make change asynchronous long events queued when executing synced commands into synchronous ones. To err on the side of caution, I've allowed this to be applied to any long event but only when a new field (`forceSynchronously`) is set to true. I could make all long events (if MP is active) synchronous, rather than specific ones, if this is preferred. Additional changes to `LongEventAlwaysSync` were also needed: - Adding the `callback` action to `action` to be executed after, as `callback` is only executed on the main thread for asynchronous long events - Adding `HarmonyPriority(Priority.HigherThanNormal)` to the patch, as this method needs to run before `SeedLongEvents` patch (to seed both action and callback) The final change is to add prefix/finalizer to `CompObelisk_Abductor.GenerateLabyrinth` to set `forceSynchronously` to true/false. If we decide to make all long events in MP synchronous then this patch will be safe to remove. --- Source/Client/Patches/Determinism.cs | 7 +++++++ Source/Client/Patches/LongEvents.cs | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 1eac7ec5..90c81df5 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -524,4 +524,11 @@ static IEnumerable Transpiler(IEnumerable inst } } + [HarmonyPatch(typeof(CompObelisk_Abductor), nameof(CompObelisk_Abductor.GenerateLabyrinth))] + static class SynchronousAbductorObelisk + { + static void Prefix() => LongEventAlwaysSync.forceSynchronously = true; + static void Finalizer() => LongEventAlwaysSync.forceSynchronously = false; + } + } diff --git a/Source/Client/Patches/LongEvents.cs b/Source/Client/Patches/LongEvents.cs index b07b4e40..ea1f6fe8 100644 --- a/Source/Client/Patches/LongEvents.cs +++ b/Source/Client/Patches/LongEvents.cs @@ -62,13 +62,25 @@ static void Postfix() } } + [HarmonyPriority(Priority.HigherThanNormal)] [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), new[] { typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action) })] static class LongEventAlwaysSync { - static void Prefix(ref bool doAsynchronously) + public static bool forceSynchronously = false; + + static void Prefix(ref bool doAsynchronously, ref Action action, Action callback) { - if (Multiplayer.ExecutingCmds) + if (Multiplayer.ExecutingCmds || forceSynchronously) + { doAsynchronously = false; + + // Callback is only called in asynchronous long events and will be skipped in + // a synchronous ones. Make sure they are called after the actual action. + // This patch also needs higher priority than SeedLongEvents + // to make sure that both action and callback are seeded properly. + if (callback != null) + action += callback; + } } } } From 4d3637662f6702b8aa28bc0566978b3702ba3422 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 16 Sep 2024 18:07:15 +0200 Subject: [PATCH 2/2] Reworked the Warped Obelisk compat - Removed forcibly making Warped Obelisk long event synchronous - Seeded long event callback - This will fix Warped Obelisk long event desync, along with other asynchronous long events using callbacks - When a long event is made synchronous (executing commands), the callback (if present) will be added to the action itself - The callback is normally skipped for synchronous long events - This is technically unnecessary with Vanilla and MP, but may become relevant with mods (or in a future update) - Removed harmony priority attribute, as it's not needed anymore --- Source/Client/Patches/Determinism.cs | 7 ------- Source/Client/Patches/LongEvents.cs | 7 +------ Source/Client/Patches/Seeds.cs | 8 +++++++- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 8acb9feb..388db51f 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -587,11 +587,4 @@ static void Postfix(UndercaveMapComponent __instance) } } - [HarmonyPatch(typeof(CompObelisk_Abductor), nameof(CompObelisk_Abductor.GenerateLabyrinth))] - static class SynchronousAbductorObelisk - { - static void Prefix() => LongEventAlwaysSync.forceSynchronously = true; - static void Finalizer() => LongEventAlwaysSync.forceSynchronously = false; - } - } diff --git a/Source/Client/Patches/LongEvents.cs b/Source/Client/Patches/LongEvents.cs index ea1f6fe8..75aa6a5f 100644 --- a/Source/Client/Patches/LongEvents.cs +++ b/Source/Client/Patches/LongEvents.cs @@ -62,22 +62,17 @@ static void Postfix() } } - [HarmonyPriority(Priority.HigherThanNormal)] [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), new[] { typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action) })] static class LongEventAlwaysSync { - public static bool forceSynchronously = false; - static void Prefix(ref bool doAsynchronously, ref Action action, Action callback) { - if (Multiplayer.ExecutingCmds || forceSynchronously) + if (Multiplayer.ExecutingCmds) { doAsynchronously = false; // Callback is only called in asynchronous long events and will be skipped in // a synchronous ones. Make sure they are called after the actual action. - // This patch also needs higher priority than SeedLongEvents - // to make sure that both action and callback are seeded properly. if (callback != null) action += callback; } diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index cef7d7d3..942fa875 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -94,12 +94,18 @@ static void Postfix(Map map, bool __state) [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action))] static class SeedLongEvents { - static void Prefix(ref Action action) + static void Prefix(ref Action action, ref Action callback) { if (Multiplayer.Client != null && (Multiplayer.Ticking || Multiplayer.ExecutingCmds)) { var seed = Rand.Int; action = (() => PushState(seed)) + action + Rand.PopState; + + if (callback != null) + { + var callbackSeed = Rand.Int; + callback = (() => PushState(callbackSeed)) + callback + Rand.PopState; + } } }