From 0f4061b645ca2b0b5c0b16b008e981fe8820407e Mon Sep 17 00:00:00 2001 From: timelic Date: Tue, 4 Aug 2026 18:03:52 +0800 Subject: [PATCH 1/2] feat(replay): support PVE recording and refine replay dock UI --- .../CollectionPanelDockButtonController.cs | 67 +++++++++++ .../CombatReplayCaptureService.cs | 12 +- .../CurrentReplayRecordingButtonController.cs | 7 +- .../CurrentReplayRecordingTooltipStyle.cs | 106 ++++++++++++++++++ .../OverlayPanels/BppUiChromeSuppression.cs | 2 +- .../PvpBattles/PvpBattleSequenceMatcher.cs | 8 +- .../Game/Settings/BppSettingsDockPlacement.cs | 2 +- .../DockButtons/replay-export-icon.png | Bin 957 -> 4071 bytes .../DockButtons/replay-recording-icon.png | Bin 2361 -> 740 bytes .../DockButtons/replay-retry-icon.png | Bin 2090 -> 6254 bytes .../DockButtons/replay-view-icon.png | Bin 1254 -> 1572 bytes 11 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs index 7c57dd9de..3cb01bda2 100644 --- a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs +++ b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs @@ -24,6 +24,8 @@ internal sealed class CollectionPanelDockButtonController private readonly CollectionPanelDockLayoutLogState _layoutLogState = new(); private bool _hasAvailableDockLayout; private int _screenshotSuppressionCount; + private int _replayRecordingSuppressionCount; + private bool _dockButtonInteractableBeforeReplayRecordingSuppression = true; internal RectTransform? DockButtonRect => _dockButtonRect; @@ -31,6 +33,7 @@ internal void SetLayoutAvailable(bool available) { _hasAvailableDockLayout = available; ApplyScreenshotSuppressionVisibility(); + ApplyReplayRecordingSuppressionInteraction(); } internal static void Attach(Button anchorButton, BppSettingsDockPlacement placement) @@ -70,6 +73,21 @@ internal static void Attach(Button anchorButton, BppSettingsDockPlacement placem return UiSuppressionScope.Begin(suppressionActions); } + internal static IDisposable? BeginReplayRecordingSuppression() + { + var controllers = FindObjectsOfType( + includeInactive: true + ); + if (controllers.Length == 0) + return null; + + var suppressionActions = new Func[controllers.Length]; + for (var index = 0; index < controllers.Length; index++) + suppressionActions[index] = controllers[index].BeginInstanceReplayRecordingSuppression; + + return UiSuppressionScope.Begin(suppressionActions); + } + private void Initialize( Button anchorButton, BppSettingsDockPlacement placement, @@ -179,6 +197,24 @@ private void EndInstanceScreenshotSuppression() ApplyScreenshotSuppressionVisibility(); } + private IDisposable BeginInstanceReplayRecordingSuppression() + { + if (_replayRecordingSuppressionCount == 0 && _dockButton != null) + _dockButtonInteractableBeforeReplayRecordingSuppression = _dockButton.interactable; + + _replayRecordingSuppressionCount++; + ApplyReplayRecordingSuppressionInteraction(); + return new ReplayRecordingSuppressionLease(this); + } + + private void EndInstanceReplayRecordingSuppression() + { + if (_replayRecordingSuppressionCount > 0) + _replayRecordingSuppressionCount--; + + ApplyReplayRecordingSuppressionInteraction(); + } + private void ApplyScreenshotSuppressionVisibility() { var shouldBeVisible = _screenshotSuppressionCount == 0 && _hasAvailableDockLayout; @@ -186,6 +222,21 @@ private void ApplyScreenshotSuppressionVisibility() _dockButtonRect.gameObject.SetActive(shouldBeVisible); } + private void ApplyReplayRecordingSuppressionInteraction() + { + if (_dockButton == null) + return; + + if (_replayRecordingSuppressionCount > 0) + { + _dockButton.interactable = false; + } + else + { + _dockButton.interactable = _dockButtonInteractableBeforeReplayRecordingSuppression; + } + } + private void OnDockButtonClicked() { CollectionPanel.OpenFromDockButton(); @@ -205,4 +256,20 @@ public void Dispose() controller.EndInstanceScreenshotSuppression(); } } + + private sealed class ReplayRecordingSuppressionLease( + CollectionPanelDockButtonController controller + ) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + controller.EndInstanceReplayRecordingSuppression(); + } + } } 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/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/CurrentReplayRecordingTooltipStyle.cs b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs new file mode 100644 index 000000000..0278085fe --- /dev/null +++ b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs @@ -0,0 +1,106 @@ +#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; + + 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; + } + + 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.MiddleCenter; + + 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; + + _styledTooltip = null; + _styledTooltipLayout = null; + } +} diff --git a/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs b/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs index 0ca5ed418..855e198be 100644 --- a/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs +++ b/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs @@ -17,7 +17,7 @@ internal static class BppUiChromeSuppression CombatStatusBarFeature.BeginScreenshotSuppression ), BppUiChromeSuppressionMode.ReplayRecording => UiSuppressionScope.Begin( - CollectionPanelDockButtonController.BeginScreenshotSuppression + CollectionPanelDockButtonController.BeginReplayRecordingSuppression ), _ => throw new ArgumentOutOfRangeException( nameof(mode), 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 a2e0f545c0403fd013691f0fd024fd43eede1fd0..3da50a847cc2ada29a04417a8e11277406cf606d 100644 GIT binary patch literal 4071 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 literal 957 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&DI|LY`7$t6sR6}X7#MzmnTu$sZZAYL$MSD+10A|b#h#1%;Y|9FyC=>PxacY;m;?T{-8@(X5Q5SS&- z#^kVTr@h&`_vV!|XPCa-vAxiTU0$ZhQ7%L!{UT5$W0JSKi##i@z+NDSy~NYkmHibv zKaZ)@amG2TfI_Mzt`Q|Ei6yC4$wjF^iowXh$V}J3T-VSv#L&pf)Y!_%OxwV~%D|vM z>q0n+hTQy=%(P0}8vdDQyasB}fZI@#nVVW%l9*e7ThB@*PZI_PCLT{0$B>F!Z)aQ# zI%FW=s;JAt%E%Zf@cIAzl@Detd3h=4*xeGD_m!^?!$?^DER&0BxCb5}flV)Rr zf(<**M1QUd=9hgFUpc?LzV_P2b3VVq-k;rGw^S&qn~z0-fsw<3fk~i&fhF(E+e0Tm zeu$X0KZf&Id}Y-1BmU-xK8BkG*Zm7qyuUMjTjR0M_m4Hq+#h_ZJn#7I|JA0Af9EJC zT>UgRxweiRVok_3Nf+ z8&rOu+VjTVRcwP+zbI7aJhn3~^ZG;B><%LL+ky85}Sb4q9e0Bmot1poj5 diff --git a/src/BazaarPlusPlus/Resources/DockButtons/replay-recording-icon.png b/src/BazaarPlusPlus/Resources/DockButtons/replay-recording-icon.png index 8e263725a32675f58f341b50fbe2ad85bfa92a2f..5e4a5f147cebac123450b11c422ecab7eeac4787 100644 GIT binary patch literal 740 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&DI|LY`7$t6sR6}X7#MzmnTu$sZZAYL$MSD+10B0Rt+#1%-NoMlU01T^#B680xRO1dP-FPMRWNzk@* z>FygJ{xTa*xbllpalyYofiH7@+b>@gd$ts)j4{dE-G!lpRn`N@VK4FYb!C6W!NMuQ zYo0W5Cs5kn)5S5Q;?~<6ck`MY1lk_*bsRX=-=tvmy?*}P(Air&4}M9o>a3hEEPO4+ z&RF@)r;m(4bHL!Am&+y}mHVQW{$hq#%$#t1w`&&T|=K^EifV_SOsisRN4qtb% zPR(qHieKf++;W}qP&Io&e&cU}UH^IS=DWPxp7gza;_t^dYMmeLQ!TzfWmo>`yZmbZ z+>PHKUitguo&T(@zqO@cL2&Q9y7I3)U;XBY4xo@~iEBhjN@7W>RdP`(kYX@0Ff!9M zu+TLy2{Ev+GBvj{vD7v&ure^HUhc$?q9HdwB{QuOw+60XzcvFkXuxeK$;?eHE=kNS fK+$7qWn^MyXbiDr-P6C$AcuOo`njxgN@xNA&v7IM literal 2361 zcmah~c{CJiAAToWL?nF@Q&h5LoopknnvG^UY@EZ=nhxaWL-+;e~Dyzg_)^PK1Wp7T5B{k`$mtjz@YPVoT%1T4&r zZP-Zsb%eOtcScp)BQ{8Sn%f`&h`hk&V*%Ltr7r;xq5{B*8vyVe07L`xo3CGH3)~)7 zX2u796|eoDpVe&4vSeB8`y?_T$^(C6S*nU!E_cmr)Zw#W1Hy~E9)aiHjT*yCZ}VcajTn`Nygmo zP2oXS7>e^+8Z7j5SKp3(dI%CDEKU(VrbzVMm=6Z=3bF7HorG2B;3hTHz zoUfyEFf;fiePQ_Z;P9jlEDU2K$4>h%C1vF(L+ng_E1^xLroZ)JHT8P7`Ni!qBDb2a zG#)6CPHP1Y<>-08r;G-W{7Hb%=DKy_lVnEEeeIgrjT&n}gz|w@HxVHDMi%JDTn9wX zVgPteAwW1W1gs}qd}R?w)>p*5v6(wDWU0TMf?P|db-N48Fh)0Zg{>GFT7e$JTX$#T zaGEtOCq5mL)QQ?Y<*S&dz`P}|!{4fj^e`fc#Oh|e zgsqfMh=9p2amiUUdbZ`u&weD)z-Cgmd46=2NbF`PJ+XN%8j=bn(CVn@FiMD?K4Yv^ z{x`QhC;FA(2s^N0??PD?|8Ww}tbs9(t@nKYBuzUw&n5Nab%<$!3zz+7oN;KBNP16X zf9|qE%^SEBfd*N!rC6YU@VsO}I&Tnc5Df8`FjWNSyNr8|&wq4z(W8h<&dlcp7cZtn z4i*dn8X1iN6AswWGOPy2JNnib^>(yEe&tZ1QqcV2@>hK*`y^53`NGknlxONJllT`J z59=BdWysKBQwuB} zq1lESGs~h}hvcl2^6B_+D1eny&lG3+r`Z?X81I^w{?>AHj+`Nk2)CV^PV?-&X#dKy&lTaAA{b!YE@aMl z8)}#r&ANhsjt|gtSJ#d3S+pWvVJt-egGHV@Up1k`^`-A|Wgp{+-JcTtKc^o-KgECb z*EX4XN@<9&E?AZ!1200Cva@W>H`P2p)Jj5=w6b*cL!rh)9ood4G$>)hz!y4xC;SBz zA>?XR2E~WHf7Z6*rOGW`1!66^sX}BoMIS6uYXo)$gP~Q5nK}&-}8fHg?sDxwUW@V&9FGIJG@uj-Z@aFF1QV!+x{?Yp_ zMv$e4?FOzstewlFrJzG0NCD(?0EtxS4E*slOdgn#Tb5<^Qjz=m7^+~?^5S|^nVN|JE#3a6sIPZWRhX#}6 zwuIxMn$BB>2tuU8y|JqSkyD#Kw%g(-h_thfdzAs{2ah(UH0BMcON(n7zLonQ{TY=Q z6ZPc&BSfd^El*F^^3M}R9rfrXC&#=YuGwG&b;ksmsksYe)qwp|IJ|Jymx!`MAF9rn;edlJp zE4N*^^=lKZMWRD@2h575YsU#o@l)+Q_{S3k3PPgo6V>mtSg+w(BD+sJvdXtMe&}*U%O*|d~zfV@-Khx-!jp4 YpF^WlysIK5*x?sxVPb7uW#|_3Ke#+XBLDyZ diff --git a/src/BazaarPlusPlus/Resources/DockButtons/replay-retry-icon.png b/src/BazaarPlusPlus/Resources/DockButtons/replay-retry-icon.png index 089f057c574e33aeb644d2c26d245ecc0b9225d8..26e720991fedfec53169a728ad31f416f9e0e8b5 100644 GIT binary patch literal 6254 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 F697hMUdR9d From 038e4e331ce81c9f25beaea1ab2d9a823d7be089 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Wed, 5 Aug 2026 22:50:05 +0800 Subject: [PATCH 2/2] fix(replay): address PVE recording review feedback --- .../CollectionPanelDockButtonController.cs | 67 ------------------- .../Game/CombatReplay/CapturedReplayRoute.cs | 29 ++++++++ .../Game/CombatReplay/CombatReplayRuntime.cs | 13 +++- .../CurrentReplayRecordingState.cs | 29 +++++--- .../CurrentReplayRecordingTooltipPadding.cs | 13 ++++ .../CurrentReplayRecordingTooltipStyle.cs | 23 ++++++- .../Video/ReplayVideoCaptureDefaults.cs | 2 +- .../OverlayPanels/BppUiChromeSuppression.cs | 2 +- .../Persistence/PvpBattleSqliteStore.cs | 5 +- tests/Architecture.Tests/CoreLayeringTests.cs | 7 ++ tests/CombatReplayAudioVideo.Tests/Program.cs | 8 +-- tests/CombatReplayRecording.Tests/Program.cs | 64 ++++++++++++++++++ 12 files changed, 177 insertions(+), 85 deletions(-) create mode 100644 src/BazaarPlusPlus/Game/CombatReplay/CapturedReplayRoute.cs create mode 100644 src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipPadding.cs diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs index 3cb01bda2..7c57dd9de 100644 --- a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs +++ b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanelDockButtonController.cs @@ -24,8 +24,6 @@ internal sealed class CollectionPanelDockButtonController private readonly CollectionPanelDockLayoutLogState _layoutLogState = new(); private bool _hasAvailableDockLayout; private int _screenshotSuppressionCount; - private int _replayRecordingSuppressionCount; - private bool _dockButtonInteractableBeforeReplayRecordingSuppression = true; internal RectTransform? DockButtonRect => _dockButtonRect; @@ -33,7 +31,6 @@ internal void SetLayoutAvailable(bool available) { _hasAvailableDockLayout = available; ApplyScreenshotSuppressionVisibility(); - ApplyReplayRecordingSuppressionInteraction(); } internal static void Attach(Button anchorButton, BppSettingsDockPlacement placement) @@ -73,21 +70,6 @@ internal static void Attach(Button anchorButton, BppSettingsDockPlacement placem return UiSuppressionScope.Begin(suppressionActions); } - internal static IDisposable? BeginReplayRecordingSuppression() - { - var controllers = FindObjectsOfType( - includeInactive: true - ); - if (controllers.Length == 0) - return null; - - var suppressionActions = new Func[controllers.Length]; - for (var index = 0; index < controllers.Length; index++) - suppressionActions[index] = controllers[index].BeginInstanceReplayRecordingSuppression; - - return UiSuppressionScope.Begin(suppressionActions); - } - private void Initialize( Button anchorButton, BppSettingsDockPlacement placement, @@ -197,24 +179,6 @@ private void EndInstanceScreenshotSuppression() ApplyScreenshotSuppressionVisibility(); } - private IDisposable BeginInstanceReplayRecordingSuppression() - { - if (_replayRecordingSuppressionCount == 0 && _dockButton != null) - _dockButtonInteractableBeforeReplayRecordingSuppression = _dockButton.interactable; - - _replayRecordingSuppressionCount++; - ApplyReplayRecordingSuppressionInteraction(); - return new ReplayRecordingSuppressionLease(this); - } - - private void EndInstanceReplayRecordingSuppression() - { - if (_replayRecordingSuppressionCount > 0) - _replayRecordingSuppressionCount--; - - ApplyReplayRecordingSuppressionInteraction(); - } - private void ApplyScreenshotSuppressionVisibility() { var shouldBeVisible = _screenshotSuppressionCount == 0 && _hasAvailableDockLayout; @@ -222,21 +186,6 @@ private void ApplyScreenshotSuppressionVisibility() _dockButtonRect.gameObject.SetActive(shouldBeVisible); } - private void ApplyReplayRecordingSuppressionInteraction() - { - if (_dockButton == null) - return; - - if (_replayRecordingSuppressionCount > 0) - { - _dockButton.interactable = false; - } - else - { - _dockButton.interactable = _dockButtonInteractableBeforeReplayRecordingSuppression; - } - } - private void OnDockButtonClicked() { CollectionPanel.OpenFromDockButton(); @@ -256,20 +205,4 @@ public void Dispose() controller.EndInstanceScreenshotSuppression(); } } - - private sealed class ReplayRecordingSuppressionLease( - CollectionPanelDockButtonController controller - ) : IDisposable - { - private bool _disposed; - - public void Dispose() - { - if (_disposed) - return; - - _disposed = true; - controller.EndInstanceReplayRecordingSuppression(); - } - } } 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/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/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 index 0278085fe..f067b953d 100644 --- a/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs +++ b/src/BazaarPlusPlus/Game/CombatReplay/CurrentReplayRecordingTooltipStyle.cs @@ -22,6 +22,8 @@ internal sealed class CurrentReplayRecordingTooltipStyle private TextAlignmentOptions _styledTooltipOriginalBodyAlignment; private VerticalLayoutGroup? _styledTooltipLayout; private TextAnchor _styledTooltipOriginalChildAlignment; + private int _styledTooltipOriginalPaddingTop; + private int _styledTooltipOriginalPaddingBottom; internal void Apply(AuxiliaryTooltipController tooltip) { @@ -48,7 +50,11 @@ internal void Apply(AuxiliaryTooltipController tooltip) _styledTooltipLayout = tooltip.auxParent?.GetComponent(); if (_styledTooltipLayout != null) + { _styledTooltipOriginalChildAlignment = _styledTooltipLayout.childAlignment; + _styledTooltipOriginalPaddingTop = _styledTooltipLayout.padding.top; + _styledTooltipOriginalPaddingBottom = _styledTooltipLayout.padding.bottom; + } } ApplyTextStyle(tooltip.headerText, _styledTooltipOriginalHeaderFontSize); @@ -59,7 +65,18 @@ internal void Apply(AuxiliaryTooltipController tooltip) tooltip.transform.localScale = _styledTooltipOriginalLocalScale * TooltipScale; if (_styledTooltipLayout != null) - _styledTooltipLayout.childAlignment = TextAnchor.MiddleCenter; + { + _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); } @@ -98,7 +115,11 @@ internal void Restore() 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/OverlayPanels/BppUiChromeSuppression.cs b/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs index 855e198be..0ca5ed418 100644 --- a/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs +++ b/src/BazaarPlusPlus/Game/OverlayPanels/BppUiChromeSuppression.cs @@ -17,7 +17,7 @@ internal static class BppUiChromeSuppression CombatStatusBarFeature.BeginScreenshotSuppression ), BppUiChromeSuppressionMode.ReplayRecording => UiSuppressionScope.Begin( - CollectionPanelDockButtonController.BeginReplayRecordingSuppression + CollectionPanelDockButtonController.BeginScreenshotSuppression ), _ => throw new ArgumentOutOfRangeException( nameof(mode), 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/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)