From 2fbfa5a6d6b6eb853e80f97a83bdf60beaf193ab Mon Sep 17 00:00:00 2001 From: kmyuhkyuk <72241714+kmyuhkyuk@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:27:21 +0800 Subject: [PATCH] Fixed ENetClient ConnectToHost can't return NetErrorInfo Added CombatStateSynchronizer Timeout and Resend mechanism --- .../Helpers/TaskGenericHelper.cs | 31 +++++ .../Patchs/CombatStateSynchronizerPatch.cs | 124 ++++++++++++++++++ .../Patchs/ENet/ENetClientPatch.cs | 6 +- .../Patchs/JoinFlowPatch.cs | 7 +- .../Patchs/Screens/RunLoadScreenPatchs.cs | 5 +- 5 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 SlayTheSpire2.LAN.Multiplayer/Helpers/TaskGenericHelper.cs create mode 100644 SlayTheSpire2.LAN.Multiplayer/Patchs/CombatStateSynchronizerPatch.cs diff --git a/SlayTheSpire2.LAN.Multiplayer/Helpers/TaskGenericHelper.cs b/SlayTheSpire2.LAN.Multiplayer/Helpers/TaskGenericHelper.cs new file mode 100644 index 0000000..6a7a4c2 --- /dev/null +++ b/SlayTheSpire2.LAN.Multiplayer/Helpers/TaskGenericHelper.cs @@ -0,0 +1,31 @@ +using MegaCrit.Sts2.Core.Debug; +using MegaCrit.Sts2.Core.Logging; + +namespace SlayTheSpire2.LAN.Multiplayer.Helpers +{ + internal static class TaskGenericHelper + { + public static Task RunSafely(Task task) + { + return LogTaskExceptions(task); + } + + private static async Task LogTaskExceptions(Task task) + { + try + { + return await task; + } + catch (Exception ex) + { + if (ex is not TaskCanceledException) + { + Log.Error(ex.ToString()); + SentryService.CaptureException(ex); + } + + throw; + } + } + } +} \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/CombatStateSynchronizerPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/CombatStateSynchronizerPatch.cs new file mode 100644 index 0000000..bdb3961 --- /dev/null +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/CombatStateSynchronizerPatch.cs @@ -0,0 +1,124 @@ +using HarmonyLib; +using MegaCrit.Sts2.Core.Context; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Multiplayer; +using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Multiplayer.Game.Lobby; +using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; +using MegaCrit.Sts2.Core.Runs; +using MegaCrit.Sts2.Core.Saves.Runs; +using Logger = MegaCrit.Sts2.Core.Logging.Logger; + +namespace SlayTheSpire2.LAN.Multiplayer.Patchs +{ + [HarmonyPatch(typeof(CombatStateSynchronizer), "WaitForSync")] + internal class CombatStateSynchronizerWaitForSyncPatch + { + private static bool Prefix(CombatStateSynchronizer __instance, Logger ____logger, + INetGameService ____netService, TaskCompletionSource? ____syncCompletionSource, + Dictionary ____syncData, RunState ____runState, RunLobby? ____runLobby, + SerializableRunRngSet? ____rngSet, SerializableRelicGrabBag? ____sharedRelicGrabBag, ref Task __result) + { + //Whether is LAN game was not checked, because the sync issue may also occur when connect via Steam + + __result = TaskHelper.RunSafely(WaitForSync(__instance, ____logger, ____netService, + ____syncCompletionSource, ____syncData, ____runState, ____runLobby, ____rngSet, + ____sharedRelicGrabBag)); + + return false; + } + + private static async Task WaitForSync(CombatStateSynchronizer instance, Logger logger, + INetGameService netService, TaskCompletionSource? syncCompletionSource, + Dictionary syncData, RunState runState, RunLobby? runLobby, + SerializableRunRngSet? rngSet, SerializableRelicGrabBag? sharedRelicGrabBag) + { + logger.Debug("Waiting to receive all sync messages from all clients"); + if (netService.Type == NetGameType.Singleplayer || instance.IsDisabled) + return; + + if (syncCompletionSource == null) + { + throw new InvalidOperationException("StartSync must be called before WaitForSync!"); + } + + var startTime = DateTime.Now; + var lastResendTick = DateTime.Now; + + const int timeoutMs = 30; + const int resendIntervalMs = 5; + + while (!syncCompletionSource.Task.IsCompleted) + { + if ((DateTime.Now - startTime).TotalMilliseconds > timeoutMs) + { + logger.Warn("Sync timeout, skipping waiting for players"); + break; + } + + if (netService.Type == NetGameType.Host && + (DateTime.Now - lastResendTick).TotalMilliseconds > resendIntervalMs && rngSet != null && + sharedRelicGrabBag != null) + { + logger.Debug("Resending rng sync message"); + + var message = new SyncRngMessage + { + rng = rngSet, + sharedRelicGrabBag = sharedRelicGrabBag + }; + + netService.SendMessage(message); + + lastResendTick = DateTime.Now; + } + + await Task.Delay(100); + } + + foreach (var syncDatum in syncData) + { + if (runLobby != null && !runLobby.ConnectedPlayerIds.Contains(syncDatum.Key)) + { + logger.Debug($"Skipping sync for disconnected player {syncDatum.Key}"); + continue; + } + + var player = runState.GetPlayer(syncDatum.Key); + if (!LocalContext.IsMe(player)) + { + player?.SyncWithSerializedPlayer(syncDatum.Value); + } + } + + if (netService.Type != NetGameType.Host) + { + if (rngSet != null) + { + runState.Rng.LoadFromSerializable(rngSet); + } + else if (runState.Players.Count > 1) + { + logger.Error( + "There are two or more players and we are a client, but we never received the RNG set!"); + } + + if (sharedRelicGrabBag != null) + { + runState.SharedRelicGrabBag.LoadFromSerializable(sharedRelicGrabBag); + } + else if (runState.Players.Count > 1) + { + logger.Error( + "There are two or more players and we are a client, but we never received the shared relic grab bag!"); + } + } + + syncData.Clear(); + var traverse = Traverse.Create(instance); + traverse.Field("_rngSet").SetValue(null); + traverse.Field("_sharedRelicGrabBag").SetValue(null); + traverse.Field("_syncCompletionSource").SetValue(null); + } + } +} \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENet/ENetClientPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENet/ENetClientPatch.cs index a1c3072..ac39189 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENet/ENetClientPatch.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENet/ENetClientPatch.cs @@ -1,7 +1,6 @@ using Godot; using HarmonyLib; using MegaCrit.Sts2.Core.Entities.Multiplayer; -using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Multiplayer.Transport; using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet; using SlayTheSpire2.LAN.Multiplayer.Helpers; @@ -23,9 +22,10 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.ENet } private static bool Prefix(ENetClient __instance, ulong netId, string ip, ushort port, - CancellationToken cancelToken, Logger ____logger, INetClientHandler ____handler, ref Task __result) + CancellationToken cancelToken, Logger ____logger, INetClientHandler ____handler, + ref Task __result) { - __result = TaskHelper.RunSafely(ConnectToHost(__instance, ____logger, ____handler, netId, ip, port, + __result = TaskGenericHelper.RunSafely(ConnectToHost(__instance, ____logger, ____handler, netId, ip, port, cancelToken)); return false; diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/JoinFlowPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/JoinFlowPatch.cs index 32af667..79fbc5f 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/JoinFlowPatch.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/JoinFlowPatch.cs @@ -4,6 +4,7 @@ using MegaCrit.Sts2.Core.Multiplayer.Connection; using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Messages.Lobby; using MegaCrit.Sts2.Core.Platform; +using SlayTheSpire2.LAN.Multiplayer.Helpers; using SlayTheSpire2.LAN.Multiplayer.Models; using SlayTheSpire2.LAN.Multiplayer.Services; @@ -17,7 +18,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs { private static void Postfix(JoinFlow __instance, ref Task __result) { - __result = AttemptJoin(__instance, __result); + __result = TaskGenericHelper.RunSafely(AttemptJoin(__instance, __result)); } private static async Task AttemptJoin(JoinFlow joinFlow, @@ -44,7 +45,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs { private static void Postfix(JoinFlow __instance, ref Task __result) { - __result = AttemptLoadJoin(__instance, __result); + __result = TaskGenericHelper.RunSafely(AttemptLoadJoin(__instance, __result)); } private static async Task AttemptLoadJoin(JoinFlow joinFlow, @@ -71,7 +72,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs { private static void Postfix(JoinFlow __instance, ref Task __result) { - __result = AttemptRejoin(__instance, __result); + __result = TaskGenericHelper.RunSafely(AttemptRejoin(__instance, __result)); } private static async Task AttemptRejoin(JoinFlow joinFlow, diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/RunLoadScreenPatchs.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/RunLoadScreenPatchs.cs index d02ca29..21cd73d 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/RunLoadScreenPatchs.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/RunLoadScreenPatchs.cs @@ -7,6 +7,7 @@ using MegaCrit.Sts2.Core.Nodes.Screens.CustomRun; using MegaCrit.Sts2.Core.Nodes.Screens.DailyRun; using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu; using MegaCrit.Sts2.Core.Platform; +using SlayTheSpire2.LAN.Multiplayer.Helpers; using SlayTheSpire2.LAN.Multiplayer.Services; // ReSharper disable UnusedMember.Global @@ -97,7 +98,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens { if (____lobby.NetService.Platform == PlatformType.None) { - __result = RunScreenService.ShouldAllowRunToBegin(____lobby); + __result = TaskGenericHelper.RunSafely(RunScreenService.ShouldAllowRunToBegin(____lobby)); return false; } @@ -112,7 +113,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens { if (____runLobby.NetService.Platform == PlatformType.None) { - __result = RunScreenService.ShouldAllowRunToBegin(____runLobby); + __result = TaskGenericHelper.RunSafely(RunScreenService.ShouldAllowRunToBegin(____runLobby)); return false; }