feat(ui): share indicator detail panels

Port the authored Link Status, Vitae, and Mini Game detail roots and register every indicator page with retail's one-active gmPanelUI owner. Helpful/Harmful and the new pages now replace Inventory, Character, or Magic at one canonical window position while preserving the DAT restore-previous flag.

Correct the retail ping wire to its payload-free request/response, publish measured RTT, and port Vitae recovery XP from the live modifier and player properties. Keep transport packet-loss averaging and mini-game gameplay explicitly tracked under AP-110.

Release build and all 5,814 tests pass with five intentional skips. Connected visual gate pending.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 10:27:41 +02:00
parent 52c529be86
commit a96767ba6d
28 changed files with 6539 additions and 32 deletions

View file

@ -81,6 +81,7 @@ public sealed record IndicatorRuntimeBindings(
Func<int?> Strength,
Func<LinkStatusSnapshot> LinkStatus,
Func<double> CurrentTime,
Action RequestLinkStatusPing,
Action EndCharacterSession);
public sealed record ToolbarRuntimeBindings(
@ -196,6 +197,7 @@ public sealed class RetailUiRuntime : IDisposable
MountCombat();
MountSpellbook();
MountEffects();
MountIndicatorDetailPanels();
MountIndicators();
MountJumpPowerbar();
MountDialogFactory();
@ -245,6 +247,9 @@ public sealed class RetailUiRuntime : IDisposable
public SpellbookWindowController? SpellbookWindowController { get; private set; }
public EffectsUiController? PositiveEffectsController { get; private set; }
public EffectsUiController? NegativeEffectsController { get; private set; }
public LinkStatusUiController? LinkStatusUiController { get; private set; }
public VitaeUiController? VitaeUiController { get; private set; }
public MiniGameUiController? MiniGameUiController { get; private set; }
public IndicatorBarController? IndicatorBarController { get; private set; }
public JumpPowerbarController? JumpPowerbarController { get; private set; }
public RetailFpsController? FpsController { get; private set; }
@ -274,6 +279,7 @@ public sealed class RetailUiRuntime : IDisposable
SpellcastingUiController?.Tick();
PositiveEffectsController?.Tick();
NegativeEffectsController?.Tick();
LinkStatusUiController?.Tick();
IndicatorBarController?.Tick();
JumpPowerbarController?.Tick();
SelectedObjectController?.Tick(deltaSeconds);
@ -880,7 +886,7 @@ public sealed class RetailUiRuntime : IDisposable
if (positive) PositiveEffectsController = controller;
else NegativeEffectsController = controller;
UiElement root = layout.Root;
RetailWindowFrame.Mount(
RetailWindowHandle handle = RetailWindowFrame.Mount(
Host.Root,
root,
_bindings.Assets.ResolveSprite,
@ -898,10 +904,184 @@ public sealed class RetailUiRuntime : IDisposable
ContentClickThrough = false,
Controller = controller,
});
_panelUi.Register(
_panelUi.RegisterMainPanel(
positive ? RetailPanelCatalog.PositiveEffects : RetailPanelCatalog.NegativeEffects,
positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects,
rootInfo);
handle,
rootInfo.TryGetEffectiveBool(
RetailPanelUiController.RestorePreviousPropertyId,
out bool restorePrevious)
&& restorePrevious);
}
private void MountIndicatorDetailPanels()
{
MountLinkStatusPanel();
MountVitaePanel();
MountMiniGamePanel();
}
private void MountLinkStatusPanel()
{
ElementInfo? rootInfo;
ImportedLayout? layout;
LinkStatusStrings strings;
lock (_bindings.Assets.DatLock)
{
rootInfo = LayoutImporter.ImportInfos(
_bindings.Assets.Dats,
LinkStatusUiController.LayoutId,
LinkStatusUiController.RootId);
var resolver = new DatStringResolver(_bindings.Assets.Dats);
layout = rootInfo is null
? null
: LayoutImporter.Build(
rootInfo,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
resolver.Resolve);
LinkStatusStrings fallback = LinkStatusStrings.English;
strings = new LinkStatusStrings(
resolver.Resolve(0x23000001u, 0x0D632F7Fu) ?? fallback.Description,
resolver.Resolve(0x23000001u, 0x038FABF3u) ?? fallback.Legend,
resolver.Resolve(0x23000001u, 0x05481084u) ?? fallback.DisconnectWarning,
resolver.Resolve(0x23000001u, 0x0CADBA93u) ?? fallback.PacketLossPrefix,
resolver.Resolve(0x23000001u, 0x0D6485F7u) ?? fallback.PingPrefix);
}
if (rootInfo is null || layout is null) return;
LinkStatusUiController? controller = Layout.LinkStatusUiController.Bind(
layout,
_bindings.Indicators.LinkStatus,
_bindings.Indicators.CurrentTime,
_bindings.Indicators.RequestLinkStatusPing,
strings,
() => CloseWindow(WindowNames.LinkStatus));
if (controller is null) return;
LinkStatusUiController = controller;
RegisterIndicatorDetailPanel(
RetailPanelCatalog.LinkStatus,
WindowNames.LinkStatus,
rootInfo,
layout.Root,
controller);
}
private void MountVitaePanel()
{
ElementInfo? rootInfo;
ImportedLayout? layout;
VitaeStrings strings;
lock (_bindings.Assets.DatLock)
{
rootInfo = LayoutImporter.ImportInfos(
_bindings.Assets.Dats,
VitaeUiController.LayoutId,
VitaeUiController.RootId);
var resolver = new DatStringResolver(_bindings.Assets.Dats);
layout = rootInfo is null
? null
: LayoutImporter.Build(
rootInfo,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
resolver.Resolve);
VitaeStrings fallback = VitaeStrings.English;
strings = new VitaeStrings(
resolver.Resolve(0x23000001u, 0x0142DDECu) ?? fallback.FullStrength,
resolver.Resolve(0x23000001u, 0x045D7665u, 0) ?? fallback.LostPrefix,
resolver.Resolve(0x23000001u, 0x045D7665u, 1) ?? fallback.LostSuffix,
resolver.Resolve(0x23000001u, 0x0DCD8C35u, 0) ?? fallback.RecoveryPrefix,
resolver.Resolve(0x23000001u, 0x0DCD8C35u, 1) ?? fallback.RecoverySuffix);
}
if (rootInfo is null || layout is null) return;
VitaeUiController? controller = Layout.VitaeUiController.Bind(
layout,
_bindings.Indicators.Spellbook,
_bindings.Indicators.Objects,
_bindings.Indicators.PlayerGuid,
strings,
() => CloseWindow(WindowNames.Vitae));
if (controller is null) return;
VitaeUiController = controller;
RegisterIndicatorDetailPanel(
RetailPanelCatalog.Vitae,
WindowNames.Vitae,
rootInfo,
layout.Root,
controller);
}
private void MountMiniGamePanel()
{
ElementInfo? rootInfo;
ImportedLayout? layout;
lock (_bindings.Assets.DatLock)
{
rootInfo = LayoutImporter.ImportInfos(
_bindings.Assets.Dats,
MiniGameUiController.LayoutId,
MiniGameUiController.RootId);
layout = rootInfo is null
? null
: LayoutImporter.Build(
rootInfo,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
new DatStringResolver(_bindings.Assets.Dats).Resolve);
}
if (rootInfo is null || layout is null) return;
MiniGameUiController? controller = Layout.MiniGameUiController.Bind(
layout,
() => CloseWindow(WindowNames.MiniGame));
if (controller is null) return;
MiniGameUiController = controller;
RegisterIndicatorDetailPanel(
RetailPanelCatalog.MiniGame,
WindowNames.MiniGame,
rootInfo,
layout.Root,
controller);
}
private void RegisterIndicatorDetailPanel(
uint panelId,
string windowName,
ElementInfo rootInfo,
UiElement root,
IRetainedPanelController controller)
{
RetailWindowHandle handle = RetailWindowFrame.Mount(
Host.Root,
root,
_bindings.Assets.ResolveSprite,
new RetailWindowFrame.Options
{
WindowName = windowName,
Chrome = RetailWindowChrome.Imported,
Left = 18f,
Top = 18f,
Visible = false,
Resizable = false,
ResizeX = false,
ResizeY = false,
ConstrainDragToParent = true,
ContentClickThrough = false,
Controller = controller,
});
_panelUi.RegisterMainPanel(
panelId,
windowName,
handle,
rootInfo.TryGetEffectiveBool(
RetailPanelUiController.RestorePreviousPropertyId,
out bool restorePrevious)
&& restorePrevious);
}
private void MountIndicators()