diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CapturedReplayRoute.cs b/src/BazaarPlusPlus/Game/CombatReplay/CapturedReplayRoute.cs new file mode 100644 index 000000000..71c213739 --- /dev/null +++ b/src/BazaarPlusPlus/Game/CombatReplay/CapturedReplayRoute.cs @@ -0,0 +1,29 @@ +#nullable enable +using BazaarPlusPlus.Game.PvpBattles; + +namespace BazaarPlusPlus.Game.CombatReplay; + +internal enum CapturedReplayRoute +{ + CurrentNative, + PersistedPvp, +} + +internal static class CapturedReplayRouter +{ + internal static CapturedReplayRoute Resolve(PvpBattleManifest manifest) + { + if (manifest == null) + throw new ArgumentNullException(nameof(manifest)); + + return manifest.CombatKind switch + { + "Combat" => CapturedReplayRoute.CurrentNative, + "PVPCombat" => CapturedReplayRoute.PersistedPvp, + _ => throw new ArgumentException( + $"Unsupported replay combat kind: {manifest.CombatKind ?? ""}.", + nameof(manifest) + ), + }; + } +} diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayCaptureService.cs b/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayCaptureService.cs index 8b63432f3..46df6c635 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayCaptureService.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayCaptureService.cs @@ -15,7 +15,7 @@ internal sealed class CombatReplayCaptureService CaptureOpponentSkillsFromOpening(message) GameSimEventCardSpawned GameSimEventPlayerSkillEquipped - return state == ERunState.PVPCombat; + return state == ERunState.Combat || state == ERunState.PVPCombat; OpponentName = candidate.OpponentName OpponentAccountId = candidate.OpponentAccountId CapturedEmpty @@ -59,7 +59,7 @@ public CombatReplayCaptureService(Func? clock = null) { if (_candidate.SpawnMessage == null) { - if (_matcher.IsPvpCombatOpeningMessage(message)) + if (_matcher.IsCombatOpeningMessage(message)) { _candidate = CreateOpeningCandidate(message, runId); } @@ -69,7 +69,7 @@ public CombatReplayCaptureService(Func? clock = null) if (_candidate.CombatMessage == null) { - if (_matcher.IsPvpCombatOpeningMessage(message)) + if (_matcher.IsCombatOpeningMessage(message)) { _candidate = CreateOpeningCandidate(message, runId); } @@ -81,11 +81,9 @@ public CombatReplayCaptureService(Func? clock = null) return null; } - if (_matcher.IsAnyCombatOpeningMessage(message)) + if (_matcher.IsCombatOpeningMessage(message)) { - _candidate = _matcher.IsPvpCombatOpeningMessage(message) - ? CreateOpeningCandidate(message, runId) - : _matcher.ResetCandidate(); + _candidate = CreateOpeningCandidate(message, runId); return null; } diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayRuntime.cs b/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayRuntime.cs index 69ff86daa..afe6bc149 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayRuntime.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/CombatReplayRuntime.cs @@ -255,10 +255,21 @@ public void ObserveMessage(BazaarGameShared.Infra.Messages.INetMessage message) if (artifact == null) return; + var route = CapturedReplayRouter.Resolve(artifact.Manifest); _currentRecordingManifest = artifact.Manifest; _currentRecording.LatchBattle(artifact.Manifest.BattleId); PrepareCurrentReplayRecordingAvailability(); - _persistence.Enqueue(artifact.Payload, artifact.Manifest); + switch (route) + { + case CapturedReplayRoute.CurrentNative: + _currentRecording.MarkCurrentNativeReady(artifact.Manifest.BattleId); + break; + case CapturedReplayRoute.PersistedPvp: + _persistence.Enqueue(artifact.Payload, artifact.Manifest); + break; + default: + throw new ArgumentOutOfRangeException(); + } } catch (Exception ex) { diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingButtonController.cs b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingButtonController.cs index 0bbf5de40..149eb2d39 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingButtonController.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingButtonController.cs @@ -29,6 +29,7 @@ internal sealed class CurrentReplayRecordingButtonController : MonoBehaviour private readonly Vector3[] _tooltipWorldCorners = new Vector3[4]; private readonly BppDockButtonScreenLayout _screenLayout = new(); private readonly CurrentReplayRecordingUiLogState _uiLogState = new(); + private readonly CurrentReplayRecordingTooltipStyle _tooltipStyle = new(); private bool _layoutAvailable; private CurrentReplayRecordingUiLayoutReasonCode _layoutReasonCode; @@ -273,6 +274,7 @@ internal void HideTooltip() StopCoroutine(_tooltipPositionCoroutine); _tooltipPositionCoroutine = null; } + _tooltipStyle.Restore(); Data.TooltipParentComponent?.HideAuxiliaryTooltipController(); } @@ -290,10 +292,13 @@ private IEnumerator PositionTooltipBesideButton() tooltip == null || tooltipParent == null || !tooltipParent.IsAuxiliaryTooltipDisplayed - || tooltip._coroutine != null ) continue; + _tooltipStyle.Apply(tooltip); + if (tooltip._coroutine != null) + continue; + tooltip.PositionOverUI(_cloneRect); var tooltipRect = tooltip.PositioningRectTransform; LayoutRebuilder.ForceRebuildLayoutImmediate(tooltipRect); diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingState.cs b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingState.cs index 09cd54d42..e05e8458d 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingState.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingState.cs @@ -34,7 +34,7 @@ internal sealed class CurrentReplayRecordingState private string? _recordingId; private string? _finalFilePath; private string? _reason; - private bool _battlePersisted; + private bool _replaySourceReady; private bool _replayStateActive; private bool _availabilityReady; private bool _nativeReplayStarted; @@ -60,7 +60,7 @@ internal void LatchBattle(string battleId) _recordingId = null; _finalFilePath = null; _reason = null; - _battlePersisted = false; + _replaySourceReady = false; _availabilityReady = false; _nativeReplayStarted = false; Phase = CurrentReplayRecordingPhase.AwaitingBattlePersistence; @@ -77,7 +77,7 @@ internal void MarkBattlePersistence(string battleId, bool succeeded, string? rea if (!MatchesBattle(battleId) || HasActiveSession) return; - _battlePersisted = succeeded; + _replaySourceReady = succeeded; _reason = succeeded ? null : reason; Phase = succeeded ? CurrentReplayRecordingPhase.Preparing @@ -85,6 +85,17 @@ internal void MarkBattlePersistence(string battleId, bool succeeded, string? rea RefreshReadyPhase(); } + internal void MarkCurrentNativeReady(string battleId) + { + if (!MatchesBattle(battleId) || HasActiveSession) + return; + + _replaySourceReady = true; + _reason = null; + Phase = CurrentReplayRecordingPhase.Preparing; + RefreshReadyPhase(); + } + internal void SetAvailability(bool ready, string? reason) { if ( @@ -100,7 +111,7 @@ or CurrentReplayRecordingPhase.Degraded if (!ready) { _reason = reason; - if (_battlePersisted && Phase != CurrentReplayRecordingPhase.Failed) + if (_replaySourceReady && Phase != CurrentReplayRecordingPhase.Failed) Phase = CurrentReplayRecordingPhase.Preparing; return; } @@ -117,7 +128,7 @@ internal bool TryArm(string recordingId) if ( string.IsNullOrWhiteSpace(recordingId) || !_replayStateActive - || !_battlePersisted + || !_replaySourceReady || !_availabilityReady || Phase is not (CurrentReplayRecordingPhase.Ready or CurrentReplayRecordingPhase.Failed) @@ -161,7 +172,7 @@ internal void RollbackArm(string recordingId, string reason) _nativeReplayStarted = false; _reason = reason; Phase = - _battlePersisted && _availabilityReady + _replaySourceReady && _availabilityReady ? CurrentReplayRecordingPhase.Ready : CurrentReplayRecordingPhase.Failed; } @@ -229,7 +240,7 @@ is CurrentReplayRecordingPhase.Succeeded or CurrentReplayRecordingPhase.Degraded; var canStart = visible - && _battlePersisted + && _replaySourceReady && _availabilityReady && Phase is CurrentReplayRecordingPhase.Ready or CurrentReplayRecordingPhase.Failed; return new CurrentReplayRecordingSnapshot( @@ -253,7 +264,7 @@ private bool MatchesSession(string recordingId, string battleId) => private void RefreshReadyPhase() { - if (_battleId == null || HasActiveSession || !_battlePersisted) + if (_battleId == null || HasActiveSession || !_replaySourceReady) return; Phase = @@ -268,7 +279,7 @@ private void Reset() _recordingId = null; _finalFilePath = null; _reason = null; - _battlePersisted = false; + _replaySourceReady = false; _replayStateActive = false; _availabilityReady = false; _nativeReplayStarted = false; diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipPadding.cs b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipPadding.cs new file mode 100644 index 000000000..63ba32cea --- /dev/null +++ b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipPadding.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace BazaarPlusPlus.Game.CombatReplay; + +internal static class CurrentReplayRecordingTooltipPadding +{ + internal static (int Top, int Bottom) Balance(int top, int bottom) + { + var total = Math.Max(0, top) + Math.Max(0, bottom); + var balancedTop = total / 2; + return (balancedTop, total - balancedTop); + } +} diff --git a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs new file mode 100644 index 000000000..f067b953d --- /dev/null +++ b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs @@ -0,0 +1,127 @@ +#nullable enable +using TheBazaar.UI.Tooltips; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace BazaarPlusPlus.Game.CombatReplay; + +internal sealed class CurrentReplayRecordingTooltipStyle +{ + private const float TooltipScale = 0.94f; + private const float TooltipFontScale = 0.94f; + + private AuxiliaryTooltipController? _styledTooltip; + private Vector3 _styledTooltipOriginalLocalScale; + private float _styledTooltipOriginalScaleRatio; + private float _styledTooltipOriginalHeaderFontSize; + private float _styledTooltipOriginalBodyFontSize; + private bool _styledTooltipOriginalHeaderAutoSizing; + private bool _styledTooltipOriginalBodyAutoSizing; + private TextAlignmentOptions _styledTooltipOriginalHeaderAlignment; + private TextAlignmentOptions _styledTooltipOriginalBodyAlignment; + private VerticalLayoutGroup? _styledTooltipLayout; + private TextAnchor _styledTooltipOriginalChildAlignment; + private int _styledTooltipOriginalPaddingTop; + private int _styledTooltipOriginalPaddingBottom; + + internal void Apply(AuxiliaryTooltipController tooltip) + { + if (!ReferenceEquals(_styledTooltip, tooltip)) + { + Restore(); + _styledTooltip = tooltip; + _styledTooltipOriginalLocalScale = tooltip.transform.localScale; + _styledTooltipOriginalScaleRatio = tooltip.scaleRatio; + + if (tooltip.headerText != null) + { + _styledTooltipOriginalHeaderFontSize = tooltip.headerText.fontSize; + _styledTooltipOriginalHeaderAutoSizing = tooltip.headerText.enableAutoSizing; + _styledTooltipOriginalHeaderAlignment = tooltip.headerText.alignment; + } + + if (tooltip.bodyText != null) + { + _styledTooltipOriginalBodyFontSize = tooltip.bodyText.fontSize; + _styledTooltipOriginalBodyAutoSizing = tooltip.bodyText.enableAutoSizing; + _styledTooltipOriginalBodyAlignment = tooltip.bodyText.alignment; + } + + _styledTooltipLayout = tooltip.auxParent?.GetComponent(); + if (_styledTooltipLayout != null) + { + _styledTooltipOriginalChildAlignment = _styledTooltipLayout.childAlignment; + _styledTooltipOriginalPaddingTop = _styledTooltipLayout.padding.top; + _styledTooltipOriginalPaddingBottom = _styledTooltipLayout.padding.bottom; + } + } + + ApplyTextStyle(tooltip.headerText, _styledTooltipOriginalHeaderFontSize); + ApplyTextStyle(tooltip.bodyText, _styledTooltipOriginalBodyFontSize); + + tooltip.scaleRatio = _styledTooltipOriginalScaleRatio * TooltipScale; + if (tooltip._coroutine == null) + tooltip.transform.localScale = _styledTooltipOriginalLocalScale * TooltipScale; + + if (_styledTooltipLayout != null) + { + _styledTooltipLayout.childAlignment = TextAnchor.MiddleLeft; + var balancedPadding = CurrentReplayRecordingTooltipPadding.Balance( + _styledTooltipOriginalPaddingTop, + _styledTooltipOriginalPaddingBottom + ); + _styledTooltipLayout.padding.top = balancedPadding.Top; + _styledTooltipLayout.padding.bottom = balancedPadding.Bottom; + LayoutRebuilder.ForceRebuildLayoutImmediate( + (RectTransform)_styledTooltipLayout.transform + ); + } + + LayoutRebuilder.ForceRebuildLayoutImmediate(tooltip.PositioningRectTransform); + } + + private static void ApplyTextStyle(TMP_Text? text, float originalFontSize) + { + if (text == null) + return; + + text.enableAutoSizing = false; + text.fontSize = originalFontSize * TooltipFontScale; + text.alignment = TextAlignmentOptions.MidlineLeft; + } + + internal void Restore() + { + var tooltip = _styledTooltip; + if (tooltip == null) + { + _styledTooltip = null; + return; + } + + tooltip.transform.localScale = _styledTooltipOriginalLocalScale; + tooltip.scaleRatio = _styledTooltipOriginalScaleRatio; + if (tooltip.headerText != null) + { + tooltip.headerText.fontSize = _styledTooltipOriginalHeaderFontSize; + tooltip.headerText.enableAutoSizing = _styledTooltipOriginalHeaderAutoSizing; + tooltip.headerText.alignment = _styledTooltipOriginalHeaderAlignment; + } + if (tooltip.bodyText != null) + { + tooltip.bodyText.fontSize = _styledTooltipOriginalBodyFontSize; + tooltip.bodyText.enableAutoSizing = _styledTooltipOriginalBodyAutoSizing; + tooltip.bodyText.alignment = _styledTooltipOriginalBodyAlignment; + } + if (_styledTooltipLayout != null) + { + _styledTooltipLayout.childAlignment = _styledTooltipOriginalChildAlignment; + _styledTooltipLayout.padding.top = _styledTooltipOriginalPaddingTop; + _styledTooltipLayout.padding.bottom = _styledTooltipOriginalPaddingBottom; + } + + _styledTooltip = null; + _styledTooltipLayout = null; + } +} diff --git a/src/BazaarPlusPlus/Game/CombatReplay/Video/ReplayVideoCaptureDefaults.cs b/src/BazaarPlusPlus/Game/CombatReplay/Video/ReplayVideoCaptureDefaults.cs index a853ed195..5784f70bd 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/Video/ReplayVideoCaptureDefaults.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/Video/ReplayVideoCaptureDefaults.cs @@ -5,7 +5,7 @@ namespace BazaarPlusPlus.Game.CombatReplay.Video; internal static class ReplayVideoCaptureDefaults { internal const int FallbackFps = 30; - internal const int MaxFps = 60; + internal const int MaxFps = 30; internal const int Crf = 23; internal const string Preset = "veryfast"; diff --git a/src/BazaarPlusPlus/Game/PvpBattles/Persistence/PvpBattleSqliteStore.cs b/src/BazaarPlusPlus/Game/PvpBattles/Persistence/PvpBattleSqliteStore.cs index 0e94b5ae4..86585c50c 100644 --- a/src/BazaarPlusPlus/Game/PvpBattles/Persistence/PvpBattleSqliteStore.cs +++ b/src/BazaarPlusPlus/Game/PvpBattles/Persistence/PvpBattleSqliteStore.cs @@ -63,7 +63,10 @@ public void Save(PvpBattleManifest manifest) if (string.IsNullOrWhiteSpace(manifest.BattleId)) throw new ArgumentException("Battle id is required.", nameof(manifest)); if (!string.Equals(manifest.CombatKind, "PVPCombat", StringComparison.Ordinal)) - return; + throw new ArgumentException( + "Only PVPCombat manifests can be saved to the PvP battle catalog.", + nameof(manifest) + ); using var connection = OpenConnection(); using var transaction = connection.BeginTransaction(); diff --git a/src/BazaarPlusPlus/Game/PvpBattles/PvpBattleSequenceMatcher.cs b/src/BazaarPlusPlus/Game/PvpBattles/PvpBattleSequenceMatcher.cs index ff6812fc1..08dce73bf 100644 --- a/src/BazaarPlusPlus/Game/PvpBattles/PvpBattleSequenceMatcher.cs +++ b/src/BazaarPlusPlus/Game/PvpBattles/PvpBattleSequenceMatcher.cs @@ -6,13 +6,7 @@ namespace BazaarPlusPlus.Game.PvpBattles; internal sealed class PvpBattleSequenceMatcher { - public bool IsPvpCombatOpeningMessage(NetMessageGameSim message) - { - var state = message.Data.CurrentState?.StateName; - return state == ERunState.PVPCombat; - } - - public bool IsAnyCombatOpeningMessage(NetMessageGameSim message) + public bool IsCombatOpeningMessage(NetMessageGameSim message) { var state = message.Data.CurrentState?.StateName; return state == ERunState.Combat || state == ERunState.PVPCombat; diff --git a/src/BazaarPlusPlus/Game/Settings/BppSettingsDockPlacement.cs b/src/BazaarPlusPlus/Game/Settings/BppSettingsDockPlacement.cs index 615073205..5487b20af 100644 --- a/src/BazaarPlusPlus/Game/Settings/BppSettingsDockPlacement.cs +++ b/src/BazaarPlusPlus/Game/Settings/BppSettingsDockPlacement.cs @@ -4,7 +4,7 @@ namespace BazaarPlusPlus.Game.Settings; internal readonly struct BppSettingsDockPlacement { - internal const float DefaultSiblingGap = 18f; + internal const float DefaultSiblingGap = 8f; private BppSettingsDockPlacement(string key, SettingsNativeButtonId buttonId) { diff --git a/src/BazaarPlusPlus/Resources/DockButtons/replay-export-icon.png b/src/BazaarPlusPlus/Resources/DockButtons/replay-export-icon.png index a2e0f545c..3da50a847 100644 Binary files a/src/BazaarPlusPlus/Resources/DockButtons/replay-export-icon.png and b/src/BazaarPlusPlus/Resources/DockButtons/replay-export-icon.png differ diff --git a/src/BazaarPlusPlus/Resources/DockButtons/replay-recording-icon.png b/src/BazaarPlusPlus/Resources/DockButtons/replay-recording-icon.png index 8e263725a..5e4a5f147 100644 Binary files a/src/BazaarPlusPlus/Resources/DockButtons/replay-recording-icon.png and b/src/BazaarPlusPlus/Resources/DockButtons/replay-recording-icon.png differ diff --git a/src/BazaarPlusPlus/Resources/DockButtons/replay-retry-icon.png b/src/BazaarPlusPlus/Resources/DockButtons/replay-retry-icon.png index 089f057c5..26e720991 100644 Binary files a/src/BazaarPlusPlus/Resources/DockButtons/replay-retry-icon.png and b/src/BazaarPlusPlus/Resources/DockButtons/replay-retry-icon.png differ diff --git a/src/BazaarPlusPlus/Resources/DockButtons/replay-view-icon.png b/src/BazaarPlusPlus/Resources/DockButtons/replay-view-icon.png index 9e1758cb7..91df738e5 100644 Binary files a/src/BazaarPlusPlus/Resources/DockButtons/replay-view-icon.png and b/src/BazaarPlusPlus/Resources/DockButtons/replay-view-icon.png differ diff --git a/tests/Architecture.Tests/CoreLayeringTests.cs b/tests/Architecture.Tests/CoreLayeringTests.cs index 58a28b8cc..4a5a0185b 100644 --- a/tests/Architecture.Tests/CoreLayeringTests.cs +++ b/tests/Architecture.Tests/CoreLayeringTests.cs @@ -2178,6 +2178,13 @@ public void Capture_modules_use_ui_chrome_suppression_seam() "CollectionPanelDockButtonController.BeginScreenshotSuppression", chromeSuppressionSource ); + Assert.Equal( + 2, + chromeSuppressionSource + .Split("CollectionPanelDockButtonController.BeginScreenshotSuppression") + .Length - 1 + ); + Assert.DoesNotContain("BeginReplayRecordingSuppression", chromeSuppressionSource); Assert.DoesNotContain("BppSettingsDockController", chromeSuppressionSource); Assert.Contains( "CombatStatusBarFeature.BeginScreenshotSuppression", diff --git a/tests/CombatReplayAudioVideo.Tests/Program.cs b/tests/CombatReplayAudioVideo.Tests/Program.cs index 5c7910499..4e7ffa738 100644 --- a/tests/CombatReplayAudioVideo.Tests/Program.cs +++ b/tests/CombatReplayAudioVideo.Tests/Program.cs @@ -441,12 +441,12 @@ file static class VideoEncoderProfileTests public static void Run() { - FrameRateUsesGamePreferenceWithSixtyFpsCap(); + FrameRateUsesGamePreferenceWithThirtyFpsCap(); CandidateOrderAndCache(); RateControlAndArguments(); } - private static void FrameRateUsesGamePreferenceWithSixtyFpsCap() + private static void FrameRateUsesGamePreferenceWithThirtyFpsCap() { var resolver = TestReflection.RequireType( "BazaarPlusPlus.Game.CombatReplay.Video.ReplayVideoFrameRateResolver" @@ -459,8 +459,8 @@ private static void FrameRateUsesGamePreferenceWithSixtyFpsCap() TestReflection.Assert(Resolve(15) == 15, "A user-selected 15 fps must be preserved."); TestReflection.Assert(Resolve(30) == 30, "A user-selected 30 fps must be preserved."); - TestReflection.Assert(Resolve(60) == 60, "The supported cap must preserve 60 fps."); - TestReflection.Assert(Resolve(120) == 60, "Recording FPS must cap the game setting at 60."); + TestReflection.Assert(Resolve(60) == 30, "Recording FPS must cap 60 fps at 30."); + TestReflection.Assert(Resolve(120) == 30, "Recording FPS must cap 120 fps at 30."); TestReflection.Assert(Resolve(-1) == 30, "An unset Unity FPS must fall back to 30."); } diff --git a/tests/CombatReplayRecording.Tests/Program.cs b/tests/CombatReplayRecording.Tests/Program.cs index 132ca1200..632788ab8 100644 --- a/tests/CombatReplayRecording.Tests/Program.cs +++ b/tests/CombatReplayRecording.Tests/Program.cs @@ -1,10 +1,15 @@ #nullable enable using BazaarPlusPlus.Game.CombatReplay; using BazaarPlusPlus.Game.PvpBattles; +using BazaarPlusPlus.Game.PvpBattles.Persistence; using BazaarPlusPlus.Infrastructure; ReplayPersistenceStateIsScopedPerRun(); ReplayPayloadStoreRoundTripsAndRejectsCorruption(); +CapturedReplayRoutesSeparatePveFromPvpPersistence(); +CurrentNativeReplayBecomesReadyWithoutPersistence(); +PvpBattleStoreRejectsPveManifests(); +ReplayRecordingTooltipBalancesVerticalPadding(); Console.WriteLine("Combat replay recording tests passed."); @@ -63,6 +68,65 @@ static void ReplayPayloadStoreRoundTripsAndRejectsCorruption() } } +static void CapturedReplayRoutesSeparatePveFromPvpPersistence() +{ + Assert( + CapturedReplayRouter.Resolve( + new PvpBattleManifest { BattleId = "pve", CombatKind = "Combat" } + ) == CapturedReplayRoute.CurrentNative, + "PvE captures must stay on the in-memory current-native route." + ); + Assert( + CapturedReplayRouter.Resolve( + new PvpBattleManifest { BattleId = "pvp", CombatKind = "PVPCombat" } + ) == CapturedReplayRoute.PersistedPvp, + "PvP captures must use the persisted catalog route." + ); +} + +static void CurrentNativeReplayBecomesReadyWithoutPersistence() +{ + var state = new CurrentReplayRecordingState(); + state.LatchBattle("pve"); + state.EnterReplayState(); + state.SetAvailability(ready: true, reason: null); + Assert(!state.Snapshot().CanStart, "An unrouted capture must not be recordable."); + + state.MarkCurrentNativeReady("pve"); + var snapshot = state.Snapshot(); + Assert(snapshot.CanStart, "A routed current-native capture should be recordable."); + Assert( + snapshot.Phase == CurrentReplayRecordingPhase.Ready, + "The current-native route should transition directly to ready." + ); +} + +static void PvpBattleStoreRejectsPveManifests() +{ + var path = Path.Combine(Path.GetTempPath(), $"bpp-pvp-route-{Guid.NewGuid():N}.db"); + var store = new PvpBattleSqliteStore(path); + try + { + store.Save(new PvpBattleManifest { BattleId = "pve", CombatKind = "Combat" }); + throw new InvalidOperationException("The PvP store accepted a PvE manifest."); + } + catch (ArgumentException) + { + // Expected: routing regressions must fail instead of reporting false persistence success. + } +} + +static void ReplayRecordingTooltipBalancesVerticalPadding() +{ + var balanced = CurrentReplayRecordingTooltipPadding.Balance(18, 48); + Assert(balanced.Top == 33, "Tooltip top padding should be vertically balanced."); + Assert(balanced.Bottom == 33, "Tooltip bottom padding should be vertically balanced."); + Assert( + balanced.Top + balanced.Bottom == 66, + "Balancing must preserve the native tooltip's total vertical padding." + ); +} + static void Assert(bool condition, string message) { if (!condition)