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>
This commit is contained in:
parent
306a1670d3
commit
db8fa328dc
6 changed files with 300 additions and 22 deletions
|
|
@ -495,6 +495,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
|
||||
private ItemCooldownUiController? _itemCooldownController;
|
||||
private VividTargetIndicatorController? _vividTargetIndicator;
|
||||
private Layout.VitalsSideBySideController? _vitalsSideBySide;
|
||||
private CharacterManagementUiMountCoordinator? _characterManagementMount;
|
||||
private CharacterCreationUiMountCoordinator? _characterCreationMount;
|
||||
private IDisposable? _characterSheetSubscription;
|
||||
|
|
@ -586,6 +587,14 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
WindowNames.JumpPowerbar,
|
||||
WindowNames.ExternalContainer,
|
||||
WindowNames.Vendor,
|
||||
// Retail owns which vitals window shows via the
|
||||
// SideBySideVitals character option (both
|
||||
// UpdateFromPlayerModule bodies @0x004CF140/@0x004D0810
|
||||
// apply it unconditionally at login) — the saved layout
|
||||
// must never restore a visibility the option decides.
|
||||
// See VitalsSideBySideController.
|
||||
WindowNames.Vitals,
|
||||
WindowNames.SideVitals,
|
||||
// Trade gate round 2 (2026-08-14): the trade window is
|
||||
// transient — visibility belongs to RuntimeTradeState's
|
||||
// open/closed lifecycle, never to the layout file (a
|
||||
|
|
@ -827,6 +836,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
{
|
||||
FpsController?.Tick();
|
||||
_vividTargetIndicator?.Tick();
|
||||
_vitalsSideBySide?.Tick();
|
||||
SpellbookWindowController?.Tick();
|
||||
AppraisalController?.Tick(deltaSeconds);
|
||||
SpellcastingUiController?.Tick();
|
||||
|
|
@ -1312,14 +1322,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
VitalsVM vm = _bindings.Vitals.ViewModel;
|
||||
VitalsController.Bind(layout,
|
||||
() => vm.HealthPercent,
|
||||
() => vm.StaminaPercent ?? 0f,
|
||||
() => vm.ManaPercent ?? 0f,
|
||||
() => (vm.HealthCurrent, vm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "",
|
||||
() => (vm.StaminaCurrent, vm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "",
|
||||
() => (vm.ManaCurrent, vm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : "");
|
||||
BindVitalsLayout(layout);
|
||||
RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
|
|
@ -1333,6 +1336,83 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ContentClickThrough = false,
|
||||
});
|
||||
Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C).");
|
||||
|
||||
MountSideVitals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The side-by-side vitals row — retail's SECOND complete vitals window
|
||||
/// (gmFloatySideVitalsUI 0x10000056, LayoutDesc 0x21000075, 460x26: the
|
||||
/// same three meters + cur/max labels laid out in one row). Retail swaps
|
||||
/// it with the stacked window on the SideBySideVitals character option
|
||||
/// (both UpdateFromPlayerModule bodies @0x004CF140/@0x004D0810 +
|
||||
/// gmGamePlayUI::RecvNotice_PlayerOptionChanged @0x004E9DA0) — see
|
||||
/// <see cref="Layout.VitalsSideBySideController"/>, which owns both
|
||||
/// windows' visibility from the per-frame option poll. Mounted hidden;
|
||||
/// the controller's first Tick applies the real bit.
|
||||
///
|
||||
/// <para>Element ids (meters 0x100000E6/EC/EE, labels 0x100000EB/ED/EF)
|
||||
/// and the detail-icon overlays are authored identically to 0x2100006C,
|
||||
/// so the same <see cref="VitalsController.Bind"/> and the inherited
|
||||
/// <see cref="UiVitalsRoot"/> click toggle apply as-is. Width constraints
|
||||
/// ride the root's authored 0x3C..0x3F (min 360 / max 3000, fixed 26
|
||||
/// height) through <see cref="RetailWindowFrame.Options.DatConstraintSource"/>.</para>
|
||||
/// </summary>
|
||||
private void MountSideVitals()
|
||||
{
|
||||
ElementInfo? info;
|
||||
ImportedLayout? layout;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, 0x21000075u);
|
||||
layout = info is null ? null : LayoutImporter.Build(
|
||||
info,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
}
|
||||
if (info is null || layout is null)
|
||||
{
|
||||
Console.WriteLine("[D.2b] side vitals: LayoutDesc 0x21000075 not found — SideBySideVitals unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
BindVitalsLayout(layout);
|
||||
RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.SideVitals,
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Left = 10f,
|
||||
Top = 30f,
|
||||
ResizeX = true,
|
||||
ResizeY = false,
|
||||
DatConstraintSource = info,
|
||||
Visible = false,
|
||||
ContentClickThrough = false,
|
||||
});
|
||||
_vitalsSideBySide = new Layout.VitalsSideBySideController(
|
||||
Host.Root,
|
||||
() => _bindings.Options.CurrentCharacterOption(
|
||||
(uint)CharacterOptionId.SideBySideVitals),
|
||||
WindowNames.Vitals,
|
||||
WindowNames.SideVitals);
|
||||
Console.WriteLine("[D.2b] side-by-side vitals window from LayoutDesc importer (0x21000075).");
|
||||
}
|
||||
|
||||
/// <summary>Binds the shared vitals ViewModel providers to a vitals layout —
|
||||
/// both windows (0x2100006C stacked / 0x21000075 side-by-side) author the
|
||||
/// same meter/label element ids.</summary>
|
||||
private void BindVitalsLayout(ImportedLayout layout)
|
||||
{
|
||||
VitalsVM vm = _bindings.Vitals.ViewModel;
|
||||
VitalsController.Bind(layout,
|
||||
() => vm.HealthPercent,
|
||||
() => vm.StaminaPercent ?? 0f,
|
||||
() => vm.ManaPercent ?? 0f,
|
||||
() => (vm.HealthCurrent, vm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "",
|
||||
() => (vm.StaminaCurrent, vm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "",
|
||||
() => (vm.ManaCurrent, vm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : "");
|
||||
}
|
||||
|
||||
private void MountRadar()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue