Commit graph

3819 commits

Author SHA1 Message Date
Erik
942a02af11 fix(ui): morning gate — map town markers: green rollover highlight + the authored map-note tooltip skin/font
User finding 3 (retail screenshot): hovering a town on the Map tab turns
its marker GREEN and shows the name on a special-font tooltip — clearly
not our generic 0x10000395 popup skin, and we had no hover highlight at
all.

Re-derivation (live-DAT probe + raw ElementDesc dump + surface
byte-decode; MapNoteLiveDatTests pins all of it):

- m_pMap (0x100001EC)'s P0x47/P0x48 = 0x100001F0 @ 0x21000026 are the
  note CONSTRUCTION template (AddMapNote @0x004a1bb0's
  CreateChildElement args) — that part we had right.
- The TEMPLATE's own DirectState authors the note's tooltip popup
  locator P0x47=0x10000398/P0x48=0x21000041 — the FOURTH popup skin,
  whose incorporated text child 0x10000396 fonts 0x40000015 where the
  other three skins font 0x40000002 (the user's "special font") — plus
  P0x50=0.0 (zero per-element tooltip delay: town tooltips fire the
  instant the dwell arms; UiRoot already honors it), P0x4B TooltipOn,
  and P0x13 RolloverEnabled. Batch C's "the template authors no locator
  of its own" claim was WRONG, and BuildTownMarkers' hardcoded
  shared-skin override was clobbering the authored values — removed.
- The hover highlight: the template's Normal/Normal_rollover states are
  PassToChildren descriptors driving the swallowed highlight child
  0x100001F1 (base 0x100002B7@0x21000042 — a four-piece frame all
  drawing 0x06004CC9, byte-decoded PURE GREEN A=FF R=00 G=FF B=00) via
  per-state P0x3B (Invisible): hidden at rest, green on rollover.

Port:
- UiButton.CascadeStateToChildren — retail UIElement::SetState
  @0x00464E70's PassToChildren cascade, keyed off the REQUESTED state id
  (properties commit unconditionally; only the sprite draw is art-gated,
  the existing #382/AP-222 distinction).
- UiDatElement.TrySetRetailState honors per-state P0x3B for NAMED states
  (OnSetAttribute @0x00462d80 case 8: SetVisible(value==0)). The
  unnamed-DirectState case is explicitly excluded — honoring it would
  un-gate ISSUES #408 (1,083 authored-invisible elements) through
  BuildWidget's post-children state reapply; measured breaking the
  spell-favorite drag tests before the scoping (note added to #408).
- MapPageController.BuildTownMarkers rebuilds the button-swallowed
  highlight child per marker through the AD-108 IconBuilder seam
  (Bindings.TemplateInfoResolver, backed by
  RowTemplateResolver.ResolveInfo — same cache) and arms it with the
  initial Normal cascade.

Register TS-85's Batch C paragraph corrected; RetailTooltipPresenter's
F10 shared-skin remark updated (MapPageController no longer a consumer).
Tests: 3 installed-DAT pins (locator/delay/rollover; per-state P0x3B +
green frame; the four-skin font sweep), UiButton cascade + UiDatElement
P0x3B units, MapHousePanel marker no-clobber + hover-highlight fixture.
App suite 5487 passed / 3 skips (5490 total, +11 over baseline);
Runtime 1744/1744.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 09:22:46 +02:00
Erik
ef567bfa20 fix(ui): morning gate — House tab renders retail's TWO houseless lines, not one
User finding 2 (retail screenshot, houseless character): the House tab
shows "You do not currently own a house." ABOVE "You may buy another
house immediately." — ours showed only the second line, and the prior
session had REFUTED the first line outright ("no such string exists
anywhere in the 2013 dump").

Re-derivation: the string exists in the binary at data_7ab688 — it is
gmHouseUI::DisplayBuyPayment @0x004a2b30's HOUSELESS branch. Two
compounding misreads hid it: (a) DisplayBuyPayment was mislabeled
houseless-silent, but its m_pHouseData gate only selects WHICH text
(jne 0x4a2b63) — the ListBox emit (@0x004a2b80 onward,
AddItemFromTemplateList + SetTextWithFont) runs in BOTH branches; and
(b) BN's pseudo-C renders both push-literal operands as spurious
&vftable.RecvNotice_* symbol matches (the TS-85/F3 artifact class), so
text sweeps of the dump find nothing — capstone byte-decode of the
PDB-paired binary resolves houseless @0x004a2b57 push 0x7ab688 =
"You do not currently own a house." and owned @0x004a2b63 push
0x7ab65c = "The purchase price for this dwelling is:\n" (+
HousePaymentList::ComposeText, still #413 item-3 scope). The morning
brief's alternate DAT-string-table hypothesis was checked and is NOT
the mechanism — plain exe string-pool literal.

RuntimeHouseState.Recompute now renders the houseless case as retail's
exact two lines in gmHouseUI::Update's fixed builder order
(DisplayBuyPayment first, DisplayPurchaseTimeText last); the owned case
is unchanged (its DisplayBuyPayment content needs ComposeText, #413
item 3). Class doc + ISSUES #413 corrected honestly — the user's retail
evidence supersedes the earlier refutation. Runtime house tests updated
to pin both lines; 9/9 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 08:54:55 +02:00
Erik
9d9280a069 fix(ui): morning gate — world tooltips ride retail's mouse-idle dwell, not the found edge
User finding 1 (side-by-side vs retail): our world-object tooltips popped
the instant the found object changed; retail's "lag". The night round's
derivation from RecvNotice_SmartBoxObjectFound @0x004E5AD0 misread the
notice as edge-MOUNTING: its immediate StartTooltipAtMouse @0x004E5DFB is
inside `if (s_pInstance->m_dragElement != 0)` (@0x004E5D8E) — and
m_dragElement is a real, distinct PDB field in acclient.h's
UIElementManager (separate from the m_pTooltipElement family), so the
immediate mount is DRAG-AND-DROP ONLY. The ordinary hover path merely
STAGES the name (SetTooltip @0x004E5D74 + the |=0x20 TooltipOn bit) and
the display rides the SAME UIElementManager::CheckTooltip @0x0045B6E0
mouse-idle dwell as UI tooltips: 250 ms (m_tooltipDelay @0x0045f75d)
since m_lastMouseMoveTime (stamped on EVERY move, MouseMoveHandler
@0x0045e736). Found swaps under an IDLE mouse replace the popup the same
frame (SetTooltip's own text-change teardown @0x004617FF -> ResetTooltip
@0x0045C360 tail-calling CheckTooltip); the 10 s duration expiry
(@0x0045b78a) requires a fresh mouse move before re-arming
(SwitchMouseOver(null) @0x0045b7b2 clears m_pElementLastEntered).

Port: UiRoot gains the unconditional last-mouse-move stamp
(m_lastMouseMoveTime 1:1 — the existing _hoverStartedMs stamps are
deliberately conditional) exposed as MouseIdleMs/NowMs;
RetailTooltipPresenter.UpdateWorldHoverTooltip now stages text at the
notice edge (ShowTooltips gate + name resolve read there, @0x004E5D21/
@0x004E5D3B, empty-name SetTooltip skip @0x004E5D48 included) and mounts
via the CheckTooltip dwell block (no-capture gate @0x0045b715,
m_tooltipEnable via MouseHover @0x0046254C — which the drag-immediate
branch faithfully bypasses). Session reset also forgets the staged text.

Tests: the world-hover fixture section rewritten to the corrected model —
found edge stages but never mounts before the dwell; a continuously
moving mouse never mounts until it rests; idle found-swap replaces
same-frame without stacking; duration auto-hide needs a move + fresh
dwell to remount; drag-in-progress mounts immediately. 38/38 pass.

Register TS-85 and ISSUES item 2 corrected honestly: the "edge-fired
(no dwell)" conclusion is superseded by the user's retail evidence and
the m_dragElement branch read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 08:49:11 +02:00
Erik
e316e190cb fix(ui): Map tab player/house icon resolution — build the button-swallowed icons from the panel-slot resolve tree, detached from the per-frame layout pass
Two mechanisms, both live-verified (register row AD-108 updated to match):

1. RESOLUTION. The player/house icons (0x100001ED/0x100001EE) are authored
   as nested dat children of m_pMap (0x100001EC), itself a Type-1 button
   whose UiButton.ConsumesDatChildren swallows them at build. The old
   ResolveSwallowedIcon re-imported them standalone via
   ImportInfos(hostLayout, iconId) — which returns null on the live DAT:
   FindDesc walks the raw top-level Elements table (one entry for
   0x2100006E) and never reaches them. Their ElementInfos only materialize
   inside the full panel-slot resolve (ImportInfos(0x2100006E, 0x1000018C))
   that MountMapHousePanel already imports — the pageInfo Bind already
   receives. The fix finds each icon's info under m_pMap's own resolved
   info subtree and BUILDS it through the new Bindings.IconBuilder seam
   (production: LayoutImporter.Build under the DAT lock — the build half
   of RowTemplateResolver's shape). An icon the normal walk DID build is
   preferred (FindDescendant first), so a future ConsumesDatChildren
   policy change cannot double-build.

2. POSITION. Found by this fix's own F1 live verification: the resolved
   ring rendered pinned to m_pMap's top-left. PlaceMarker owns marker
   position outright (retail's gmMapUI::Update re-places every tick;
   retail's UpdateForParentSizeChange runs only on real parent resize),
   but acdream re-runs ApplyAnchor per frame and the icon's compatibility
   anchor had captured the authored (0,0) rect while the panel was still
   hidden, re-asserting it over PlaceMarker's writes every frame.
   PrepareIcon now sets Anchors=None (clearing any imported LayoutPolicy),
   the established runtime-positioned-element convention.

Live numeric gate (session character +Acdream, cell 0xF07E003F):
independent computation (gid_to_lcoord -> display (90.8E, 0.5S) ->
byte-decoded PlaceMarkerOnMap formula, 17x16 icon, marker area
(6,8)-(247,258)) predicts local pixel (226,125); the connected client's
UI-tree dump shows the icon at screen (1166,195) under m_pMap (940,70) =
local (226,125) — exact match in both panel-open dumps. Coordinate text
"0.5S,90.8E", Holtburg town-marker tooltip (real-mouse hover), and the
House tab's "You may buy another house immediately." sentence all
confirmed on screen; ACE-confirmed graceful logout.

New pin: MapHousePanelLiveDatMountTests ([InstalledDatFact]) reproduces
the production mount recipe against the installed DATs — the test that
would have caught this at Batch C: pins the cold-import null, the
panel-slot resolution of both icons with non-degenerate extents, AND
that PlaceMarker's writes survive the per-frame ApplyAnchor pass.

Gates: Release build green; App suite (live-DAT mode) 5479/3 skips
(baseline 5478 + the new pin); Runtime 1744/0; full solution green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 05:58:17 +02:00
Erik
38c580ff6d docs: night-round review — live verification found the AD-108 icon-resolve mechanism is actually broken
Live verification against the connected client (part of the F1-F15
gate) found the Map tab's player/house icons never mount:
"[D.2b] Map tab: icon 0x100001ED did not resolve" / "...0x100001EE did
not resolve". AD-108 (filed earlier this session for F9) had described
the standalone re-import mechanism as working; it does not.

A throwaway diagnostic (not committed) confirmed the root cause:
LayoutImporter.ImportInfos(dats, hostLayoutId, elementId)'s FindDesc
walks the LayoutDesc's raw top-level Elements table (one entry) and
recurses through ElementDesc.Children with no tab-page/state resolution
— calling it directly with these icon ids returns null. Resolving the
panel's own slot first (what MountMapHousePanel actually does) and
searching THAT tree finds m_pMap with both icon children present, so
the icons are real, just unreachable via a cold standalone import.

This is pre-existing (predates this session, confirmed via git log)
and unrelated to any F1-F15 fix — it means F1's byte-decoded
PlaceMarkerOnMap formula could not be visually confirmed against the
running client this round; it remains verified only at the unit-test/
golden-pixel level. Filed a follow-up task for the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 05:19:12 +02:00
Erik
0b0c7aa485 fix(ui): night-round review — F11/F13/F14/F15 one-liners
F11: logs when a Map tab town-marker's template resolves to something
other than a UiButton — that path previously silently skipped the
TooltipText write with no diagnostic, leaving a mounted-but-empty
tooltip popup indistinguishable from "no template configured".

F13: RetailSkillFormula.FormatFormula now reads Attribute1Multiplier/
Attribute2Multiplier/AdditiveBonus/Divisor through the SAME unsigned
reinterpretation TryCalculate already uses (this class's own doc
comment already stated the invariant; FormatFormula just didn't follow
it). A high-bit-set value would previously both mis-gate hasAttr1/
hasAttr2 and print a negative number, out of sync with what
TryCalculate actually computes with for the same formula. Added
regression tests, empirically verified to fail without the fix.

F14: documented the RefreshHouseMarker gap rather than guessing at the
byte-decode — Position::get_outside_cell_id @0x004527b0 is itself
BN-mangled (its `(eax_2 - eax_2) & objcell_id` return is the same
decompiler-obscures-a-real-conditional artifact class this round hit
elsewhere) and depends on LandDefs::adjust_to_outside, a genuinely
larger port than this round's other findings. HousePosition is wired
() => null in production today (ISSUES #413's remaining scope), so
this method is currently unreachable; left a TODO citing the retail
call chain for whenever that lands.

F15: fixed RefreshCoordinatesAndPlayerMarker's gate to AND-on-both-
present, matching gmMapUI::Update @0x004a2078's exact
`if (m_pCoordinateText != 0 && m_pPlayerLocationIcon != 0)` condition.
The prior `_coordinateText is null && _playerIcon is null` check only
skipped when BOTH were absent (proceeding whenever EITHER was
present), letting coordinate text and the player marker update
independently instead of as the single gated unit retail treats them
as. Added a regression test (player-icon template resolution failure
must also skip the coordinate-text write), empirically verified to
fail without the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 05:06:17 +02:00
Erik
c403f57815 fix(ui): night-round review — F9/F10/F12 register + structure riders
F9: filed register row AD-108 for MapPageController.ResolveSwallowedIcon
— the standalone re-import of the Map tab's player/house icons, which
m_pMap's own Type-1 UiButton authoring swallows as dat children
(UiButton.ConsumesDatChildren). This adaptation was implemented but
never had a register row.

F10: extracted the popup-locator pair (0x10000395/0x21000041),
previously duplicated as three separately-cited private constants
across UiItemSlot.cs, RetailTooltipPresenter.cs, and
MapPageController.cs, into ONE public pair on RetailTooltipPresenter
(SharedPopupSkinRootElementId/SharedPopupSkinLayoutDid) with a single
canonical citation. The other two sites now reference it instead of
carrying their own copy.

F12: fixed TS-85's SetTooltip-site arithmetic. The register (and a
mirrored ISSUES.md log entry) claimed "15 known sites, all accounted
for" — recounting the row's own enumerated list finds 17 distinct
sites (the tally had dropped gmPaperDollUI::UpdateItemSlotTooltip
@0x004A52EF and undercounted by one more), of which 16 are ported and
one — UIElement_Text::RecalculateTruncation @0x00466F80, the headline
highest-volume site sub-mechanism (1) itself named as deliberately
deferred — was never actually closed. The "all 15 accounted for"
close was wrong twice over: wrong count, and a site the row's own text
already scoped as open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:56:16 +02:00
Erik
df062d2eda fix(ui): night-round review — F8 House not-expired purchase-restriction text
gmHouseUI::DisplayPurchaseTimeText @0x004a3110's not-yet-expired
branch was wrongly marked "unrecoverable from this decomp dump" — a
direct capstone disassembly resolves all three concatenated pieces:
prefix "You may buy another landscape house at " @0x7ab790 (pushed
@0x004a3265), the strftime "%c" format literal @0x7ab7ec (pushed
@0x004a321d) applied to localtime(timestamp + 0x278d00) — the expiry
moment, 30 days after the purchase timestamp — and suffix ". This
restriction does not apply to apartments." @0x7ab7b8 (pushed
@0x004a3235).

Ported in RuntimeHouseState.Recompute, substituting .NET's
culture-default DateTime.ToString() for the CRT's strftime("%c", ...)
(different formatting engine, same "process locale, full date+time"
intent) — filed as register row IA-23 (an approximation, not a gap).
TimeProvider.LocalTimeZone (overridable, defaulting to
TimeZoneInfo.Local in production) keeps the conversion deterministically
testable while matching retail's own localtime() call.

Updated RuntimeHouseStateTests: the not-expired case now asserts the
composed prefix/suffix structure and the exact expiry instant (pinned
via a UTC-fixed test TimeProvider), replacing the old "renders nothing"
assertion. Un-claimed "unrecoverable" in ISSUES #413 item 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:50:36 +02:00
Erik
ab84b54dfa fix(ui): night-round review — F5/F6 structural single-tooltip invariant
F5: moved the unconditional RemovePopup() call into
RetailTooltipPresenter.TryBuildAndMountPopup itself so the single-
popup invariant (retail's own single m_pTooltipElement slot) is
enforced structurally rather than relying on every caller to have
already cleared a stale popup. Closes a real hole: UpdateWorldHoverTooltip's
own clear is gated on _worldTooltipShowing (only true when the WORLD
path itself mounted the current popup), and its "a UI popup cannot be
showing here" comment assumed the host's hover query is null whenever
that branch runs — an assumption that breaks the instant a modal opens
over a stationary cursor. UiRoot.Modal claims EXCLUSIVE hit-testing, so
Pick(MouseX, MouseY) can return null even though a UI-dwell tooltip is
still mounted underneath; UpdateWorldHoverTooltip would then mount a
second popup on top without ever clearing the first.

F6: fixed WorldHover_ThenUiDwellTooltip_ReplacesRatherThanStacks to
actually exercise the transition with a follow-up presenter.Tick()
(the old test only proved OnTooltipShow's own clear worked, never
checked the world-side bookkeeping after). Added
UiDwellTooltip_ThenModalStealsHitTesting_WorldHoverReplacesRatherThanStacks
for F5's own case, using UiRoot.Modal to reproduce the exclusive-hit-
testing hole precisely — empirically verified this new test fails
(2 popups instead of 1) with the structural RemovePopup() reverted,
confirming it is a real regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:45:49 +02:00
Erik
4a24614fd1 fix(ui): night-round review — F3/F4/F7 cast-button tooltip strings
F3: TS-85 had claimed the plain-spell branch's three SetTooltip format
strings were "genuine gmNoticeHandler vtable SLOTS" and unrecoverable
from the decomp dump. That was itself the artifact — Binary Ninja's
pseudo-C rendering of PStringBase::sprintf's second argument as
"&gmSpellcastingUI::`vftable'.RecvNotice_XXX" was a spurious symbol
match, not the true operand. A direct capstone disassembly of the raw
bytes at gmSpellcastingUI::UpdateCastButtonTooltip @0x004c6a30's four
call sites (0x4c6e48/0x4c6ea4/0x4c6f18/0x4c6f5d) resolves the actual
pushed literals: "CAST %hs" @0x7b63a4 (untargeted/self-cast, and
targeted+compatible with " on %s" @0x7b6464 appended), "You must
select an appropriate target for %hs" @0x7b6348 (incompatible target),
"You must select a target for %hs" @0x7b63b8 (no target). %hs is the
spell's own name throughout.

Added RuntimeSpellCastState.EvaluateCastGate (SpellCastGate: NoTarget-
Needed/TargetCompatible/TargetIncompatible/NoTargetSelected/Unknown),
refactoring IsTargetReady to use it, and wired
SpellcastingUiController.ComputeSpellCastState to the four-state
tooltip text, replacing the bare-spell-name fallback.

F4: the endowment branch's "USE the %s" (and both select-target
strings) vararg is NOT the bare item name — retail composes
"%s (%hs)" @0x7b64d8 (item name, spell name) once at @0x004c6bb6-ef
and reuses it for all three format strings, byte-confirmed by all
three sprintf call sites (0x4c6c7f/0x4c6ca4/0x4c6d46) reading the
identical stack slot. Added ComposeEndowmentName and wired it in place
of the bare item name.

F7: added test coverage for the two genuinely NEW disabled states
(needs-target, needs-appropriate-target) neither branch had any
coverage for before, plus the enabled untargeted/targeted-compatible
states and both endowment-branch composed-name cases.

Corrected the register's TS-85 row (the "cannot be recovered" claim
and the endowment operand claim) with the byte-decoded findings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:39:17 +02:00
Erik
353ae3bb0c fix(ui): night-round review — F2 HouseQuery fires at login, not tab-open
CM_House::Event_QueryHouse @0x006aaa00 (opcode 0x21e) tail-calls
unconditionally from the end of CPlayerSystem::InitializePlayer
@0x00563570 — the same once-per-session function
AttemptSendLoginCompleteNotification lives in (both guarded by the
player_initialized flag), right after that notification. Retail never
sends it from gmHouseUI::PostInit or gmMapUI::PostInit on House-tab
activation.

Moved WorldSession.SendHouseQuery() to the direct (non-portal)
first-entry completion edges — the same places acdream already sends
the analogous "initial session bootstrap" LoginComplete:
  - graphical: LiveSessionRuntimeFactory's RuntimeFirstEntryDriveController
    localPlayerCompleted callback
  - headless: HeadlessSessionHost's equivalent callback
  - headless content-less direct host: RuntimeLiveEntitySessionController.OnSpawned

Portal-space re-entries (LocalPlayerTeleportController's F751 path,
RuntimeLiveEntitySessionController.TryAdvancePortalCompletion) do NOT
resend it, matching retail's single-shot guard.

Removed the invented House-tab-open -> SendHouseQuery trigger
(InteractionRetainedUiComposition's HouseShown binding) and retired
register row AD-107, which had documented that adaptation.

Updated RuntimeLiveEntitySessionControllerTests' exact game-action
assertions for the content-less path, which now also captures the
HouseQuery send alongside LoginComplete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:28:46 +02:00
Erik
6ee3d88863 fix(ui): night-round review — F1 real PlaceMarkerOnMap formula
The prior PlaceMarker() reading ("center at markerX0+x") was wrong.
Binary Ninja elides gmMapUI::PlaceMarkerOnMap @0x004a18b0's entire FPU
chain to bare, operand-less _ftol2() calls, so the pseudo-C
under-specifies the function. A capstone disassembly of the raw bytes
in the PDB-paired acclient.exe recovers the real formula: retail
projects the AC display coordinate (range ~-102.4..102.4) onto the
marker-area rect via a fixed-point-style transform, not a raw pixel
add:

  X = m_x0 - w/2 - (int)((m_x1-m_x0+1) * (x*10+1024)        * (-1/2048))
  Y = m_y0 - h/2 - (int)((m_y1-m_y0+1) * (2047-(y*10+1024)) * (-1/2048))

Constants read directly from .rdata: 0x79bac8=10.0, 0x7aac78=1024.0,
0x7aac70=-1/2048, 0x7aac68=2047.0. The Y axis's FSUBR is retail's
north-up flip. w/h halve with truncating integer division (matching
retail's cdq;sub;sar idiom), not float division.

Extracted the pure math into MapPageController.ComputeMarkerPosition
so it's directly testable, and retargeted MapPageControllerTests to
GOLDEN PIXEL values computed independently from the formula (never
from the port's own output): the reviewer's canonical (0,0)->(122,128)
case, a far-west and far-north case, and a real town-table entry
(Arwic's landblock, cross-checked against RadarCoordinates). Applies
to the green ring, house pin, and all 53 static town hotspots, which
all resolve through the same PlaceMarker call.

Corrected the recon doc's "accepted as-is" note, which had mistaken
"the FPU argument-passing is BN-mangled" for a narrow issue instead of
the whole-formula elision it actually was.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:19:29 +02:00
Erik
06512f0957 feat(ui): House tab ownership text — DisplayPurchaseTimeText + RuntimeHouseState
Derived the mechanism from the decomp before writing code: neither
gmHouseUI::PostInit @0x004a2710 nor gmMapUI::PostInit @0x004a1c70 sends a
HouseQuery, and six of gmHouseUI's seven Display* builders early-return on
m_pHouseData == 0. The only text a houseless character's House tab shows is
gmHouseUI::DisplayPurchaseTimeText @0x004a3110's expired branch (it doesn't
gate on m_pHouseData) — the local player's PropertyInt.HousePurchaseTimestamp
plus HouseSystem::HasPurchaseWaitPeriodExpired renders exactly "You may buy
another house immediately." for a fresh character. Exhaustive search of the
2013 EoR decomp, ACE, and the live DAT found zero support for a second
"You do not currently own a house." line the task brief described — this
commit ports what the decomp actually shows.

Ships:
- RuntimeHouseState: a minimal (no disposal, no construction-transaction
  Fault() point) Runtime owner per ISSUES #413's own sizing note, wired
  through GameEventWiring's existing HouseData/HouseStatus delegate holes,
  LiveSessionEventRouter, and GameRuntime.HouseOwner. Participates in
  RuntimeGenerationReset (new House stage) since a fresh login must not
  show a stale character's house state.
- HousePageController.Bindings.Lines/OnShown wired to real data; OnShown
  fires WorldSession.SendHouseQuery() on tab-open (AD-107: an acdream
  trigger, not a ported retail call site — filed in the divergence
  register).
- Fixed a real bug found along the way: HousePageController.Bind never
  wired UiTemplateListBox.TemplateResolver, so no row could ever render
  regardless of Lines content. Now reuses the Map tab's generic hotspot
  resolver.

Live-verified against a real local ACE server and the +Acdream character
(--session-config auto-select + a UI automation script): screenshot and
structural UI-tree dump both confirm the House tab renders exactly "You may
buy another house immediately." Graceful logout confirmed both launches.

ISSUES #413 narrowed to its one remaining piece: the six owned-house-only
Display* builders (DisplayBuyPayment/RentPayment/BuyTime/RentTimes/
Location/WarningText), unexercisable without a test character that owns a
house.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 03:38:16 +02:00
Erik
eb6f3bd8c8 docs: TS-85 register — correct Batch C's tooltip-mechanism claim
The row committed alongside the panel shell (8799acd2) described the
Batch C map-marker tooltip as using AuthoredTooltipText/Enabled — that
was the pre-live-verification code. Slice 5 found it never rendered
live and the actual fix (commit e5629d71) uses UiButton.TooltipText
(retail's runtime m_TTText/SetTooltip mechanism) plus a hardcoded
popup-skin locator matching UiItemSlot's precedent. Updates the row
to describe the shipped mechanism instead of the abandoned one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:46:22 +02:00
Erik
e5629d713d fix(ui): Map/House panel — marker tooltips use the wrong property
Live verification (slice 5) found town-marker tooltips never appeared:
RetailTooltipPresenter.OnTooltipShow gates unconditionally on
AuthoredTooltipRootElementId == 0 -> return, with no fallback, but the
markers only set AuthoredTooltipText/Enabled (the DAT-authored P0x49
path). gmMapUI::AddMapNote's UIElement::SetTooltip call is retail's
RUNTIME m_TTText/SetTooltip mechanism, not the authored path — the
correct seam is UiButton.TooltipText (backing GetTooltipText()'s
override), which ResolveTooltipText consults before authored text.

The popup-skin locator (AuthoredTooltipRootElementId/LayoutDid) is
still required even on the runtime-text path with no built-in
fallback, so markers now hardcode the same shared popup skin
UiItemSlot already uses (0x10000395/0x21000041) — matching that
established precedent exactly.

Verified live: hovering a town marker (Aerlinthe Island) now renders
its tooltip correctly. 21/21 Map/House controller tests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:43:40 +02:00
Erik
22b6281192 docs: fix HousePageController doc — correct a claim about unshipped RuntimeHouseState wiring
The class doc referenced RuntimeHouseState.PurchaseAvailabilityText as
already wired this session; it isn't (deferred to #413, the
RuntimeHouseState owner integration). Corrected to accurately describe
what shipped (mount + wire parsing groundwork) vs what's still open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:31:53 +02:00
Erik
8799acd285 docs: TS-85 register — Batch C closes the map-notes tooltip item
gmMapUI::AddMapNote's 53 town-hotspot tooltips are now ported
(MapPageController.BuildTownMarkers), closing the last remaining
SetTooltip call site TS-85's sub-mechanism (1) enumeration tracked.
Sub-mechanism (2) (the P0x3D wrap-width override) remains open and
unrelated to this batch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:30:59 +02:00
Erik
2881af0bfc docs: file #413 — House tab content (RuntimeHouseState owner + 6 Display* builders)
Precise scope note for the remainder of the House tab wire, per the task's
pre-authorized fallback: RuntimeHouseState owner integration,
DisplayPurchaseTimeText's port (the one builder simple enough to have
landed this session but deferred for time), and the other six Display*
line builders (only exercisable once a house is actually owned).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:10:35 +02:00
Erik
dd6c7e09f0 feat(net): Map/House panel — slice 4a, House wire parsing groundwork
Adds the outbound HouseQuery action (0x021E, ClientCommandRequests.
BuildHouseQuery / WorldSession.SendHouseQuery — ACE GameActionHouseQuery.
Handle reads no payload) and inbound parsers for all four House wire
opcodes GameEventType already defined (0x0225-0x0228, gmHouseUI::PostInit's
registered notice handlers): GameEvents.ParseHouseData (BuyTime/RentTime/
Type/MaintenanceFree/Buy list/Rent list/Position — the Position field
reuses CreateObject.ServerPosition's existing 32-byte Cell+Pos.XYZ+
Rotation.WXYZ shape rather than a new type), ParseHouseStatus (WeenieError
u32), ParseUpdateRentTime, ParseUpdateRentPayment. Wire shapes verified
against ACE's HouseDataExtensions/HousePaymentExtensions (references/ACE/
Source/ACE.Server/Network/Structure/HouseData.cs, HousePayment.cs) — noted
that ACE's own UpdateRentTime/UpdateRentPayment writers are stubs (always
0u / always an empty list), captured as such rather than assumed live.

GameEventWiring.WireAll gets four new optional delegate holes
(onHouseData/onHouseStatus/onHouseUpdateRentTime/onHouseUpdateRentPayment)
following the exact trade-family precedent — registered only when non-null,
every existing caller compiles unchanged.

This is the "enum/parser groundwork" half of Slice 4's pre-authorized
fallback. NOT included (filed as an ISSUES entry): a RuntimeHouseState
GameRuntime owner (construction-transaction ceremony, fault-injection
points, disposal/convergence tracking — the same weight as
RuntimeTradeState's integration, judged disproportionate for tonight
alongside the completed Map tab), HousePageController's real Lines/
OnShown wiring, the DisplayPurchaseTimeText port, and the six other
Display* line builders. The House tab currently mounts with genuinely
empty content, matching retail's own PostInit (verified via
MapHousePanelSlotProbeTests' live-DAT probe, not assumed).

9 new HouseEventsTests (parser round-trips + truncation), 1 new
GameEventWiringTests case (all four opcodes reach their callbacks).
Core.Net.Tests: 1004/1004 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:09:34 +02:00
Erik
6b0fa4ff0d feat(ui): Map/House panel — slices 2+3, panel shell + Map tab
Mounts host 0x2100006E slot 0x1000018C (RetailPanelCatalog.MapHouse = 16)
as a two-tab UiTabPanel (Map default, House second) through the OP3/FA3
recipe (LayoutImporter.Build -> Bind -> ActivateTabBehavior). Toolbar
button 0x1000019A un-ghosts (added to both RetailPanelCatalog.Mounted and
.Toolbar). Combined into one commit because MapHousePanelController.Bind
depends on both MapPageController and HousePageController existing —
splitting them would mean landing dead code first.

Map tab (gmMapUI, MapPageController):
- Calendar formatter matching gmMapUI::Update's "Date: %s\nTime: %s"
  shape, reusing WorldTimeService.CurrentCalendar (new
  Func<DerethDateTime.Calendar> dependency threaded through
  InteractionRetainedUiDependencies/GameWindow — a stable long-lived
  service, not routed through the deferred-binding machinery Radar's
  per-session state needs). MonthName enum values already match retail
  display text; HourName's "AndHalf" suffix is rewritten to "-and-Half".
- Coordinate math + marker placement reuse RadarCoordinates/
  LandDefs.GidToLcoord verbatim (both already byte-exact ports of
  CPlayerSystem::InqPlayerCoords/LandDefs::gid_to_lcoord) — no re-port.
  PlaceMarkerOnMap's centering math (m_x0 + x - w/2) ported from
  gmMapUI::PlaceMarkerOnMap @0x004a18b0. Indoor gating clears the
  coordinate text and hides the player marker, matching
  gmMapUI::Update's else branch.
- 53-town s_rgLocations table ported verbatim into MapLocations.cs.
  Markers built once at bind time via the panel's own RowTemplateResolver
  against m_pMap's authored hotspot-template attrs (0x47/0x48), with
  literal-string tooltips through AuthoredTooltipText/Enabled
  (RetailTooltipPresenter) — closes divergence-register row TS-85's last
  item, gmMapUI::AddMapNote @0x004A1C51.
- Structural finding: m_pMap (0x100001EC) is itself authored as a Type-1
  BUTTON (the GM click-to-teleport hook at
  gmMapUI::ListenToElementMessage), and the player/house icons
  (0x100001ED/EE) are its own NESTED children, not siblings —
  UiButton.ConsumesDatChildren swallows them from the normally-built
  tree. Both are re-resolved standalone through the same template
  resolver the town hotspots use and reattached under m_pMap.

House tab (gmHouseUI, HousePageController): mounts the ListBox
(0x100001E6) with its authored row template, wired to an empty Lines()
source by default — genuinely empty until Slice 4's wire lands, matching
retail's own PostInit (no Update call, no static content).

21 new tests (7 MapHousePanelControllerTests, 14 MapPageControllerTests):
tab table pairing, close button, town-hotspot count/tooltips, calendar
formatter golden values (Frostfell 27/119 P.Y., every HourName incl.
AndHalf), player/house marker placement and indoor-gating reproduced
against the real fixture via already-tested RadarCoordinates (no
re-derivation). Fixture map_house_2100006E_1000018C.json captured via
the shared RetailLayoutFixtureGenerator (other 34 fixtures deliberately
NOT regenerated — out of scope for this batch, would touch unrelated
panels' schema drift).

Full solution builds clean; App suite 5391/0 failed/71 skipped (non-live;
one earlier flaky streaming failure unrelated to this change, confirmed
pre-existing on the branch before these commits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:04:19 +02:00
Erik
04841b6807 feat(ui): Map/House panel — slice 1 discovery probe
Live-DAT probe confirming the desk-verified facts before implementation:
host 0x2100006E slot 0x1000018C carries panelId 16 (gmMapUI::PostInit
signature children 0x100001EB-EF; gmHouseUI's ListBox 0x100001E6),
tabTableCount=2 with Map (button 0x100001F3 -> page 0x100001F6) as the
authored default and House (0x100001F4 -> 0x100001F7) second, close button
0x100001F5. Toolbar button 0x1000019A carries the matching panelId 16 —
the Map/House entry among the toolbar's three ghosted buttons. m_pMap's
own marker-area rect is (6,8)-(247,258); its hotspot template attrs
(0x47/0x48) resolve to element 0x100001F0 in LayoutDesc 0x21000026, a
10x10 Type-1 button with 3 states. The House ListBox authors exactly one
row template (LayoutDesc 0x21000025 element 0x100001E7, a bare
UIElement_Text row, no scrollbar) and zero static child rows — the box is
genuinely empty until the first server notice, refuting the recon's
"authored default content" hypothesis for the no-house case.

Kept as a permanent env-gated pin (ACDREAM_PROBE_LIVE_MOUNT=1), matching
FaPanelSlotProbeTests' precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:03:49 +02:00
Erik
4817c17600 docs: Map/House panel recon — panelId 16/slot 0x1000018C resolved, LandDefs.GidToLcoord reuse, wire enum already present
Saves the overnight-round recon (embedded findings + this session's desk
verification) for auditability before implementation starts. Corrects two
handoff claims: the panel id is 16 (already resolved by the existing FA
campaign's full 16-slot gmPanelUI::SetupChildren dump, not a guess from
{1,2,6,14}), and GameEventType already defines all four House opcodes
(0x0225-0x0228) — what's missing is routing, not the enum. Identifies that
LandDefs.GidToLcoord/LcoordToGid (src/AcDream.Core/Physics/LandDefs.cs) is
an existing tested port of LandDefs::gid_to_lcoord, reusable for both the
Map tab's coordinate math and the House location display — no re-port
needed. Cites the toolbar button (0x1000019A, panel id 16), the 53-entry
s_rgLocations marker table verbatim, the ServerPosition wire struct reuse
for HouseData.Position, and the AuthoredTooltipText/RetailTooltipPresenter
seam that will close register row TS-85's last item (gmMapUI::AddMapNote).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 01:27:28 +02:00
Erik
39c49e140e feat(ui): spellcasting cast-button + character-panel attribute/skill tooltips — gmSpellcastingUI::UpdateCastButtonTooltip @0x004C6A30, AttributeInfoRegion/Attribute2ndInfoRegion/SkillInfoRegion @0x004F1530/0x004F1680/0x004F2140
TS-85 remainder batch (hover/UI overnight round, batch B). Audit found
three of the four listed spellcasting SetTooltip sites (endowment icon,
favorite, submenu) were already correct via UiCatalogSlot's pre-existing
Label-driven GetTooltipText; only the cast button (UiButton, no tooltip
wiring at all) was a real gap. Ports the verified literal states
("Select a spell to cast" / "You have no spells ready to cast" / the
full endowment-item USE-the-%s branch) plus a documented, narrower
fallback (spell name only) for the one sub-branch whose exact wording
sits behind a genuine gmNoticeHandler vtable-slot collision in the
pseudo-C dump rather than the unlabeled-string-pool class the rest of
this batch recovered.

Character panel: new UiClickablePanel.TooltipText seam (same pattern as
UiButton.TooltipText) carries the six hardcoded attribute descriptions
and three pair-shared vitals descriptions (byte-decoded from the retail
string pool) plus skill tooltips composed from the already-DAT-parsed
SkillBase.Description/.Formula — no hand-transcription needed for the
~30+ skill strings. The formula-to-text algorithm itself
(SkillSystem::InqSkillFormula) was recovered by byte-decoding six short
fragments Binary Ninja left completely unlabeled between two
gmSpellcastingUI vtable declarations.

Live-verified against the local ACE server: 34 real skills' composed
tooltips and both reachable cast-button states captured via a temporary
probe (stripped before this commit). Full solution suite green
(14,647 tests, 0 failures) both before and after the probe strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 01:11:22 +02:00
Erik
c623b57ad3 fix(ui): Options panel Config tab content escapes the window frame — stale viewport anchor capture, not a missing clip
The Config tab's footer sat mid-panel with further rows drawing below the
window's bottom edge. Live-DAT measured: the mounted tab-host root is
authored 300x362 (retail's real default window size), but the Config page
slot underneath keeps its own larger design geometry (298x575 against a
300x600 canvas) until retail's real four-edge UiLayoutPolicy
(UIElement::UpdateForParentSizeChange @0x00462640) shrinks it on the first
ApplyAnchor pass -- verified stable, this part already worked.

The actual bug: UiTemplateListBox.Viewport (the UiScrollablePanel that
hosts + clips every row) is a programmatic C# element seeded at Bind time,
BEFORE the tree's first draw frame -- before the ListBox has ever shrunk.
Its legacy anchor baseline is captured lazily on its own first ApplyAnchor
call, which lands AFTER the ListBox has already shrunk earlier in that same
frame (parent-before-child draw order). That capture measures a negative
bottom margin the stretch math preserves forever: the viewport stayed
locked at its original 560px design height, clipping rows to a bound
retail never actually gave the window on screen.

Fix: force the viewport's anchor capture to happen immediately after
seeding it, while its Width/Height still exactly equal a zero-margin
baseline against the CURRENT (pre-shrink) parent, instead of lazily on the
first draw frame against an already-shrunk parent. This is #372's sequel --
#372 fixed the 0x0 collapse case; this is the "ListBox itself later
shrinks" case #372's own fixture never exercised.

Three new tests (UiTemplateListBoxViewportTests using the live-DAT-measured
298x575/276x560 numbers, plus two ConfigOptionsPageControllerTests against
the real production Bind path and the committed host fixture) all fail
pre-fix, confirmed by temporarily reverting the change. Scoped to
UiTemplateListBox's own viewport; UiScrollablePanel/ApplyAnchor/
ComputeAnchoredRect are untouched, so chat's transcript scrolling and every
other UiScrollablePanel/UiItemList consumer are unaffected.

fix #412

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 00:24:33 +02:00
Erik
97a7be12ee fix(ui): world tooltips never cleared, stacking dozens of popups — single-slot invariant restored on every found-object edge
RetailTooltipPresenter.UpdateWorldHoverTooltip only called RemovePopup()
on the found-object-LOST edge (found == 0u). An A->B found-object CHANGE
(walking past a run of NPCs/doors/lifestones with no intervening "nothing
found" frame) skipped straight to TryBuildAndMountPopup with the previous
popup still mounted as a child of _host -- only the _popupRoot reference
got overwritten, so every earlier popup was orphaned in the tree and never
removed. Matches the user's screenshot of 15+ stacked name boxes.

Fix: clear any showing world popup on ANY found-object edge -- change or
loss -- before evaluating whether to mount a new one, mirroring
OnTooltipShow's own unconditional RemovePopup() at its top.

Live-verified against local ACE (testaccount/+Acdream, session-config
launch): a temporary probe logged 103 mount/102 remove events across many
direct object-to-object transitions (Silver Tusker, Armored Tusker,
+Acdream); hostChildren never exceeded baseline+1 and popupSkinChildren
never exceeded 1 -- confirmed at most one tooltip ever exists. Probe
stripped before landing; two new fixture regressions
(WorldHover_FoundObjectChangesDirectly_ReplacesThePopupWithoutStacking,
WorldHover_ThenUiDwellTooltip_ReplacesRatherThanStacks) both fail pre-fix.

fix #409 (follow-on)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 00:23:54 +02:00
Erik
63b0668fb9 Merge campaign-409-tooltips: hover-feedback completion — item names, world-object tooltips, item cursor swap
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Item-name tooltips on every UiItemSlot surface (UIElement_UIItem::UpdateTooltip
@0x004E1CB0, stack-count prefix, the 47-prototype shared popup locator);
world-object hover tooltips via retail's SmartBox found-object pipeline
(RecvNotice_SmartBoxObjectFound @0x004E5AD0 — immediate, ShowTooltips-gated,
no stack prefix — live-verified: 'Silver Tusker' + DefaultFound cursor from
one pipeline); #411 CLOSED — retail's set_found_object fires unconditionally
over UI items, the user's memory was right, our target-mode gate was wrong.
TS-85 narrowed to the enumerated remainder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:04:06 +02:00
Erik
708e35f610 docs: hover-feedback completion round — #409/#411 bookkeeping, TS-85 narrowed
Records the round that shipped item-cell tooltips, the world-object
hover tooltip, and the #411 cursor-swap fix: #409's write-up gains a
"hover-feedback completion round" section covering all three items
with live-verification notes; #411 is closed with the corrected
decomp reading; register row TS-85 is narrowed to reflect the two
newly-ported SetTooltip call sites (UIElement_UIItem::UpdateTooltip,
UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound) and the
still-open ones (spellcasting endowment/cast-button/favorite/submenu,
map notes, character-panel attribute/skill info).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:02:50 +02:00
Erik
aed423174b fix(ui): #411 — pointer swaps over inventory items unconditionally, not only in UseTarget mode
Corrects an incomplete reading from #411's original investigation.
UIElement_SmartBoxWrapper::FindObject @0x004E5430 calls
SmartBox::set_found_object(itemID, 0xFFFFFFFF) whenever the hovered
UI element (m_pElementLastOver) casts to UIElement_UIItem
(class 0x10000032) — UNCONDITIONALLY, not gated on target mode, and
returns WITHOUT running the 3D raycast. ClientUISystem::
UpdateCursorState @0x00564630 computes its "found" flag ONCE at the
top of the function (ebx = SmartBox::get_found_object_id() != 0,
@0x00564642) and every later branch (default/melee-missile/magic/
use/examine/use-target/busy) reads that SAME flag — so hovering an
occupied item cell shows the cursor's "...Found" variant in EVERY
mode, not only during an active UseTarget selection.

CursorFeedbackController.Update(UiRoot) already had the item-hover
special case wired from an earlier round but incorrectly gated it to
TargetMode.UseTarget only; that one-line gate is removed.
ResolveGlobalKind needed no changes at all — it already read the
snapshot's HoverTargetGuid unconditionally across every mode.

Two new tests pin the widened behavior in ordinary peace mode and in
combat mode. Live-DAT-independent (pure decomp + unit fixture).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:02:42 +02:00
Erik
fe1bc70753 feat(ui): world-object hover tooltip — UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound port
NOT the UI-element dwell-timer path. Retail's mechanism is
UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004E5AD0,
fed every frame by FindObject @0x004E5430/Global_Loop @0x004E5620
using the current mouse position regardless of input focus. It fires
IMMEDIATELY (no dwell wait) on the found-object id CHANGING, gated by
the PlayerModule::ShowTooltips character option (already modeled in
CharacterOptionTable, default true), with text
ACCWeenieObject::GetObjectName(id, NAME_APPROPRIATE, 0) — the SAME
name call as item tooltips, but WITHOUT the item-cell's separate
stack-count prefix (a ground pile of arrows shows "Arrows", not
"20 Arrows" — a real, decomp-confirmed asymmetry).

Ported as RetailTooltipPresenter.UpdateWorldHoverTooltip, driven by
the SAME world-hover pick CursorFeedbackController's own found-cursor
already uses (WorldSelectionQuery.PickAtCursor, includeSelf: true —
own player is included on that precedent) and the SAME
ClientObjectTable-backed name resolver SocialAllegiancePageController's
ResolveWorldObjectName already established as this codebase's
pattern. New WorldTooltipRuntimeBindings threads it through
RetailUiRuntimeBindings; wired at InteractionRetainedUiComposition
alongside the existing cursorFeedback construction.

Queried only when no UI element is hovered — a narrowing from
retail's literal "raycast even under non-item UI chrome" (FindObject's
m_pElementLastOver check), called out in the class's own doc note as
a scoped interpretation rather than a byte-exact port.

The exact popup skin is an inference, not a measured value: an
exhaustive live-DAT sweep found UIElement_SmartBoxWrapper (class
0x10000030) has NO authored ElementDesc anywhere installed — unlike
every other tooltip trigger, it is evidently constructed directly by
gmGamePlayUI's own mode setup, not from a walkable LayoutDesc. This
port reuses the same P0x47=0x10000395/P0x48=0x21000041 pair every
other game-code SetTooltip caller in this family resolves to — the
best-evidenced choice, called out in register row TS-85 rather than
silently assumed exact.

Live-verified against a connected ACE session (session-config launch,
+Acdream): hovering a "Silver Tusker" near spawn mounted the correct
popup text and simultaneously flipped the cursor to its DefaultFound
variant, confirming the shared found-object pipeline drives both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:02:29 +02:00
Erik
e29c61a3a4 feat(ui): inventory/shortcut/paperdoll item-name tooltips — UIElement_UIItem::UpdateTooltip port
Retail UIElement_UIItem::UpdateTooltip @0x004E1CB0 caches the item's
NAME_APPROPRIATE display name (stack-count-prefixed "%d %s" when
StackSize > 1) as m_TTText every UIItem_Update refresh; the generic
UIElementManager::CheckTooltip dwell timer is what actually shows it
on hover — no special-cased trigger of its own.

UiItemSlot cells are built programmatically (never through
LayoutImporter.Build), so #409's original round left this deferred:
the class carried neither the popup locator (P0x47/P0x48) nor a name
source. A live-DAT sweep of the shared UIItem cell-template catalog
(ItemListCellTemplate.CatalogLayoutId, 0x21000037) found all 47
UIItem-type (class 0x10000032) prototypes — inventory's cell, every
toolbar slot, every paperdoll/armor slot skin — resolve the IDENTICAL
popup locator (P0x47=0x10000395/P0x48=0x21000041) through catalog
inheritance, with no literal text authored on any of them. UiItemSlot
now hardcodes that pair and exposes GetTooltipText() via a new
TooltipTextResolve delegate, wired at every physical-item
construction site: InventoryController (main-pack cell + grid cells),
ExternalContainerController, PaperdollController (closes the
separate gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF gap too —
same cell class, same fix), VendorUiController (shop/buying/selling
lists), SecureTradeUiController, ToolbarController.

Text is the new ClientObject.GetTooltipDisplayName(): GetAppropriateName()
prefixed with the stack count via "{count} {name}" when StackSize > 1,
matching UpdateTooltip's exact NAME_APPROPRIATE + "%d %s" sprintf.
UiCatalogSlot (spell/component catalog cells, a different UiItemSlot
subclass) is unaffected — it already overrides GetTooltipText() with
its own Label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:02:10 +02:00
Erik
34ec397e9e Merge campaign-409-tooltips: #409 runtime-tooltip resolution fix — live-verified
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Retail reads the runtime m_TTText FIRST (StartTooltipAtMouse @0x00460DA3)
with authored P0x49 as fallback; the presenter read only authored text,
so every runtime-written tooltip (Options rows, checkbox bitfields, both
social pages — writers acdream already had) never showed. Fixed with
retail's resolution order + the P0x48 own-layout fallback; live-verified
on a connected client. The authored-243 population measured as entirely
chargen-resident. Deferred honestly: inventory item-name tooltips
(UIElement_UIItem::UpdateTooltip — UiItemSlot lacks the plumbing) and
#411 (hover cursor/rollover feedback, full mechanism mapped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 21:45:37 +02:00
Erik
5f9ca18155 fix(ui): #409 live-failure round — tooltips read the RUNTIME text first
User gate on 1.0.3-tt.a: tooltips appeared NOWHERE in-world except one on
the paperdoll. Root-caused, fixed, and live-verified against a connected
client the same day. Two findings, both measured; neither is a broken
hover/hit-test.

1. DOMINANT ROOT CAUSE — RetailTooltipPresenter.OnTooltipShow gated on
   widget.AuthoredTooltipText (P0x49) alone. Retail's
   UIElement::StartTooltipAtMouse @0x00460D70 takes the RUNTIME m_TTText
   first (@0x00460DA3 IsValid -> @0x00460DAA verbatim) and only falls back
   to InqProperty(0x49) at @0x00460DDF. acdream ALREADY had the runtime
   layer — UiElement.GetTooltipText(), written by the Options/Chat/Config
   page controllers, KeyboardConfigController, the social pages and
   UiCheckboxBitfield64 — but nothing read it.

   Live-DAT measured: the Options toggle-row checkbox (0x2100002B template
   root 0x10000218, leaf 0x10000219) authors P0x47=0x10000397
   P0x48=0x21000041 P0x4B=true and an EMPTY P0x49 — the popup locator and
   the on-bit are authored; only the text arrives at runtime, exactly as
   UIOption_CheckboxBitfield64::CreateChildren @0x00485E65 stamps its
   siTooltip array. Re-measured client-wide: ALL 187 no-literal-text
   tooltip elements author both locator ids, i.e. the whole set is
   runtime-text targets.

   Fixed by ResolveTooltipText (retail's order), plus:
   - the P0x4B gate now applies only to the AUTHORED-text path, because
     retail's eight game-code SetTooltip sites set the on-bit themselves
     (__bitfield164 |= 0x20 at @0x004E1D5E/@0x004A52F4/@0x004C63AC/
     @0x004C67ED/@0x004C7000/@0x004C7218/@0x004D9617/@0x00467076);
   - the P0x48-absent fallback to the element's own LayoutDesc
     (@0x00460E7E, this->m_layout->m_DID) is ported via the new
     UiElement.SourceLayoutDid, threaded from LayoutImporter.Build's new
     sourceLayoutDid parameter and passed by Import + the four template
     resolvers.

2. THE "243 SHOWABLE" NUMBER WAS NEVER AN IN-WORLD NUMBER. Grouped
   re-sweep: all 243 sit in CHARACTER-CREATION layouts. The inventory
   window (0x21000023) and paperdoll (0x21000024) author exactly two
   between them — 0x100001D6 "Drag clothing and armor here to wear them"
   (the doll drag mask) and 0x100005BE (the Slots button). The first IS
   the user's single working tooltip, so the paperdoll was never a
   differential against a broken mechanism. Reachability was measured and
   is fine: 238/243 build as real non-ClickThrough hover targets.

LIVE VERIFICATION (connected testaccount/+Acdream, Release,
ACDREAM_RETAIL_UI=1): Options -> Character -> "Vivid Targeting Indicator"
now shows its full ID_PlayerOption_*_Help sentence; a temporary hover probe
confirmed the hover target is element 0x10000219 with runtime=True. The
paperdoll tooltip still shows. An inventory ITEM still shows nothing —
that is UIElement_UIItem::UpdateTooltip @0x004E1CB0 (retail shows the item
name, "%d %s"-prefixed when the stack is > 1), which stays deferred:
UiItemSlot is constructed programmatically at 6+ sites and carries neither
the P0x47 locator nor a name source, so it is its own slice.

Bookkeeping: register TS-85 narrowed (m_TTText READ side now ported; the
row now enumerates all 15 SetTooltip call sites split into ported vs
no-acdream-analog). #409's gate note rewritten to lead with the in-world
surfaces — the old note listed only chargen, which is why it could not
have caught this. Filed #411 for the hover-cursor scope addition: an
exhaustive raw scan of every ElementDesc found only 101 authored
MediaDescCursor entries, all on Dragbar/Resizebar with the 5 DIDs
RetailCursorCatalog already hardcodes, so retail has NO per-element cursor
for inventory items; the likely mechanism is the rollover STATE
(UIElement::MouseOverTop @0x004615D0) that UiItemSlot lacks entirely.

Gates: Release build 0 errors; App suite (live-DAT env) 5424/5421 passed/3
skips (was 5416/5413/3, +8 new tests); Runtime 1735/0; full solution (no
env) 14,631/14,561 passed/70 skipped/0 failed (was 14,623/14,554/69).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 21:44:44 +02:00
Erik
ebb227c1f6 Merge campaign-409-tooltips: #409 — the client-wide retail tooltip system
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Retail's five-part tooltip mechanism ported end to end: authored trigger
properties (P0x47-0x4B, P0x50) through the importer, the RetailTooltipPresenter
instantiating the authored 0x21000041 popup with retail's auto-resize/clamps,
the +32px cursor offset with display clamping, mouse-idle dwell (0.25s
default, per-element override), 10s auto-hide, retail's exact dismissal set,
capture suppression, and the Misc TooltipEnable/Delay client-local
preferences. 243 authored-text elements light up client-wide. Opus dual-lens
review + F1-F11 fix round complete; TS-85 records the honestly-deferred
m_TTText/SetTooltip family headed by the P0xD0 truncated-text auto-tooltip;
AD-106 records the z-order adaptation. Pending: the user's visual gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 20:55:29 +02:00
Erik
2719782dc0 fix(ui): #409 tooltip review fix round — F1-F11
Opus review of a377b9bf returned architectural PASS-with-findings /
retail-fidelity FAIL with F1-F12 (F12 info-only). All eleven fixed,
each re-derived against docs/research/named-retail/acclient_2013_pseudo_c.txt:

- F1 PositionAtMouse: retail offsets BOTH axes +32px before the clamp
  (StartTooltip @0x00459700, @0x00459739/@0x00459747) — was landing
  flush at the cursor.
- F2 UiRoot: the dwell timer now anchors to mouse-IDLE like retail's
  m_lastMouseMoveTime (MouseMoveHandler @0x0045E710), resetting on
  every move within the same widget while !_tooltipFired, not just on
  hover-enter.
- F3 register TS-85 rewritten: the "dynamic InqProperty(0x49) override"
  framing was false — UIElement::InqProperty @0x004638D0's base impl
  reads the same authored bags this port already reads. The real
  second text source (m_TTText/SetTooltip, headed by the P0xD0
  truncated-text auto-tooltip @0x00466F80) needs a per-line-position
  truncation model UiText doesn't have — sized disproportionate for
  this round and left honestly deferred rather than stubbed.
- F4 OnTooltipShow: null LayoutPolicy + Anchors=None on the popup root
  and text child before resizing, mirroring RetailMessageDialogView's
  sibling shape.
- F5 OnTooltipShow: return without mounting when the P0x4A text child
  doesn't resolve to a UiText (retail's DynamicCast gate,
  StartTooltip @0x0045DE90 @0x0045df65/@0x0045df6f) — was mounting an
  empty 30x30 bevel artifact.
- F6 UiRoot.Tick: the dwell-arm branch now requires Captured is null
  (CheckTooltip @0x0045B6E0 @0x0045b715) — a widget hovered before a
  drag/resize/capture began must not pop mid-gesture.
- F7 UiRoot.ReleaseCapture: no longer resets _tooltipFired
  (ReleaseMouseCapture @0x0045D2B0 touches only the idle timestamp) —
  a mouse-up while a tooltip is shown no longer tears it down and
  silently re-fires it 250ms later.
- F8 ApplyTooltipText: applies ResizeTo's own max/min width/height
  clamps (P0x3C/0x3D/0x3E/0x3F, @0x00463C30) before assigning the
  grown size; zeroes text.Padding to keep the measured size margin-
  comparable. New ElementInfo/UiElement plumbing for the four
  properties, same shape as the existing tooltip fields.
- F9 doc precision: sweep counts corrected 434->430 / 191->187 (live-
  DAT re-measured), the "243 showable" claim now measured exactly
  (not assumed) via a new Showable column in the sweep test, and the
  MiscSettings citation split into its two real mechanisms
  (RegisterPreference in Init vs. AttachPreference/SetPreferenceRange
  elsewhere).
- F10 register AD-106: the topmost guarantee is versus dialogs/screens
  only (the overlay popup layer and drag ghost still paint above
  regardless), and the per-tick BringToFront ratchet has four rungs,
  not three.
- F11 RetailUiRuntime.ResetSessionDialogs: now also calls the new
  UiRoot.ResetTooltipTracking() so a post-reset hover re-shows
  immediately instead of waiting out the stale fired-latch.

New pinning tests (RetailTooltipPresenterTests: F1/F2/F5/F6/F7/F8) each
verified to fail against the pre-fix behavior via a temporary revert-
and-rerun before being confirmed against the restored fix.
PortalProjectionTests.ProjectToClipLease_ReusesPooledWorkWithoutResultArrays
recurrence logged on issue #346 (already the tracking issue for this
load-sensitive flake) — hit twice under load this review, standalone
26/26, unrelated to #409.

Gates: Release build 0 errors; App suite (live-DAT env) 5416/5413
passed/3 skips (was 5410/5407/3, +6 new tests); Runtime 1735/0;
UI.Abstractions 926/0; full solution (no env, 69 skips expected)
14,623/14,554 passed/69 skipped/0 failed (was 14,617/14,548, +6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 20:54:15 +02:00
Erik
a377b9bff7 feat(ui): #409 — client-wide retail tooltip system
Full re-derivation from named-retail decomp: UIElement::StartTooltipAtMouse
@0x00460D70 -> UIElementManager::StartTooltip @0x0045DE90/@0x00459700,
UIElement::MouseHover @0x00462520 (P0x4B TooltipOn gate + global
m_tooltipEnable), UIElementManager::CheckTooltip @0x0045B6E0 (dwell/
auto-hide timer, default 0.25s/10s), SwitchMouseOver/DeletingElement
(dismissal). Corrects the earlier GF-16 investigation: P0x47 is the
element-desc id WITHIN the popup LayoutDesc (P0x48), not a "behavior
enum"; P0x4A is read off the popup's own instantiated root, not the
trigger element.

- ElementInfo/UiElement gain six tooltip data fields (P0x47/48/49/4A/4B/50),
  read generically by ElementReader and copied through LayoutImporter,
  mirroring the existing AuthoredInvisible passthrough pattern.
- UiRoot's existing CheckTooltip-derived hover timer gains TooltipShow/
  TooltipHide events, a per-element P0x50 delay override, and dismissal
  wiring at every retail-confirmed teardown site.
- RetailTooltipPresenter (owned by RetailUiRuntime, mounted alongside
  RetailDialogFactory) builds the popup via the existing LayoutImporter
  dat-lock seam, auto-resizes by the measured-vs-authored text delta
  (word-wrapped via the existing UiText.WrapWords primitive), positions
  at the mouse clamped to the display, and stays topmost over dialogs via
  its own later per-tick BringToFront (register AD-106).
- Misc.TooltipEnable/Misc.TooltipDelay are client-local UserPreferences
  (retail's own 2013 Config tab authors no visible row for either) —
  SettingsStore gains a MiscSettings section, no new options-panel row.
- Live-DAT sweep: 434 elements author >=1 trigger property (243 with
  literal text this port shows; 191 rely on retail's dynamic
  InqProperty(0x49) override, deferred as register TS-85 alongside the
  unmodeled P0x3D wrap-width override).

Gates: Release build 0 errors; App suite (live-DAT env) 5410/5407 passed/
3 skipped (was 5379/3); Runtime 1735/0 unchanged; UI.Abstractions 926/0;
full solution 14,617/14,548 passed/69 skipped/0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 20:03:24 +02:00
Erik
d3755eb231 Merge Campaign LA + Campaign CC: the acdream launcher and retail character creation, both CLOSED user-accepted
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Campaign LA (2026-08-14/15): Avalonia launcher/installer/updater
(Windows+Linux), retail character-management screen, session-config +
status-stream contract (§LA1), two connected gate rounds USER-PASSED.

Campaign CC (2026-08-15/16): the full retail character-creation flow —
chargen data layer, byte-exact 0xF656 + complete 0xF643 handling,
RuntimeCharacterCreationState, the six-page gmCharGenMainUI screen with
live 3D preview and the real color wheel, RandomizeCharacter open-roll,
launcher payload cycle. Seven slices review-closed; the connected gate's
extended round (GF-1..16, R2/R3/R4 re-tests) PASSED 2026-08-16 on build
1.0.2-cc.o. Milestone: the first live character created by acdream
against ACE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:14:30 +02:00
Erik
922c3f3d1a docs: CLAUDE.md Current state — Campaign CC CLOSED user-accepted 2026-08-16
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:13:55 +02:00
Erik
ff8b1ebc89 docs: Campaign CC connected gate PASSED — campaign CLOSED user-accepted 2026-08-16
The extended gate round (GF-1..16, R2/R3/R4 re-tests, fix batches A-G +
closeout + two re-test rounds) closed with the user's pass on build
1.0.2-cc.o. Plan status and ledger flipped; findings doc carries the
full round history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:13:36 +02:00
Erik
e6acb800cc fix(chargen): Campaign CC gate round 1 re-test 3 — R4-1..R4-4
Four visual residuals from the lead's own live-client captures of
1.0.2-cc.m, all root-caused via decomp + live-DAT evidence:

- R4-1: Skills credits value overlapped mid-caption again. Root cause
  was a missing UiLayoutPolicy raw-edge reflow on UiButton's value-child
  rect (the child is base-inherited across four sibling buttons of
  differing widths, so its baked-in OriginalParentWidth diverges from
  the actual 231px-wide Skills credits button) plus an HJustify.Right
  value child mapped to Center instead of a real far-edge Right.
- R4-2: the single-sprite scrollbar thumb tiled (GL_REPEAT) instead of
  drawing once — DrawTiled was reused for a small fixed marker graphic
  whose native size is far smaller than the track-proportional thumb
  rect. New DrawThumbMarker draws exactly one native-size instance.
- R4-3: the skills info-box formula line clipped past the surrounding
  gold frame's own authored bottom edge (the pane's own raw box is 20px
  taller than the frame that visually contains it) — clamp the pane's
  Height to the frame's bottom (register AD-105, since retail's
  ShowSkillsText has no code relationship to the frame to cite).
- R4-4: the Appearance help text started mid-sentence — the box was
  never touched by its page controller, so it kept UiText's chat-style
  PreserveEndOnLayout=true default; the scroll model's wasAtEnd check is
  vacuously true on its first-ever overflow transition, pinning the
  first render to the bottom. Set PreserveEndOnLayout=false (a static
  top-oriented report, not a transcript) and wired the box's own nested
  authored scrollbar, never wired before.

App suite live-DAT env 5372/3 -> 5379/3 (+7, zero regressions). Runtime
1735/0 unchanged. Full solution 14585/4 skips/1 failure (the documented
Core.Net NakEmission full-solution-only flake, confirmed standalone-pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:05:23 +02:00
Erik
28704db4bf docs: Campaign CC gate round 1 re-test 3 findings R4-1..R4-4 (lead's live captures)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 18:08:13 +02:00
Erik
91f84dec9f test(chargen): Campaign CC gate round 1 re-test 2 — R3-8 name-field exhaustive dump + closeout docs
R3-8: dumped EVERY property present on 0x10000402 (not just P0x17) across
every state, cross-referenced against UIElement_Text::OnSetAttribute's
complete case list (no UIElement_TextInput class exists in retail — the
name field is a plain UIElement_Text/m_filter-bearing field). The full
recognized-property space has no placeholder/prompt mechanism independent
of P0x17. The BaseElement/prototype-inheritance hypothesis is also ruled
out — the existing regression test already probes the fully-merged
ElementInfo (post BaseElement resolution) and finds nothing. The only
StringInfo-kind property present, 0x49, resolves to "Your name can be 32
characters long and cannot contain numbers or symbols." — but 0x49 is
part of the same five-property tooltip family ISSUES #409/GF-16 already
document client-wide (0x48's own DID, 0x21000041, is the EXACT tooltip
popup LayoutDesc #409 cites) — a hover tooltip, not an in-field
placeholder. No code change, per this batch's own "do not invent a
placeholder" contract — third independent negative result on this
question via three different mechanisms. The lead should request a live
retail screenshot before any further investigation.

Also carries the shared live-DAT regression suite for R3-1 through R3-7
(CharacterCreationLiveDatTests.cs holds tests spanning multiple findings
in one file, so they land together) and the RE-TEST 2 findings-doc
closeout writeup for all eight items.

App suite live-DAT env 5358/3 -> 5372/3 (+14, zero regressions). Runtime
1735/0 unchanged (untouched this round). Full solution: 14578 tests / 4
skips / 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:20:42 +02:00
Erik
2886f79f37 fix(chargen): Campaign CC gate round 1 re-test 2 — R3-5/R3-6 color-key swatch/gradient textures
Re-derived gmCGAppearancePage::DoColorSpots @0x0047d850 and DoGradDisk
@0x0047da90: retail does NOT multiply-tint the swatch/grad-circle's
authored sprite. It builds a fresh composited surface once (CreateLocalSurface
+ Blit), then calls SurfaceWindow::ReplaceColor against old-color
RGBAColor(0,0,0,1) (opaque black — the spot template's own placeholder
fill, live-DAT-pixel-confirmed: the 37x44 "spot" resource has a genuine
solid-black CENTER and a genuine non-black RING) — swapping every exact
opaque-black pixel for the swatch's real color while leaving the ring
untouched. A multiply-tint (Batch G's mechanism) is architecturally wrong:
black multiplied by any color stays black (never recolors the center),
and multiplying the ring's own non-black pixels corrupts them — exactly
the reported "we tint the ring" symptom.

Beyond-count swatches (R3-5b) use a COMPLETELY DIFFERENT authored resource
(enum 0x1000000f, "blank" — pixel-confirmed almost no black at all, i.e.
genuinely different art) shown untinted, and retail's own
pColor->SetVisible(1) is unconditional for all 9 swatches (never hidden).
For Eyes (R3-6), DoGradDisk's Eyes branch blits the "grad plug" icon
(enum 0x10000010) untinted, and SetSelection's own Eyes/non-Eyes tail
never hides m_pGradCircle at all — a correction to this port's prior
"_gradCircle.Visible = !isEyes" line.

Ported via a new ChargenColorSpotComposer (CPU-side decode-once + per-color
bake-and-cache-once through the existing TextureCache.UploadRgba8 seam —
the same shape IconComposer.GetSpellComponentIcon already established for
item icons, just matching black instead of white) and a new opt-in
UiButton.ColorKeyFaceResolver / reuse of the existing
UiDatElement.RuntimeImageTexture seam — both additive. Tint keeps its
existing meaning for every reader/test; the grad circle's Tint stays a
genuine multiply for the non-Eyes case (retail's own Blit_Multiply there).
Wired as a fourth late-bound composition seam (SwatchTextureSource), same
pattern/site as the existing three color-computation seams.

Code-complete, unit/live-DAT-tested (including pixel-level proof of the
spot/blank templates' actual content); the user's connected visual gate
is owed — no client launches this batch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:20:31 +02:00
Erik
c9313edc63 fix(chargen): Campaign CC gate round 1 re-test 2 — R3-4/R3-7 scrollbar thumb
Retail authors TWO distinct UIElement_Scrollbar thumb shapes.
DatWidgetFactory.BuildScrollbar's existing vertical-thumb detection was
built against chat's own scrollbar (0x10000012) — a 3-slice composite
where the thumb child carries no media of its own and three Type-3
grandchildren supply the top-cap/middle/bottom-cap sprites. The chargen
Skills listbox scrollbar (0x100003f8), Summary's OVERVIEW listbox
scrollbar (0x10000401), the Summary how-to box's scrollbar (0x100002e7),
and the shade slider (0x10000321) all instead author a SIMPLE
single-sprite thumb: the same structural child (Type 1, id 1, not the
inc/dec button) carries its OWN direct media and has ZERO children — the
3-slice-only search found nothing for this shape, so every Thumb*Sprite
stayed 0 regardless of overflow.

Fixed by falling back to the thumb's own DefaultImage when the slice
search finds nothing — additive; a thumb WITH real slice children (chat)
is unaffected. This one fix covers R3-4's three listbox thumbs, R3-7, and
— as a natural consequence of the same structural shape — the shade-slider
indicator half of R3-5(c); no separate fix was needed there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:20:16 +02:00
Erik
7f6e93033f fix(chargen): Campaign CC gate round 1 re-test 2 — R3-3 skills info-box VerticalJustify
The info-box title (0x100003fb, Y=435 H=100) and description (0x100003fc,
Y=460 H=100) panes' own authored boxes overlap by 75px, live-DAT-measured
— retail relies on vertical justification, not disjoint rects, to keep
them visually separate. Neither pane authors dat property 0x15, so both
fall to this port's shared unauthored-VJustify default (currently Center).

Byte-traced retail's real ctor default (UIElement_Text::UIElement_Text
@0x004685ff, m_eVerticalJustification = 4) against UIElement_Text::
CalcJustification @0x00467260's actual enum semantics (1=Center, 3-or-5=
the far edge/Bottom, anything else INCLUDING the ctor's own default of 4
= the near edge/Top): the correct unauthored default is Top, not Center —
a genuine client-wide enum-mapping bug in this port. Under Top both panes
render near their own box's top edge (25px apart, no collision); under
Center both cluster toward the middle of their overlapping boxes.

Scoped fix: CharacterCreationSkillsPage force-sets VerticalJustify=Top on
both panes directly, rather than fixing the shared mapping/default — that
bug is client-wide and could regress already-shipped FROZEN surfaces
(vitals, chat, main game UI, Options) that may rely on the current Center
default. The shared fix is filed as ISSUES #410 / register AD-104 for its
own dedicated investigation + regression sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:20:08 +02:00
Erik
7d6a7898f6 fix(chargen): Campaign CC gate round 1 re-test 2 — R3-1/R3-2 caption wrap
Batch E's UiButton.DrawBlockLabel/WrapBlockLines auto-wrapped any caption
that didn't fit its box width — decomp-wrong. UIElement_Text::
CalcJustification @0x00467260 (shared by GlyphList::Recalculate's
horizontal/vertical branches) shows retail's real per-glyph break decision
(both the width-triggered wrap AND the explicit-newline break) sits behind
ONE gate keyed on the OneLine flag; nothing in the decomp confines a
caption's wrap width to a sibling element's rect (Batch E's own ValueBox
confinement for the coexisting-value-label shape).

Live-DAT evidence: the Coordination attribute-slider label (0x100002ed)
authors OneLine=true (should never wrap); the Skills credits button's
"Available Skill Credits" caption measures 193px against its own full
231px button width (fits comfortably) — the 113px confined width Batch E
fed the wrap decision was never a real retail quantity.

Fixed: WrapBlockLines now splits ONLY on the explicit (already-normalized)
'\n' — never width-based. Strict superset of the pre-Batch-E single-line
draw for every already-correct caption; "Attribute\n Credits" still works.
The ValueBox confinement computation stays in OnDraw (still feeds the
Center-alignment tx formula) but no longer gates the wrap decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:19:57 +02:00
Erik
956b8d5b6b docs: Campaign CC gate round 1 re-test 2 findings R3-1..R3-9
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:02:18 +02:00
Erik
9efcd80e34 docs: Campaign CC gate round 1 closeout — register/ISSUES/ledger bookkeeping (F3, F12, F15)
Doc-only findings from the round review, plus the register rows the
three code commits' own bookkeeping notes reference:

- F3: AP-229 amended with the dialog-as-sibling z-order addendum — the
  same flat-sibling-list mechanism that motivates AP-229's own screen-
  layering row also covers RetailDialogFactory's open dialogs, which was
  GF-15's actual root cause (now fixed, but the underlying divergence —
  dialogs and screens sharing one z-order list at all — remains and
  could reintroduce the same failure class via a future sibling's own
  unconditional per-tick BringToFront).
- F5/F6: AP-230 amended with the second narrow-honor addendum (the
  LayoutImporter carve-out fix landed in the Group 3 code commit); the
  findings doc's "CHAT INPUT" label corrected to "chat transcript" in
  both places it appeared (0x2100006F/0x10000011 is the transcript
  display, not the input textbox).
- F12: the AD section header recounted 77 -> 79 (a direct physical count
  found it undercounted by 2); the AP section header's own "one high"
  drift-direction note corrected to "one low" — verified against the
  actual commit history (Batch A ended with 165 physical rows but a 164
  header; Batch B's recount correctly landed on 164, the header was
  never overcounting).
- F15: ISSUES.md #406 gains the crash-vs-incomplete-shutdown precedence
  sentence — ReportExited's _runFailure check runs first and returns
  immediately, so a crash always wins over a subsequently-failed
  shutdown for the same session's reported reason.
- AP-231 filed (the Group 2 commit's own ComposeFormula connector-text
  approximation — referenced in that commit's message but the register
  row itself was missed until this pass; 161 active AP rows).
- Campaign CC plan ledger gains a "Gate round 1" row with the full
  commit list for batches A-G plus this session's three closeout
  commits, superseding the ledger's stale "sole remaining acceptance
  step" framing (written before the connected gate ran and found the
  GF-1..GF-16 / R2-1..R2-8 findings this whole round fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:37:46 +02:00
Erik
bd359d5181 fix(chargen): Campaign CC gate round 1 closeout — Group 3: round review fixes (F4-F11, F14, F16)
The remaining code-bearing findings from the round review, F4-F16 minus
the doc-only items (batched separately):

- F4: three client-wide UiButton corpus sweeps (LabelBox path — exactly
  the 4 Town buttons, confined to chargen; conflicting custom-selection-
  pair + standard Normal/Highlight media — zero found, no gate
  tightening needed; per-state label-color map — 209 matches beyond
  chargen, confirming AP-222's mechanism has always been broadly active
  since it shipped generically in DatWidgetFactory).
- F5/F6: LayoutImporter's Batch C un-consumed-children carve-out now
  honors a child's own AuthoredInvisible flag (a narrow honor scoped to
  exactly that carve-out, not the general #408 client-wide one) — the
  chat transcript's new-text indicator (0x1000048C) was building as a
  visible phantom element retail never shows; verified both directions
  against the gold-frame pieces, which do not author Invisible.
- F7: BoundedProcessOutputCapture.AppendLine combines the line text and
  its trailing newline into one buffer and one file open/write/close
  instead of two.
- F9: corrected a stale comment in RuntimeSettingsTargets — #407 split
  DisplayModeCatalog's Resolutions/WindowedResolutions in two, so the
  fullscreen validator's own narrower list is now DELIBERATELY different
  from the Config dropdown's fuller offering, not the "must match" bug
  the comment described.
- F10: documented (not changed) why the LabelBox path's default 3px
  inset and the face-relative +4px gap in DatWidgetFactory.BuildButton
  are deliberately different numbers — neither carries a retail
  citation, and moving either to match the other would be an unfounded
  guess on a button that currently works correctly.
- F11: Heritage/Profession/Summary/Town description pages now compose
  DatRichText.Compose's result ONCE inside their already revision-gated
  Refresh, caching the built line list instead of re-wrapping on every
  draw call.
- F14: documented (not changed) why PrivateEntityViewportRenderer's
  _animatedIds set carrying a reserved-but-never-drawn backdrop id is
  harmless — BuildDrawEntities already excludes a null/empty backdrop
  from the actual draw list, so the id is never looked up.
- F16: the Summary preview now uses its own render-id pair
  (SummaryPreviewRenderId/SummaryPreviewBackdropRenderId, 0xDA11D035/
  0xDA11D036) instead of sharing the Appearance page's
  (0xDA11D032/0xDA11D034) — confirmed by tracing
  FixedEntityTextureOwnerLease through TextureCache to
  CompositeTextureArrayCache's shared owner tracker that both pages'
  previews share ONE process-wide TextureCache, so sharing render ids
  was a real cross-page texture-release collision (either page's own
  re-dress or disposal could release the OTHER page's still-active
  textures), not a theoretical one.

F3's own register bookkeeping (AP-229 addendum) and F12's register/AD
header-count corrections land in the docs-only commit alongside F15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:34:07 +02:00
Erik
0fed5fdd91 fix(chargen): Campaign CC gate round 1 closeout — Group 2: Skills page four-bucket model
Ports the last remaining half of retail's Skills page: the four-bucket
sorted skill list (Specialized/Trained/UseableUntrained/UnuseableUntrained,
UpdateSkillEntry's own iMinlevel <= 1 test), plus the info box's
description + formula completion.

- ChargenSkillDetail/ChargenSkillFormula (Core) thread SkillBase.MinLevel/
  Description/Formula from the global SkillTable, exposed via a new
  ChargenOptions.TryGetSkillDetail (nullable-with-default parameter, so
  every pre-existing ChargenOptions call site compiles unchanged).
  ChargenTableReader.Project populates it from the same SkillTable loop
  that already builds GlobalSkillCostsBySkillId.
- CharacterCreationSkillsPage.RebuildRows now groups every costable skill
  into SkillBucket, sorts each bucket alphabetically by name
  (InsertEntrySorted's wcscmp, ported as string.CompareOrdinal), and
  builds one Templates[0] header row per bucket ahead of that bucket's
  Templates[1] skill rows — DoSkillRecords' own unconditional
  4-header-then-populate order. A level change re-buckets the row
  (detected per-refresh against each row's own cached bucket, then a
  full rebuild with the current selection explicitly preserved).
- RefreshInfoBox now composes description (word-wrapped via
  DatRichText.Compose) + the level-gated bonus line (an exact, unwrapped
  literal — NOT routed through word-wrap, which would have collapsed its
  authored double-space formatting) + ComposeFormula's "Formula : ..."
  line (MakeSkillFormula ported with high confidence for the prefix/
  per-attribute-term/divisor/bonus-suffix shape; the two-attribute
  connector text is a disclosed approximation, register AP-231, since
  the decompiled function's own connector literals could not be
  recovered byte-exact by this session's static-only tooling).

Register: AP-213 RETIRED (160 active rows). Live-DAT gate: the installed
SkillTable's MinLevel distribution matches the investigation's own
recorded finding exactly (38 entries, 23 useable-untrained / 15
trained-required). 3 new fixture tests + 1 new live-DAT test; 3
pre-existing integration tests fixed (they captured row widget
references before a bucket-changing click, which now rebuilds and
discards those references — a real, correct consequence of the new
model, not a bug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:33:39 +02:00
Erik
e1d7d09599 fix(chargen): Campaign CC gate round 1 closeout — Group 1: real color wheel wiring
Lands Batch G's two STOPPED items, making the real palette-color swatch
wheel visually live instead of inert:

- UiButton and UiDatElement gain a per-instance Tint property threaded
  into every existing DrawSprite call (defaults to Vector4.One, so every
  pre-existing button/element is byte-identical unless a caller sets a
  non-identity tint).
- CharacterCreationAppearancePage now sets Tint directly on each color
  swatch button and the GradCircle element, replacing the Batch G
  flat-fill ChargenSwatchColorTile overlay outright — an opaque
  rectangle drawn on top can never reproduce retail's actual
  SurfaceWindow::BlitAndColor(..., Blit_Multiply, color) multiply blend,
  only a genuine per-instance sprite tint can, so the overlay approach is
  deleted rather than layered under the new mechanism.
- CharacterCreationUiController and RetailUiRuntime grow pass-through
  properties (AppearancePalSetSource/AppearanceClothingTableSource/
  AppearancePaletteColorSource) mirroring the existing PreviewControl
  seam, so LivePresentationComposition can wire a DAT-backed
  ChargenAppearanceCatalog into the Appearance page (wiring itself lands
  with the Group 3 commit, since it shares a file with an unrelated F16
  fix).

Register: AP-216/AP-217 RETIRED (161 -> now further reduced in later
commits) — both rows' remaining gaps are closed, not merely narrowed.
CharacterCreationAppearancePageSwatchColorTests updated for the new
Tint-based assertions (two pre-existing assertions were carried over
incorrectly from the old overlay-visibility model and are corrected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:33:19 +02:00