diff --git a/RunReplays/Commands/TakeCardCommand.cs b/RunReplays/Commands/TakeCardCommand.cs index 448dad3..634c297 100644 --- a/RunReplays/Commands/TakeCardCommand.cs +++ b/RunReplays/Commands/TakeCardCommand.cs @@ -116,34 +116,28 @@ private ExecuteResult ExecuteSkip(NCardRewardSelectionScreen screen) var extras = ExtraOptionsField?.GetValue(screen) as IReadOnlyList; - // Find the skip option (AfterSelected == DismissScreenAndKeepReward). - CardRewardAlternative? skipAlt = null; + int skipIndex = -1; if (extras != null) { - foreach (var alt in extras) + for (int i = 0; i < extras.Count; i++) { - if (alt.AfterSelected == MegaCrit.Sts2.Core.Entities.Rewards.PostAlternateCardRewardAction.DismissScreenAndKeepReward) + if (extras[i].AfterSelected == MegaCrit.Sts2.Core.Entities.Rewards.PostAlternateCardRewardAction.EndSelectionAndDoNotCompleteReward) { - skipAlt = alt; + skipIndex = i; break; } } } - if (skipAlt != null) + if (skipIndex < 0) { - TaskHelper.RunSafely(skipAlt.OnSelect()); - OnAlternateRewardSelectedMethod?.Invoke(screen, new object[] { skipAlt.AfterSelected }); - } - else - { - // Fallback: dismiss with KeepReward directly. - OnAlternateRewardSelectedMethod?.Invoke(screen, new object[] - { - MegaCrit.Sts2.Core.Entities.Rewards.PostAlternateCardRewardAction.DismissScreenAndKeepReward - }); + PlayerActionBuffer.LogMigrationWarning( + "[TakeCard] No skip option on selection screen — retrying."); + return ExecuteResult.Retry(200); } + OnAlternateRewardSelectedMethod?.Invoke(screen, new object[] { skipIndex }); + ReplayState.CardRewardSelectionScreen = null; ReplayDispatcher.DispatchNow(); return ExecuteResult.Ok(); @@ -161,20 +155,19 @@ private ExecuteResult ExecuteSacrifice(NCardRewardSelectionScreen screen) return ExecuteResult.Retry(200); } - CardRewardAlternative? sacrifice = null; - foreach (var alt in extras) + int sacrificeIndex = -1; + for (int i = 0; i < extras.Count; i++) { - if (alt.OptionId.Contains("sacrifice", System.StringComparison.OrdinalIgnoreCase) - || alt.OptionId.Contains("pael", System.StringComparison.OrdinalIgnoreCase)) + if (extras[i].OptionId.Contains("sacrifice", System.StringComparison.OrdinalIgnoreCase) + || extras[i].OptionId.Contains("pael", System.StringComparison.OrdinalIgnoreCase)) { - sacrifice = alt; + sacrificeIndex = i; break; } } - sacrifice ??= extras[0]; + if (sacrificeIndex < 0) sacrificeIndex = 0; - TaskHelper.RunSafely(sacrifice.OnSelect()); - OnAlternateRewardSelectedMethod?.Invoke(screen, new object[] { sacrifice.AfterSelected }); + OnAlternateRewardSelectedMethod?.Invoke(screen, new object[] { sacrificeIndex }); ReplayState.CardRewardSelectionScreen = null; ReplayDispatcher.DispatchNow(); diff --git a/RunReplays/Commands/UsePotionCommand.cs b/RunReplays/Commands/UsePotionCommand.cs index 20314d7..3701712 100644 --- a/RunReplays/Commands/UsePotionCommand.cs +++ b/RunReplays/Commands/UsePotionCommand.cs @@ -41,7 +41,7 @@ public override ExecuteResult Execute() { // Wait until the game is in the play phase before using a combat potion. var combat = MegaCrit.Sts2.Core.Combat.CombatManager.Instance; - if (combat != null && !combat.IsPlayPhase) + if (combat != null && MegaCrit.Sts2.Core.Runs.RunManager.Instance.ActionQueueSynchronizer.CombatState != MegaCrit.Sts2.Core.Entities.Multiplayer.ActionSynchronizerCombatState.PlayPhase) return ExecuteResult.Retry(200); Player? player = CardPlayReplayPatch.ResolveLocalPlayer(); diff --git a/RunReplays/Patches/Record/RunContinuePatch.cs b/RunReplays/Patches/Record/RunContinuePatch.cs index 616e617..8dbba15 100644 --- a/RunReplays/Patches/Record/RunContinuePatch.cs +++ b/RunReplays/Patches/Record/RunContinuePatch.cs @@ -11,13 +11,16 @@ namespace RunReplays.Patches.Record; using RunReplays; /// -/// Harmony postfix on RunManager.SetUpSavedSinglePlayer() that restores the +/// Harmony postfix on RunManager.InitializeSavedRun() that restores the /// action buffer from the most recent log files for the continued run. /// -/// Timing: this postfix fires after InitializeShared() has already constructed -/// a fresh ActionExecutor and cleared the buffer, so enqueuing here is safe. +/// Retargeted from SetUpSavedSingleplayer (which became async Task in v0.107.x; +/// a postfix on it would fire at the first await — before InitializeShared cleared +/// the buffer). InitializeSavedRun is the last *synchronous* call inside the setup +/// path, so by the time it runs InitializeShared has already constructed a fresh +/// ActionExecutor and cleared the buffer — enqueuing here is safe. /// -[HarmonyPatch(typeof(RunManager), nameof(RunManager.SetUpSavedSinglePlayer))] +[HarmonyPatch(typeof(RunManager), "InitializeSavedRun")] public static class RunContinuePatch { [HarmonyPostfix] diff --git a/RunReplays/Patches/Record/TakeCardRecordPatch.cs b/RunReplays/Patches/Record/TakeCardRecordPatch.cs index 9fb4188..29960f8 100644 --- a/RunReplays/Patches/Record/TakeCardRecordPatch.cs +++ b/RunReplays/Patches/Record/TakeCardRecordPatch.cs @@ -1,6 +1,7 @@ using System.Reflection; using Godot; using HarmonyLib; +using MegaCrit.Sts2.Core.Entities.CardRewardAlternatives; using MegaCrit.Sts2.Core.Entities.Rewards; using MegaCrit.Sts2.Core.Models; using MegaCrit.Sts2.Core.Nodes.Screens.CardSelection; @@ -19,6 +20,10 @@ public static class TakeCardRecordPatch typeof(NCardRewardSelectionScreen).GetField( "_cardRow", BindingFlags.NonPublic | BindingFlags.Instance); + private static readonly FieldInfo? ExtraOptionsField = + typeof(NCardRewardSelectionScreen).GetField( + "_extraOptions", BindingFlags.NonPublic | BindingFlags.Instance); + /// /// Fired when the player clicks a card holder to take it. /// Records "TakeCard {index} # {cardTitle}". @@ -66,22 +71,26 @@ public static void SelectCardPrefix(NCardRewardSelectionScreen __instance, objec /// /// Fired when the player selects an alternate reward option. - /// Sacrifice (DismissScreenAndRemoveReward) records "TakeCard sacrifice". - /// Skip (DismissScreenAndKeepReward) records "TakeCard skip". + /// Sacrifice (EndSelectionAndCompleteReward) records "TakeCard sacrifice". + /// Skip (EndSelectionAndDoNotCompleteReward) records "TakeCard skip". /// [HarmonyPrefix] [HarmonyPatch("OnAlternateRewardSelected")] - public static void OnAlternatePrefix(PostAlternateCardRewardAction afterSelected) + public static void OnAlternatePrefix(NCardRewardSelectionScreen __instance, int index) { if (ReplayEngine.IsActive) return; - if (afterSelected == PostAlternateCardRewardAction.DismissScreenAndRemoveReward) + var extras = ExtraOptionsField?.GetValue(__instance) as IReadOnlyList; + if (extras == null || index < 0 || index >= extras.Count) return; + PostAlternateCardRewardAction afterSelected = extras[index].AfterSelected; + + if (afterSelected == PostAlternateCardRewardAction.EndSelectionAndCompleteReward) { var cmd = TakeCardCommand.Sacrifice(); cmd.Comment = "sacrifice"; PlayerActionBuffer.Record(cmd.ToLogString()); } - else if (afterSelected == PostAlternateCardRewardAction.DismissScreenAndKeepReward) + else if (afterSelected == PostAlternateCardRewardAction.EndSelectionAndDoNotCompleteReward) { var cmd = TakeCardCommand.Skip(); cmd.Comment = "skip"; diff --git a/RunReplays/Patches/Replay/CardPlayReplayPatch.cs b/RunReplays/Patches/Replay/CardPlayReplayPatch.cs index 8b5d085..072cfd8 100644 --- a/RunReplays/Patches/Replay/CardPlayReplayPatch.cs +++ b/RunReplays/Patches/Replay/CardPlayReplayPatch.cs @@ -206,7 +206,7 @@ internal static bool IsCombatReady() return false; // Cards can't be played while the game is drawing cards. - if (!CombatManager.Instance.IsPlayPhase) + if (MegaCrit.Sts2.Core.Runs.RunManager.Instance.ActionQueueSynchronizer.CombatState != MegaCrit.Sts2.Core.Entities.Multiplayer.ActionSynchronizerCombatState.PlayPhase) return false; var state = CombatManager.Instance.DebugOnlyGetState(); @@ -492,7 +492,7 @@ internal static bool TryEndTurn() } // Wait until combat is in progress, in the play phase, and a player is available. - if (!CombatManager.Instance.IsInProgress || !CombatManager.Instance.IsPlayPhase || ResolveLocalPlayer() == null) + if (!CombatManager.Instance.IsInProgress || MegaCrit.Sts2.Core.Runs.RunManager.Instance.ActionQueueSynchronizer.CombatState != MegaCrit.Sts2.Core.Entities.Multiplayer.ActionSynchronizerCombatState.PlayPhase || ResolveLocalPlayer() == null) { return false; } diff --git a/RunReplays/ReplayDispatcher.cs b/RunReplays/ReplayDispatcher.cs index 1b82a2a..e194fe3 100644 --- a/RunReplays/ReplayDispatcher.cs +++ b/RunReplays/ReplayDispatcher.cs @@ -456,7 +456,7 @@ private static HashSet GetDispatchableTypes() && GodotObject.IsInstanceValid(CrystalSphereReplayPatch.ActiveScreen)) types.Add(typeof(CrystalSphereClickCommand)); - if (CombatManager.Instance.IsInProgress && CombatManager.Instance.IsPlayPhase) + if (CombatManager.Instance.IsInProgress && MegaCrit.Sts2.Core.Runs.RunManager.Instance.ActionQueueSynchronizer.CombatState == MegaCrit.Sts2.Core.Entities.Multiplayer.ActionSynchronizerCombatState.PlayPhase) { types.Add(typeof(PlayCardCommand)); types.Add(typeof(EndTurnCommand)); diff --git a/RunReplays/RunReplayMenu.cs b/RunReplays/RunReplayMenu.cs index 50371be..971d767 100644 --- a/RunReplays/RunReplayMenu.cs +++ b/RunReplays/RunReplayMenu.cs @@ -565,7 +565,7 @@ private static async Task LoadSaveAsync(ReplayEntry entry) $"gameMode={serializableRun.GameMode} ascension={serializableRun.Ascension} " + $"character={serializableRun.Players?.FirstOrDefault()?.CharacterId?.Entry}"); RunState runState = RunState.FromSerializable(serializableRun); - RunManager.Instance.SetUpSavedSinglePlayer(runState, serializableRun); + await RunManager.Instance.SetUpSavedSingleplayer(runState, serializableRun); NAudioManager.Instance?.StopMusic(); SfxCmd.Play(runState.Players[0].Character.CharacterTransitionSfx); @@ -695,7 +695,7 @@ private static async Task LoadSaveAndReplayAsync(ReplayEntry startFrom) $"gameMode={serializableRun.GameMode} ascension={serializableRun.Ascension} " + $"character={serializableRun.Players?.FirstOrDefault()?.CharacterId?.Entry}"); RunState runState = RunState.FromSerializable(serializableRun); - RunManager.Instance.SetUpSavedSinglePlayer(runState, serializableRun); + await RunManager.Instance.SetUpSavedSingleplayer(runState, serializableRun); NAudioManager.Instance?.StopMusic(); SfxCmd.Play(runState.Players[0].Character.CharacterTransitionSfx); diff --git a/RunReplays/RunReplays.csproj b/RunReplays/RunReplays.csproj index 6dd8afb..f21a71f 100644 --- a/RunReplays/RunReplays.csproj +++ b/RunReplays/RunReplays.csproj @@ -11,6 +11,7 @@ ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2\data_sts2_windows_x86_64\sts2.dll + false ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2\mods\BaseLib.dll diff --git a/RunReplays/RunReplays.json b/RunReplays/RunReplays.json index 6026111..ee5f501 100644 --- a/RunReplays/RunReplays.json +++ b/RunReplays/RunReplays.json @@ -3,9 +3,9 @@ "name": "RunReplays", "author": "boardengineer", "description": "Replay your runs!", - "version": "v0.1.2", - "has_pck": true, + "version": "v0.2.0", + "has_pck": false, "has_dll": true, - "dependencies": ["BaseLib"], + "dependencies": [{ "id": "BaseLib", "min_version": null }], "affects_gameplay": true }