diff --git a/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/AppStateHandlerInstaller.cs b/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/AppStateHandlerInstaller.cs index bebc82e87..7b1554973 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/AppStateHandlerInstaller.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/AppStateHandlerInstaller.cs @@ -84,6 +84,6 @@ await BootstrapManagerInitializer.WaitUntilAsync( timeout: PresentationReadyTimeout ); - await ReplayItemPresentationReadiness.WaitForActiveSetupsAsync(PresentationReadyTimeout); + await ReplayCardPresentationReadiness.WaitForActiveTasksAsync(PresentationReadyTimeout); } } diff --git a/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayBootstrap.cs b/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayBootstrap.cs index 1c8849ef5..33b814a80 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayBootstrap.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayBootstrap.cs @@ -73,7 +73,7 @@ internal static async Task InjectSavedReplayAsync( Func? publishStarting = null ) { - using var setupTracking = ReplayItemPresentationReadiness.BeginTracking(); + using var presentationTracking = ReplayCardPresentationReadiness.BeginTracking(); ReplaySavedStateNormalizer.Normalize(manifest, sequence); ObserveQualityStep( () => PlayerAttributeRepairer.EnsureSequencePlayerAttributes(sequence, outcome), @@ -147,7 +147,7 @@ await ObserveQualityStepAsync( ); Singleton.Instance.ToggleOpponentPortrait(isVisible: true); await AppStateHandlerInstaller.WaitForPresentationReadyAsync(); - setupTracking.Dispose(); + presentationTracking.Dispose(); await ObserveQualityStepAsync( () => PresentationWarmer.WarmPresentationAssetsAsync(manifest, sequence, outcome), outcome, diff --git a/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayItemPresentationReadiness.cs b/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayCardPresentationReadiness.cs similarity index 67% rename from src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayItemPresentationReadiness.cs rename to src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayCardPresentationReadiness.cs index 4c3fa0f9c..90d3aaad9 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayItemPresentationReadiness.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/Bootstrap/ReplayCardPresentationReadiness.cs @@ -4,10 +4,15 @@ namespace BazaarPlusPlus.Game.CombatReplay.Bootstrap; -internal static class ReplayItemPresentationReadiness +/// +/// Tracks asynchronous native presentation work started while a saved replay is rebuilt. +/// Item setup and socket-effect VFX loading must both finish before their pooled controllers +/// can safely cross the replay-state boundary. +/// +internal static class ReplayCardPresentationReadiness { private static readonly object Sync = new(); - private static readonly Dictionary LatestSetups = []; + private static readonly Dictionary LatestTasks = []; private static bool _tracking; internal static IDisposable BeginTracking() @@ -16,17 +21,17 @@ internal static IDisposable BeginTracking() { if (_tracking) throw new InvalidOperationException( - "Replay item setup tracking is already active." + "Replay card presentation tracking is already active." ); - LatestSetups.Clear(); + LatestTasks.Clear(); _tracking = true; } return new TrackingScope(); } - internal static Task Track(ItemController controller, Task setup) + internal static Task Track(MonoBehaviour controller, Task setup) { if (controller == null) return setup; @@ -37,17 +42,17 @@ internal static Task Track(ItemController controller, Task setup) return setup; var tracked = AwaitOriginal(setup); - LatestSetups[controller.GetInstanceID()] = new TrackedSetup(controller, tracked); + LatestTasks[controller.GetInstanceID()] = new TrackedPresentation(controller, tracked); return tracked; } } - internal static async Task WaitForActiveSetupsAsync(TimeSpan timeout) + internal static async Task WaitForActiveTasksAsync(TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (true) { - var pending = SnapshotActivePendingSetups(); + var pending = SnapshotActivePendingTasks(); if (pending.Length > 0) { await AwaitBeforeDeadlineAsync(Task.WhenAll(pending), deadline); @@ -55,24 +60,24 @@ internal static async Task WaitForActiveSetupsAsync(TimeSpan timeout) } await AwaitBeforeDeadlineAsync(WaitForRenderBoundaryAsync(), deadline); - if (SnapshotActivePendingSetups().Length == 0) + if (SnapshotActivePendingTasks().Length == 0) return; } } - private static Task[] SnapshotActivePendingSetups() + private static Task[] SnapshotActivePendingTasks() { lock (Sync) { if (!_tracking) - throw new InvalidOperationException("Replay item setup tracking is not active."); + throw new InvalidOperationException( + "Replay card presentation tracking is not active." + ); - foreach ( - var stale in LatestSetups.Where(pair => pair.Value.Controller == null).ToList() - ) - LatestSetups.Remove(stale.Key); + foreach (var stale in LatestTasks.Where(pair => pair.Value.Controller == null).ToList()) + LatestTasks.Remove(stale.Key); - return LatestSetups + return LatestTasks .Values.Where(setup => setup.Controller != null && setup.Controller.gameObject.activeInHierarchy @@ -87,7 +92,7 @@ private static async Task AwaitBeforeDeadlineAsync(Task task, DateTime deadline) { var remaining = deadline - DateTime.UtcNow; if (remaining <= TimeSpan.Zero || await Task.WhenAny(task, Task.Delay(remaining)) != task) - throw new TimeoutException("Timed out while waiting for replay item presentation."); + throw new TimeoutException("Timed out while waiting for replay card presentation."); await task; } @@ -113,7 +118,7 @@ private static IEnumerator CompleteAfterRenderBoundary(TaskCompletionSource await setup; - private sealed record TrackedSetup(ItemController Controller, Task Task); + private sealed record TrackedPresentation(MonoBehaviour Controller, Task Task); private sealed class TrackingScope : IDisposable { @@ -126,7 +131,7 @@ public void Dispose() lock (Sync) { - LatestSetups.Clear(); + LatestTasks.Clear(); _tracking = false; } _disposed = true; diff --git a/src/BazaarPlusPlus/Patches/Combat/CombatReplayCardPresentationReadinessPatch.cs b/src/BazaarPlusPlus/Patches/Combat/CombatReplayCardPresentationReadinessPatch.cs new file mode 100644 index 000000000..705c5eadd --- /dev/null +++ b/src/BazaarPlusPlus/Patches/Combat/CombatReplayCardPresentationReadinessPatch.cs @@ -0,0 +1,26 @@ +#nullable enable +#pragma warning disable CS0436 + +using BazaarGameClient.Domain.Models.Cards; +using BazaarPlusPlus.Game.CombatReplay.Bootstrap; +using HarmonyLib; + +namespace BazaarPlusPlus.Patches.Combat; + +[HarmonyPatch(typeof(ItemController), nameof(ItemController.Setup), [typeof(Card)])] +internal static class CombatReplayItemPresentationReadinessPatch +{ + [HarmonyPostfix] + private static void Postfix(ItemController __instance, ref Task __result) => + __result = ReplayCardPresentationReadiness.Track(__instance, __result); +} + +[HarmonyPatch(typeof(SocketEffectController), "LoadVFXAssets")] +internal static class CombatReplaySocketEffectPresentationReadinessPatch +{ + // SocketEffectController.UpdateData is async void. Replacing the Task returned by its + // awaited native loader is the only seam that lets replay bootstrap observe completion. + [HarmonyPostfix] + private static void Postfix(SocketEffectController __instance, ref Task __result) => + __result = ReplayCardPresentationReadiness.Track(__instance, __result); +} diff --git a/src/BazaarPlusPlus/Patches/Combat/CombatReplayItemPresentationReadinessPatch.cs b/src/BazaarPlusPlus/Patches/Combat/CombatReplayItemPresentationReadinessPatch.cs deleted file mode 100644 index e68cbac62..000000000 --- a/src/BazaarPlusPlus/Patches/Combat/CombatReplayItemPresentationReadinessPatch.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable enable -#pragma warning disable CS0436 - -using BazaarGameClient.Domain.Models.Cards; -using BazaarPlusPlus.Game.CombatReplay.Bootstrap; -using HarmonyLib; - -namespace BazaarPlusPlus.Patches.Combat; - -[HarmonyPatch(typeof(ItemController), nameof(ItemController.Setup), [typeof(Card)])] -internal static class CombatReplayItemPresentationReadinessPatch -{ - [HarmonyPostfix] - private static void Postfix(ItemController __instance, ref Task __result) => - __result = ReplayItemPresentationReadiness.Track(__instance, __result); -}