acdream/tests/AcDream.App.Tests/UI/Layout/VitalsSideBySideControllerTests.cs
Erik db8fa328dc feat(ui): vitals — Side By Side Vitals swaps retail's two vitals windows
Port of retail's SideBySideVitals character option, derived end-to-end:
retail authors TWO complete vitals windows and swaps their VISIBILITY on
the option bit — nothing is rearranged in place.

- gmFloatySideVitalsUI (0x10000056, Register @0x004D0490) is a second
  full vitals window from LayoutDesc 0x21000075: 460x26, the same three
  meters (0x100000E6/EC/EE), cur/max labels (0x100000EB/ED/EF), and
  detail-icon overlays authored id-for-id with the stacked window — so
  the same VitalsController.Bind and the inherited UiVitalsRoot click
  toggle apply unchanged. Authored constraints ride the root's
  0x3C..0x3F (fixed 26 height, width 360..3000) through
  DatConstraintSource.
- Visibility ownership: gmFloatyVitalsUI::UpdateFromPlayerModule
  @0x004CF140 shows the stacked window iff PlayerModule::SideBySideVitals
  == 0; gmFloatySideVitalsUI::UpdateFromPlayerModule @0x004D0810 shows
  the side row iff set; gmGamePlayUI::RecvNotice_PlayerOptionChanged
  @0x004E9DA0 flips both live on option id 0x13.
- The bit: PlayerModule::SideBySideVitals @0x005D3070 =
  (options_ >> 0x15) & 1 — CharacterOptions1 0x00200000, ACE-confirmed;
  CharacterOptionTable already carried the exact row (PlayerModule-blob
  group, not a 0x0005 auto-save id).

acdream shape: MountSideVitals mounts the second window hidden;
VitalsSideBySideController polls the borrowed J4 option bit once per
frame from RetailUiRuntime.Tick and applies BOTH windows' visibility on
the edge — covering the mount default, the PlayerModule blob arriving
after mount, and the Character tab's live checkbox with one mechanism.
Both window names join stateManagedVisibilityWindows so the saved layout
never restores a visibility the option owns. The Character tab's
SideBySideVitals row un-dims (StoreOnly → Live) with a real reader —
33 dimmed / 17 live.

4 new controller tests (initial apply both directions, live edge swap
both directions, steady-bit non-reassertion). App suite Release live-DAT
5499 passed / 3 skips; Runtime 1744/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 10:40:49 +02:00

95 lines
3.4 KiB
C#

using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Retail's "Side By Side Vitals" character option swaps which of the TWO
/// complete vitals windows is visible — nothing is rearranged in place:
/// <c>gmFloatyVitalsUI::UpdateFromPlayerModule @0x004CF140</c> (stacked
/// visible iff the bit is CLEAR), <c>gmFloatySideVitalsUI::
/// UpdateFromPlayerModule @0x004D0810</c> (side row visible iff SET), and the
/// live flip in <c>gmGamePlayUI::RecvNotice_PlayerOptionChanged @0x004E9DA0</c>.
/// The bit is <c>PlayerModule::SideBySideVitals @0x005D3070</c> =
/// CharacterOptions1 0x00200000 (option id 0x13) — ACE-confirmed.
/// </summary>
public sealed class VitalsSideBySideControllerTests
{
[Fact]
public void FirstTick_AppliesTheCurrentBit_BothDirections()
{
var (root, stacked, side) = Windows();
bool bit = false;
var controller = new VitalsSideBySideController(
root, () => bit, WindowNames.Vitals, WindowNames.SideVitals);
controller.Tick();
Assert.True(stacked.Visible);
Assert.False(side.Visible);
Assert.False(controller.Applied!.Value);
}
[Fact]
public void FirstTick_WithBitSet_ShowsTheSideRow()
{
var (root, stacked, side) = Windows();
var controller = new VitalsSideBySideController(
root, () => true, WindowNames.Vitals, WindowNames.SideVitals);
controller.Tick();
Assert.False(stacked.Visible);
Assert.True(side.Visible);
}
[Fact]
public void BitEdge_SwapsLive_BothDirections()
{
// The Character tab's checkbox (or the inbound PlayerModule blob)
// flips the stored bit; the next frame swaps the windows — retail's
// RecvNotice_PlayerOptionChanged live apply.
var (root, stacked, side) = Windows();
bool bit = false;
var controller = new VitalsSideBySideController(
root, () => bit, WindowNames.Vitals, WindowNames.SideVitals);
controller.Tick();
bit = true;
controller.Tick();
Assert.False(stacked.Visible);
Assert.True(side.Visible);
bit = false;
controller.Tick();
Assert.True(stacked.Visible);
Assert.False(side.Visible);
}
[Fact]
public void SteadyBit_DoesNotReassertVisibility()
{
// Edge-detected: a steady bit must not fight other visibility
// machinery every frame (only the option EDGE applies, mirroring
// retail's notice-driven apply rather than a per-frame force).
var (root, stacked, side) = Windows();
var controller = new VitalsSideBySideController(
root, () => false, WindowNames.Vitals, WindowNames.SideVitals);
controller.Tick();
// Simulate an out-of-band hide (e.g. a future toggle surface).
stacked.Visible = false;
controller.Tick();
Assert.False(stacked.Visible);
}
private static (UiRoot Root, UiPanel Stacked, UiPanel Side) Windows()
{
var root = new UiRoot { Width = 800, Height = 600 };
var stacked = new UiPanel { Width = 160, Height = 58 };
var side = new UiPanel { Width = 460, Height = 26, Visible = false };
root.AddChild(stacked);
root.AddChild(side);
root.RegisterWindow(WindowNames.Vitals, stacked, stacked, null);
root.RegisterWindow(WindowNames.SideVitals, side, side, null);
return (root, stacked, side);
}
}