fix(ui): match retail vitae and character info

Restore Vitae's omitted penalty paragraph, replace the invented character summary with gmCharacterInfoUI's ordered report and property meanings, preserve authored translucent body surfaces, and initialize the end-session button in its visible Normal DAT state.

Release build and all 5,830 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 12:06:14 +02:00
parent d1d603105f
commit 16c21e299c
21 changed files with 771 additions and 303 deletions

View file

@ -919,6 +919,7 @@ public sealed class RetailUiRuntime : IDisposable
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top
| AnchorEdges.Right | AnchorEdges.Bottom,
ContentClickThrough = false,
DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo),
Controller = controller,
});
_panelUi.RegisterMainPanel(
@ -943,12 +944,14 @@ public sealed class RetailUiRuntime : IDisposable
{
ElementInfo? rootInfo;
ImportedLayout? layout;
CharacterInfoStrings strings;
lock (_bindings.Assets.DatLock)
{
rootInfo = LayoutImporter.ImportInfos(
_bindings.Assets.Dats,
CharacterController.LayoutId,
CharacterController.RootId);
var resolver = new DatStringResolver(_bindings.Assets.Dats);
layout = rootInfo is null
? null
: LayoutImporter.Build(
@ -956,7 +959,9 @@ public sealed class RetailUiRuntime : IDisposable
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
new DatStringResolver(_bindings.Assets.Dats).Resolve);
resolver.Resolve);
strings = CharacterInfoStrings.FromDat(key => resolver.ResolveAll(
0x23000001u, DatStringResolver.ComputeHash(key)));
}
if (rootInfo is null || layout is null) return;
@ -964,7 +969,8 @@ public sealed class RetailUiRuntime : IDisposable
layout,
_bindings.Character.Provider.BuildSheet,
_bindings.Assets.DefaultFont,
() => CloseWindow(WindowNames.CharacterInformation));
() => CloseWindow(WindowNames.CharacterInformation),
strings);
RegisterIndicatorDetailPanel(
RetailPanelCatalog.CharacterInformation,
WindowNames.CharacterInformation,
@ -1042,11 +1048,19 @@ public sealed class RetailUiRuntime : IDisposable
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);
Resolve("ID_Vitae_Text_Full", 0, fallback.FullStrength),
Resolve("ID_Vitae_Text_Vitae", 0, fallback.LostPrefix),
Resolve("ID_Vitae_Text_Vitae", 1, fallback.LostSuffix),
Resolve("ID_Vitae_Text_Skills", 0, fallback.SkillsPrefix),
Resolve("ID_Vitae_Text_Skills", 1, fallback.SkillsSuffix),
Resolve("ID_Vitae_Text_Experience", 0, fallback.RecoveryPrefix),
Resolve("ID_Vitae_Text_Experience", 1, fallback.RecoverySuffix));
string Resolve(string key, int token, string fallbackValue)
=> resolver.Resolve(
0x23000001u,
DatStringResolver.ComputeHash(key),
token) ?? fallbackValue;
}
if (rootInfo is null || layout is null) return;
@ -1127,6 +1141,7 @@ public sealed class RetailUiRuntime : IDisposable
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top
| AnchorEdges.Right | AnchorEdges.Bottom,
ContentClickThrough = false,
DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo),
Controller = controller,
});
_panelUi.RegisterMainPanel(
@ -1139,6 +1154,28 @@ public sealed class RetailUiRuntime : IDisposable
&& restorePrevious);
}
/// <summary>
/// Retail's Vitae and Character Information children carry their own
/// 0x06004CC2 body surface below the 25px title row. The standalone shared
/// window contributes the border only in that case; painting the same
/// translucent center twice makes the panel look opaque.
/// </summary>
internal static bool AuthorsFullPanelCenter(ElementInfo rootInfo)
{
const float RetailTitleHeight = 25f;
return Visit(rootInfo);
bool Visit(ElementInfo info)
{
bool coversBody = info.Width >= rootInfo.Width
&& info.Height >= rootInfo.Height - RetailTitleHeight;
if (coversBody && info.StateMedia.Values.Any(
media => media.File == RetailChromeSprites.CenterFill))
return true;
return info.Children.Any(Visit);
}
}
private void MountIndicators()
{
ImportedLayout? layout = Import(IndicatorBarController.LayoutId);