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>
84 lines
3.4 KiB
C#
84 lines
3.4 KiB
C#
using System;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Owns which of the two vitals windows is visible — retail's
|
|
/// "Side By Side Vitals" character option (Options → Character tab).
|
|
///
|
|
/// <para>
|
|
/// Retail authors TWO complete vitals windows and swaps their visibility on
|
|
/// the option bit; nothing is rearranged in place:
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><description><c>gmFloatyVitalsUI</c> (LayoutDesc 0x2100006C, the
|
|
/// stacked 160x58 window): <c>UpdateFromPlayerModule @0x004CF140</c> —
|
|
/// visible iff <c>PlayerModule::SideBySideVitals() == 0</c>.</description></item>
|
|
/// <item><description><c>gmFloatySideVitalsUI</c> (LayoutDesc 0x21000075, the
|
|
/// 460x26 single-row window): <c>UpdateFromPlayerModule @0x004D0810</c> —
|
|
/// visible iff the bit is set.</description></item>
|
|
/// <item><description>Live apply: <c>gmGamePlayUI::RecvNotice_PlayerOptionChanged
|
|
/// @0x004E9DA0</c> flips both windows the moment the
|
|
/// <c>SideBySideVitals_PlayerOption</c> (0x13) changes; both
|
|
/// <c>UpdateFromPlayerModule</c> bodies re-apply on PlayerDescReceived.</description></item>
|
|
/// <item><description>The bit is <c>PlayerModule::SideBySideVitals @0x005D3070</c>
|
|
/// = <c>(options_ >> 0x15) & 1</c> — CharacterOptions1 0x00200000,
|
|
/// option id 0x13, riding the 0x01A1 PlayerModule blob (not a 0x0005
|
|
/// auto-save id) — <c>CharacterOptionTable</c> row confirmed against ACE.</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>
|
|
/// acdream shape: the option lives in the J4 Runtime owner
|
|
/// (<c>RuntimeCharacterState.Options</c>); this controller polls the borrowed
|
|
/// bit once per frame from <c>RetailUiRuntime.Tick</c> (the OP campaign's
|
|
/// established live-apply seam) and applies BOTH windows' visibility on the
|
|
/// edge — covering the mount-time default, the PlayerModule blob arriving
|
|
/// after mount, and the Character tab's live toggle with one mechanism.
|
|
/// Both window names ride <c>stateManagedVisibilityWindows</c> so the saved
|
|
/// window layout never restores a visibility this option owns.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class VitalsSideBySideController
|
|
{
|
|
private readonly UiRoot _root;
|
|
private readonly Func<bool> _sideBySideVitals;
|
|
private readonly string _stackedWindow;
|
|
private readonly string _sideWindow;
|
|
private bool? _applied;
|
|
|
|
public VitalsSideBySideController(
|
|
UiRoot root,
|
|
Func<bool> sideBySideVitals,
|
|
string stackedWindow,
|
|
string sideWindow)
|
|
{
|
|
_root = root ?? throw new ArgumentNullException(nameof(root));
|
|
_sideBySideVitals = sideBySideVitals
|
|
?? throw new ArgumentNullException(nameof(sideBySideVitals));
|
|
_stackedWindow = stackedWindow;
|
|
_sideWindow = sideWindow;
|
|
}
|
|
|
|
/// <summary>The last applied bit (null before the first apply). Exposed for tests.</summary>
|
|
public bool? Applied => _applied;
|
|
|
|
public void Tick()
|
|
{
|
|
bool sideBySide = _sideBySideVitals();
|
|
if (_applied == sideBySide) return;
|
|
_applied = sideBySide;
|
|
|
|
// Retail's exact swap (RecvNotice_PlayerOptionChanged @0x004E9DBE):
|
|
// option ON → stacked hidden, side shown; OFF → the opposite.
|
|
if (sideBySide)
|
|
{
|
|
_root.HideWindow(_stackedWindow);
|
|
_root.ShowWindow(_sideWindow);
|
|
}
|
|
else
|
|
{
|
|
_root.ShowWindow(_stackedWindow);
|
|
_root.HideWindow(_sideWindow);
|
|
}
|
|
}
|
|
}
|