diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 1770bdd1..e4fc67c5 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -64,7 +64,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 83 active rows (AD-108 filed 2026-08-17 at the night-round review fix round (F9) — `MapPageController.ResolveSwallowedIcon`'s standalone re-import of the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring; AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) +## 2. Adaptation (AD) — 83 active rows (AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate visible-cell availability, full-catalog containment-root validation, and the @@ -107,7 +107,7 @@ readiness/requeue adaptation. See | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| -| AD-108 | **Filed 2026-08-17 at the night-round review fix round (F9); CORRECTED same day during that round's own live-verification step.** `MapPageController.ResolveSwallowedIcon` re-imports the Map tab's player-location and house-location icons (`0x100001ED`/`0x100001EE`) STANDALONE via the panel's template resolver rather than finding them as ordinary descendants of the built page tree. Live-DAT-confirmed structural cause (`MapHousePanelSlotProbeTests`' follow-up dump): `m_pMap` (`0x100001EC`) is itself authored as a Type-1 `UIElement_Button` — the GM click-to-teleport feature `gmMapUI::ListenToElementMessage @0x004a2350` idMessage `0x1c` reads — and the two icons are authored as ITS OWN nested dat children, not siblings. `UiButton.ConsumesDatChildren` swallows a button's dat children as skin/label parts during the normal import walk, so they never appear anywhere `UiElement.FindDescendant` can reach against the built page root. **CORRECTION: this row originally described the standalone re-import as WORKING (reusing the town-hotspot pattern). Live verification the same session found the connected client logging `[D.2b] Map tab: icon 0x100001ED did not resolve` / `...0x100001EE did not resolve` for BOTH icons — the standalone re-import does not actually find them.** A throwaway diagnostic (not committed) confirmed why: `LayoutImporter.ImportInfos(dats, hostLayoutId, elementId)`'s `FindDesc` walks the LayoutDesc's raw top-level `Elements` table (exactly ONE entry for host layout `0x2100006E`) recursing through `ElementDesc.Children` — a purely structural walk with no tab-page/state-descriptor resolution — and calling it directly with `0x100001EC`/`0x100001ED`/`0x100001EE` returns null. Resolving the panel's own SLOT first (`ImportInfos(dats, 0x2100006E, 0x1000018C)` — what `MountMapHousePanel` actually does to build the whole panel) and searching THAT tree DOES find `m_pMap` with both icon children present — so the icons are real and correctly nested, but only reachable through the full panel-slot resolve pathway (likely tab-page wiring), not a cold `ImportInfos(hostLayout, elementId)` call starting from the element id alone. `ResolveSwallowedIcon`'s "re-import as if standalone" approach is architecturally wrong for these two elements, unlike the town-hotspot template (a genuine standalone catalog entry addressable by `(templateLayoutId, templateElementId)`, which DOES work). Filed as a follow-up task (see `spawn_task` "Fix Map tab player/house icon resolution"). | `src/AcDream.App/UI/Layout/MapPageController.cs:144-145` (the two `ResolveSwallowedIcon` call sites in `Bind`); `:178-191` (`ResolveSwallowedIcon`'s own body) | The icons' authored local position from a successful standalone import would be irrelevant since `PlaceMarker` overwrites `Left`/`Top` on every `Refresh` anyway — but this reasoning is currently moot since the import never succeeds at all on the installed DAT. | **This is not a future risk — it is the CURRENT live-DAT state, confirmed 2026-08-17.** `ResolveSwallowedIcon` already logs a `[D.2b]` warning and returns null rather than throwing, so the failure mode is "no player/house marker ever shows" — live-observed, not hypothetical. F1's byte-decoded `PlaceMarkerOnMap` formula (this same session) cannot be visually confirmed against the running client until this is fixed; it remains verified only at the unit-test/golden-pixel level. | `gmMapUI::PostInit @0x004a1c70` (child resolution); `gmMapUI::ListenToElementMessage @0x004a2350` idMessage `0x1c` (confirms `m_pMap` IS a button, not a passive container) | +| AD-108 | **Filed 2026-08-17 at the night-round review fix round (F9); MECHANISM REPLACED the same day at the overnight round's final fix, after live verification found the row's original standalone re-import resolving NOTHING.** Retail authors the Map tab's player-location and house-location icons (`0x100001ED`/`0x100001EE`) as ordinary nested dat children of `m_pMap` (`0x100001EC`) — itself a Type-1 `UIElement_Button`, the GM click-to-teleport feature `gmMapUI::ListenToElementMessage @0x004a2350` idMessage `0x1c` reads — and `gmMapUI::PostInit @0x004a1c70` resolves them as ordinary live child elements. acdream's `UiButton.ConsumesDatChildren` swallows a button's dat children as skin/label parts during the normal import walk, so the two icons never exist in the built tree and `UiElement.FindDescendant` against the page root returns null for them. **The shipped adaptation:** `MapPageController.Bind` finds each icon's `ElementInfo` under `m_pMap`'s own ALREADY-RESOLVED info subtree — `pageInfo`, a subtree of the panel-slot resolve `ImportInfos(dats, 0x2100006E, 0x1000018C)`, the ONLY pathway that materializes these infos at all — and BUILDS it through the new `Bindings.IconBuilder` seam (production: `LayoutImporter.Build(info, ...)` under the DAT lock — the build half of `RowTemplateResolver`'s shape, no import half), attaching the result as a runtime child of the built `m_pMap`. Live-DAT-pinned structural facts (`MapHousePanelLiveDatMountTests`, the pin the original gap proved missing): a cold `ImportInfos(dats, hostLayoutId, iconElementId)` returns null for BOTH icons — its `FindDesc` walks the LayoutDesc's raw top-level `Elements` table (exactly ONE entry for host layout `0x2100006E`) recursing through `ElementDesc.Children`, a purely structural walk with no tab-page/state-descriptor resolution — while the full panel-slot resolve materializes both icons nested under `m_pMap` with real authored extents. (The town-hotspot template `0x100001F0` is different in kind: a genuine standalone catalog entry addressable by `(templateLayoutId, templateElementId)`, whose import-then-build resolution is correct and unchanged.) `ResolveSwallowedIcon` also prefers an icon the normal build walk DID produce (`FindDescendant` under `m_pMap` first), so a future `ConsumesDatChildren` policy change cannot leave a second, permanently-static copy behind the live marker. **Second mechanism half (found by this fix's own F1 live verification):** both icons are detached from the per-frame authored layout pass (`PrepareIcon` sets `Anchors = AnchorEdges.None`, which also clears any imported `LayoutPolicy`) because `PlaceMarker` owns their position outright (retail's `gmMapUI::Update` re-places both markers every tick, and retail's `UpdateForParentSizeChange` runs only on actual parent resize) — acdream re-runs `ApplyAnchor` per frame, and the icon's compatibility anchor had captured the authored `(0,0)` rect while the panel window was still hidden, re-asserting it every frame over PlaceMarker's writes: a live-observed visible green ring pinned to `m_pMap`'s top-left corner regardless of player position, with only the coordinate text correct. | `src/AcDream.App/UI/Layout/MapPageController.cs` (`Bind`'s two `ResolveSwallowedIcon` call sites, `ResolveSwallowedIcon`'s body, `Bindings.IconBuilder`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`MountMapHousePanel`'s `BuildSwallowedIcon`) | The rebuilt icons carry their authored ids and extents — `PlaceMarkerOnMap`'s centering divides the icon's own Width/Height, and the pin test asserts non-degenerate extents on the installed DAT. Their authored local position is irrelevant: `PlaceMarker` overwrites `Left`/`Top` on every 5 s `Refresh`, and each icon starts hidden until the first refresh decides real visibility — same net presentation as retail's find-the-child. | A future DAT regeneration that reauthors the icons OUTSIDE `m_pMap`'s subtree would leave `FindInfo(mapInfo, iconId)` null again — the same silent "[D.2b] … not authored under m_pMap" log-and-hide failure mode this row's original defect had, but now caught by `MapHousePanelLiveDatMountTests` failing on the next suite run instead of only at a connected gate. | `gmMapUI::PostInit @0x004a1c70` (child resolution); `gmMapUI::ListenToElementMessage @0x004a2350` idMessage `0x1c` (confirms `m_pMap` IS a button, not a passive container); `gmMapUI::PlaceMarkerOnMap @0x004a18b0` (the marker math consuming the rebuilt icons) | | AD-106 | **Filed 2026-08-16 at #409 (client-wide retail tooltip system).** Retail's tooltip popup is a separate always-on-top presentation surface — `UIElementManager::StartTooltip @0x00459700` positions and latches it into `m_pTooltipElement`, drawn independently of the ordinary `UIElement` sibling tree (the SAME class of separation the AP-229 register row already establishes for retail's dialogs vs acdream's flat sibling list under one `Host.Root`). `RetailTooltipPresenter` instead mounts the popup as an ordinary `UiRoot` child sibling (`_host.AddChild(root)`) and keeps it topmost by calling `BringToFront` from its OWN `Tick()`, which `RetailUiRuntime.Tick` schedules AFTER both `RetailDialogFactory.Tick()` and `Host.Tick()` in the same frame — guaranteeing the tooltip wins whatever z-order race those two just ran, every frame, regardless of which dialog/screen last called its own `BringToFront`. | `src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs` (`Tick`, `OnTooltipShow`'s `AddChild`/`BringToFront`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`Tick`'s three-call ordering, `MountTooltipPresenter`) | Reproduces the one observable invariant a user can check (tooltips always draw on top of dialogs and screens) without porting retail's literal separate-layer architecture (no second draw pass, no dedicated presentation root) — the SAME tradeoff AP-229 already accepted for dialogs, extended one layer further. The ordering is enforced structurally (three sequential calls in one method), not by convention, so it cannot silently regress from an unrelated edit reordering unrelated `Tick` calls elsewhere. **F10 correction (2026-08-16 review round), two honest additions:** (1) the guarantee is versus dialogs/screens ONLY — `UiRoot.DrawCore`'s own second pass (`ctx.BeginOverlayLayer(); DrawOverlays(ctx); DrawDragGhost(ctx);`) routes open dropdown/menu popups and the drag ghost to a renderer overlay layer that paints over the WHOLE sibling tree unconditionally, so both still paint above a shown tooltip regardless of any `BringToFront` ordering — no z-order fix in the sibling tree can reach that layer. (2) counting the full chain by its own actual participants (not just the three calls local to `RetailUiRuntime.Tick`'s tooltip-adjacent lines), the per-tick `BringToFront` ratchet has FOUR rungs in frame order: `CharacterManagementUiController.Tick`, `CharacterCreationUiController.Tick` (both named in `RetailDialogFactory`'s own GF-15 doc comment as the screens it re-asserts over), `RetailDialogFactory.Tick`, then `RetailTooltipPresenter.Tick`. Four independent per-tick self-reraises stacked by tick ORDER is a design smell — a correct z-order model would need at most one authoritative comparison, not N racing assertions — but is bounded and enumerable in practice (no unbounded surface list, the order is fixed source, not runtime-discovered) so it is left as observed rather than restructured this round. | A FUTURE always-on-top UI surface that calls its own unconditional per-tick `BringToFront` AFTER `TooltipPresenter?.Tick()` in `RetailUiRuntime.Tick`'s ordering could bury a currently-shown tooltip — the exact failure class AP-229 already named for dialogs-vs-screens, now with four layers instead of two. | `UIElementManager::StartTooltip @0x00459700` (`m_pTooltipElement` ownership); AP-229's own dialog/screen precedent | | AD-73 | Filed 2026-08-11 at the Campaign OP OP2 rework (fix round after a double REJECT). `UiTabPanel` (dat Type 8, formerly `UiTabControl`) does NOT perform retail's automatic tab-table wiring / default-page activation at construction. Retail `UIElement_Panel::SetupTabPageHash @0x0046C2E0` + `::Update @0x0046BD00` unconditionally activate the authored default page for ANY instance that carries a tab table. `UiTabPanel` instead stays DORMANT — no click binding, no page-visibility flip, no tab Open/Closed write — until a controller explicitly calls `ActivateTabBehavior()`. | `src/AcDream.App/UI/UiTabPanel.cs` (`ActivateTabBehavior`); factory site `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` (Type-8 arm) | Four already-shipped Type-8 hosts author a tab table today — character sheet root `0x10000227`, spellbook root `0x100002A8`, and vendor `0x100000B8` already implement this exact switching in their own C# controllers (`CharacterStatController`/`SpellbookWindowController`/`VendorUiController`); activating `UiTabPanel`'s own copy unconditionally would double-drive the same page-visibility/tab-state writes those controllers already own. Combat `0x100000A2` has no controller at all and is INTENTIONALLY left inert (its 8 stance pages have no switching UI yet) rather than have `UiTabPanel` silently take ownership. Only newly-authored hosts opt in (Options panel, Campaign OP slice OP3+; Configure Keyboard, OP8). This is what let the unconditional Type-8 factory mapping become safe after the OP2 REJECT (`docs/research/2026-08-11-op2-review-blast.md`, `docs/research/2026-08-11-op2-review-mechanism.md`). | A future panel that authors a Type-8 tab table but never gets a controller call to `ActivateTabBehavior()` renders with every tab button at its authored default (Closed) and every page slot at its default `Visible=true` — i.e. every page overlapping, no single active page — instead of retail's exactly-one-visible-page behavior. This is silent unless the diagnostic `UnresolvedEntries`/`BehaviorActive` surface is checked; a controller author who forgets the activation call will see a visually broken tab host, not a crash. | `UIElement_Panel::SetupTabPageHash @0x0046C2E0`; `UIElement_Panel::Update @0x0046BD00`; `UIElement_Panel::OpenTab @0x0046BE20`. ADDENDUM (2026-08-11, re-review closure): `UiTemplateListBox` additionally reports `ConsumesDatChildren = true` where the pre-rework fallback did not — inert against every shipped layout because no Type-5 element in any of the 32 fixtures authors children (now conformance-PINNED in `OP2ReworkBlastRadiusConformanceTests`, so an authored child appearing in a future DAT regeneration fails the build instead of silently vanishing) | | ~~AD-53~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `Transition.CliffSlide` now consumes only `collision_info.last_known_contact_plane.N`, exactly as retail does. The invented `LastWalkablePlane -> LastKnownContactPlane -> UnitZ` fallback chain is gone; invalid/default or parallel data takes retail's degenerate `OK_TS` return. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07) | diff --git a/src/AcDream.App/UI/Layout/MapPageController.cs b/src/AcDream.App/UI/Layout/MapPageController.cs index 53c577c1..07eaea6d 100644 --- a/src/AcDream.App/UI/Layout/MapPageController.cs +++ b/src/AcDream.App/UI/Layout/MapPageController.cs @@ -58,7 +58,15 @@ public sealed class MapPageController // branch — the house icon starts/stays hidden) so this page works // standalone before that lands. Func HousePosition, - Func TemplateResolver); + Func TemplateResolver, + // Builds one UiElement subtree from an ALREADY-RESOLVED ElementInfo + // (production: LayoutImporter.Build under the DAT lock — the same + // build half RowTemplateResolver uses, without the import half). + // Used for m_pMap's two button-swallowed icon children, whose + // ElementInfos only exist inside the full panel-slot resolve tree — + // a cold ImportInfos(hostLayout, iconId) re-import CANNOT find them + // (register row AD-108's live-DAT finding; see Bind's own doc). + Func IconBuilder); private readonly UiElement? _dateTimeText; private readonly UiElement? _map; @@ -108,13 +116,20 @@ public sealed class MapPageController /// siblings. swallows a /// button's dat children as skin/label parts, so they never appear in /// the normally-built tree — - /// against the page root always returns null for them. They're - /// resolved the SAME way the town hotspot template is: re-imported - /// standalone via 's - /// against the panel's own host - /// LayoutDesc, then attached under m_pMap directly — their - /// authored local position is irrelevant since - /// overwrites it every refresh. + /// against the page root always returns null for them. Their + /// s, however, DO survive: + /// is a subtree of the panel's full slot resolve + /// (ImportInfos(0x2100006E, 0x1000018C)), the only pathway that + /// materializes them at all — a cold + /// ImportInfos(hostLayoutId, iconElementId) starting from the + /// icon id returns null on the live DAT because the raw LayoutDesc + /// Elements-table walk never reaches them (register row AD-108's + /// live-DAT finding, 2026-08-17). So the icons are resolved by finding + /// their infos under m_pMap's own already-resolved info and + /// BUILDING each via , then attached + /// under m_pMap directly — their authored local position is + /// irrelevant since overwrites it every + /// refresh. /// /// public static MapPageController? Bind(UiElement page, ElementInfo pageInfo, Bindings bindings) @@ -141,8 +156,8 @@ public sealed class MapPageController markerArea = (x0, x1, y0, y1); } - UiElement? playerIcon = ResolveSwallowedIcon(map, bindings.TemplateResolver, PlayerIconId); - UiElement? houseIcon = ResolveSwallowedIcon(map, bindings.TemplateResolver, HouseIconId); + UiElement? playerIcon = ResolveSwallowedIcon(map, mapInfo, bindings.IconBuilder, PlayerIconId); + UiElement? houseIcon = ResolveSwallowedIcon(map, mapInfo, bindings.IconBuilder, HouseIconId); var controller = new MapPageController( UiElement.FindDescendant(page, DateTimeTextId), @@ -170,23 +185,60 @@ public sealed class MapPageController return controller; } - /// Re-resolves one of m_pMap's button-swallowed nested - /// icon children standalone (see 's own doc) and - /// attaches it under . Starts hidden — the first + /// Resolves one of m_pMap's button-swallowed nested + /// icon children by finding its under + /// — the panel-slot resolve tree, the ONLY + /// place these infos exist (see 's own doc + register + /// row AD-108) — building it via , and + /// attaching it under . Starts hidden — the first /// call (from ) decides real /// visibility. private static UiElement? ResolveSwallowedIcon( - UiElement map, Func templateResolver, uint iconElementId) + UiElement map, ElementInfo? mapInfo, Func iconBuilder, uint iconElementId) { - UiElement? icon = templateResolver(MapHousePanelController.HostLayoutId, iconElementId); + // If the normal build walk ever stops swallowing m_pMap's dat + // children (a future UiButton.ConsumesDatChildren policy change), + // the icon already exists in the built tree — use it rather than + // building a second, permanently-static copy behind the live + // marker. Retail's own PostInit is exactly this find-the-child. + UiElement? existing = UiElement.FindDescendant(map, iconElementId); + if (existing is not null) + return PrepareIcon(existing); + + ElementInfo? iconInfo = mapInfo is null ? null : FindInfo(mapInfo, iconElementId); + if (iconInfo is null) + { + Console.WriteLine( + $"[D.2b] Map tab: icon 0x{iconElementId:X8} not authored under m_pMap's resolved " + + "info tree — it will not be shown."); + return null; + } + + UiElement? icon = iconBuilder(iconInfo); if (icon is null) { Console.WriteLine( - $"[D.2b] Map tab: icon 0x{iconElementId:X8} did not resolve — it will not be shown."); + $"[D.2b] Map tab: icon 0x{iconElementId:X8} did not build — it will not be shown."); return null; } + map.AddChild(PrepareIcon(icon)); + return icon; + } + + /// Marks one marker icon as runtime-positioned. + /// owns the element's position outright (retail's gmMapUI::Update + /// re-places both markers every tick) — but acdream re-runs the authored + /// layout pass per frame, so the compatibility anchor capture (and any + /// imported raw-edge , which the + /// setter clears) would re-assert the + /// authored (0,0) rect every frame, silently overwriting PlaceMarker's + /// writes — the F1 live finding: a visible green ring pinned to m_pMap's + /// top-left corner regardless of the player's true position. Starts + /// hidden — the first decides real visibility. + private static UiElement PrepareIcon(UiElement icon) + { + icon.Anchors = AnchorEdges.None; icon.Visible = false; - map.AddChild(icon); return icon; } diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index f8712a41..96cc726b 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -3329,6 +3329,25 @@ public sealed class RetailUiRuntime : IDisposable return hotspotTemplate.Resolve(templateLayoutId, templateElementId); } + // m_pMap's two button-swallowed icon children (player/house markers) + // only exist as ElementInfos INSIDE rootInfo's own panel-slot resolve + // tree — a cold ImportInfos(hostLayout, iconId) re-import returns + // null on the live DAT (register row AD-108). MapPageController.Bind + // locates each icon's info under m_pMap and calls this seam to build + // it: the build half of RowTemplateResolver's shape, no import half. + // Monitor re-entrancy on DatLock is established for this mount path + // (Bind itself runs under the lock below, same as + // ResolveHotspotTemplate's own re-entrant take). + UiElement? BuildSwallowedIcon(ElementInfo iconInfo) + { + lock (_bindings.Assets.DatLock) + return LayoutImporter.Build( + iconInfo, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont).Root; + } + MapHouseRuntimeBindings mh = _bindings.MapHouse; var callbacks = new Layout.MapHousePanelController.Callbacks( Toggle: () => ToggleWindow(WindowNames.MapHouse), @@ -3336,7 +3355,8 @@ public sealed class RetailUiRuntime : IDisposable CurrentCalendar: mh.CurrentCalendar, PlayerCellId: mh.PlayerCellId, HousePosition: mh.HousePosition ?? (static () => null), - TemplateResolver: ResolveHotspotTemplate), + TemplateResolver: ResolveHotspotTemplate, + IconBuilder: BuildSwallowedIcon), House: new Layout.HousePageController.Bindings( Lines: mh.HouseLines ?? (static () => Array.Empty()), OnShown: mh.HouseShown, diff --git a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs index 518bad8c..47ca4eef 100644 --- a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs @@ -17,17 +17,14 @@ namespace AcDream.App.Tests.UI.Layout; /// public sealed class MapHousePanelControllerTests { - /// Serves BOTH the town-hotspot template (any (layoutId, - /// elementId) pair not the player/house icon ids) and the two icons - /// re-resolves standalone - /// (m_pMap's own button-swallowed children). Returns a + /// Serves the town-hotspot template. Returns a /// — matching the live template's own authored /// Type 1 (MapHousePanelSlotProbeTests: "hotspot template /// type=1") — so 's /// marker is UiButton tooltip-text branch is actually exercised /// by these tests. A real would set /// DatElementId the same way - /// does, so tests that need to find these icons back by id after the + /// does, so tests that need to find these markers back by id after the /// fact need it too. private static UiElement? FakeHotspotTemplate(uint layoutId, uint elementId) => new UiButton(new ElementInfo(), static _ => (0u, 0, 0)) @@ -37,6 +34,22 @@ public sealed class MapHousePanelControllerTests DatElementId = elementId, }; + /// The + /// seam: builds m_pMap's two button-swallowed icon children from their + /// OWN s inside the panel-slot resolve tree + /// (register row AD-108 — a standalone re-import cannot find them on + /// the live DAT, so the icons are found under the already-resolved + /// pageInfo and built through this seam instead). Mirrors + /// production's LayoutImporter.Build(info, ...).Root, which sets + /// DatElementId from the info's own id. + private static UiElement? FakeIconBuilder(ElementInfo info) + => new UiButton(new ElementInfo(), static _ => (0u, 0, 0)) + { + Width = 10f, + Height = 10f, + DatElementId = info.Id, + }; + /// The House ListBox's own row template resolves to a /// in the live DAT (MapHousePanelSlotProbeTests: /// "row template type=12" — UIElement_Text), unlike the Map tab's @@ -59,7 +72,8 @@ public sealed class MapHousePanelControllerTests CurrentCalendar: currentCalendar ?? (static () => default), PlayerCellId: playerCellId ?? (static () => 0u), HousePosition: housePosition ?? (static () => null), - TemplateResolver: FakeHotspotTemplate), + TemplateResolver: FakeHotspotTemplate, + IconBuilder: FakeIconBuilder), House: new HousePageController.Bindings( Lines: houseLines ?? (static () => Array.Empty()), OnShown: () => calls.Add("house-shown"), @@ -158,10 +172,10 @@ public sealed class MapHousePanelControllerTests UiElement? map = UiElement.FindDescendant(controller!.Root, MapPageController.MapWidgetId); Assert.NotNull(map); - // m_pMap's own children are the player/house icons (re-resolved - // standalone — see MapPageController.Bind's doc on why m_pMap being - // a Button swallows its authored nested children) PLUS the 53 town - // hotspots. + // m_pMap's own children are the player/house icons (found under the + // panel-slot resolve tree and rebuilt via Bindings.IconBuilder — see + // MapPageController.Bind's doc on why m_pMap being a Button swallows + // its authored nested children) PLUS the 53 town hotspots. var townMarkers = map!.Children .Where(c => c.DatElementId != MapPageController.PlayerIconId && c.DatElementId != MapPageController.HouseIconId) diff --git a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelLiveDatMountTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelLiveDatMountTests.cs new file mode 100644 index 00000000..a784036b --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelLiveDatMountTests.cs @@ -0,0 +1,157 @@ +using System.IO; +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using AcDream.Core.Net.Messages; +using DatReaderWriter; +using DatReaderWriter.Options; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// Live-DAT mount pin for the Map tab's player/house marker icons — THE test +/// that would have caught the Batch C icon-resolution gap (register row +/// AD-108) at commit time. +/// +/// +/// The fixture-based could not +/// catch it: their fake resolvers answer ANY id, so the production resolve +/// mechanism itself was never exercised against real data. This test +/// reproduces 's exact +/// recipe against the INSTALLED DATs (sprite/font resolution stubbed — +/// structure only, same as every committed-fixture build): the panel-slot +/// LayoutImporter.ImportInfos(dats, hostLayoutId, slotElementId) +/// import, , real +/// import-then-build hotspot/template resolution, and the +/// build seam — +/// then asserts the two icons actually materialize as built elements. +/// +/// +/// +/// Gated like every other installed-DAT family here: +/// [InstalledDatFact], opt in with +/// ACDREAM_PROBE_LIVE_MOUNT=1 (the App suite's live-DAT baseline +/// mode); ACDREAM_DAT_DIR overrides the ordinary +/// Documents/Asheron's Call location. +/// +/// +public sealed class MapHousePanelLiveDatMountTests +{ + private static string DatDirectory => + Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Documents", + "Asheron's Call"); + + [InstalledDatFact] + public void MountRecipe_ResolvesPlayerAndHouseIcons_UnderTheMapWidget() + { + using var dats = new DatCollection(DatDirectory, DatAccessType.Read); + + // ── AD-108's structural facts, pinned ──────────────────────────── + // 1) A COLD standalone re-import starting from the icon's own id + // returns null: the raw LayoutDesc Elements-table walk never + // reaches m_pMap's nested children. This is WHY the IconBuilder + // seam exists — if a future DAT regeneration makes this resolve, + // this pin flags that the seam could be revisited. + Assert.Null(LayoutImporter.ImportInfos( + dats, + MapHousePanelController.HostLayoutId, + MapPageController.PlayerIconId)); + Assert.Null(LayoutImporter.ImportInfos( + dats, + MapHousePanelController.HostLayoutId, + MapPageController.HouseIconId)); + + // 2) The full panel-slot resolve — what MountMapHousePanel actually + // imports — DOES materialize both icons, nested under m_pMap. + ElementInfo? rootInfo = LayoutImporter.ImportInfos( + dats, + MapHousePanelController.HostLayoutId, + MapHousePanelController.SlotElementId); + Assert.NotNull(rootInfo); + + // ── The production mount recipe (sprites/fonts stubbed) ────────── + ImportedLayout layout = LayoutImporter.Build(rootInfo!, static _ => (0u, 0, 0), null); + + UiElement? ResolveTemplate(uint layoutId, uint elementId) + { + ElementInfo? info = LayoutImporter.ImportInfos(dats, layoutId, elementId); + return info is null + ? null + : LayoutImporter.Build(info, static _ => (0u, 0, 0), null).Root; + } + + UiElement? BuildIcon(ElementInfo info) + => LayoutImporter.Build(info, static _ => (0u, 0, 0), null).Root; + + // Mutable cell: Bind sees "no position yet" (0 — the real mount-time + // state, the panel mounts before the session enters world), then the + // player lands outdoors and the 5 s cadence re-refreshes. + uint playerCell = 0u; + var callbacks = new MapHousePanelController.Callbacks( + Toggle: static () => { }, + Map: new MapPageController.Bindings( + CurrentCalendar: static () => default, + PlayerCellId: () => playerCell, + HousePosition: static () => (CreateObject.ServerPosition?)null, + TemplateResolver: ResolveTemplate, + IconBuilder: BuildIcon), + House: new HousePageController.Bindings( + Lines: static () => Array.Empty(), + TemplateResolver: ResolveTemplate)); + + MapHousePanelController? controller = + MapHousePanelController.Bind(rootInfo!, layout, callbacks); + Assert.NotNull(controller); + + // ── Pin 1: both icons resolve as BUILT elements ────────────────── + UiElement? map = UiElement.FindDescendant( + controller!.Root, MapPageController.MapWidgetId); + Assert.NotNull(map); + + UiElement? playerIcon = UiElement.FindDescendant( + controller.Root, MapPageController.PlayerIconId); + UiElement? houseIcon = UiElement.FindDescendant( + controller.Root, MapPageController.HouseIconId); + Assert.NotNull(playerIcon); + Assert.NotNull(houseIcon); + + // Attached directly under m_pMap (PlaceMarker positions them in its + // local space), with real authored extents — PlaceMarker's centering + // divides the icon's own Width/Height, so a zero-sized build would + // silently mis-center every marker. + Assert.Same(map, playerIcon!.Parent); + Assert.Same(map, houseIcon!.Parent); + Assert.True(playerIcon.Width > 0 && playerIcon.Height > 0, + $"player icon built with degenerate extent {playerIcon.Width}x{playerIcon.Height}"); + Assert.True(houseIcon.Width > 0 && houseIcon.Height > 0, + $"house icon built with degenerate extent {houseIcon.Width}x{houseIcon.Height}"); + + // ── Pin 2: PlaceMarker's writes survive the per-frame layout pass ─ + // The F1 live finding's second half: the client re-runs the authored + // layout pass (parent → child.ApplyAnchor) every frame. Pre-fix the + // icon's compatibility anchor captured the authored (0,0) rect while + // the panel sat indoors/hidden, then re-asserted it every frame — + // a visible ring pinned to m_pMap's top-left corner regardless of + // the player's position. Reproduce that exact frame order here. + playerIcon.ApplyAnchor(map!.Width, map.Height); // frame while cell unknown + houseIcon.ApplyAnchor(map.Width, map.Height); + Assert.False(playerIcon.Visible); + + playerCell = 0x11CE0001u; // Arwic (independently pinned: + // display coords -88.3 / 62.9) + controller.Tick(MapPageController.RefreshIntervalSeconds + 0.01); + Assert.True(playerIcon.Visible); + + (float expectedLeft, float expectedTop) = MapPageController.ComputeMarkerPosition( + markerX0: 6, markerX1: 247, markerY0: 8, markerY1: 258, + (int)playerIcon.Width, (int)playerIcon.Height, -88.30000000000001, 62.900000000000006); + Assert.Equal(expectedLeft, playerIcon.Left); + Assert.Equal(expectedTop, playerIcon.Top); + + playerIcon.ApplyAnchor(map.Width, map.Height); // the next frame's pass + Assert.Equal(expectedLeft, playerIcon.Left); + Assert.Equal(expectedTop, playerIcon.Top); + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/MapPageControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapPageControllerTests.cs index 3b7146cb..006079bf 100644 --- a/tests/AcDream.App.Tests/UI/Layout/MapPageControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/MapPageControllerTests.cs @@ -184,7 +184,8 @@ public sealed class MapPageControllerTests CurrentCalendar: static () => default, PlayerCellId: () => cellId, HousePosition: static () => (CreateObject.ServerPosition?)null, - TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }), + TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }, + IconBuilder: info => new UiText { Width = 10f, Height = 10f, DatElementId = info.Id }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks); @@ -218,7 +219,8 @@ public sealed class MapPageControllerTests CurrentCalendar: static () => default, PlayerCellId: () => indoorCellId, HousePosition: static () => (CreateObject.ServerPosition?)null, - TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }), + TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }, + IconBuilder: info => new UiText { Width = 10f, Height = 10f, DatElementId = info.Id }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks); @@ -230,12 +232,12 @@ public sealed class MapPageControllerTests } [Fact] - public void Refresh_PlayerIconTemplateResolutionFails_CoordinateTextStaysEmptyToo() + public void Refresh_PlayerIconResolutionFails_CoordinateTextStaysEmptyToo() { // F15 (night-round review): gmMapUI::Update @0x004a2078's gate is // `if (m_pCoordinateText != 0 && m_pPlayerLocationIcon != 0)` — BOTH - // widgets present, not "at least one". A player-icon template - // resolution failure (leaving _playerIcon null, e.g. a future DAT + // widgets present, not "at least one". A player-icon resolution + // failure (leaving _playerIcon null, e.g. a future DAT // regression) must skip the coordinate-text write too, not just the // marker placement — the OLD `_coordinateText is null && // _playerIcon is null` gate only skipped when BOTH were absent, so @@ -252,13 +254,14 @@ public sealed class MapPageControllerTests CurrentCalendar: static () => default, PlayerCellId: () => cellId, HousePosition: static () => (CreateObject.ServerPosition?)null, - // Simulates the player icon's own standalone template - // resolution failing (ResolveSwallowedIcon's own null path) - // while every other swallowed-icon/town-marker resolution - // still succeeds normally. - TemplateResolver: (_, e) => e == MapPageController.PlayerIconId + TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }, + // Simulates the player icon's own build failing + // (ResolveSwallowedIcon's null-build path, e.g. a future DAT + // regression) while the house icon and every town-marker + // resolution still succeed normally. + IconBuilder: info => info.Id == MapPageController.PlayerIconId ? null - : new UiText { Width = 10f, Height = 10f, DatElementId = e }), + : new UiText { Width = 10f, Height = 10f, DatElementId = info.Id }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks); @@ -283,7 +286,8 @@ public sealed class MapPageControllerTests CurrentCalendar: static () => default, PlayerCellId: static () => 0u, HousePosition: static () => (CreateObject.ServerPosition?)null, - TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }), + TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }, + IconBuilder: info => new UiText { Width = 10f, Height = 10f, DatElementId = info.Id }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks);