Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 17 additions & 24 deletions RunReplays/Commands/TakeCardCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,34 +116,28 @@ private ExecuteResult ExecuteSkip(NCardRewardSelectionScreen screen)
var extras = ExtraOptionsField?.GetValue(screen)
as IReadOnlyList<CardRewardAlternative>;

// 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();
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion RunReplays/Commands/UsePotionCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 7 additions & 4 deletions RunReplays/Patches/Record/RunContinuePatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ namespace RunReplays.Patches.Record;
using RunReplays;

/// <summary>
/// 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.
/// </summary>
[HarmonyPatch(typeof(RunManager), nameof(RunManager.SetUpSavedSinglePlayer))]
[HarmonyPatch(typeof(RunManager), "InitializeSavedRun")]
public static class RunContinuePatch
{
[HarmonyPostfix]
Expand Down
19 changes: 14 additions & 5 deletions RunReplays/Patches/Record/TakeCardRecordPatch.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

/// <summary>
/// Fired when the player clicks a card holder to take it.
/// Records "TakeCard {index} # {cardTitle}".
Expand Down Expand Up @@ -66,22 +71,26 @@ public static void SelectCardPrefix(NCardRewardSelectionScreen __instance, objec

/// <summary>
/// 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".
/// </summary>
[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<CardRewardAlternative>;
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";
Expand Down
4 changes: 2 additions & 2 deletions RunReplays/Patches/Replay/CardPlayReplayPatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion RunReplays/ReplayDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ private static HashSet<Type> 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));
Expand Down
4 changes: 2 additions & 2 deletions RunReplays/RunReplayMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions RunReplays/RunReplays.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<Reference Include="sts2">
<HintPath>..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2\data_sts2_windows_x86_64\sts2.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="BaseLib">
<HintPath>..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2\mods\BaseLib.dll</HintPath>
Expand Down
6 changes: 3 additions & 3 deletions RunReplays/RunReplays.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}