diff --git a/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHostHelper.cs b/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHostHelper.cs index 33eb855..f7b38dd 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHostHelper.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHostHelper.cs @@ -1,4 +1,6 @@ -using Godot; +using System.Reflection; +using Godot; +using HarmonyLib; using MegaCrit.Sts2.Core.Daily; using MegaCrit.Sts2.Core.Entities.Multiplayer; using MegaCrit.Sts2.Core.Helpers; @@ -24,6 +26,15 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers { internal class LanHostHelper { + private static readonly Action UpdateButtons; + + static LanHostHelper() + { + UpdateButtons = AccessTools.MethodDelegate>( + typeof(NMultiplayerSubmenu).GetMethod("UpdateButtons", + BindingFlags.Instance | BindingFlags.NonPublic)!); + } + public static void StartHost(GameMode gameMode, Control loadingOverlay, NSubmenuStack stack, ushort port, int maxPlayers) { @@ -163,7 +174,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers } } - public static async Task TryAbandonMultiplayerRun(Action updateButtons) + public static async Task TryAbandonMultiplayerRun(NMultiplayerSubmenu instance) { var header = new LocString("main_menu_ui", "ABANDON_RUN_CONFIRMATION.header"); var body = new LocString("main_menu_ui", "ABANDON_RUN_CONFIRMATION.body"); @@ -209,7 +220,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers } LanRunSaveManagerService.Instance.DeleteCurrentMultiplayerRun(); - updateButtons(); + UpdateButtons(instance); } } } \ No newline at end of file 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/Models/LanPlayerNameResponseMessage.cs b/SlayTheSpire2.LAN.Multiplayer/Models/LanPlayerNameResponseMessage.cs index b8a5f00..bce1c63 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Models/LanPlayerNameResponseMessage.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Models/LanPlayerNameResponseMessage.cs @@ -1,6 +1,7 @@ using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Multiplayer.Transport; +using SlayTheSpire2.LAN.Multiplayer.Helpers; namespace SlayTheSpire2.LAN.Multiplayer.Models { @@ -14,7 +15,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Models public void Serialize(PacketWriter writer) { - writer.WriteInt(playerNames.Count); + PacketHelper.WriteVarInt(writer, (uint)playerNames.Count); foreach (var keyValue in playerNames) { writer.WriteULong(keyValue.Key); @@ -24,7 +25,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Models public void Deserialize(PacketReader reader) { - var count = reader.ReadInt(); + var count = PacketHelper.ReadVarInt(reader); playerNames = new PlayerNames(); for (var i = 0; i < count; i++) { diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/CombatStateSynchronizerPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/CombatStateSynchronizerPatch.cs new file mode 100644 index 0000000..728fa98 --- /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 timeoutSeconds = 30; + const int resendIntervalSeconds = 5; + + while (!syncCompletionSource.Task.IsCompleted) + { + if ((DateTime.Now - startTime).TotalSeconds > timeoutSeconds) + { + logger.Warn("Sync timeout, skipping waiting for players"); + break; + } + + if (netService.Type == NetGameType.Host && + (DateTime.Now - lastResendTick).TotalSeconds > resendIntervalSeconds && 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/RunManagerPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/RunManagerPatch.cs index 4cdf9ed..ad341b2 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/RunManagerPatch.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/RunManagerPatch.cs @@ -1,5 +1,16 @@ using HarmonyLib; +using MegaCrit.Sts2.Core.Achievements; +using MegaCrit.Sts2.Core.Context; +using MegaCrit.Sts2.Core.Daily; +using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Platform; +using MegaCrit.Sts2.Core.Rooms; using MegaCrit.Sts2.Core.Runs; +using MegaCrit.Sts2.Core.Runs.Metrics; +using MegaCrit.Sts2.Core.Saves; using SlayTheSpire2.LAN.Multiplayer.Services; // ReSharper disable UnusedMember.Global @@ -16,4 +27,137 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs LanPlayerNameService.Instance.SetDefaultPlayerNames(); } } + + [HarmonyPatch(typeof(RunManager), "OnEnded")] + internal class RunManagerOnEndedPatch + { + [HarmonyReversePatch] + [HarmonyPatch(typeof(RunManager), "UpdatePlayerStatsInMapPointHistory")] + private static void UpdatePlayerStatsInMapPointHistory(RunManager instance) + { + throw new NotImplementedException(); + } + + [HarmonyReversePatch] + [HarmonyPatch(typeof(RunManager), "CheckUpdateEnemyDiscoveryAfterLoss")] + private static void CheckUpdateEnemyDiscoveryAfterLoss(Player player, ModelId monster) + { + throw new NotImplementedException(); + } + + private static bool Prefix(RunManager __instance, bool isVictory, ref bool ____runHistoryWasUploaded, + ref SerializableRun __result) + { + if (__instance.NetService.Platform == PlatformType.None) + { + UpdatePlayerStatsInMapPointHistory(__instance); + var state = Traverse.Create(__instance).Property("State").GetValue(); + if (state == null) + { + throw new Exception($"{nameof(state)} is null"); + } + + var me = LocalContext.GetMe(state); + if (me == null) + { + throw new Exception($"{nameof(me)} is null"); + } + + if (state is { CurrentRoom: CombatRoom combatRoom, CurrentMapPointHistoryEntry: not null }) + { + state.CurrentMapPointHistoryEntry.Rooms.Last().TurnsTaken = combatRoom.CombatState.RoundNumber; + } + + var serializableRun = __instance.ToSave(null); + var me2 = LocalContext.GetMe(serializableRun); + if (me2 == null) + { + throw new Exception($"{nameof(me2)} is null"); + } + + if (____runHistoryWasUploaded) + { + __result = serializableRun; + return false; + } + + ____runHistoryWasUploaded = true; + if (!isVictory && state.CurrentRoom is CombatRoom combatRoom2) + { + foreach (var monstersWithSlot in combatRoom2.Encounter.MonstersWithSlots) + { + var item = monstersWithSlot.Item1; + CheckUpdateEnemyDiscoveryAfterLoss(me, item.Id); + } + } + + if (__instance.ShouldSave) + { + using (SaveManager.Instance.BeginSaveBatch()) + { + SaveManager.Instance.UpdateProgressWithRunData(serializableRun, isVictory); + foreach (var discoveredEpoch in me2.DiscoveredEpochs) + { + if (!me.DiscoveredEpochs.Contains(discoveredEpoch)) + { + me.DiscoveredEpochs.Add(discoveredEpoch); + } + } + + AchievementsHelper.AfterRunEnded(state, me, isVictory); + RunHistoryUtilities.CreateRunHistoryEntry(serializableRun, isVictory, __instance.IsAbandoned, + __instance.NetService.Platform); + MetricUtilities.UploadRunMetrics(serializableRun, isVictory, __instance.NetService.NetId); + if (SaveManager.Instance.Progress.NumberOfRuns == 5) + { + MetricUtilities.UploadSettingsMetric(); + } + + switch (__instance.NetService.Type) + { + case NetGameType.Singleplayer: + SaveManager.Instance.DeleteCurrentRun(); + break; + case NetGameType.Host: + LanRunSaveManagerService.Instance.DeleteCurrentMultiplayerRun(); + break; + case NetGameType.None: + case NetGameType.Client: + case NetGameType.Replay: + default: + break; + } + } + + if (isVictory) + { + var score = ScoreUtility.CalculateScore(serializableRun, isVictory); + StatsManager.IncrementArchitectDamage(score); + } + } + + if (__instance.DailyTime.HasValue) + { + var type = __instance.NetService.Type; + if ((uint)(type - 1) <= 1u) + { + var score2 = ScoreUtility.CalculateScore(serializableRun, isVictory); + TaskHelper.RunSafely(DailyRunUtility.UploadScore(__instance.DailyTime.Value, score2, + serializableRun.Players)); + } + else if (__instance.NetService.Type == NetGameType.Client) + { + TaskHelper.RunSafely(DailyRunUtility.UploadScore(__instance.DailyTime.Value, -99999, + serializableRun.Players)); + } + } + + __result = serializableRun; + + return false; + } + + return true; + } + } } \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/NMultiplayerSubmenuPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/NMultiplayerSubmenuPatch.cs index 1302cc4..c409b52 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/NMultiplayerSubmenuPatch.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/Screens/NMultiplayerSubmenuPatch.cs @@ -18,13 +18,6 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens [HarmonyPatch(typeof(NMultiplayerSubmenu), "_Ready")] internal class NMultiplayerSubmenuReadyPatch { - [HarmonyReversePatch(HarmonyReversePatchType.Snapshot)] - [HarmonyPatch(typeof(NMultiplayerSubmenu), "UpdateButtons")] - private static void UpdateButtons(NMultiplayerSubmenu instance) - { - throw new NotImplementedException(); - } - private static void Prefix(NMultiplayerSubmenu __instance) { var buttonContainerNode = __instance.GetNode("ButtonContainer"); @@ -105,8 +98,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens lanAbandonButton.Connect(NClickableControl.SignalName.Released, Callable.From(_ => { - TaskHelper.RunSafely( - LanHostHelper.TryAbandonMultiplayerRun(() => UpdateButtons(__instance))); + TaskHelper.RunSafely(LanHostHelper.TryAbandonMultiplayerRun(__instance)); })); LanMultiplayerSubmenuButtonService.Instance.LanAbandonButton = lanAbandonButton; 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; }