diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md
index a471ea5d..7f4876cc 100644
--- a/docs/architecture/retail-divergence-register.md
+++ b/docs/architecture/retail-divergence-register.md
@@ -67,7 +67,7 @@ accepted-divergence entries (#96, #49, #50).
---
-## 2. Adaptation (AD) — 87 active rows (AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; 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; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal-
+## 2. Adaptation (AD) — 88 active rows (AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; 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; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal-
-to-line-break shaping, which retail's `ItemExamineUI::AddItemInfo @0x004AC050` does not do (wire text appends verbatim; the escape decode retail runs at `StringInfo` resolution now lives at our string source, `DatStringResolver` → `RetailStringEscapes`); 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
@@ -111,6 +111,7 @@ readiness/requeue adaptation. See
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
+| AD-114 | **Filed 2026-08-25 at Campaign AS slice AS2, owner-ruled 2026-08-25 (verbatim "we animate it, and I like it").** acdream's examination-window preview (`CreatureAppraisalFramePresenter` / `RetailCreatureAppraisalCloneFactory`) shares the assessed target's already-resolved live MeshRefs and re-synchronizes them every frame, so the preview clone plays the SAME current animated pose the live target is actually doing right now (attack, cast, run, idle, ...). Retail's `BasicCreatureExamineUI::Init @0x004AB9C0` instead clones the selected physics object ONCE, fixes its heading at 191.367905°, and lets its own private `CreatureMode` animate that clone independently — decoupled from whatever the live target is currently doing. | `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs` (`CreatureAppraisalFramePresenter`, `RetailCreatureAppraisalCloneFactory`) | Explicit owner direction, 2026-08-25 (`docs/plans/2026-08-25-assess-window-parity-campaign.md`: "The animated 3D paperdoll is an INTENTIONAL acdream deviation... Keep it"), noted alongside the owner's own observation that retail's static-clone colors are buggy — porting the decoupled-motion clone would not even be a faithfulness win here. | None expected — a deliberate, user-approved visual improvement over retail's decoupled clone motion, not a game-feel divergence; a future faithfulness audit assuming the preview mirrors retail's independent `CreatureMode` cycle would be surprised to see it track the live target's pose instead. | `BasicCreatureExamineUI::Init @0x004AB9C0`; `docs/plans/2026-08-25-assess-window-parity-campaign.md` |
| AD-112 | **Filed 2026-08-23 with the sky default-script port (issues #28/#2, phase C.1.5c).** Retail plays the sky carriers' aurora/lightning/thunder PES by creating real `CPhysicsObj`s in the viewer-centered sky cells (`GameSky::MakeObject @0x00506EE0` → `CPhysicsObj::makeObject @0x00513970`; the Setup's `DefaultScript` marks `state \|= 0x80000` and `CPhysicsObj::animate_static_object @0x00513DF0` ticks `ScriptManager::UpdateScripts` + the ParticleManager). acdream plays the same scripts through `PhysicsScriptRunner` synthetic owners anchored at the camera (`SkyPesFrameController`), pass-routed by `props & 1` into the existing SkyPreScene/SkyPostScene particle draws — no sky-cell physics objects exist. Slot persistence keys on (index, gfx id, properties), the `CreateDeletePhysicsObjects @0x005073C0` identity law. The heading/rotation → pose axis mapping uses `UnitY` where retail's `GameSky::CalcFrame @0x00506F80` runs `set_heading` + `grotate`; every current PES carrier authors 0°/0° so the axis is unexercised. | `src/AcDream.App/Rendering/SkyPesFrameController.cs`; `src/AcDream.Core/World/SkyDescLoader.cs` (`SkyObjectData.DefaultScriptId`) | Retail's sky cells are camera-centered, so a camera-anchored world-space owner is the same geometry; the script/emitter engines are the production ones shared with entity effects; `SkyPesFrameControllerTests` pins the identity/persistence/window lifecycle. | A sky carrier authored with nonzero heading/rotation angles would orbit around the wrong axis; a PES whose hooks depend on real part frames (part_index targeting) would find only the synthetic owner's root pose. | `GameSky::MakeObject @0x00506EE0`; `GameSky::CreateDeletePhysicsObjects @0x005073C0`; `CPhysicsObj::animate_static_object @0x00513DF0`; `docs/research/2026-08-23-sky-default-script-port.md`; `SkyPesFrameControllerTests` |
| AD-111 | **Filed 2026-08-17 at the systemic escape-normalization round (commit 967b9c57).** The appraisal report's WIRE-string shaping (`ItemAppraisalTextLayout.Shape`) converts a literal two-character `
` in server-sent fragment text (long description, use text) into a real line break. Retail does NOT: `ItemExamineUI::AddItemInfo @ 0x004AC050` hands wire text straight to `UIElement_Text::AppendTextWithFont` with no `StringTableMetaLanguage::UnescapeString` pass (that decode belongs to `StringInfo` resolution — DAT/authored strings — which the same round ported to `DatStringResolver`/`RetailStringEscapes` as the single source decode), so retail renders a wire backslash-n literally. Pre-existing behavior documented as wire-domain at the same round (it shipped inside the user-accepted Slice-3 assessment surface); the sibling inscription path (`IndicatorDetailText.Shape`) was returned to retail-verbatim in the same commit. | `src/AcDream.App/UI/Layout/ItemAppraisalReport.cs` (`ItemAppraisalTextLayout.Shape`'s domain-commented replace) | Accommodates literal "
diff --git a/src/AcDream.App/UI/Layout/AppraisalUiController.cs b/src/AcDream.App/UI/Layout/AppraisalUiController.cs
index b5b9a109..d30c59e5 100644
--- a/src/AcDream.App/UI/Layout/AppraisalUiController.cs
+++ b/src/AcDream.App/UI/Layout/AppraisalUiController.cs
@@ -43,6 +43,12 @@ public sealed class AppraisalUiController : IRetainedPanelController
public const uint SpellFormulaListId = 0x1000032Du;
private const uint TemplateStringProperty = 5u;
+ ///
+ /// PropertyInt 0x105 (261 decimal) — CharacterTitleId. Doubles as the
+ /// Character-subview dispatch marker () and, as
+ /// of AS2, the source id for 's
+ /// current-display-title line (element 0x10000151).
+ ///
private const uint CharacterMarkerIntProperty = 0x105u;
private const uint DisplayedNameStringProperty = 0x34u;
private const double CreatureRefreshSeconds = 0.75;
@@ -77,6 +83,15 @@ public sealed class AppraisalUiController : IRetainedPanelController
private readonly Func _resolveComponentIcon;
private readonly Func> _spellComponents;
private readonly Func _magicSkill;
+ ///
+ /// AS2: element 0x10000151's current-display-title chain
+ /// (CharacterTitleResolver.Resolve under the composition layer's
+ /// DatLock — resolved PER CALL, never captured once at mount time, per
+ /// the secure-trade deferred-Func lesson). Defaults to "never resolves"
+ /// so a caller that supplies nothing simply falls back to String 5
+ /// Template every time, matching retail's own fallback branch.
+ ///
+ private readonly Func _resolveCharacterTitle;
private readonly SpellExamineComponentTemplateFactory? _spellComponentTemplates;
private readonly UiText _spellSchool;
private readonly UiText _spellMana;
@@ -132,7 +147,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
Func? resolveComponentIcon,
Func>? spellComponents,
Func? magicSkill,
- SpellExamineComponentTemplateFactory? spellComponentTemplates)
+ SpellExamineComponentTemplateFactory? spellComponentTemplates,
+ Func? resolveCharacterTitle)
{
_layout = layout;
_objects = objects;
@@ -159,6 +175,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
_resolveComponentIcon = resolveComponentIcon ?? (_ => 0u);
_spellComponents = spellComponents ?? (_ => []);
_magicSkill = magicSkill ?? (_ => 0u);
+ _resolveCharacterTitle = resolveCharacterTitle ?? (_ => null);
_spellComponentTemplates = spellComponentTemplates;
_spellSchool = (UiText)layout.FindElement(SpellSchoolTextId)!;
_spellMana = (UiText)layout.FindElement(SpellManaTextId)!;
@@ -287,7 +304,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
Func? resolveComponentIcon = null,
Func>? spellComponents = null,
Func? magicSkill = null,
- SpellExamineComponentTemplateFactory? spellComponentTemplates = null)
+ SpellExamineComponentTemplateFactory? spellComponentTemplates = null,
+ Func? resolveCharacterTitle = null)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(objects);
@@ -339,7 +357,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
resolveComponentIcon,
spellComponents,
magicSkill,
- spellComponentTemplates);
+ spellComponentTemplates,
+ resolveCharacterTitle);
}
///
@@ -687,9 +706,10 @@ public sealed class AppraisalUiController : IRetainedPanelController
if (character)
{
- SetText(0x10000150u, GetString(p, 4u));
- SetText(0x10000151u, GetString(p, 47u));
- SetText(0x10000152u, GetString(p, 11u));
+ SetText(0x10000150u, BuildCharacterHeritageDisplay(p));
+ SetText(0x10000151u, BuildCharacterTitleDisplay(p));
+ SetText(0x10000152u, BuildPlayerKillerDisplay(obj));
+ SetText(0x1000053Au, BuildAllegianceDisplay(p));
}
else
{
@@ -700,13 +720,91 @@ public sealed class AppraisalUiController : IRetainedPanelController
RebuildCreatureStats(appraisal);
- SetText(0x1000053Au, appraisal.Success
- ? string.Empty
- : "Assessment incomplete");
if (newlySelected)
ResetCreatureScroll();
}
+ ///
+ /// Composes examination element 0x10000150 (Heritage) for a
+ /// player target. Ground truth: docs/research/2026-08-25-campaign-as-
+ /// ground-truth.md §2a. Gap G1.
+ ///
+ private string BuildCharacterHeritageDisplay(PropertyBundle p)
+ {
+ int gender = GetInt(p, CharacterIdentityText.GenderPropertyId);
+ int heritageGroup = GetInt(p, CharacterIdentityText.HeritageGroupPropertyId);
+ string creatureFallback = heritageGroup == 0
+ ? _creatureNames.Resolve(GetInt(p, 2u))
+ : string.Empty;
+ return CharacterIdentityText.GenderHeritageDisplay(
+ gender, heritageGroup, creatureFallback);
+ }
+
+ ///
+ /// Composes examination element 0x10000151 (Profession/title) for
+ /// a player target: the current display title from Int 261
+ /// CharacterTitleId (, resolved
+ /// through CharacterTitleResolver's CharacterTitleTable::
+ /// GetCharacterTitleFromID @0x005C6ED0 chain), falling back to
+ /// String 5 Template verbatim when the id is absent or unresolvable.
+ /// Ground truth §2a, gap G2.
+ /// (0x105 == 261 decimal) is the SAME property
+ /// already keys the Character-subview dispatch on.
+ ///
+ private string BuildCharacterTitleDisplay(PropertyBundle p)
+ {
+ if (p.Ints.TryGetValue(CharacterMarkerIntProperty, out int titleId)
+ && titleId != 0
+ && _resolveCharacterTitle(unchecked((uint)titleId)) is { Length: > 0 } resolved)
+ {
+ return resolved;
+ }
+ return GetString(p, TemplateStringProperty);
+ }
+
+ /// PWD bit 5 — ACCWeenieObject::IsPK @0x0058C8B0.
+ private const uint PlayerKillerPwdBit = 0x20u;
+
+ /// PWD bit 0x19 (25) — ACCWeenieObject::IsPKLite @0x0058C8A0.
+ private const uint PlayerKillerLitePwdBit = 0x02000000u;
+
+ ///
+ /// Composes examination element 0x10000152 (PlayerKiller) for a
+ /// player target. Ruling R7: retail reads the LOCAL weenie's PWD bits —
+ /// never the appraisal payload's PropertyInt 134 (AP-109's CT4 lesson).
+ /// is the SAME
+ /// already resolves names from — the assessed
+ /// object located before ApplyCreature ever
+ /// runs. When that lookup fails, already bails out
+ /// before reaching this method, matching retail's "weenie is gone, leave
+ /// the line cleared" behavior. Ground truth §2a, gap G3.
+ ///
+ private static string BuildPlayerKillerDisplay(ClientObject obj)
+ {
+ uint bitfield = obj.PublicWeenieBitfield ?? 0u;
+ if ((bitfield & PlayerKillerPwdBit) != 0u)
+ return "Player Killer";
+ if ((bitfield & PlayerKillerLitePwdBit) != 0u)
+ return "Player Killer Lite";
+ return "Non-Player Killer";
+ }
+
+ ///
+ /// Composes examination element 0x1000053A (AllegianceName).
+ /// Retail clears the element first (
+ /// already does this for every ApplyCreature call), then sets it
+ /// to String 47 AllegianceName ONLY inside the Int 30 AllegianceRank
+ /// >= 1 gate — no gate, no allegiance line. Deletes the former
+ /// invented "Assessment incomplete" literal (zero retail provenance;
+ /// was the only occurrence in the repo). Ground truth §2a, gap G5.
+ /// Retail's CreatureExamineUI (monsters) never binds this
+ /// element at all — only CharExamineUI (players) does — so this
+ /// composer is scoped to the character branch, matching that
+ /// per-class split.
+ ///
+ private static string BuildAllegianceDisplay(PropertyBundle p)
+ => GetInt(p, 30u) >= 1 ? GetString(p, 47u) : string.Empty;
+
private void RebuildCreatureStats(AppraiseInfoParser.Parsed appraisal)
{
if (_creatureStats is null || _creatureRowTemplates is null)
diff --git a/src/AcDream.App/UI/Layout/CharacterIdentityText.cs b/src/AcDream.App/UI/Layout/CharacterIdentityText.cs
index 2aab47c0..d1339159 100644
--- a/src/AcDream.App/UI/Layout/CharacterIdentityText.cs
+++ b/src/AcDream.App/UI/Layout/CharacterIdentityText.cs
@@ -116,6 +116,32 @@ internal static class CharacterIdentityText
_ => null,
};
+ ///
+ /// Campaign AS slice AS2 (2026-08-25): examination window element
+ /// 0x10000150 (Heritage), CharExamineUI::SetAppraiseInfo
+ /// @0x004B45F0 via AppraisalSystem::InqGenderHeritageDisplay
+ /// @0x005B5AE0. Unlike (the
+ /// stat-management panel's header, which never appends a title and has
+ /// no creature-type fallback), this composes ONLY
+ /// "<Gender> <Heritage>" — falling back to
+ /// (the assessed object's
+ /// creature-type display name, Int 2, resolved through the SAME
+ /// EnumMapper family already
+ /// serves the monster view's own 0x1000014E line) ONLY when
+ /// is exactly 0, matching retail's own
+ /// integer comparison rather than "the lookup failed".
+ ///
+ public static string GenderHeritageDisplay(
+ int gender,
+ int heritageGroup,
+ string? creatureTypeFallback)
+ {
+ string? heritage = heritageGroup == 0
+ ? creatureTypeFallback
+ : HeritageGroupDisplayName(heritageGroup);
+ return Join(GenderDisplayName(gender), heritage);
+ }
+
private static string Join(params string?[] parts)
{
return string.Join(" ", parts
diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs
index 6ef49d48..0e9cdc48 100644
--- a/src/AcDream.App/UI/RetailUiRuntime.cs
+++ b/src/AcDream.App/UI/RetailUiRuntime.cs
@@ -2113,6 +2113,20 @@ public sealed class RetailUiRuntime : IDisposable
return;
}
+ // AS2 (Campaign AS, gap G2): the examination header's title line
+ // (element 0x10000151) reuses the SAME CharacterTitleResolver
+ // instance the D.2b Character panel already owns
+ // (_bindings.Character.TitleResolver, constructed once per
+ // composition in InteractionRetainedUiComposition — see
+ // MountCharacter's identical TitleResolver local function). Resolved
+ // PER CALL under DatLock, never captured once, per the secure-trade
+ // deferred-Func lesson.
+ string? ResolveCharacterTitle(uint titleId)
+ {
+ lock (_bindings.Assets.DatLock)
+ return _bindings.Character.TitleResolver.Resolve(titleId);
+ }
+
AppraisalUiController? controller = AppraisalUiController.Bind(
layout,
_bindings.Inventory.Objects,
@@ -2132,7 +2146,8 @@ public sealed class RetailUiRuntime : IDisposable
resolveComponentIcon: _bindings.Magic.ResolveComponentIcon,
spellComponents: _bindings.Magic.SpellComponents,
magicSkill: _bindings.Magic.MagicSkill,
- spellComponentTemplates: spellComponentTemplates);
+ spellComponentTemplates: spellComponentTemplates,
+ resolveCharacterTitle: ResolveCharacterTitle);
if (controller is null)
{
Console.WriteLine(
diff --git a/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs
index e4a55791..aaeb3d56 100644
--- a/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs
@@ -413,6 +413,352 @@ public sealed class AppraisalUiControllerTests
Assert.Equal(new[] { ObjectId, otherObjectId }, sent);
}
+ // ── Campaign AS slice AS2: examination header identity block ──────────
+ // Ground truth: docs/research/2026-08-25-campaign-as-ground-truth.md §2a,
+ // gap ledger G1/G2/G3/G5, rulings R6/R7. First-ever AppraisalView.Character
+ // controller coverage (gap G10).
+
+ [Fact]
+ public void CharacterResponse_ComposesRetailHeaderIdentityBlock()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Dww",
+ Type = ItemType.Creature,
+ PublicWeenieBitfield = 0x20u, // PWD bit 5 -> IsPK
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { },
+ resolveCharacterTitle: titleId => titleId == 13u ? "War Mage" : null)!;
+
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+ var properties = new PropertyBundle();
+ properties.Ints[0x71u] = 2; // Gender: Female
+ properties.Ints[0xBCu] = 1; // HeritageGroup: Aluvian
+ properties.Ints[0x105u] = 13; // CharacterTitleId (also the Character-view marker)
+ properties.Ints[30u] = 5; // AllegianceRank >= 1
+ properties.Strings[47u] = "The Empire"; // AllegianceName
+
+ Assert.True(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+ Assert.Equal(AppraisalView.Character, controller.ActiveView);
+
+ Assert.Equal("Female Aluvian", HeaderText(layout, 0x10000150u));
+ Assert.Equal("War Mage", HeaderText(layout, 0x10000151u));
+ Assert.Equal("Player Killer", HeaderText(layout, 0x10000152u));
+ Assert.Equal("The Empire", HeaderText(layout, 0x1000053Au));
+ }
+
+ [Fact]
+ public void CharacterResponse_HeritageFallsBackToCreatureTypeWhenGroupIsZero()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Something Odd",
+ Type = ItemType.Creature,
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { },
+ creatureNames: new CreatureDisplayNameResolver(
+ new Dictionary { [42u] = "Olthoi Guardian" }))!;
+
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+ var properties = new PropertyBundle();
+ properties.Ints[2u] = 42; // CreatureType fallback source
+ properties.Ints[0xBCu] = 0; // HeritageGroup == 0 -> use creature-type fallback
+ properties.Strings[5u] = "Template"; // Character-view marker
+
+ Assert.True(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+ Assert.Equal(AppraisalView.Character, controller.ActiveView);
+ Assert.Equal("Olthoi Guardian", HeaderText(layout, 0x10000150u));
+ }
+
+ [Fact]
+ public void CharacterResponse_TitleFallsBackToTemplateStringWhenTitleIdAbsent()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Dww",
+ Type = ItemType.Creature,
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { })!;
+
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+ var properties = new PropertyBundle();
+ properties.Strings[5u] = "Test Template"; // both the fallback text AND the view marker
+
+ Assert.True(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+ Assert.Equal(AppraisalView.Character, controller.ActiveView);
+ Assert.Equal("Test Template", HeaderText(layout, 0x10000151u));
+ }
+
+ [Fact]
+ public void CharacterResponse_TitleClearsWhenIdUnresolvableAndTemplateAbsent()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Dww",
+ Type = ItemType.Creature,
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { },
+ // Never resolves any title id.
+ resolveCharacterTitle: _ => null)!;
+
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+ var properties = new PropertyBundle();
+ // Int 261 present (satisfies the Character-view marker) but does not
+ // resolve, and String 5 Template is absent -> element clears.
+ properties.Ints[0x105u] = 999;
+
+ Assert.True(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+ Assert.Equal(AppraisalView.Character, controller.ActiveView);
+ Assert.Equal(string.Empty, HeaderText(layout, 0x10000151u));
+ }
+
+ [Theory]
+ [InlineData(0x20u, "Player Killer")]
+ [InlineData(0x02000000u, "Player Killer Lite")]
+ [InlineData(0u, "Non-Player Killer")]
+ public void CharacterResponse_PlayerKillerLineReflectsLocalObjectPwdBits(
+ uint publicWeenieBitfield,
+ string expected)
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Dww",
+ Type = ItemType.Creature,
+ PublicWeenieBitfield = publicWeenieBitfield,
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { })!;
+
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+ var properties = new PropertyBundle();
+ properties.Strings[5u] = "Template";
+
+ Assert.True(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+ Assert.Equal(expected, HeaderText(layout, 0x10000152u));
+ }
+
+ [Fact]
+ public void MissingAssessedObject_PlayerKillerElementStaysClearedAndApplyReturnsFalse()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ // ObjectId is deliberately never added to the table — retail's
+ // "the weenie is gone" case (ruling R7).
+ var objects = new ClientObjectTable();
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { })!;
+
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+ var properties = new PropertyBundle();
+ properties.Strings[5u] = "Template";
+
+ UiText pk = Assert.IsType(layout.FindElement(0x10000152u));
+ Func> providerBeforeApply = pk.LinesProvider;
+
+ Assert.False(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+
+ // Apply bailed out before ApplyCreature/SetText ever ran for this
+ // element — it never had the chance to be anything but cleared.
+ Assert.Same(providerBeforeApply, pk.LinesProvider);
+ }
+
+ [Fact]
+ public void AllegianceElement_ClearsUnlessRankIsAtLeastOne()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Dww",
+ Type = ItemType.Creature,
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { })!;
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+
+ var noRank = new PropertyBundle();
+ noRank.Strings[5u] = "Template";
+ noRank.Strings[47u] = "The Empire";
+ Assert.True(controller.Apply(Parsed(noRank, MinimalCreatureProfile())));
+ Assert.Equal(string.Empty, HeaderText(layout, 0x1000053Au));
+
+ var zeroRank = new PropertyBundle();
+ zeroRank.Strings[5u] = "Template";
+ zeroRank.Strings[47u] = "The Empire";
+ zeroRank.Ints[30u] = 0;
+ Assert.True(controller.Apply(Parsed(zeroRank, MinimalCreatureProfile())));
+ Assert.Equal(string.Empty, HeaderText(layout, 0x1000053Au));
+
+ var gatedRank = new PropertyBundle();
+ gatedRank.Strings[5u] = "Template";
+ gatedRank.Strings[47u] = "The Empire";
+ gatedRank.Ints[30u] = 1;
+ Assert.True(controller.Apply(Parsed(gatedRank, MinimalCreatureProfile())));
+ Assert.Equal("The Empire", HeaderText(layout, 0x1000053Au));
+ }
+
+ [Fact]
+ public void FailedAssess_StillRendersHeaderLinesFromPresentTableEntries()
+ {
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Dww",
+ Type = ItemType.Creature,
+ PublicWeenieBitfield = 0x02000000u, // PKLite
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { },
+ resolveCharacterTitle: titleId => titleId == 13u ? "War Mage" : null)!;
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+
+ var properties = new PropertyBundle();
+ properties.Ints[0x71u] = 1; // Gender: Male
+ properties.Ints[0xBCu] = 1; // HeritageGroup: Aluvian
+ properties.Ints[0x105u] = 13; // CharacterTitleId
+ properties.Ints[30u] = 5; // AllegianceRank
+ properties.Strings[47u] = "The Empire";
+
+ Assert.True(controller.Apply(
+ Parsed(properties, MinimalCreatureProfile(), success: false)));
+
+ Assert.Equal("Male Aluvian", HeaderText(layout, 0x10000150u));
+ Assert.Equal("War Mage", HeaderText(layout, 0x10000151u));
+ Assert.Equal("Player Killer Lite", HeaderText(layout, 0x10000152u));
+ Assert.Equal("The Empire", HeaderText(layout, 0x1000053Au));
+ }
+
+ [Fact]
+ public void CreatureResponse_HeaderIdentityElementsUnaffectedByPlayerFix()
+ {
+ // Regression pin (task constraint): the character:false (monster)
+ // path must keep its pre-AS2 behavior for the four header elements
+ // AS2 remapped for players. Pre-fix, 0x10000150/51/52 were never
+ // touched for monsters (stay at their cleared default) and
+ // 0x1000053A was set to string.Empty whenever Success was true —
+ // identical to today's post-fix "gated, nothing gates it open"
+ // result for a bundle with no allegiance data.
+ ImportedLayout layout = FixtureLoader.LoadExamination();
+ var objects = new ClientObjectTable();
+ objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = ObjectId,
+ Name = "Specter",
+ Type = ItemType.Creature,
+ });
+ using var interaction = NewInteraction(objects, []);
+ using AppraisalUiController controller = Bind(
+ layout,
+ objects,
+ interaction,
+ new CombatState(),
+ [],
+ [],
+ () => { },
+ () => { },
+ creatureNames: new CreatureDisplayNameResolver(
+ new Dictionary { [77u] = "Ghost" }))!;
+ interaction.ExamineSelectedOrEnterMode(ObjectId);
+
+ var properties = new PropertyBundle();
+ properties.Ints[2u] = 77; // CreatureType — no String 5 / Int 261 marker present.
+ properties.Ints[25u] = 80;
+
+ Assert.True(controller.Apply(Parsed(properties, MinimalCreatureProfile())));
+ Assert.Equal(AppraisalView.Creature, controller.ActiveView);
+
+ Assert.Equal("Ghost", HeaderText(layout, AppraisalUiController.CreatureDisplayNameId));
+ Assert.Equal(string.Empty, HeaderText(layout, 0x10000150u));
+ Assert.Equal(string.Empty, HeaderText(layout, 0x10000151u));
+ Assert.Equal(string.Empty, HeaderText(layout, 0x10000152u));
+ Assert.Equal(string.Empty, HeaderText(layout, 0x1000053Au));
+ }
+
[Fact]
public void ResponseForNeitherPendingNorCurrent_IsIgnored()
{
@@ -832,7 +1178,8 @@ public sealed class AppraisalUiControllerTests
Func? resolveComponentIcon = null,
Func>? spellComponents = null,
Func? magicSkill = null,
- SpellExamineComponentTemplateFactory? spellComponentTemplates = null)
+ SpellExamineComponentTemplateFactory? spellComponentTemplates = null,
+ Func? resolveCharacterTitle = null)
=> AppraisalUiController.Bind(
layout,
objects,
@@ -852,7 +1199,8 @@ public sealed class AppraisalUiControllerTests
resolveComponentIcon,
spellComponents,
magicSkill,
- spellComponentTemplates);
+ spellComponentTemplates,
+ resolveCharacterTitle);
private static ItemInteractionController NewInteraction(
ClientObjectTable objects,
@@ -871,13 +1219,14 @@ public sealed class AppraisalUiControllerTests
private static AppraiseInfoParser.Parsed Parsed(
PropertyBundle properties,
AppraiseInfoParser.CreatureProfile? creature = null,
- uint guid = ObjectId)
+ uint guid = ObjectId,
+ bool success = true)
=> new(
Guid: guid,
Flags: creature is null
? AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable
: AppraiseInfoParser.IdentifyResponseFlags.CreatureProfile,
- Success: true,
+ Success: success,
Properties: properties,
SpellBook: [],
ArmorProfile: null,
@@ -889,6 +1238,35 @@ public sealed class AppraisalUiControllerTests
WeaponEnchantments: null,
ResistEnchantments: null);
+ /// Minimal CreatureProfile — just enough for
+ /// 's CreatureProfile-not-null
+ /// gate; every attribute is intentionally absent since these tests pin
+ /// the header identity block, not the attribute rows.
+ private static AppraiseInfoParser.CreatureProfile MinimalCreatureProfile()
+ => new(
+ Flags: 0,
+ Health: 1u,
+ HealthMax: 1u,
+ Strength: null,
+ Endurance: null,
+ Quickness: null,
+ Coordination: null,
+ Focus: null,
+ Self: null,
+ Stamina: null,
+ Mana: null,
+ StaminaMax: null,
+ ManaMax: null,
+ AttributeHighlights: null,
+ AttributeColors: null);
+
+ private static string HeaderText(ImportedLayout layout, uint elementId)
+ {
+ UiText text = Assert.IsType(layout.FindElement(elementId));
+ return string.Join(
+ '\n', text.LinesProvider().Select(line => line.Text));
+ }
+
private static void AssertSpellText(
ImportedLayout layout,
uint elementId,