Commit graph

6 commits

Author SHA1 Message Date
Erik
0a37a28e76 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>
2026-08-25 01:47:25 +02:00
Erik
f532f28c5b 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>
2026-08-25 01:15:32 +02:00
Erik
e7e32409c2 fix(ui): Campaign CT4 fix round — luminance text, verbatim title, PK PWD bits
Opus dual-lens review of ed652ed8 found 2 blockers + 5 should-fix. All applied.

BLOCKERS:
- Bind the luminance pair (0x100005C5/0x100005C6): caption "Luminance:"
  (UTF-16, PE-byte-decoded from the gmStatManagementUI vftable-adjacent
  data at @0x007c3dd4) and value "<available> / <maximum>" (narrow
  "%s / %s" @0x007c3dcc) — both literals independently re-derived from the
  raw acclient.exe bytes and confirmed byte-exact against the review's
  claim. Numbers format through a new shared FormatXp helper
  (.ToString("N0", InvariantCulture) — retail's ExperienceSystem::XPToString
  equivalent), also now used by Total XP / XP-to-next-level (previously an
  un-invariant bare "N0"). Hide path switched from Visible=false to
  retail's own UIElement_Text::ClearAllText mechanism
  (@0x004f0e31/@0x004f0e3c — empty LinesProvider, leave layout); each
  LinesProvider re-reads data() on every draw, so no separate refresh call
  is needed.
- CharacterIdentityText.StripLeadingArticle deleted: retail AppendText's
  the resolved title VERBATIM (@0x004f0990); 26 real ACE CharacterTitle
  entries begin with "The" and were being mangled. The dead
  CharacterSheet.Race fallback is deleted alongside it — retail's
  InqGenderHeritageDisplay creature-type argument is a hardcoded literal 0
  (@0x004f08db), no producer exists.

SHOULD-FIX:
- PK line re-sourced: classifies off the live ClientObject.PublicWeenieBitfield
  PWD bits (0x20 IsPK / 0x02000000 IsPKLite — ACCWeenieObject::IsPK/IsPKLite
  @0x0058c8b0/@0x0058c8a0) instead of a bitwise test against raw
  PropertyInt 134, which carries ACE's own PlayerKillerStatus enum bit
  layout, not the PWD layout. PropertyInt 134 already drives the correct
  bits via the existing PlayerKillerStatusBitfield.Apply; this is a
  re-source, not new wiring. Deleted the 0x4|0x8 combined-flag test case,
  which asserted a non-retail answer.
- Register AP-109 row: restores CT3's Titles-page narrowing paragraph
  (CT4's edit had compressed it to a bare pointer phrase), corrects the
  rank-prefix source to PropertyInt 0x1E (AllegianceRank) read live off
  the qualities bundle — not RuntimeAllegianceState, which is a different
  UI's (SocialAllegiancePageController) own documented substitute —
  corrects the title-table size from an estimated 22 functions/~200
  strings to the actual 17 functions/~170 strings (AllegianceSystem::GetTitle's
  dispatch switch read directly), and downgrades the evidence claim.
  Filed AP-235 for the gender/heritage hardcoded-table-vs-live-EnumMapper
  mechanism divergence, pointing at the ALREADY-EXISTING
  RetailDataIdResolver.Resolve helper as CT5's unification seam.
- CharacterPanelLiveDatTests.HeaderElements_AuthorExpectedFontsAndColors
  extended with the luminance pair's own occurrence-count + font/color
  pins, matching every other header id's pattern.

Also landed: an InstalledDat pin
(GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain) proving
CharacterIdentityText.GenderDisplayName/HeritageGroupDisplayName match the
live retail EnumMapper chain (master map category 1 ->
ClientEnumToID[0x10000001]/[0x10000002] -> EnumMapper DIDs
0x2200000A/0x2200000B) byte-exact, including the two entries the review
flagged as unverified guesses (10 "Penumbraen", 12 "Olthoi" — both
correct). CharacterSheetProvider.BuildSheet's level read switched from a
GetInt+ContainsKey double lookup to one TryGetValue. Plan ledger's
test-provenance sentence corrected (Bind_HeaderElements_... predates CT4,
extended to cover PkStatusId).

Tests: CharacterStatControllerTests (verbatim title incl. "The Noob",
luminance content/gate, luminance text binding, extended
Bind_HeaderElements_... covering PkStatusId), CharacterSheetProviderTests
(PK status driven through ClientObjectTable.UpdateIntProperty instead of
a raw property write), CharacterPanelLiveDatTests (luminance pin, gender/
heritage EnumMapper pin). Full hermetic solution suite green under Release
(0 failures, 15 projects); InstalledDat pins green (197/197, excluding one
confirmed pre-existing unrelated failure — TowerAscentReplayTests, verified
to fail identically with these changes stashed out).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 00:54:33 +02:00
Erik
ed652ed8ad feat(ui): Campaign CT slice CT4 — header identity block retail-exact
Retires the rest of AP-109's UI half: the character panel's Name/Heritage/
PkStatus/Level header identity block is now live and DAT-faithful on both
Attributes and Skills pages (verified: CharacterStatController.Bind already
scopes Label/LabelAuthoredColor to the ONE physically-visible page container,
so both tabs share the same bound widgets).

- Name/Heritage/PkStatus/Level switch from hand-picked Body/Gold runtime
  colors to the widget's own authored DefaultColor (LabelAuthoredColor) —
  CT1's live-DAT pin (HeaderElements_AuthorExpectedFontsAndColors) confirmed
  all four already carry the correct FontColor (white/white/white/pale-gold
  with Outline); the former "runtime color, dat carries none" comment was
  false.
- PkStatus resolves through StringTable 0x23000001 by key
  (ID_StatManagement_Header_PKStatus_PK/_PKL/_NPK) with a bitwise
  IsPK/IsPKLite test (gmStatManagementUI::UpdatePKStatus @0x004F00A0) instead
  of the prior exact-equality switch, which silently dropped combined-flag
  PlayerKillerStatus values. Live-DAT-verified strings: "Player Killer" /
  "Player Killer Lite" / "Non-Player Killer" (new InstalledDat pin
  PkStatusKeys_ResolveExpectedAuthoredStrings).
- Level shows "%d"-formatted InqInt(0x19) or the PE-recovered literal "???"
  when absent (CharacterSheet.Level is now int?).
- Heritage line appends CT2/CT3's resolved RuntimeCharacterTitleState
  display title through CharacterTitleResolver, refreshing live on both
  TableReplaced (0x0029) and DisplayTitleChanged (0x002B) —
  CharacterSheetProvider's ChangeBinding now subscribes to both.
- Name-line ruling: ships the PLAIN-NAME case only. Retail's allegiance
  rank-title prefix (AllegianceData::GetFullName @0x005B6950 ->
  AllegianceSystem::GetTitle @0x005B8DD0) needs a ~200-string, 22-function
  heritage x gender table (verbatim decomp literals, e.g.
  GetAluvianMaleTitle @0x005B7BC0's Yeoman/Baronet/.../High King) judged out
  of reasonable size for this slice. RuntimeAllegianceState already carries
  the local player's own rank; only the string table is missing. Registered,
  not silently omitted.
- Luminance pair (0x100005C5/0x100005C6): CharacterSheet.AvailableLuminance/
  MaximumLuminance (PropertyInt64 6/7) already flow generically through both
  the PlayerDescription snapshot and the live 0x02CF private-update parsers
  (no wiring gap). The retail show/hide gate (Level >= 200 &&
  MaximumLuminance != 0, UpdateExperience @0x004F0A70) is wired and toggles
  Visible on both elements every sheet refresh; the exact caption/value text
  could not be recovered this slice (retail's SetText source resolves
  through a Binary-Ninja-mislabeled data pointer, not a StringTable key — a
  DAT string-table sweep found no match), so content stays unbound rather
  than guessed.
- AP-109 narrowed accordingly (register row amended in the same commit).

Tests: CharacterStatControllerTests (heritage composition + live title
update, name stays plain, level int/"???" with authored — not constant —
color across 3 cases, PK line shows resolved text in authored color across
3 statuses, luminance visibility across 5 level/luminance combinations) and
CharacterSheetProviderTests (PK key-by-status resolution including a
combined-flag case, no-resolver leaves PkStatus null, Level null-vs-present,
title resolution + live refresh on both title events + unsubscribe-on-
dispose, luminance Int64 read-through). Full hermetic solution suite green
under Release (0 failures across all 14 test projects); InstalledDat pins
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 00:02:11 +02:00
Erik
e264d8392f docs(CT): CT1 fix round — sealed RowHighlightSprite verdict, verified resize mechanism, strengthened pins
Applies the Opus dual-lens review corrections to CT1's DAT ground-truth
research (docs/research/2026-08-24-campaign-ct-dat-ground-truth.md):

- Window constraints (BLOCKER): replaced the "likely a hardcoded
  ResizeTo/SetMinSize" guess with the verified mechanism —
  UIElement::ResizeTo clamps only via element attributes 0x3C-0x3F,
  nothing writes them at runtime, and retail resizes the SHARED
  gmPanelUI host (LayoutDesc 0x2100006E, slot 0x1000018E) rather than
  0x2100002E's own content root. Flags the unresolved 300x600-vs-300x362
  size tension for CT3/CT6 and marks the host elements NOT PROBED by
  CT1.
- RowHighlightSprite upgraded from a flagged hedge to a SEALED VERDICT:
  the stat row's selected-state media is 0x06000F93
  (gmAttributeUI::UpdateSelection -> InfoRegion::SetState on template
  0x10000248), not 0x06001397 (which is legitimately the spellbook
  row's separate selected-overlay mechanism). Falsifies the matching
  comment in CharacterStatController.cs and dated-corrects the older
  2026-06-26 doc at the spot that originated the wrong sprite id.
- Replaced the "18px gutter + 7px = 25px" derived story with the bare
  authored rectangles (the numbers don't compose cleanly: 300-281=19,
  and the 282px row overlaps the 281px scrollbar band by 1px) — CT5
  must implement the authored numbers directly, never a derived
  listWidth-18 formula.
- Plan doc: corrected the UpdateButtons ghost rule (no selection ->
  Ghosted, not "ghosts when selected == current") and added the
  AddTitleToList row-write contract for CT3.
- Pins: CharacterPanelLiveDatTests now honors ACDREAM_DAT_DIR first
  (matching InstalledDatFactAttribute and its sibling live-DAT test
  classes), hoists five vacuous bare-foreach assertions to counted
  .ToList() pins, and adds the stat ListBox + scrollbar rect pins that
  CT5/CT6 depend on.
- Doc hygiene: marked several probe-session observations (header
  geometry "identical" claim, 0x06004CC2 characterization, the
  master-map/category-map dump) as unpinned inference vs. committed
  fact, corrected the 0x1000052D "throwaway container" mislabel, and
  stated the header table's parent-relative coordinate frame.
- Recorded the CT5 gold this round found: InfoRegion::InfoRegion's
  icon-DID lookup (a third GetDIDByEnum consumer, category
  0x10000002) and gmSkillUI::RebuildSkillList's section-header order
  confirmation, plus the RowHeight=22-vs-authored-20 divergence for
  attribute rows.

Verified: ACDREAM_RUN_INSTALLED_DAT_TESTS=1 CharacterPanelLiveDatTests
filter 9/9 green; hermetic App suite filter (CI's Lane exclusion list)
6111/6111 green. No production code changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:58:27 +02:00
Erik
ca4100e76a docs #CT1: character-panel DAT ground truth + InstalledDat pins
Campaign CT slice CT1 (docs/plans/2026-08-24-character-panel-parity-campaign.md):
establishes the authored ground truth for LayoutDesc 0x2100002E that CT2-CT6
build against, so those slices port against verified DAT facts instead of
guessing. No production code changed.

Header findings: the PK line is authored pure white (the CT4 bug is in
CharacterStatController's runtime color choice, not a DAT gap); the level
color is a pale-gold (1, 0.949, 0.498) WITH an authored outline, diverging
from the current hardcoded Gold constant. The stat ListBox's row-template
list (LayoutDesc 0x21000045) is unreachable via the whole-layout
ImportInfos overload (the #375 same-layout template-list skip filter) --
the targeted ImportInfos(dats, layoutId, elementId) overload is required,
same as UiTemplateListBox's TemplateResolver already uses. The shared
attribute/skill row template (0x10000248) authors a 20x20 icon flush at
X=0 (current code: 16x16 at X=4), fixed 150px/100px name/value columns at
X=25/X=175 (current code: a width-fraction split), and a 7px gap between
the value's right edge and the row's own edge -- the scrollbar-gutter
margin the owner reported missing. The Titles page roster, row template
(LayoutDesc 0x2100005E), and window constraints are also pinned; the
character window's root authors NO min/max size properties at all (unlike
chat's self-contained window layout), and RetailUiRuntime.MountCharacter
never wires DatConstraintSource -- correcting the plan's "already in-tree"
claim for CT6.

Also derives and pins the full CharacterTitleTable::GetCharacterTitleFromID
chain (title id -> EnumMapper(0x22000041) canonical name -> compute_str_hash
-> StringTable(0x2300000E) localized text), resolved via the two-level
DBObj::GetDIDByEnum master-map indirection (0x25000000 -> category map ->
target DID) and verified end to end against ACE's CharacterTitle.WarMage=13
-> "War Mage". This independently cross-validates RetailKeyNames' existing
0x2300000A/0x2300000B/0x23000007 constants, which turn out to be the same
category-4 map's enum 4/5/3 entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:31:16 +02:00