Aligns the hand-built attribute/skill rows in CharacterStatController with the authored shared row template 0x10000248 (LayoutDesc 0x21000045, InfoRegion::InfoRegion @0x004F1450 template index 0 — the same template gmAttributeUI and gmSkillUI both instantiate): - Row geometry replaced with AUTHORED PIXEL VALUES instead of derived fractions: icon flush left 20x20 (was 16x16 at X=4, vertically centered), name column X=25 W=150 fixed (was RowPadX+IconSize+IconGap offset with a width*0.60 fraction), value column X=175 W=100 right-justified (its right edge sits 7px short of the row's 282px right edge — the authored gutter the owner reported). Row width itself now clamps to the authored 282px template width (RowContentWidth) rather than the ListBox's raw 300px container width. Attribute-row height fixed at 20px (was 22px, no dat basis); SkillRowHeight folded into the same RowHeight constant since both row kinds share H=20. - RowHighlightSprite corrected from 0x06001397 to 0x06000F93 — CT1's ground-truth research sealed the verdict that gmAttributeUI:: UpdateSelection @0x0049DEE0 (SetState(6) -> InfoRegion::SetState @0x004F0EE0) swaps the row's Highlight-state media (0x06000F93), a full-row background swap. 0x06001397 belongs to a different mechanism entirely (the spellbook row's UIElement_UIItem::SetSelectedState overlay child) and SpellbookRowStyle.cs is untouched. - UiClickablePanel.UseSelectionBars/SelectionBarHeight retired outright (UiPanel.cs): they existed only to emulate 0x06001397's dark-bars art; the correct retail rendering is the full-panel sprite stretch the base UiPanel.OnDraw already performs, so the override is dead code once the correct sprite is used. No consumer existed outside CharacterStatController. - Per-attribute/per-vital icon DIDs now resolve through the live DBObj::GetDIDByEnum chain (RetailDataIdResolver.Resolve, AP-235's unification seam) when a resolver is supplied — RetailUiRuntime. MountCharacter wires one under the shared DatLock — falling back to the hardcoded AttrRows/VitalRows column otherwise (tests, no dat). gmAttributeUI::PostInit @0x0049DB70 read verbatim: attributes resolve via category 0x10000002 (statId order 1,2,4,3,5,6, matching AttrRows' authored display order exactly); vitals via category 0x10000003. Live-DAT-verified: every hardcoded fallback value already matched the resolved DID byte-exact (new InstalledDat pin AttributeAndVitalIconDids_MatchTheRetailEnumMapperChain). - RetailAppraisalNameResolver.ResolveHeritage's independent re-implementation of the 2/5/13 heritage overrides deleted; it now delegates straight to CharacterIdentityText.HeritageGroupDisplayName (which already bakes in the same overrides) — one owner, byte-identical behavior. AP-235's register row updated to reflect the single-owner fix (the underlying hardcoded-vs-live-DAT mechanism divergence itself stays open — out of CT5's scope). Hand-built-vs-template ruling: rows stay HAND-BUILT rather than converting to UiTemplateListBox instantiation. The hand-built path hits every authored number byte-exact (proven by the CT1 InstalledDat pin AttributeRowTemplate_IconIsFlushLeftTwentyPixels_NameAndValueAreFixedColumns), while conversion would touch ~15 call sites (raise-button affordability, footer State A/B, per-row tooltip, section bucketing, live-refresh, selection-highlight) for a geometry-only slice — smaller-risk path per the task's own judgment-call guidance. Tests: CharacterStatControllerTests' sprite/UseSelectionBars assertions corrected to the authored geometry; new InstalledDat pin for the icon-DID chain. Full hermetic solution suite green (App/Core/Runtime/Headless/ Launcher/Content/etc., 0 failures) and the full InstalledDat lane green (203 App.Tests pins, TowerAscentReplayTests' known Status=KnownFailure case excluded per the acceptance filter). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
248 lines
11 KiB
C#
248 lines
11 KiB
C#
using System;
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Rectangular container with an optional translucent background and
|
|
/// border. Used as the base of every retail panel (attributes, chat,
|
|
/// inventory, login, etc.).
|
|
///
|
|
/// Retail has panel background art stored as 9-slice sprite assets in
|
|
/// the <c>0x06xxxxxx</c> RenderSurface range, and composed via
|
|
/// <c>LayoutDesc</c> (<c>0x21xxxxxx</c>) trees. Until our
|
|
/// <c>AcFont</c>/<c>UiSpriteBatch</c> consumes those directly, we draw a
|
|
/// simple translucent rectangle so panels are visible during development.
|
|
/// </summary>
|
|
public class UiPanel : UiElement
|
|
{
|
|
/// <summary>Background fill color. Set <see cref="Vector4.Zero"/> to skip.</summary>
|
|
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f);
|
|
|
|
/// <summary>Border color. Set <see cref="Vector4.Zero"/> to skip.</summary>
|
|
public Vector4 BorderColor { get; set; } = new(0.15f, 0.15f, 0.2f, 0.8f);
|
|
|
|
public float BorderThickness { get; set; } = 1f;
|
|
|
|
/// <summary>Optional dat RenderSurface id for the panel background sprite, drawn
|
|
/// in place of (or alongside) <see cref="BackgroundColor"/>. 0 = none.
|
|
/// When set, the sprite is stretched to fill the panel rect — the same
|
|
/// full-swap semantics retail's <c>InfoRegion::SetState</c> uses. Used by
|
|
/// the character-panel attribute/skill row's selected-row highlight
|
|
/// (sprite 0x06000F93, template 0x10000248's Highlight-state media).</summary>
|
|
public uint BackgroundSprite { get; set; }
|
|
|
|
/// <summary>Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
|
|
/// Required when <see cref="BackgroundSprite"/> is non-zero.</summary>
|
|
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
if (BackgroundSprite != 0 && SpriteResolve is { } sr)
|
|
{
|
|
var (tex, tw, th) = sr(BackgroundSprite);
|
|
if (tex != 0 && tw != 0 && th != 0)
|
|
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
|
}
|
|
else if (BackgroundColor.W > 0f)
|
|
{
|
|
// Panel fills are backgrounds. Draw them through the sprite/fill
|
|
// bucket so children that render as sprites/dat-font glyphs stay on
|
|
// top in painter order; DrawRect flushes after sprites and would
|
|
// cover text/icons.
|
|
ctx.DrawFill(0, 0, Width, Height, BackgroundColor);
|
|
}
|
|
|
|
if (BorderColor.W > 0f && BorderThickness > 0f)
|
|
ctx.DrawRectOutline(0, 0, Width, Height, BorderColor, BorderThickness);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static text label. Draws a single line of text using the context's
|
|
/// default font (or an override). Does not consume input.
|
|
///
|
|
/// Equivalent retail primitive: wide-string appended to a CString via
|
|
/// <c>FUN_0040b8f0</c> then drawn by the widget's draw method through
|
|
/// <c>FUN_00698330</c>.
|
|
/// </summary>
|
|
public class UiLabel : UiElement
|
|
{
|
|
public string Text { get; set; } = string.Empty;
|
|
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
|
|
|
/// <summary>
|
|
/// Optional live text reader, evaluated each draw and preferred over
|
|
/// <see cref="Text"/> when set. Markup <c>{Binding}</c> labels use this so
|
|
/// the displayed value tracks the binding object instead of freezing at
|
|
/// whatever it held when the panel was built — which is what a plugin
|
|
/// status line needs.
|
|
/// </summary>
|
|
public Func<string?>? TextSource { get; set; }
|
|
|
|
/// <summary>
|
|
/// Retail dat font. When set the label renders through the same glyph path
|
|
/// every authored panel uses, so plugin text matches the rest of the
|
|
/// interface instead of falling back to the development bitmap font.
|
|
/// </summary>
|
|
public UiDatFont? DatFont { get; set; }
|
|
|
|
/// <summary>Two-plane glyph outline, as retail draws interface text.</summary>
|
|
public bool Outline { get; set; } = true;
|
|
|
|
public UiLabel() { ClickThrough = true; }
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
string text = TextSource?.Invoke() ?? Text;
|
|
if (DatFont is { } dat)
|
|
ctx.DrawStringDat(dat, text, 0, 0, TextColor, Outline);
|
|
else
|
|
ctx.DrawString(text, 0, 0, TextColor);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple clickable button: panel background + centered label + click
|
|
/// callback. Retail equivalent is Keystone's button widget, driven by
|
|
/// a <c>StateDesc</c> per <c>UIStateId</c> (normal / hot / pressed /
|
|
/// disabled) from the panel layout.
|
|
/// Note: the dat-widget button (Type 1 / UIElement_Button) is <see cref="AcDream.App.UI.UiButton"/>
|
|
/// in <c>UiButton.cs</c> — that is the production widget used by D.2b panels.
|
|
/// This class is the earlier dev-scaffold button (plain rect + text; no dat sprites).
|
|
/// </summary>
|
|
public class UiSimpleButton : UiPanel
|
|
{
|
|
public string Text { get; set; } = string.Empty;
|
|
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
|
|
|
/// <summary>
|
|
/// Optional live caption reader, preferred over <see cref="Text"/> when
|
|
/// set, so a markup-bound button can change its own label (Buff / Stop)
|
|
/// without the binding object touching UI objects.
|
|
/// </summary>
|
|
public Func<string?>? TextSource { get; set; }
|
|
|
|
/// <summary>Retail dat font for the caption; see <see cref="UiLabel.DatFont"/>.</summary>
|
|
public UiDatFont? DatFont { get; set; }
|
|
|
|
/// <summary>Two-plane glyph outline, as retail draws interface text.</summary>
|
|
public bool Outline { get; set; } = true;
|
|
|
|
public event System.Action? Click;
|
|
|
|
/// <summary>
|
|
/// Without this the button is unclickable inside any draggable window.
|
|
/// UiRoot's press handling asks the pressed widget whether it owns the
|
|
/// pointer; a widget that does not claim the press falls through to
|
|
/// "move the ancestor window", which swallows the release and never emits
|
|
/// a Click. <see cref="UiButton"/> and <see cref="UiClickablePanel"/>
|
|
/// already declare it — this one did not, which is why a markup plugin
|
|
/// panel's button did nothing while its hit-test was perfectly fine.
|
|
/// </summary>
|
|
public override bool HandlesClick => true;
|
|
|
|
public UiSimpleButton()
|
|
{
|
|
BackgroundColor = new Vector4(0.1f, 0.1f, 0.15f, 0.8f);
|
|
BorderColor = new Vector4(0.45f, 0.45f, 0.55f, 1f);
|
|
}
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
if (e.Type == UiEventType.Click && Enabled)
|
|
{
|
|
Click?.Invoke();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
base.OnDraw(ctx);
|
|
string caption = TextSource?.Invoke() ?? Text;
|
|
if (caption.Length == 0) return;
|
|
|
|
if (DatFont is { } dat)
|
|
{
|
|
float datW = dat.MeasureWidth(caption);
|
|
ctx.DrawStringDat(
|
|
dat, caption,
|
|
(Width - datW) * 0.5f, (Height - dat.LineHeight) * 0.5f,
|
|
TextColor, Outline);
|
|
return;
|
|
}
|
|
|
|
if (ctx.DefaultFont is null) return;
|
|
float textW = ctx.DefaultFont.MeasureWidth(caption);
|
|
float tx = (Width - textW) * 0.5f;
|
|
float ty = (Height - ctx.DefaultFont.LineHeight) * 0.5f;
|
|
ctx.DrawString(caption, tx, ty, TextColor);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A <see cref="UiPanel"/> that fires an <see cref="OnClick"/> callback when the user
|
|
/// left-clicks it. Used for the attribute-list rows in the Character window — each row
|
|
/// is a transparent container that needs to respond to pointer hits while its children
|
|
/// (icon, name, value) are ClickThrough decorations.
|
|
///
|
|
/// <para>Retail analog: the <c>AttributeInfoRegion</c> row widget in <c>gmAttributeUI</c>
|
|
/// catches <c>UIEvent_LeftClick</c> (0x01) and calls <c>SetSelectedAttribute</c> on the
|
|
/// parent window. In acdream we wire the equivalent via this action callback instead of
|
|
/// the retail message bus.</para>
|
|
///
|
|
/// <para>Campaign CT slice CT5 (2026-08-25): the selected-row highlight draws through the
|
|
/// inherited <see cref="UiPanel.OnDraw"/> full-panel <see cref="UiPanel.BackgroundSprite"/>
|
|
/// stretch — no override here. This class previously had its own "selection bars" draw
|
|
/// mode (a thin top/bottom-bar rendering tuned to look like sprite 0x06001397's dark
|
|
/// bars); CT1's ground-truth research found that sprite belongs to a DIFFERENT retail
|
|
/// mechanism entirely (the spellbook row's overlay child), and the row's actual retail
|
|
/// Highlight state (<c>InfoRegion::SetState</c>, media 0x06000F93) is a plain full-row
|
|
/// background SWAP — exactly what the inherited <see cref="UiPanel.OnDraw"/> already
|
|
/// draws. The bars mode was therefore retired rather than reconfigured to a wrong sprite's
|
|
/// geometry; no consumer outside <c>CharacterStatController</c> ever set it.</para>
|
|
/// </summary>
|
|
public class UiClickablePanel : UiPanel
|
|
{
|
|
/// <summary>Called when the user releases the left mouse button over this panel.</summary>
|
|
public Action? OnClick { get; set; }
|
|
|
|
/// <summary>Settable tooltip, surfaced through the shared
|
|
/// <see cref="UiElement.GetTooltipText"/> hover pipeline (same pattern as
|
|
/// <see cref="UiButton.TooltipText"/> / <see cref="UiCatalogSlot"/>). TS-85's
|
|
/// character-panel gap: retail's <c>AttributeInfoRegion</c> /
|
|
/// <c>Attribute2ndInfoRegion</c> / <c>SkillInfoRegion</c> row constructors
|
|
/// (<c>UIElement::SetTooltip</c> at 0x004f1617 / 0x004f1777 / 0x004f222f) stamp
|
|
/// this once per row at construction — retail never updates it afterward, so a
|
|
/// plain settable string (not a live provider) matches.</summary>
|
|
public string? TooltipText { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public override string? GetTooltipText() =>
|
|
string.IsNullOrWhiteSpace(TooltipText) ? null : TooltipText;
|
|
|
|
public UiClickablePanel()
|
|
{
|
|
// Rows must receive pointer events — override the UiPanel default (ClickThrough=false,
|
|
// which is the UiElement base default). Explicit for clarity.
|
|
ClickThrough = false;
|
|
}
|
|
|
|
/// <summary>HandlesClick = true ensures this row receives its own Click even when
|
|
/// it is nested inside a Draggable ancestor window frame (e.g. the future character
|
|
/// window with a whole-window drag handle). Without this, the press would be consumed
|
|
/// by the ancestor's drag logic and the Click would never fire.</summary>
|
|
public override bool HandlesClick => true;
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
if (e.Type == UiEventType.Click && Enabled)
|
|
{
|
|
OnClick?.Invoke();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|