fix(combat): restore retail combat bar controls

Resolve authored StringInfo labels from local.dat, port gmCombatUI's runtime option captions and checkbox widgets, and bind horizontal scrollbar media by retail structural roles so the green power jewel is a thumb instead of a tiled track. Persist the three combat options and make Auto Target govern target acquisition.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 20:26:52 +02:00
parent 2215c76c7e
commit 927fa7881a
19 changed files with 24591 additions and 48 deletions

View file

@ -52,7 +52,8 @@ public static class DatWidgetFactory
/// <returns>The widget for this element. Never null — every type produces a widget.</returns>
public static UiElement? Create(ElementInfo info,
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont,
Func<uint, UiDatFont?>? fontResolve = null)
Func<uint, UiDatFont?>? fontResolve = null,
Func<UiStringInfoValue, string?>? stringResolve = null)
{
// Retail Type 3 = UIElement_Field (reg :126190), but in acdream's CURRENT layouts
// (vitals 0x2100006C / chat 0x21000006) Type-3 elements are sprite-bearing chrome +
@ -75,13 +76,14 @@ public static class DatWidgetFactory
UiElement e = info.Type switch
{
UiRadar.RetailClassId => new UiRadar(), // gmRadarUI (Register 0x004D8B80)
1 => new UiButton(info, resolve), // UIElement_Button (reg :125828)
1 => BuildButton(info, resolve, elementFont, stringResolve), // UIElement_Button
6 => new UiMenu(), // UIElement_Menu (reg :120163)
7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter
0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137)
12 => BuildText(info, resolve, elementFont), // UIElement_Text (reg :115655)
12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text
0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
0x10000035u => BuildCheckbox(info, resolve, elementFont, stringResolve), // UIOption_Checkbox
_ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers)
};
@ -147,20 +149,22 @@ public static class DatWidgetFactory
if (bar.Horizontal)
{
// gmToolbarUI stack slider (0x100001A4) has two direct Type-3 media
// children: a full-width 90x14 track (element 4) and a 16x14 thumb
// (element 1). Their geometry, not a button class, defines the roles.
ElementInfo? track = info.Children
.Where(child => DefaultImage(child) != 0u)
.OrderByDescending(child => child.Width)
.FirstOrDefault();
ElementInfo? scalarThumb = info.Children
.Where(child => !ReferenceEquals(child, track) && DefaultImage(child) != 0u)
.OrderBy(child => child.Width)
.FirstOrDefault();
bar.TrackSprite = track is null ? 0u : DefaultImage(track);
// Retail horizontal scrollbars use structural child ids: element 1 is
// the thumb and element 4 is the optional child-authored track.
ElementInfo? scalarThumb = info.Children.FirstOrDefault(child => child.Id == 1u);
bar.TrackSprite = DefaultImage(info);
bar.ThumbSprite = scalarThumb is null ? 0u : DefaultImage(scalarThumb);
// The toolbar stack slider authors its track on structural child 4,
// while gmCombatUI authors it on the scrollbar's DirectState. Geometry
// is never the role discriminator: inheritance can reflow child 1 and
// otherwise turn the 12px combat jewel into a tiled background.
if (bar.TrackSprite == 0u)
{
ElementInfo? authoredTrack = info.Children.FirstOrDefault(child => child.Id == 4u);
bar.TrackSprite = authoredTrack is null ? 0u : DefaultImage(authoredTrack);
}
// gmCombatUI's desired-power slider (0x1000004F) authors the
// live charge as a nested Type-7 meter. UiScrollbar consumes its
// DAT children, so retain the meter's fill image on the scalar
@ -390,7 +394,8 @@ public static class DatWidgetFactory
/// <see cref="ImportedLayout.FindElement"/> and set <see cref="UiText.DatFont"/> afterward
/// still override this — the build-time value is just the starting point.</param>
private static UiElement BuildText(ElementInfo info, Func<uint, (uint, int, int)> resolve,
UiDatFont? elementFont = null)
UiDatFont? elementFont = null,
Func<UiStringInfoValue, string?>? stringResolve = null)
{
uint bg = info.StateMedia.TryGetValue(
!string.IsNullOrEmpty(info.DefaultStateName) ? info.DefaultStateName
@ -464,6 +469,67 @@ public static class DatWidgetFactory
if (info.FontColor.HasValue)
t.DefaultColor = info.FontColor.Value;
if (ResolveAuthoredString(info, stringResolve) is { Length: > 0 } authored)
{
UiText.Line[] line = [new UiText.Line(authored, t.DefaultColor)];
t.LinesProvider = () => line;
}
return t;
}
private static UiButton BuildButton(
ElementInfo info,
Func<uint, (uint, int, int)> resolve,
UiDatFont? elementFont,
Func<UiStringInfoValue, string?>? stringResolve)
=> new(info, resolve)
{
Label = ResolveAuthoredString(info, stringResolve),
LabelFont = elementFont,
LabelColor = info.FontColor ?? System.Numerics.Vector4.One,
};
/// <summary>
/// Retail UIOption_Checkbox is a UIElement_Button whose visible face is its
/// authored indicator child. Its label lives on the option object rather than
/// in a child UIElement_Text.
/// </summary>
private static UiButton BuildCheckbox(
ElementInfo info,
Func<uint, (uint, int, int)> resolve,
UiDatFont? elementFont,
Func<UiStringInfoValue, string?>? stringResolve)
{
ElementInfo? indicator = info.Children.FirstOrDefault(child => DefaultImage(child) != 0u);
var button = new UiButton(info, resolve, indicator)
{
Label = ResolveAuthoredString(info, stringResolve),
LabelFont = elementFont,
LabelColor = info.FontColor ?? System.Numerics.Vector4.One,
LabelAlign = UiButton.LabelAlignment.Left,
};
if (indicator is not null)
{
button.FaceLeft = indicator.X;
button.FaceTop = indicator.Y;
button.FaceWidth = indicator.Width;
button.FaceHeight = indicator.Height;
button.LabelOffsetX = indicator.X + indicator.Width + 4f;
}
return button;
}
private static string? ResolveAuthoredString(
ElementInfo info,
Func<UiStringInfoValue, string?>? stringResolve)
{
if (stringResolve is null
|| !info.TryGetEffectiveProperty(0x17u, out var property)
|| property.Kind != UiPropertyKind.StringInfo)
return null;
return stringResolve(property.StringInfoValue);
}
}