Skip to content
This repository was archived by the owner on Sep 20, 2026. It is now read-only.
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ await BootstrapManagerInitializer.WaitUntilAsync(
timeout: PresentationReadyTimeout
);

await ReplayItemPresentationReadiness.WaitForActiveSetupsAsync(PresentationReadyTimeout);
await ReplayCardPresentationReadiness.WaitForActiveTasksAsync(PresentationReadyTimeout);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ internal static async Task InjectSavedReplayAsync(
Func<ReplayPlaybackPublishOutcome>? publishStarting = null
)
{
using var setupTracking = ReplayItemPresentationReadiness.BeginTracking();
using var presentationTracking = ReplayCardPresentationReadiness.BeginTracking();
ReplaySavedStateNormalizer.Normalize(manifest, sequence);
ObserveQualityStep(
() => PlayerAttributeRepairer.EnsureSequencePlayerAttributes(sequence, outcome),
Expand Down Expand Up @@ -147,7 +147,7 @@ await ObserveQualityStepAsync(
);
Singleton<BoardManager>.Instance.ToggleOpponentPortrait(isVisible: true);
await AppStateHandlerInstaller.WaitForPresentationReadyAsync();
setupTracking.Dispose();
presentationTracking.Dispose();
await ObserveQualityStepAsync(
() => PresentationWarmer.WarmPresentationAssetsAsync(manifest, sequence, outcome),
outcome,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

namespace BazaarPlusPlus.Game.CombatReplay.Bootstrap;

internal static class ReplayItemPresentationReadiness
/// <summary>
/// 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.
/// </summary>
internal static class ReplayCardPresentationReadiness
{
private static readonly object Sync = new();
private static readonly Dictionary<int, TrackedSetup> LatestSetups = [];
private static readonly Dictionary<int, TrackedPresentation> LatestTasks = [];
private static bool _tracking;

internal static IDisposable BeginTracking()
Expand All @@ -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;
Expand All @@ -37,42 +42,42 @@ 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);
continue;
}

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
Expand All @@ -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;
}
Expand All @@ -113,7 +118,7 @@ private static IEnumerator CompleteAfterRenderBoundary(TaskCompletionSource<bool

private static async Task AwaitOriginal(Task setup) => await setup;

private sealed record TrackedSetup(ItemController Controller, Task Task);
private sealed record TrackedPresentation(MonoBehaviour Controller, Task Task);

private sealed class TrackingScope : IDisposable
{
Expand All @@ -126,7 +131,7 @@ public void Dispose()

lock (Sync)
{
LatestSetups.Clear();
LatestTasks.Clear();
_tracking = false;
}
_disposed = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Comment on lines +10 to +12
[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);
}

This file was deleted.