feat(ui): Campaign CT slice CT5 — attribute/skill row geometry + selection media
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>
This commit is contained in:
parent
01e44a7018
commit
f532f28c5b
7 changed files with 266 additions and 125 deletions
|
|
@ -134,8 +134,14 @@ public static class CharacterStatController
|
|||
// the level element authors its own pale-gold FontColor (+ Outline);
|
||||
// LabelAuthoredColor reads it from the widget instead.
|
||||
|
||||
/// <summary>Row highlight color — semi-translucent gold, matches retail
|
||||
/// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent.</summary>
|
||||
/// <summary>Row highlight FALLBACK color — used only when
|
||||
/// <c>spriteResolve</c> is unavailable (tests, headless mode) and the
|
||||
/// real <see cref="RowHighlightSprite"/> art cannot be drawn. CT5 fix
|
||||
/// round: the former comment here claimed this tint "matches retail
|
||||
/// sprite 0x06001397 visual intent" — that was never accurate (0x06001397
|
||||
/// is the spellbook's unrelated overlay sprite, and this solid tint was
|
||||
/// never tuned to any specific sprite's actual pixels either way); it is
|
||||
/// a synthetic no-art placeholder, not a retail-faithful color.</summary>
|
||||
private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f);
|
||||
// LayoutDesc 0x2100002E, FooterTitle 0x1000024E property 0x1B:
|
||||
// [0]=white, [1]=green, [2]=red, [3]=light blue (#7FFFFF).
|
||||
|
|
@ -144,24 +150,73 @@ public static class CharacterStatController
|
|||
private static readonly Vector4 RetailVitaeBlue = new(127f / 255f, 1f, 1f, 1f);
|
||||
|
||||
// ── Row layout constants ─────────────────────────────────────────────────
|
||||
// RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height
|
||||
// and rows tighter. 16px icon fits inside 22px row with 3px vertical padding each side.
|
||||
// The larger row font (0x40000001, MaxCharHeight=18) is clipped to the 22px height which
|
||||
// gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
|
||||
private const float RowHeight = 22f;
|
||||
private const float IconSize = 16f;
|
||||
private const float RowPadX = 4f;
|
||||
private const float IconGap = 6f;
|
||||
// Campaign CT slice CT5 (2026-08-25): the shared attribute/skill data-row
|
||||
// template 0x10000248 (LayoutDesc 0x21000045 — InfoRegion::InfoRegion
|
||||
// @0x004F1450 template index 0, used for BOTH gmAttributeUI and
|
||||
// gmSkillUI rows) authors W=282 H=20 with icon 0x10000129 FLUSH LEFT
|
||||
// (X=0 Y=0 W=20 H=20, full row height), name 0x1000012A at FIXED X=25
|
||||
// W=150, and value 0x1000012B at FIXED X=175 W=100 right-justified —
|
||||
// its right edge (275) sits 7px short of the row's own 282px right
|
||||
// edge, the authored gutter the owner reported (item 2). These are
|
||||
// AUTHORED PIXEL VALUES, not width-relative fractions or derived
|
||||
// offsets — the former RowHeight=22/IconSize=16px-at-X=4/
|
||||
// nameW=width*0.60 geometry had no dat basis at all. Ground truth +
|
||||
// the explicit warning against composing a derived "gutter" formula:
|
||||
// docs/research/2026-08-24-campaign-ct-dat-ground-truth.md §2, pinned
|
||||
// by CharacterPanelLiveDatTests.AttributeRowTemplate_IconIsFlushLeftTwentyPixels_NameAndValueAreFixedColumns.
|
||||
private const float RowHeight = 20f;
|
||||
private const float RowIconX = 0f;
|
||||
private const float RowIconSize = 20f;
|
||||
private const float RowNameX = 25f;
|
||||
private const float RowNameW = 150f;
|
||||
private const float RowValueX = 175f;
|
||||
private const float RowValueW = 100f;
|
||||
|
||||
// Section-header caption inset ONLY (AddSkillHeader's 0x10000249..0x1000024C
|
||||
// captions carry their own authored L5/R5 margins property, distinct from
|
||||
// the data-row template above) — retained at its pre-CT5 value; CT1 found
|
||||
// no divergence in the header captions, so this constant is out of CT5's
|
||||
// scope.
|
||||
private const float RowPadX = 4f;
|
||||
|
||||
private const float SkillRowHeight = 20f;
|
||||
private const float SkillHeaderHeight = 20f;
|
||||
// Authored row-template width (0x10000248 W=282) — CT5: the row's own
|
||||
// width now comes from THIS authored constant directly rather than the
|
||||
// ListBox's raw 300px Width (see RowContentWidth below); do not derive
|
||||
// a "listWidth minus gutter" formula, per the ground-truth doc's
|
||||
// explicit warning.
|
||||
private const float SkillContentWidth = 282f;
|
||||
|
||||
private const uint SkillHeaderSpecializedSprite = 0x06000F90u;
|
||||
private const uint SkillHeaderTrainedSprite = 0x06000F86u;
|
||||
private const uint SkillHeaderUntrainedSprite = 0x06000F98u;
|
||||
private const uint SkillHeaderUnusableSprite = 0x06000F89u;
|
||||
private const uint RowHighlightSprite = 0x06001397u;
|
||||
|
||||
// CT5 SEALED VERDICT (ground-truth doc §2 "SEALED VERDICT: RowHighlightSprite
|
||||
// is wrong, not merely flagged"): gmAttributeUI::UpdateSelection
|
||||
// @0x0049DEE0 calls SetState(selected ? 6 : 1) on the row; InfoRegion::
|
||||
// SetState @0x004F0EE0 forwards that to the row instantiated from THIS
|
||||
// exact template (0x10000248), whose Highlight-state media is
|
||||
// 0x06000F93 — a full-row background SWAP, not an overlay. The former
|
||||
// 0x06001397 constant belongs to a DIFFERENT mechanism entirely: the
|
||||
// spellbook row's separate selected-overlay CHILD element
|
||||
// (UIElement_UIItem::SetSelectedState @0x004E1240, SpellbookRowStyle.cs)
|
||||
// — that file and its tests are correct and must NOT be touched.
|
||||
private const uint RowHighlightSprite = 0x06000F93u;
|
||||
|
||||
// CT5 (AP-235 unification, gmAttributeUI::PostInit @0x0049DB70 verbatim):
|
||||
// per-attribute icon DIDs resolve via DBObj::GetDIDByEnum(statEnum,
|
||||
// category 0x10000002); per-vital (Attribute2ndInfoRegion) icon DIDs via
|
||||
// DBObj::GetDIDByEnum(vitalEnum, category 0x10000003). Both categories
|
||||
// are consumed through the shared RetailDataIdResolver.Resolve seam
|
||||
// (the same master-map -> category-map -> value indirection already
|
||||
// ported for the title chain and RetailKeyNames) rather than a fourth
|
||||
// ad-hoc hardcoded DID table. Live-DAT-verified (2026-08-25): every
|
||||
// hardcoded fallback value in AttrRows/VitalRows below already matches
|
||||
// the resolved DID byte-exact — see
|
||||
// CharacterPanelLiveDatTests.AttributeAndVitalIconDids_MatchTheRetailEnumMapperChain.
|
||||
private const uint AttributeIconCategory = 0x10000002u;
|
||||
private const uint VitalIconCategory = 0x10000003u;
|
||||
|
||||
// Scrollbar chrome from base layout 0x2100003E, shared with chat/
|
||||
// inventory — sprite set + retail button seating live in
|
||||
|
|
@ -270,6 +325,17 @@ public static class CharacterStatController
|
|||
/// a (GL tex handle, pixel width, pixel height) triple. Pass <c>null</c> in tests where
|
||||
/// icon rendering is not asserted.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// CT5: <paramref name="iconDidResolve"/> ports <c>DBObj::GetDIDByEnum(enumValue,
|
||||
/// category)</c> (master map -> category map -> value) for the per-attribute/
|
||||
/// per-vital row icon DIDs — pass <c>(enumValue, category) =>
|
||||
/// RetailDataIdResolver.Resolve(dats, enumValue, category)</c> under the
|
||||
/// caller's dat lock. <c>null</c> (tests, or no live dat) falls back to
|
||||
/// <see cref="AttrRows"/>/<see cref="VitalRows"/>' hardcoded DID column,
|
||||
/// which the CT5 InstalledDat pin proves already matches the live-resolved
|
||||
/// value byte-exact.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// #431-CA5 gate fix (2026-08-24): the data-changed refresh. Retail's
|
||||
|
|
@ -288,7 +354,8 @@ public static class CharacterStatController
|
|||
UiDatFont? rowDatFont = null,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve = null,
|
||||
RaiseRequestHandler? onRaiseRequest = null,
|
||||
Action? onClose = null)
|
||||
Action? onClose = null,
|
||||
Func<uint, uint, uint>? iconDidResolve = null)
|
||||
{
|
||||
// rowDatFont: larger font for attribute row name/value text (18px vs 16px default).
|
||||
// Falls back to datFont when null (tests, or dat missing).
|
||||
|
|
@ -656,7 +723,7 @@ public static class CharacterStatController
|
|||
skillScrollbar.Visible = false;
|
||||
}
|
||||
currentAttributeRows = BuildAttributeRows(statList, rowDatFont, spriteResolve, data, attrSel,
|
||||
allRaise1, allRaise10, SetFooterSelected);
|
||||
allRaise1, allRaise10, SetFooterSelected, iconDidResolve);
|
||||
activeListEntries.AddRange(currentAttributeRows);
|
||||
}
|
||||
else
|
||||
|
|
@ -668,7 +735,7 @@ public static class CharacterStatController
|
|||
Top = 0f,
|
||||
Width = contentW,
|
||||
Height = statList.Height,
|
||||
LineHeight = (int)SkillRowHeight,
|
||||
LineHeight = (int)RowHeight,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
};
|
||||
statList.AddChild(viewport);
|
||||
|
|
@ -823,6 +890,34 @@ public static class CharacterStatController
|
|||
: SkillContentWidth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CT5: the row's own rendered width is the AUTHORED row-template width
|
||||
/// (282px, <see cref="SkillContentWidth"/>), never the ListBox's raw
|
||||
/// container width (300px, <see cref="CharacterStatController.ListBoxId"/>'s
|
||||
/// own dat rect) — the two numbers do not compose into a "gutter" formula
|
||||
/// (ground-truth doc §2's explicit warning); a shorter container clamps
|
||||
/// the row down, but a wider one never stretches it past 282.
|
||||
/// </summary>
|
||||
private static float RowContentWidth(UiElement list)
|
||||
=> list.Width > 0f ? MathF.Min(list.Width, SkillContentWidth) : SkillContentWidth;
|
||||
|
||||
/// <summary>
|
||||
/// CT5 (AP-235): resolve a row icon DID through the live
|
||||
/// <c>DBObj::GetDIDByEnum</c> chain when a resolver is available,
|
||||
/// falling back to the hardcoded <see cref="AttrRows"/>/<see cref="VitalRows"/>
|
||||
/// column value otherwise (tests, or a resolve miss).
|
||||
/// </summary>
|
||||
private static uint ResolveIconDid(
|
||||
Func<uint, uint, uint>? resolver,
|
||||
uint enumValue,
|
||||
uint category,
|
||||
uint fallback)
|
||||
{
|
||||
if (resolver is null) return fallback;
|
||||
uint resolved = resolver(enumValue, category);
|
||||
return resolved != 0u ? resolved : fallback;
|
||||
}
|
||||
|
||||
// ── 9-row attribute list ─────────────────────────────────────────────────
|
||||
|
||||
private static List<UiClickablePanel> BuildAttributeRows(
|
||||
|
|
@ -833,9 +928,10 @@ public static class CharacterStatController
|
|||
int[] sel,
|
||||
List<UiButton> allRaise1,
|
||||
List<UiButton> allRaise10,
|
||||
Action<bool> setFooterSelected)
|
||||
Action<bool> setFooterSelected,
|
||||
Func<uint, uint, uint>? iconDidResolve)
|
||||
{
|
||||
float listW = list.Width;
|
||||
float listW = RowContentWidth(list);
|
||||
float y = 0f;
|
||||
var rows = new List<UiClickablePanel>();
|
||||
|
||||
|
|
@ -846,7 +942,7 @@ public static class CharacterStatController
|
|||
|
||||
var row = AddRow(list, datFont, spriteResolve,
|
||||
left: 0f, top: y, width: listW, height: RowHeight,
|
||||
iconDid: iconDid,
|
||||
iconDid: ResolveIconDid(iconDidResolve, statId, AttributeIconCategory, iconDid),
|
||||
nameText: rowName,
|
||||
valueProvider: () =>
|
||||
{
|
||||
|
|
@ -883,7 +979,7 @@ public static class CharacterStatController
|
|||
|
||||
var row = AddRow(list, datFont, spriteResolve,
|
||||
left: 0f, top: y, width: listW, height: RowHeight,
|
||||
iconDid: iconDid,
|
||||
iconDid: ResolveIconDid(iconDidResolve, maxStatId, VitalIconCategory, iconDid),
|
||||
nameText: rowName,
|
||||
valueProvider: () =>
|
||||
{
|
||||
|
|
@ -922,7 +1018,7 @@ public static class CharacterStatController
|
|||
Action<bool> setFooterSelected,
|
||||
out List<SkillRowBinding> skillRows)
|
||||
{
|
||||
float listW = list.Width > 0f ? MathF.Min(list.Width, SkillContentWidth) : SkillContentWidth;
|
||||
float listW = RowContentWidth(list);
|
||||
float y = 0f;
|
||||
var entries = new List<UiElement>();
|
||||
var bindings = new List<SkillRowBinding>();
|
||||
|
|
@ -951,7 +1047,7 @@ public static class CharacterStatController
|
|||
CharacterSkill LiveSkill() =>
|
||||
FindSkill(data(), skill.Id) ?? skill;
|
||||
var row = AddRow(list, datFont, spriteResolve,
|
||||
left: 0f, top: y, width: listW, height: SkillRowHeight,
|
||||
left: 0f, top: y, width: listW, height: RowHeight,
|
||||
iconDid: skill.IconDid,
|
||||
nameText: skill.Name,
|
||||
valueProvider: () => LiveSkill().CurrentLevel.ToString(),
|
||||
|
|
@ -967,7 +1063,7 @@ public static class CharacterStatController
|
|||
};
|
||||
bindings.Add(new SkillRowBinding(row, skill));
|
||||
entries.Add(row);
|
||||
y += SkillRowHeight;
|
||||
y += RowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1131,10 +1227,10 @@ public static class CharacterStatController
|
|||
Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
|
||||
|
||||
// Update highlight on all rows.
|
||||
// Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
|
||||
// for the selected row background. When spriteResolve is available, apply the
|
||||
// sprite; otherwise fall back to the translucent gold tint.
|
||||
const uint HighlightSprite = 0x06001397u;
|
||||
// CT5: retail's InfoRegion::SetState swaps the row's whole background
|
||||
// to the template's Highlight-state media (RowHighlightSprite,
|
||||
// 0x06000F93 — see its own doc comment) when spriteResolve is
|
||||
// available; otherwise fall back to the translucent gold tint.
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
var row = rows[i];
|
||||
|
|
@ -1143,7 +1239,7 @@ public static class CharacterStatController
|
|||
if (spriteResolve is not null)
|
||||
{
|
||||
row.BackgroundColor = Vector4.Zero;
|
||||
row.BackgroundSprite = HighlightSprite;
|
||||
row.BackgroundSprite = RowHighlightSprite;
|
||||
row.SpriteResolve = spriteResolve;
|
||||
}
|
||||
else
|
||||
|
|
@ -1201,14 +1297,12 @@ public static class CharacterStatController
|
|||
row.BackgroundColor = Vector4.Zero;
|
||||
row.BackgroundSprite = RowHighlightSprite;
|
||||
row.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); };
|
||||
row.UseSelectionBars = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
row.BackgroundColor = HighlightBg;
|
||||
row.BackgroundSprite = 0u;
|
||||
row.SpriteResolve = null;
|
||||
row.UseSelectionBars = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -1216,7 +1310,6 @@ public static class CharacterStatController
|
|||
row.BackgroundColor = Vector4.Zero;
|
||||
row.BackgroundSprite = 0u;
|
||||
row.SpriteResolve = null;
|
||||
row.UseSelectionBars = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1615,9 +1708,24 @@ public static class CharacterStatController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a single attribute/vital row to <paramref name="list"/> as a
|
||||
/// <see cref="UiClickablePanel"/> containing icon + name + value children.
|
||||
/// Returns the panel so the caller can wire <see cref="UiClickablePanel.OnClick"/>.
|
||||
/// Add a single attribute/vital/skill row to <paramref name="list"/> as a
|
||||
/// <see cref="UiClickablePanel"/> containing icon + name + value children,
|
||||
/// laid out at the AUTHORED template 0x10000248 pixel geometry (icon
|
||||
/// flush left 20x20, name at X=25 W=150, value at X=175 W=100
|
||||
/// right-justified — see the row-layout-constants block above). Returns
|
||||
/// the panel so the caller can wire <see cref="UiClickablePanel.OnClick"/>.
|
||||
///
|
||||
/// <para>CT5 hand-built-vs-template ruling: this row stays HAND-BUILT
|
||||
/// (not converted to <c>UiTemplateListBox</c> row instantiation).
|
||||
/// Converting would touch every one of the ~15 call sites this method's
|
||||
/// row feeds — raise-button affordability, footer State A/B, per-row
|
||||
/// tooltip, section-header bucketing, live-refresh-on-quality-change,
|
||||
/// and the selection-highlight swap — for a purely cosmetic slice whose
|
||||
/// authored numbers this hand-built path can already hit byte-exact
|
||||
/// (proven by <c>CharacterPanelLiveDatTests.AttributeRowTemplate_...</c>).
|
||||
/// That is a large-blast-radius rewrite for a geometry-only fix; the
|
||||
/// smaller-risk path is implementing the authored constants directly
|
||||
/// here, which this method now does.</para>
|
||||
/// </summary>
|
||||
private static UiClickablePanel AddRow(
|
||||
UiElement list,
|
||||
|
|
@ -1628,9 +1736,7 @@ public static class CharacterStatController
|
|||
string nameText,
|
||||
Func<string> valueProvider,
|
||||
Func<Vector4>? valueColorProvider = null,
|
||||
Vector4? nameColor = null,
|
||||
uint backgroundSprite = 0u,
|
||||
bool useSelectionBars = true)
|
||||
Vector4? nameColor = null)
|
||||
{
|
||||
var row = new UiClickablePanel
|
||||
{
|
||||
|
|
@ -1649,23 +1755,20 @@ public static class CharacterStatController
|
|||
Width = width,
|
||||
Height = height,
|
||||
BackgroundColor = Vector4.Zero, // transparent until selected
|
||||
BackgroundSprite = spriteResolve is not null ? backgroundSprite : 0u,
|
||||
SpriteResolve = spriteResolve is not null && backgroundSprite != 0u
|
||||
? id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); }
|
||||
: null,
|
||||
BackgroundSprite = 0u,
|
||||
SpriteResolve = null,
|
||||
BorderColor = Vector4.Zero,
|
||||
UseSelectionBars = useSelectionBars,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top,
|
||||
};
|
||||
|
||||
float iconY = (height - IconSize) * 0.5f;
|
||||
|
||||
// Icon 0x10000129: flush left (X=0), Y=0, 20x20 — full row height,
|
||||
// no vertical centering math needed since RowIconSize == RowHeight.
|
||||
var iconEl = new UiText
|
||||
{
|
||||
Left = RowPadX,
|
||||
Top = iconY,
|
||||
Width = IconSize,
|
||||
Height = IconSize,
|
||||
Left = RowIconX,
|
||||
Top = 0f,
|
||||
Width = RowIconSize,
|
||||
Height = RowIconSize,
|
||||
ClickThrough = true,
|
||||
DatFont = null,
|
||||
BackgroundSprite = spriteResolve is not null ? iconDid : 0u,
|
||||
|
|
@ -1676,17 +1779,15 @@ public static class CharacterStatController
|
|||
Anchors = AnchorEdges.Left | AnchorEdges.Top,
|
||||
};
|
||||
|
||||
float nameX = RowPadX + IconSize + IconGap;
|
||||
float nameW = width * 0.60f;
|
||||
float nameY = 0f;
|
||||
|
||||
// Name 0x1000012A: X=25 W=150 — fixed authored pixels, not a
|
||||
// width-relative fraction.
|
||||
string capturedName = nameText;
|
||||
Vector4 capturedNameColor = nameColor ?? Body;
|
||||
var nameEl = new UiText
|
||||
{
|
||||
Left = nameX,
|
||||
Top = nameY,
|
||||
Width = nameW,
|
||||
Left = RowNameX,
|
||||
Top = 0f,
|
||||
Width = RowNameW,
|
||||
Height = height,
|
||||
DatFont = datFont,
|
||||
ClickThrough = true,
|
||||
|
|
@ -1697,14 +1798,14 @@ public static class CharacterStatController
|
|||
};
|
||||
nameEl.LinesProvider = () => new[] { new UiText.Line(capturedName, capturedNameColor) };
|
||||
|
||||
float valueW = width - nameX - nameW - RowPadX;
|
||||
float valueX = nameX + nameW;
|
||||
|
||||
// Value 0x1000012B: X=175 W=100, right-justified — its right edge
|
||||
// (275) sits 7px short of the row's own 282px right edge, the
|
||||
// authored gutter the owner reported.
|
||||
var valueEl = new UiText
|
||||
{
|
||||
Left = valueX,
|
||||
Top = nameY,
|
||||
Width = valueW > 0f ? valueW : 40f,
|
||||
Left = RowValueX,
|
||||
Top = 0f,
|
||||
Width = RowValueW,
|
||||
Height = height,
|
||||
DatFont = datFont,
|
||||
ClickThrough = true,
|
||||
|
|
|
|||
|
|
@ -71,15 +71,18 @@ public sealed class RetailAppraisalNameResolver
|
|||
public string ResolveCreature(int creatureType)
|
||||
=> _creatures.Resolve(creatureType);
|
||||
|
||||
// Campaign CT slice CT5 (2026-08-25): the three hardcoded overrides this
|
||||
// method used to re-implement inline (2/5/13 -> "Gharu'ndim"/"Umbraen"/
|
||||
// "Olthoi") are dead duplication — CharacterIdentityText.HeritageGroupDisplayName
|
||||
// already bakes the identical AppraisalSystem::InqHeritageGroupDisplayName
|
||||
// overrides into its own switch (including id 12, which this method used
|
||||
// to leave to the fallback branch and get the same "Olthoi" answer
|
||||
// anyway). One owner for the override table now; behavior is unchanged
|
||||
// (byte-identical for every heritage id — verified by the existing
|
||||
// GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain pin,
|
||||
// which exercises the shared table directly).
|
||||
public string ResolveHeritage(int heritageGroup)
|
||||
=> heritageGroup switch
|
||||
{
|
||||
2 => "Gharu'ndim",
|
||||
5 => "Umbraen",
|
||||
13 => "Olthoi",
|
||||
_ => CharacterIdentityText.HeritageGroupDisplayName(heritageGroup)
|
||||
?? string.Empty,
|
||||
};
|
||||
=> CharacterIdentityText.HeritageGroupDisplayName(heritageGroup) ?? string.Empty;
|
||||
|
||||
public string ResolveMaterial(int materialType)
|
||||
=> materialType > 0
|
||||
|
|
|
|||
|
|
@ -4038,6 +4038,17 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
}
|
||||
CharacterSheetProvider provider = _bindings.Character.Provider;
|
||||
CharacterSheet currentSheet = provider.BuildSheet();
|
||||
// CT5 (AP-235): resolve per-attribute/per-vital row icon DIDs through
|
||||
// the live DBObj::GetDIDByEnum chain (RetailDataIdResolver.Resolve)
|
||||
// instead of leaving CharacterStatController's hardcoded fallback as
|
||||
// the only source. DatCollection is documented not thread-safe, so
|
||||
// this closure takes the same DatLock every other dat-touching
|
||||
// delegate below (TitleTemplateResolver/TitleResolver) already uses.
|
||||
uint IconDidResolve(uint enumValue, uint category)
|
||||
{
|
||||
lock (_bindings.Assets.DatLock)
|
||||
return RetailDataIdResolver.Resolve(_bindings.Assets.Dats, enumValue, category);
|
||||
}
|
||||
Action refreshRows = CharacterStatController.Bind(
|
||||
layout,
|
||||
() => currentSheet,
|
||||
|
|
@ -4045,7 +4056,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Assets.ResolveFont(0x40000001u) ?? _bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
(request, completed) => HandleCharacterRaise(provider, request, completed),
|
||||
() => CloseWindow(WindowNames.Character));
|
||||
() => CloseWindow(WindowNames.Character),
|
||||
IconDidResolve);
|
||||
// #431-CA5 gate fix: rebuild the ROWS on every authoritative sheet
|
||||
// change, not only on clicks — a train's skill record must move the
|
||||
// row to the trained section (and un-ghost the raise controls) the
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ public class UiPanel : UiElement
|
|||
|
||||
/// <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.
|
||||
/// Used by the attribute-list selected-row highlight (sprite 0x06001397 = Button state 6).</summary>
|
||||
/// 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).
|
||||
|
|
@ -191,29 +193,22 @@ public class UiSimpleButton : UiPanel
|
|||
/// parent window. In acdream we wire the equivalent via this action callback instead of
|
||||
/// the retail message bus.</para>
|
||||
///
|
||||
/// <para>When <see cref="UseSelectionBars"/> is true and <see cref="UiPanel.BackgroundSprite"/>
|
||||
/// is non-zero, draws the sprite as a thin full-width bar at the TOP and BOTTOM edges of
|
||||
/// the row (not stretched to fill). This matches retail's selection highlight which shows
|
||||
/// a horizontal dark bar on both the top and bottom edge of the selected attribute row,
|
||||
/// with NO left/right end-caps. Bar height is <see cref="SelectionBarHeight"/> pixels
|
||||
/// (default 3px).</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>When true and <see cref="UiPanel.BackgroundSprite"/> is non-zero, draws
|
||||
/// the sprite as a thin horizontal bar at the top AND bottom edges of the panel,
|
||||
/// NOT as a full-height stretched fill. Matches retail's selected-row highlight
|
||||
/// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill).
|
||||
/// Default false (preserves legacy full-stretch behavior).</summary>
|
||||
public bool UseSelectionBars { get; set; }
|
||||
|
||||
/// <summary>Height in pixels of each selection bar (top and bottom). Default 3px.
|
||||
/// Ignored when <see cref="UseSelectionBars"/> is false.</summary>
|
||||
public float SelectionBarHeight { get; set; } = 3f;
|
||||
|
||||
/// <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
|
||||
|
|
@ -250,33 +245,4 @@ public class UiClickablePanel : UiPanel
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr)
|
||||
{
|
||||
// Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row.
|
||||
// The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at
|
||||
// native height (SelectionBarHeight), stretched to full panel width (UV tile
|
||||
// horizontally). No left/right end-caps: u0=0, u1=Width/nativeW (UV repeat).
|
||||
var (tex, tw, th) = sr(BackgroundSprite);
|
||||
if (tex != 0 && tw > 0 && th > 0)
|
||||
{
|
||||
float barH = SelectionBarHeight;
|
||||
float uTile = tw > 0 ? Width / tw : 1f;
|
||||
// Top bar: shows the top barH px of the sprite (v = 0 → barH/th).
|
||||
float vBot = th > 0 ? barH / th : 1f;
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, barH, 0f, 0f, uTile, vBot, Vector4.One);
|
||||
// Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1).
|
||||
float vTop2 = th > 0 ? 1f - barH / th : 0f;
|
||||
ctx.DrawSprite(tex, 0f, Height - barH, Width, barH, 0f, vTop2, uTile, 1f, Vector4.One);
|
||||
}
|
||||
// Selection-bar mode draws no border (rows have BorderColor=Zero by design).
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default UiPanel draw: handles BackgroundSprite, BackgroundColor, AND border.
|
||||
base.OnDraw(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue