fix(ui): Campaign CT5 fix round — Normal-state row media, geometry test, padding, doc corrections

Opus review of CT5 (f532f28c) found 0 blockers, 4 SHOULD-FIX, 6 NOTEs; all
applied here.

SHOULD-FIX 1 (visible retail gap): unselected attribute/skill rows now draw
the row template's Normal-state media (0x06004CC2 — pinned by
CharacterPanelLiveDatTests.AttributeRowTemplate_...) instead of drawing
nothing. Independently decoded against the installed DAT: PFID_A8R8G8B8,
48x48, uniform (0,0,0,175) — a ~69%-opaque black tile the native-size
copy-or-tile blit repeats across the row. Wired at all three sites
(HandleRowClick, ApplySkillSelectionVisuals, AddRow). Selected rows keep
0x06000F93 (RowHighlightSprite) unchanged.

SHOULD-FIX 2: added Bind_AttributeRow_/Bind_VitalRow_/Bind_SkillRow_
geometry tests asserting the authored template pixels against BUILT rows
(not DAT pins) — width, height, icon/name/value column positions. The
skill-row case reproduces the real production scrollbar (X=281, per
CharacterPanelLiveDatTests.StatListBox_AuthorsFiveRowTemplatesInSharedLayout)
to prove the documented 281px clamp (scrollbar.Left - list.Left), one pixel
short of the attribute/vital rows' 282px ceiling.

SHOULD-FIX 3: AddRow's name-column Padding corrected from 1f to 0f — the
authored template carries no margin on 0x1000012A; Padding=1f re-created
the X=26 glyph-start bug this slice existed to fix.

SHOULD-FIX 4: reworded both UiPanel.BackgroundSprite doc comments — the
draw is a native-size copy-or-tile blit (UV-repeat), never a stretch.
Decoded 0x06000F93 as exactly 282x20 (matches the row natively, draws as a
plain copy) vs 0x06004CC2's 48x48 tile. Retail's UIRegion::SetImageByDID
(@0x0069F960) decompiles to a pure BlitMode selector switch — param_2==2 ->
Blit_3Alpha, ==3 -> Blit_4Alpha, else Blit_Normal — with no width/height
touched anywhere in the function, answering CT1's open "draw mode 3"
question: it's an alpha-blend selector, not a resize flag.

NOTEs:
  a. AddRow's nameEl now sets OneLine=true so the authored VJustify=Center
     takes the same single-line vertical-centering path the value column
     already uses.
  b. Tempered the "row width is 282" wording in the SkillContentWidth /
     RowContentWidth doc comments — that's a ceiling attribute/vital rows
     land on, not a fact true for skill rows (281, via SkillViewportWidth's
     scrollbar-gutter measurement).
  c. Reworded the section-header (RowPadX) comment — CT1 verified only the
     four header SPRITES; the caption label's own authored margins were
     never checked. Recorded as an open residual, not a cleared divergence.
  d. AttrRows/VitalRows are now internal (InternalsVisibleTo("AcDream.App.Tests")
     already covers AcDream.App.Tests); CharacterPanelLiveDatTests iterates
     them directly instead of a re-typed duplicate array, and now also
     asserts the vitals 2/4/6 current-enum aliasing claim the doc comment
     made but never enforced.
  e. Deleted the stale pre-CT5 0x06001397 narrative in
     CharacterPanelLiveDatTests; the pin's comment now describes the
     post-CT5 state (a regression guard, not an open bug).
  f. Unified ApplySkillSelectionVisuals' selected-branch SpriteResolve
     wrapper closure with HandleRowClick's direct assignment.

Build green; full hermetic solution suite green (Release,
Lane!=InstalledDat&...&Status!=KnownFailure filter); InstalledDat lane
green (ACDREAM_RUN_INSTALLED_DAT_TESTS=1, Lane=InstalledDat&Status!=KnownFailure).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-25 01:47:25 +02:00
parent 657b84c297
commit 0a37a28e76
4 changed files with 303 additions and 75 deletions

View file

@ -26,10 +26,35 @@ 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 — 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>
/// <para>CT5 fix round (SHOULD-FIX 4): drawn at NATIVE SIZE — retail's
/// copy-or-tile blit, never scaled (see <c>UiDatElement.OnDraw</c>'s own
/// doc comment, ~lines 273-360, for the full ground truth). The UV
/// rectangle below (<c>Width / tw, Height / th</c>) is deliberately
/// UV-REPEAT, not a fixed 0..1 a stretch would use — GL_REPEAT-wrapped
/// UI textures tile past their native pixel size rather than scaling.
/// Retail's generic sprite blit (<c>Graphic::Draw</c> 0x00693b20 /
/// <c>Graphic::PutImage</c> 0x00693a30) has exactly two behaviors, copy
/// or tile, and can never scale a source image up to fill a larger
/// destination.</para>
/// <para>Used by the character-panel attribute/skill row's Highlight-state
/// swap (sprite 0x06000F93, template 0x10000248's Highlight-state
/// media) and CT5's Normal-state row background (0x06004CC2). The two
/// sprites behave differently under this same tile formula: 0x06000F93
/// is authored at exactly the row's own 282x20 native size (decoded
/// against the installed DAT, 2026-08-25: PFID_R8G8B8, 282x20), so it
/// draws as a plain COPY with no visible seam; 0x06004CC2 is a 48x48
/// uniform-color tile (PFID_A8R8G8B8, single color (0,0,0,175)) that
/// visibly TILES across the wider row — both are the same code path,
/// just different source-vs-destination ratios.</para>
/// <para>Retail's own <c>UIRegion::SetImageByDID</c> (@0x0069F960)
/// confirms its third parameter is a <c>BlitMode</c> COLOR-BLEND
/// selector, not a resize flag — the decompiled body switches purely on
/// that value (<c>param_2 == 2</c> -&gt; <c>Blit_3Alpha</c>,
/// <c>== 3</c> -&gt; <c>Blit_4Alpha</c>, else <c>Blit_Normal</c>) and
/// never touches width/height at all. This answers CT1's open "icon
/// draw mode 3" question (ground-truth doc §2): mode 3 selects
/// <c>Blit_4Alpha</c>, an alpha-blend variant, not a resize.</para>
/// </summary>
public uint BackgroundSprite { get; set; }
/// <summary>Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
@ -195,14 +220,17 @@ public class UiSimpleButton : UiPanel
///
/// <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>
/// draw (native-size copy-or-tile — see that property's own doc comment for the CT5 fix
/// round's correction of the mechanism; NOT a 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 (0x06000F93 is authored at exactly the row's
/// own 282x20 native size, so the swap needs no scaling to look right). 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
{