diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 940fff66..fc44fb20 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -199,7 +199,7 @@ readiness/requeue adaptation. See --- -## 3. Documented approximation (AP) — 156 active rows (AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; ~~AP-204~~ RETIRED 2026-08-11 at the OP8 rework — the silent-auto-reassign narrowing it recorded is fixed by a real `RetailDialogFactory` confirm-before-reassign dialog; see its retirement note below. AP-203/AP-202 filed 2026-08-11 at Campaign OP slice OP8 (Configure Keyboard) remain active — AP-202 records D4's `.keymap`-file-interchange narrowing (`keybinds.json` only), AP-203 records that roughly half of the DAT ActionMap's 306 user-bindable rows (82 of 87 Emotes, all 48 CharacterSettings hotkeys, all 10 CameraAlternateControls rows per the M2 de-alias fix, and assorted UI/Combat odds) render/bind/persist on the Configure Keyboard screen with no live acdream gameplay consumer yet; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) +## 3. Documented approximation (AP) — 158 active rows (AP-222 filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — the current-part spin highlight is a measured no-op for all nine spins, no Highlight media authored on any of them; AP-221 filed the same re-review (R2) — the chargen preview's one-shot-composition-vs-retryable-coordinator binding gap; AP-217 rewritten and AP-220 tightened the same re-review (R3 corrects the GradCircle from a dead click target to unported paint-art; N1 narrows the Gearknight-exit wording to non-Olthoi); AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; ~~AP-204~~ RETIRED 2026-08-11 at the OP8 rework — the silent-auto-reassign narrowing it recorded is fixed by a real `RetailDialogFactory` confirm-before-reassign dialog; see its retirement note below. AP-203/AP-202 filed 2026-08-11 at Campaign OP slice OP8 (Configure Keyboard) remain active — AP-202 records D4's `.keymap`-file-interchange narrowing (`keybinds.json` only), AP-203 records that roughly half of the DAT ActionMap's 306 user-bindable rows (82 of 87 Emotes, all 48 CharacterSettings hotkeys, all 10 CameraAlternateControls rows per the M2 de-alias fix, and assorted UI/Combat odds) render/bind/persist on the Configure Keyboard screen with no live acdream gameplay consumer yet; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered @@ -391,10 +391,12 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-210 | **Filed 2026-08-15 at Campaign CC slice CC3.** Retail's `ApplyTemplate @ 0x005C5080` applies a chosen template's six attributes one at a time through the individually-guarded setters (`SetStrength(this, row.strength, 0)` … `SetSelf(this, row.self, 0)`), each of which can silently refuse to RAISE its value when `GetAbsRemainingCredits` for that specific attribute is exactly zero at the moment it runs — a narrow but real cross-attribute ordering effect when switching heritage/template leaves stale attribute values from a PRIOR selection still resident during the sequential apply. `RuntimeCharacterCreationState.ApplyTemplateLocked` instead assigns `_attributes = row.Attributes` as one atomic replacement. | `src/AcDream.Runtime/Session/RuntimeCharacterCreationState.cs` (`ApplyTemplateLocked`) | Every template row in the installed CharGen DAT is curated, self-consistent data (CC1's installed-DAT gates), so the guard is not expected to trip for any real heritage/template pair in isolation; the ordering effect only matters when switching directly between two heritages/templates with very different attribute totals, which is a corner case not yet gated by a connected test. | A rapid heritage-switch-then-template-switch sequence could theoretically leave an attribute at a value retail's sequential guard would have refused to reach; unreachable through this slice's own commands (heritage selection always re-derives the FULL budget before applying), but a future direct-attribute-manipulation caller bypassing `TrySelectHeritage`/`TrySelectTemplate` could differ from retail. | `CharGenState::ApplyTemplate @ 0x005C5080`; `CharGenState::SetStrength @ 0x005C4660` (representative of all six) | | AP-215 | **Filed 2026-08-15 at Campaign CC slice CC6b-MOUNT (Appearance page visual substitutions).** Two narrow, DECIDED substitutions where acdream reaches the same functional selection through a different widget mechanism than retail's own: (1) the nine color swatches (`0x1000030f-0x10000317`) use their own `UiButton.Selected` highlight state for "this is the current color" instead of toggling the separate Type-3 companion overlay element (`0x10000318-0x10000320`) retail's `SetColor @ 0x0047DD50` shows/hides via `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible` — the composited pixel result is UNVERIFIED to match, not asserted identical (same "measured, not assumed" discipline AD-103's own F5 note established for a different swallowed-child case). (2) the four icon-only style spins (hair/eyes/nose/mouth — CC1's `ChargenHairStyle`/`ChargenEyeStrip`/`ChargenFaceStrip` carry only an `IconId`, no name string) show a 1-based ordinal number instead of retail's actual icon thumbnail; the four clothing spins (headgear/shirt/trousers/footwear) DO show a real name since `ChargenGearOption.Name` exists. Icon rendering for chargen's own preview icons is out of this round's scope entirely (no icon-texture pipeline is wired to ANY chargen widget yet). | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`RefreshColorAndShadeControls`'s swatch loop; `SetStyleSpinLabel`) | Both substitutions reach the SAME underlying selection (the swatch highlight still shows which color index is active; the ordinal still lets a player cycle deterministically and see which slot they're on) through existing widget primitives (`UiButton.Selected`, `UiButton.Label`) rather than adding new rendering infrastructure (a second overlay-visibility channel, or an icon-texture pipeline) this slice's scope doesn't otherwise need. | A pixel-level side-by-side against retail would show a different (simpler) selected-swatch visual and text labels where retail shows icon art — a cosmetic gap only; no selection state, index, or wire value differs. A future icon-rendering pass (if chargen ever needs one, e.g. for the heritage/template icons too) would naturally close the label half of this row. | `gmCGAppearancePage::SetColor @0x0047DD50` (the `m_tColorWheel` overlay toggle); `ChargenHairStyle`/`ChargenEyeStrip`/`ChargenFaceStrip`/`ChargenGearOption` (CC1, `src/AcDream.Core/CharGen/ChargenAppearanceOptions.cs`) | | AP-216 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 1).** Retail's `gmCGAppearancePage::DoColorSpots @0x0047d850` blits each of the nine swatch buttons with the ACTUAL color it represents (computed from the current part's own palette) and blits blank art for any swatch beyond the current part's real color count. acdream's swatches show only their authored (static) DAT art regardless of which color they represent or whether the current part even has that many colors — AP-215's `.Selected` substitution covers WHICH swatch is chosen, not what each swatch itself looks like. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`RefreshColorAndShadeControls`'s swatch loop — sets `.Selected` only, never touches swatch appearance) | The nine swatches already reach the correct SELECTION semantics through `DatWidgetFactory`'s existing `UiButton` primitive; painting each swatch with a computed color needs either a per-swatch dynamic-color render path (new UI infrastructure this scope doesn't otherwise need) or a fallback to static art, which is what this round shipped. | A side-by-side against retail shows every swatch drawing the SAME authored art regardless of which color it represents, and swatches beyond a part's real color count staying visibly "on" instead of blanking — a real visual gap on a screen the player stares at while picking a color, not a selection-correctness gap. | `gmCGAppearancePage::DoColorSpots @0x0047d850` | -| AP-217 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 4) — corrects the campaign plan's own CC6b-MOUNT ledger row, which wrongly claimed AP-215 already named the GradCircle.** Retail's `gmCGAppearancePage::DoGradDisk @0x0047da90` drives the GradCircle (`0x1000030e`) as an interactive hue/gradient picker, click-mapped to a color. acdream imports the GradCircle through the generic Type-3 `UiDatElement` fallback (the risk-item-4 color-wheel scouting result) with no click handling wired to it at all — it is purely decorative in this round. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`GradCircleId` is resolved by the live-DAT test only; the page's own constructor never binds a handler to it) | The nine swatch buttons already provide a full, decomp-cited color-selection path (`SetColor`'s own cases `5`-`0xd`); the GradCircle's own click-to-color-position mapping has no decomp citation yet in this campaign's research. | A user clicking the GradCircle in acdream gets no response at all, where retail would change the current part's color — a dead-control gap a visual gate would surface immediately. | `gmCGAppearancePage::DoGradDisk @0x0047da90` | +| AP-217 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 4); rewritten 2026-08-15 at the re-review of fix commit `d2a71152` (R3) — the original row misdescribed both the retail mechanism and the acdream gap.** `gmCGAppearancePage::ListenToElementMessage @0x0047ef30`'s dispatch switch on `idElement - 0x1000030a` has NO `case 4` (present cases: `0`,`1`,`5`-`0xd`,`0x17`,`0x19`-`0x1c`,`0xa5`-`0xa9`,`0xab`-`0xae`) — retail routes NO UI message from the GradCircle (`0x1000030e`, offset `4`) at all; it is not a click target. `DoGradDisk @0x0047da90` is a PAINT-only routine, called from `SetColor` (`@0x0047de18`) and `SetSelection` (`@0x0047e873`/`@0x0047e85d`): it `BlitAndColor`s the gradient graphic with the current part's color and `UIRegion::SetImage`s it onto `m_pGradCircle` (`@0x0047dc9e`/`@0x0047dca9`/`@0x0047dd26`) for every part except Eyes, or blits the blank "grad plug" graphic instead (`@0x0047dcec`, `DoGradDisk(this, 1)`) for Eyes — the GradCircle is authored, retail-driven *decorative art reflecting the current color*, not an input control. acdream imports the GradCircle through the generic Type-3 `UiDatElement` fallback and never paints it: no `BlitAndColor`-equivalent repaint on color change, and no Eyes-blank equivalent. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`GradCircleId` is resolved by the live-DAT test only; the page never repaints it) | The nine swatch buttons already provide the full, decomp-cited color-selection input path (`SetColor`'s own cases `5`-`0xd`); porting the GradCircle's own gradient-graphic repaint (a `Blit_Multiply` composite against `m_pGradGraphic`/`m_pGradPlug`, not a click handler) is separate follow-up work with no decomp citation yet for the composite art assets. | A user in acdream sees the GradCircle stay static instead of visually reflecting the current swatch color (and never blanking for Eyes) — a cosmetic paint gap, not a dead/unresponsive control; clicking it does nothing in retail either. | `gmCGAppearancePage::ListenToElementMessage @0x0047ef30`; `gmCGAppearancePage::DoGradDisk @0x0047da90`; `gmCGAppearancePage::SetColor @0x0047dd50`; `gmCGAppearancePage::SetSelection @0x0047e260` (calls at `@0x0047e873`/`@0x0047e85d`) | | AP-218 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 5).** Retail's `gmCGAppearancePage::Update` sets the Hair/Eyes/Skin spins' text to a heritage-flavored STATIC caption via `UIElement_Text::SetStringInfoWithFont` — normal heritage: `ID_CharGen_HairStyle`/`ID_CharGen_Eyes`/`ID_CharGen_Skin`; Olthoi/OlthoiAcid: `ID_CharGen_OlthoiText_HairButton`/`_EyesButton`/`_SkinButton`; Gearknight: `ID_CharGen_GearText_HairButton`/`_EyesButton`/`_SkinButton`. acdream's `SetStyleSpinLabel` instead overwrites the SAME label slot with a raw 1-based ordinal (or `"-"` when Unset) on all four icon-only spins (Hair/Eyes/Nose/Mouth) — neither the caption text nor its heritage-specific swap survives, and the ordinal itself is already a scope-cut stand-in for retail's icon thumbnail (CC1/AP-215). | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`SetStyleSpinLabel`) | The icon-rendering gap (CC1/AP-215) already means the spin can't show retail's icon thumbnail either way this round; reusing the SAME `.Label` slot for a numeric position indicator gives the player SOME feedback about which style is selected without adding a second text element this round's widget catalog doesn't otherwise carry. | A side-by-side against retail shows a numbered ordinal where retail shows static caption text (heritage-flavored) with an icon for the value — a cosmetic/informational gap, not a selection-correctness gap; a Gearknight or Olthoi player sees the SAME generic ordinal a normal-heritage player would, losing the heritage-specific caption entirely. | `gmCGAppearancePage::Update` caption writes @0x0047ebad (`ID_CharGen_HairStyle`), @0x0047ebe3 (`ID_CharGen_Eyes`), @0x0047ec6a (`ID_CharGen_Skin`); @0x0047ed5b/@0x0047ed91/@0x0047ee15 (Olthoi `OlthoiText_*` variants); @0x0047e9ef/@0x0047ea25/@0x0047eaa9 (Gearknight `GearText_*` variants) | | AP-219 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 6).** Retail's `gmCGAppearancePage::Update` repositions the Skin spin vertically when Nose/Mouth are hidden, closing the gap those two spins would otherwise leave: `m_pSkinSpin->MoveTo(0, 0x5a)` (Y=90) for Olthoi/OlthoiAcid (`@0x0047edef`) and Gearknight (`@0x0047ea83`), vs `MoveTo(0, 0xb4)` (Y=180) for every other heritage (`@0x0047ec41`). acdream hides Nose/Mouth (`Refresh`'s `clothesHidden` branch) but never repositions Skin, leaving a visible vertical gap in the Face tab's spin list for these three heritages. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`Refresh`'s `clothesHidden` branch — hides Nose/Mouth, never moves Skin) | The spins are laid out via their authored LayoutDesc positions (`DatWidgetFactory`), which this campaign's slice doesn't runtime-reposition for any other case; the targeted behavior this round was visibility (hiding unreachable spins), not repositioning the ones that remain. | A side-by-side against retail on Olthoi/OlthoiAcid/Gearknight shows a visible vertical gap where Nose/Mouth used to sit, instead of Skin sliding up to close it — a layout/cosmetic gap, not a functional one. | `gmCGAppearancePage::Update` `MoveTo` calls `@0x0047edef` (Olthoi/OlthoiAcid), `@0x0047ea83` (Gearknight), `@0x0047ec41` (every other heritage, the "normal" position) | -| AP-220 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 7).** Retail's `gmCGAppearancePage::Update` calls `CharGenState::RandomizeAppearance(state, 0)` + `CharGenState::RandomizeClothing(state, 1)` exactly once, on the SPECIFIC frame the heritage crosses the Gearknight boundary in either direction — entering Gearknight from something else (`@0x0047e973`, gated on `m_LastHeritageGroup != 6`) or leaving Gearknight for something else (`@0x0047eb58`, gated on `m_LastHeritageGroup == 6`). acdream's `Refresh` (the `Update` analogue) has no heritage-transition-edge tracking at all and never calls anything on a Gearknight-boundary crossing. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`Refresh` — no `_lastHeritageId`-style transition tracking or randomize call) | This is the SAME six-primitive gap AP-212 (the Random button) and AP-214 (ctor-time `RandomizeCharacter`) already track — `RandomizeAppearance`/`RandomizeClothing` are two of AP-212's six named-but-unported `CharGenState` primitives; a THIRD call site for the identical missing primitives doesn't widen the underlying gap, just where it's also reachable. | Switching heritage into or out of Gearknight in acdream leaves the character's prior appearance/clothing selections untouched (whatever indices were already set, now possibly out-of-range and silently clamped by `ConstrainAppearanceByGenderLocked` rather than freshly randomized), where retail re-rolls both — a behavioral gap a connected gate switching heritage to/from Gearknight would observe directly. | `gmCGAppearancePage::Update` `@0x0047e973` (entering Gearknight) and `@0x0047eb58` (leaving Gearknight); `CharGenState::RandomizeAppearance @0x005c4f10`; `CharGenState::RandomizeClothing @0x005c6770` (both already cited by AP-212) | +| AP-220 | **Filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round (F2 item 7); tightened 2026-08-15 at the re-review of fix commit `d2a71152` (N1) — "leaving Gearknight for something else" over-claimed the exit side.** Retail's `gmCGAppearancePage::Update` calls `CharGenState::RandomizeAppearance(state, 0)` + `CharGenState::RandomizeClothing(state, 1)` exactly once, on the SPECIFIC frame the heritage crosses the Gearknight boundary in either direction — entering Gearknight from something else (`@0x0047e973`, gated on `m_LastHeritageGroup != 6`) or leaving Gearknight for a non-Olthoi heritage (`@0x0047eb58`, gated on `m_LastHeritageGroup == 6` inside the `else` arm of the `mHeritageGroup == 0xc || mHeritageGroup == 0xd` Olthoi/OlthoiAcid test `@0x0047eb46` — leaving Gearknight FOR Olthoi or OlthoiAcid takes the Olthoi-specific `if` arm instead and does NOT randomize). acdream's `Refresh` (the `Update` analogue) has no heritage-transition-edge tracking at all and never calls anything on a Gearknight-boundary crossing. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`Refresh` — no `_lastHeritageId`-style transition tracking or randomize call) | This is the SAME six-primitive gap AP-212 (the Random button) and AP-214 (ctor-time `RandomizeCharacter`) already track — `RandomizeAppearance`/`RandomizeClothing` are two of AP-212's six named-but-unported `CharGenState` primitives; a THIRD call site for the identical missing primitives doesn't widen the underlying gap, just where it's also reachable. | Switching heritage into or out of Gearknight in acdream leaves the character's prior appearance/clothing selections untouched (whatever indices were already set, now possibly out-of-range and silently clamped by `ConstrainAppearanceByGenderLocked` rather than freshly randomized), where retail re-rolls both — a behavioral gap a connected gate switching heritage to/from Gearknight would observe directly. | `gmCGAppearancePage::Update` `@0x0047e973` (entering Gearknight) and `@0x0047eb58` (leaving Gearknight); `CharGenState::RandomizeAppearance @0x005c4f10`; `CharGenState::RandomizeClothing @0x005c6770` (both already cited by AP-212) | +| AP-221 | **Filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (R2) — records the F8 one-shot-binding disposition the re-reviewer accepted as a scoped, documented call, but which shipped without a register row of its own.** The chargen Appearance-page preview's GPU-side renderer/viewport binding in `LivePresentationComposition`'s chargen block reads `RetailUiRuntime.ChargenPreviewViewportWidget` exactly ONCE, synchronously, during the single `GameWindow.OnLoad` composition pass. `ChargenPreviewViewportWidget` is computed-through `CharacterCreationUiMountCoordinator`, which IS explicitly retryable/idempotent — ticked once per frame (via `RetailUiRuntime.Tick`) until its own DAT/resource read succeeds. If the coordinator's synchronous construction-time mount has NOT succeeded by that one composition pass (DATs not readable on that exact frame), the coordinator's later per-frame retries can still restore the rest of the mounted chargen SCREEN, but this GPU-side lease/binding is never retried — the preview stays permanently unbound for the rest of the session: no lease acquired, no renderer assigned to `chargenViewport`, `RetailUiRuntime.ChargenPreviewControl` never set, and the Appearance page's zoom/rotate controls silently no-op for the whole session. The narrowed diagnostic added at R1 (this same commit) is the only operator-visible evidence, and only fires when retained UI is actually mounted. | `src/AcDream.App/Composition/LivePresentationComposition.cs` (the chargen preview viewport block, the `if (dispatcherLease.Resource is { } chargenDispatcher && interaction.RetainedUi?.Runtime.ChargenPreviewViewportWidget is { } chargenViewport)` arm and its `else if` diagnostic); `src/AcDream.App/UI/RetailUiRuntime.cs` (`ChargenPreviewViewportWidget`); `src/AcDream.App/UI/Layout/CharacterCreationUiMountCoordinator.cs` | Retrofitting cross-frame retry into this one binding would mean restructuring the whole composition's one-shot GPU-resource-wiring contract shared by paperdoll (`PaperdollViewportWidget`) and creature-appraisal in the SAME method, plus the fixed `PrivateEntityViewportFrameGroup` array `FrameRootComposition` builds from the result — out of the CC6b-MOUNT fix round's blast radius; the re-reviewer accepted the narrower diagnostic-only fix (R1) as sufficient for this round with this row as the tracked follow-up. | On the specific unlucky frame where the coordinator's construction-time `Tick()` has not yet succeeded (a DAT/resource read not ready that frame), a user gets a chargen screen that otherwise mounted fine but whose 3D preview zoom/rotate controls are dead for the ENTIRE session with no visible error beyond the (narrowed) console diagnostic — a session-permanent, hard-to-reproduce loss a future retry-aware rewrite of this binding (CC5 or a follow-up slice) should close. | `src/AcDream.App/Composition/LivePresentationComposition.cs:996-1104` (chargen preview block's own F8 disposition comment); `RetailUiRuntime.ChargenPreviewViewportWidget`'s doc comment (retry-vs-one-shot contrast) | +| AP-222 | **Filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — discovered while adding the nit's own requested media pin, MEASURED against the installed EoR dat rather than assumed.** F2 item 2's current-part spin highlight (`CharacterCreationAppearancePage.RefreshColorAndShadeControls` calling `spin.TrySetRetailState(UiButtonStateMachine.Highlight)` on the previously-current and newly-current spin, mirroring `gmCGAppearancePage::SetSelection @0x0047e260`'s `SetState(1)`/`SetState(6)` pair) is a COMPLETE NO-OP for all nine spins against the installed dat: `TrySetRetailState` itself always reports success for a `ToggleBehavior` button regardless of media (it just sets `Selected` and lets `UiButton.UpdateVisualState` resolve the actual draw state), but every one of the nine spins' two consumed arrow face segments (`UiButton`'s composite-body mechanism, AD-103's sibling convention) authors ONLY `Normal`/`Normal_rollover`/`Ghosted` state media — no `Highlight`/`Highlight_rollover`/`Highlight_pressed` art exists anywhere on any spin. `UiButton.UpdateVisualState`'s own committed-state gate (`_availableStates.Contains(requested)`, `UiButton.cs:647`) then silently keeps `ActiveState` at `"Normal"` instead of ever reaching `"Highlight"`. The PRE-EXISTING F2-item-2 live-DAT pin (`AppearancePage_HasGenderChoiceSpinsSwatchesShadeAndViewport`) only verified the `ToggleBehavior` PROPERTY that gates the state-machine branch, never whether that branch has anything to actually draw — so this shipped, unnoticed, since the fix round that added the highlight call. | `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`RefreshColorAndShadeControls`'s spin loop); `src/AcDream.App/UI/UiButton.cs` (`UpdateVisualState`, `TrySetRetailState`'s `ToggleBehavior` branch) | Not yet resolved which side is wrong: retail's own `SetState(6)` call could ALSO be a visual no-op if retail's spin art likewise lacks Highlight media (this codebase's own `TrySetRetailState` `#382` comment already documents that a committed StateDesc with no media draws nothing in EITHER client) — or retail's current-part indicator might use an entirely different, unported mechanism (an overlay, like AP-215's swatch-selection ring, rather than a state swap on the spin itself). Deciding requires a decomp read of whichever retail function actually renders the spin's per-frame face, out of this residual round's scope (N2 was filed as a media-pin nit, not an investigation). | The F2 "current-part highlight" feature is presentation-dead for every spin today: clicking Hair/Eyes/Nose/Mouth/Skin/Headgear/Shirt/Trousers/Footwear changes the selected part but produces no visible highlight change anywhere on the Appearance page, which a visual gate comparing "does the current spin look selected" against retail would catch immediately, in either direction (parity if retail is equally silent, a real gap if retail is not). | `gmCGAppearancePage::SetSelection @0x0047e260` (`SetState(1)`/`SetState(6)` calls); `UiButton.cs:647` (`UpdateVisualState`'s commit gate); `UiButton.cs:244-303` (`TrySetRetailState`'s `#382` comment on committed-but-medialess StateDesc behavior) | | AP-214 | **Filed 2026-08-15 at Campaign CC slice CC6b-MOUNT (AD-101's retirement research).** Retail's chargen screen does NOT open blank: `gmCharGenMainUI::gmCharGenMainUI @ 0x004e7eb0` calls `CharGenState::RandomizeCharacter(state, hasToD) @ 0x005c6d80` (~`0x004e81f5`-`0x004e8218`) BEFORE constructing any page (Heritage/Profession/Skills/Appearance/Town/Summary all `InitializePage` AFTER this call) — `RandomizeCharacter` itself Resets then rolls a random heritage (`RollDice(1, hasToD?4:3)`), a random gender (`RollDice(1,2)`), `RandomizeAppearance`, `RandomizeHeadgear`/`Shirt`/`Trousers`/`Footwear`, `RandomizeTemplate`, and `RandomizeStartArea`, freezing heritage/sex/appearance. This ALSO resolves the plan's risk item 5 "gender-flip-on-init oddity" at `gmCGAppearancePage::InitializePage @0x0047FDD0` (~`0x004802DA`-`0x00480303`): since `RandomizeCharacter` already assigned a real (non-zero) gender before the Appearance page constructs, that page's own gender-read-and-FLIP-to-the-opposite code ALWAYS fires on first open, deterministically inverting `RandomizeCharacter`'s random gender pick — a genuine, always-reachable retail quirk, not a latent/unreachable one. acdream does not port `RandomizeCharacter` this round — the same six missing Runtime primitives (`RandomizeHeritageGroup`/`RandomizeGender`-via-`SetGender`/`RandomizeAppearance`/`RandomizeClothing`(via the four Randomize* gear calls)/`RandomizeTemplate`/`RandomizeStartArea`) AP-212 already tracks for the Random BUTTON are the SAME gap that would be needed here — so acdream's chargen screen opens honestly blank (heritage/gender/appearance all `Unset`) and the player makes every choice explicitly, including gender on the Appearance page (AD-101's retirement). | `src/AcDream.App/UI/Layout/CharacterCreationUiController.cs` (no `RandomizeCharacter`-equivalent call at construction — the gap itself); `src/AcDream.App/UI/Layout/CharacterCreationHeritagePage.cs` (`Select`, AD-101's retirement point) | Full-fidelity would require porting `RandomizeCharacter` and its six sub-primitives into Runtime (AP-212's own "known landing site" note) — out of this slice's scope, which is the Appearance page's own controls, not a fourth cut at the Random button's primitives. Landing this WOULD ALSO close AP-212's gap for the "Random button while on Summary" case, since retail's `DoRandom`'s own Summary branch is a direct `RandomizeCharacter` call. | A connected two-client visual gate comparing "what does the chargen preview show on first open" against retail would see a blank/default acdream character versus retail's fully-randomized one — an expected, documented divergence, not a bug; the FLIP quirk itself has zero acdream analogue to diverge from (there's nothing to flip when gender starts Unset). **Latent Finish-path interaction noted at the CC6b-MOUNT review fix round (F12, 2026-08-15):** with AD-101 retired, honest-blank heritage/gender means `RuntimeCharacterCreationState.TryBeginFinish` can be reached with `_genderKey == 0` (or an unselected heritage) — `TryBeginFinish`'s four refusals (NoName/AttributeCreditsUnspent/AlreadyPending/RosterFull) have no heritage/gender gate today. Currently LATENT ONLY (Finish is hard-disabled + `OnClick` null this round — TS-82); CC5's own scope is now AMENDED (see the plan doc's Slices table) to land BOTH a heritage/gender refusal in `TryBeginFinish` AND a real `RandomizeCharacter` port before the connected user gate opens Finish for real use, since a gate alone does not reproduce retail's actual guarantee (retail's ctor-time `RandomizeCharacter` means heritage/gender are NEVER unset by the time a player can reach Finish at all). | `gmCharGenMainUI::gmCharGenMainUI @0x004e7eb0` (`~0x004e81f5-0x004e8218`); `CharGenState::RandomizeCharacter @0x005c6d80`; `CharGenState::Reset @0x005c68a0` (confirms `SetGender(this,0)` is the ONLY other gender-touching call in the reset path); `gmCGAppearancePage::InitializePage @0x0047FDD0` (`~0x004802da-0x00480303`, the gender-flip arm) | | AP-213 | **Filed 2026-08-15 at Campaign CC slice CC4 (the Skills page listbox).** Retail's `gmCGSkillsPage` sorts every skill into four buckets — Specialized, Trained, UseableUntrained, UnuseableUntrained — via `InsertEntrySorted @ 0x00480a40` and re-buckets on every level change through `UpdateSkillEntry @ 0x00480bf0`, giving each row a category-relative position instead of a fixed order. `CharacterCreationSkillsPage` instead builds ONE flat listbox, rows in ascending skill-id order, each showing `"{name}: {level} (T{trainedCost}/S{specializedCost})"`, with a single click-to-advance/double-click-to-retreat interaction replacing retail's separate per-row Increase/Decrease affordances (`IncreaseSkillLevel @ 0x00480ca0`/`DecreaseSkillLevel @ 0x00480d60`). | `src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs` (`RebuildRows`, `FormatSkillLabel`, `Advance`, `Retreat`) | The four-bucket sorted model is a pure presentation refinement (grouping/ordering, not a rules difference) — every skill's costs, current level, and the credits gate CC3's `RuntimeCharacterCreationState` enforces are byte-identical; a flat list surfaces the same information with less UI-layer code for this slice's scope. | A player scanning for "what's already Trained" has to read each row's own level text instead of finding it grouped at the top of a bucket — a discoverability/polish gap, not a correctness gap; a future slice wanting the exact retail grouping can layer it on top of the SAME `RuntimeCharacterCreationState` commands without touching Runtime. | `gmCGSkillsPage::InsertEntrySorted @ 0x00480a40`; `gmCGSkillsPage::UpdateSkillEntry @ 0x00480bf0`; `gmCGSkillsPage::IncreaseSkillLevel @ 0x00480ca0`; `gmCGSkillsPage::DecreaseSkillLevel @ 0x00480d60` | | AP-212 | **Filed 2026-08-15 at Campaign CC slice CC4 (the Random button, element `0x100003cb`); primitives named+cited in the review fix round (F8, 2026-08-15).** `gmCharGenMainUI::DoRandom @ 0x004e7d70` switches on the current page and dispatches to six NAMED, fully decompiled retail primitives, one per page: Heritage -> `CharGenState::RandomizeHeritageGroup(state, hasToD) @ 0x005c6a20` (called with `CPlayerSystem::AccountHasThroneOfDestiny`); Profession -> `CharGenState::RandomizeTemplate(state) @ 0x005c6500`; Skills -> `CharGenState::RandomizeSkills(state) @ 0x005c57e0`; Appearance -> `CharGenState::RandomizeAppearance(state, 0) @ 0x005c4f10` or `CharGenState::RandomizeClothing(state, 1) @ 0x005c6770` depending on the page's current sub-choice (`m_eCurType == ECG_CHOICE_CLOTHES`); Town -> `CharGenState::SetStartArea(state, RandInt(hasToD ? 4 : 3))`; Summary -> `CharGenState::RandomizeCharacter(state, hasToD) @ 0x005c6d80`. None of these six is exposed as a CC3 Runtime command primitive today. CC4's Random handler approximates the Heritage/Profession/Town cases with a UNIFORM pick over every valid option reachable through the page's own existing commands (`SelectHeritage`/`SelectTemplate`/`SelectStartArea`), and disables the button outright on Skills, Appearance (CC6b-MOUNT review fix F5 correction: NOT a placeholder — retail's own `DoRandom` case 3 fully enables Random here; the disable rests on the same unported `RandomizeAppearance`/`RandomizeClothing` primitives this row already names), and Summary (this round's placeholder — no `CharacterCreationSummaryPage` exists yet to host a randomize-warning dialog; see TS-82). | `src/AcDream.App/UI/Layout/CharacterCreationUiController.cs` (`OnRandom`, `ApplyProgressState`'s `_random.Enabled` gate); `src/AcDream.App/UI/Layout/CharacterCreationHeritagePage.cs` (`Randomize`); `src/AcDream.App/UI/Layout/CharacterCreationProfessionPage.cs` (`Randomize`); `src/AcDream.App/UI/Layout/CharacterCreationTownPage.cs` (`Randomize`) | Random is a convenience affordance, not a gate any create can fail without — every value it can produce is independently reachable (and independently retail-cited) through the page's own ordinary Select commands; a uniform distribution over "every DAT-installed option" is the closest available stand-in without porting six more retail algorithms this slice did not scope. This is DEFERRED work with a known landing site, not an unrecoverable gap: all six primitives are named and decompiled above, and the natural home for a faithful port is Runtime, beside CC3's other `CharGenState` ports (`RuntimeCharacterCreationState`), exposed as new commands the App-layer `Randomize` methods on each page would call instead of picking uniformly. | A retail-parity test that checks the STATISTICAL distribution of repeated Random clicks (not just "produces a valid selection") would find acdream's uniform-over-all-options distribution differs from retail's own (e.g. `RandomizeTemplate`'s exact weighting, or the ToD-account-gated 3-vs-4 town bound — see AD-102). Skills/Appearance/Summary have no Random affordance at all until their respective primitives/pages land. | `gmCharGenMainUI::DoRandom @ 0x004e7d70`; `CharGenState::RandomizeHeritageGroup @ 0x005c6a20`; `CharGenState::RandomizeTemplate @ 0x005c6500`; `CharGenState::RandomizeSkills @ 0x005c57e0`; `CharGenState::RandomizeAppearance @ 0x005c4f10`; `CharGenState::RandomizeClothing @ 0x005c6770`; `CharGenState::RandomizeCharacter @ 0x005c6d80`; `CharGenState::SetStartArea` random-bound call site | diff --git a/docs/plans/2026-08-15-character-creation-campaign.md b/docs/plans/2026-08-15-character-creation-campaign.md index f7fa2011..187ba0e4 100644 --- a/docs/plans/2026-08-15-character-creation-campaign.md +++ b/docs/plans/2026-08-15-character-creation-campaign.md @@ -258,4 +258,4 @@ the user gate. **Review fix round (F1-F12, same session):** F1 (BLOCKING) — `hairStyle.AlternateSetup != 0` / `setupId == 0` tested the wrong sentinel; retail's Setup "unset" is `INVALID_DID` (0xFFFFFFFF — `CharGenState::GetSetupID @0x005C5B22`), not 0, so an `AlternateSetup` field storing that value would have been ADOPTED as a literal Setup id, nulling `Get` and killing the whole preview. Fixed at both sites (`ChargenAppearanceFactory.cs`, new `InvalidDid` constant); two new hand-built tests plus a new installed-DAT sweep (`EveryHairStyleOfEveryHeritageGender_ComposesToARealInstalledSetupId`, 869 selections across all 26 heritage/gender combinations, zero unresolved). F2 (BLOCKING) — TS-84's register row, `ChargenClothingTable.cs`'s doc comment, and this ledger row all understated Undead's measured gap as "headgear/trousers/footwear" (3 slots) with a self-contradicting "4 of 4 non-shirt slots" aside; corrected everywhere to the true measured ALL FOUR slots (headgear, trousers, shirt, footwear). F3 (BLOCKING) — the "three independent sources" palette-math claim overcounted; corrected to the two that actually hold (decomp control flow + ACE's cited port) in `ChargenPalSetMath.cs`'s doc and this row (see above). F4 (MEDIUM, landed despite no CC6a call site yet) — `ChargenPreviewEntityBuilder.TryBuild` did unlocked dat reads; `DatCollection` is not thread-safe and every sibling dat-touching resolver in this layer takes a shared `object datLock`. Added a required `datLock` parameter; every dat read (Setup fetch, held-pose resolution, per-part GfxObj checks, surface-override resolution) now happens inside one `lock`, mirroring `RetailPaperdollPoseApplicator.Apply`'s "resolve under lock, process after" shape. F5 (LOW) — `Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate` is a PRE-EXISTING timing flake unrelated to any chargen code (passes 15/15 in isolation per the reviewer); noted here so a future session doesn't chase it as a CC6a regression. F6 (LOW) — `ChargenPreviewCamera.cs`'s rotation doc cited a nonexistent `RotationDegreesPerSecond` identifier in a dimensionally-wrong expression; corrected to retail's actual per-tick formula (`DoRotation @0x0047CAC7`: `deltaDegrees = ((now - lastRotateTime) / RotationSecondsPerRevolution) * 360`). F7 (LOW-MEDIUM) — the installed-DAT tests' env-gated skip returns green with a console note when no dat dir is configured (confirmed this IS the house pattern — no Content installed-DAT test in the project uses `Assert.Skip`, so it was kept rather than diverging), but the TS-84 measurement was WriteLine-only; now pinned with real assertions (zero gaps for the 9 standard heritages, exactly the 4 measured Undead table ids on both genders — `[0x10000009, 0x100000F9, 0x10000001, 0x10000007]`, same order both genders). F8 (LOW) — the inner PalSet-miss loop recorded-and-continued past a miss; retail's own loop (`ClothingTable::BuildObjDesc` ~0x005A7B24-0x005A7BD3) returns 0 immediately on a miss at ~0x005A7B32, ABORTING every remaining choice in that garment — `continue` changed to `break`, new test proves a second (present) PalSet's choice is correctly NOT applied when it follows a missing one. F9 (LOW) — three dangling `` doc-comment references (the method is `TryCompose`) fixed. F10 (LOW) — the packed `(byte)(range.Offset/8)`/`(byte)(range.NumColors/8)` narrowing on dat-sourced data was unchecked (a real `NumColors` of 2048 wraps 256→0 as an unchecked byte cast, which HAPPENS to match retail's own "0 means whole palette" sentinel); replaced with explicit `PackOffset`/`PackNumColors` helpers that document the 2048→0 equivalence deliberately and throw `ArgumentOutOfRangeException` on any other unrepresentable shape, with two new tests (the sentinel case, the throwing case). F11/F12 (LOW, CC6b scope, no code this round) — noted in the CC6b row below: the second `m_alternateSetupID` override source (the appearance-page option checkbox — Penumbraen crown `@0x004DFB3F`, Undead no-flame `@0x004E0C54`, precedence at `@0x004EEA51`) is unmodelled; a shared `RetailHeldPose` helper is worth extracting before a fourth held-pose consumer exists (paperdoll, appraisal's live-target case is different, chargen — a third, not yet fourth). **F11 CONCEDED MIS-SCOPED at the CC6b-PRE review fix round (2026-08-15):** the two cited write sites are `gmBarberUI`'s, not `gmCGAppearancePage`'s — see the CC6b-PRE row's own corrected item 4 for the citation table (enclosing-function scan) and the resulting directive that CC6b-mount must NOT build an option checkbox here. **Test counts after the fix round (measured, not projected):** Core.Tests 4772/1 skip (+5 from F1's two hand-built tests, F8's one, F10's two), Content.Tests 147/0 skips (+1 from F1's new installed-DAT sweep — F7 added assertions to the EXISTING installed-DAT test rather than a new one), App.Tests 5121/6 skips (unchanged pass count; F5's named flake did NOT reproduce in this session's full-suite run) — zero failures, full solution Release build green. | | CC6b-PRE | PRE-MOUNT HALF CODE-COMPLETE 2026-08-15 (the mount-independent scope only — idle animation, rotation, zoom for the chargen preview; the page-mount half — Appearance page, spin controls, color wheels, viewport wiring — is a SEPARATE follow-up landing after CC4 merges, per the original CC6 split) | `8dfee111` (pre-mount half), plus a same-round review fix commit (F1-F7 + the F11-concession rewrite) | Dual-lens review returned architectural PASS with reservations + retail fidelity PASS with reservations, merge after F1 — landed this round along with F2-F7 and the ALSO item (the reviewer's claim-2 barber refutation was UPHELD; claim-1's idle-by-default CONCLUSION was correct but its "elided ctor byte" argument was unsound, replaced with the real `InitializePage` evidence) | **Idle animation loop, TS-83 RETIRED:** decomp re-read of `gmCGAppearancePage::Update`'s own trailing gate (~0x0047EF01-0x0047EF12: `if (m_bZoomedIn == 0) StartAnimation(); else StopAnimation();`, unconditional on every Update call — heritage/gender change or page becoming visible) plus the DIRECT ASSIGNMENT evidence located at the re-review — `gmCGAppearancePage::InitializePage @0x0047FDD0` writes an explicit `m_bZoomedIn = 0` at `0x004802C3`, right after setting the camera to the zoomed-IN per-heritage eye at `0x00480286-0x0048029E` (the null-tween quirk); the earlier elided-ctor-byte argument was UNSOUND (heap-new members are indeterminate, not zero) and is superseded — settles a fact CC6a's own TS-83 row left as "not yet located precisely": **retail's chargen preview defaults to the idle loop PLAYING, not the frozen rest pose** — the rest pose only appears once the user presses Zoom In, which retail's own `ZoomIn`/`ZoomOut` (`0x0047CF00`/`0x0047D050`) call `gmCG3DView::StopAnimation`/`StartAnimation` for IMMEDIATELY (before the camera's own 0.6s tween even starts). New Core primitive `RetailAnimationCyclePlayback` (`src/AcDream.Core/Physics/`, pure, unit-tested) ports `CPhysicsObj::set_sequence_animation @ 0x0050F6F0`'s effect (advance-with-wrap + lerp/slerp) — the SAME algorithm this codebase's App layer already carries inline for its no-`AnimationSequencer` NPC idle path (`LiveEntityAnimationPresenter.Present`'s legacy branch); the two call sites are NOT consolidated this round (that file is live, heavily-tested, in-flight production entity-rendering code unrelated to this preview-only feature — a deliberate blast-radius call, not an oversight, noted in the new type's own doc comment for a future mechanical pass). New App type `ChargenPreviewAnimator` (`src/AcDream.App/Rendering/`) owns the per-tick idle-frame advance / rest-pose freeze swap; `ChargenPreviewEntityBuilder` gained `TryBuildAnimated` (returns a `ChargenPreviewAnimatedBuild`: the entity, resolved drawable parts, precomputed rest pose, resolved idle Animation + frame range) alongside the ORIGINAL `TryBuild` (kept RESULT-identical, not byte-identical internally — F6: it now also resolves the idle DID and loads the idle Animation before discarding them; a thin wrapper now, all 3 of its existing tests still pass unchanged) — `ResolveIdleAnimEnum` resolves `m_didAnimation`'s enum key (0x10000006 standard, 0x10000011 Olthoi, 0x10000013 OlthoiAcid) alongside the existing `ResolveRestPoseEnum` (0x10000005/0x10000011/0x10000013) — **Olthoi and OlthoiAcid use the SAME enum key for BOTH idle and rest** (retail quirk, decomp-confirmed at ~0x004ee7e9/0x004ee7ff and ~0x004ee892/0x004ee8a8: those two heritages show no visible difference between "playing" and "zoomed in and frozen"). **Rotation controller:** new `ChargenPreviewRotationController` (`src/AcDream.App/Rendering/`) ports `gmCGAppearancePage::Rotate`/`DoRotation` (`0x0047CB50`/`0x0047CA80`) verbatim — toggle-to-stop-same-direction, `deltaDegrees = ((now - lastRotateTime) / RotationSecondsPerRevolution) * 360`, a SINGLE-PASS ±360 clamp (not a full modulo — retail's own tail only corrects once, reproduced as-is rather than "improved"), the `-1.0` sentinel `Rotate()` writes to invalidate `m_dLastRotateTime` (bit-confirmed: high dword `0xbff00000` + zero low dword). `ECG_ROTATE_CLOCKWISE=1`/`ECG_ROTATE_COUNTERCLOCKWISE=2` confirmed from `acclient.h:6848-6852` — CLOCKWISE adds to heading, everything else subtracts. Applies to the ENTITY's heading via `MoveToMath.SetHeading` (the exact existing `CPhysicsObj::set_heading` port, reused rather than reinvented), not the camera — confirming CC6a's own architecture note. **Zoom tween:** new `ChargenPreviewZoomController` ports `ZoomIn`/`ZoomOut`/`DoZoomAnimation` (`0x0047CF00`/`0x0047D050`/`0x0047C960`) — a LINEAR (not eased — the decomp shows a straight `(targ-start)*t+start` per axis with no easing curve anywhere in the function) 0.6s tween between `ChargenPreviewCamera`'s already-recorded default/zoomed-out eye profiles, using the same `-0.1` invalidation-sentinel idiom as rotation; `ZoomIn`/`ZoomOut` call into `ChargenPreviewAnimator.SetZoomedIn` IMMEDIATELY (synchronously, inside the button-press method itself — not gated on the tween's own completion), matching the decomp's call ORDER exactly. **Fix round F2:** the controller and the animator originally kept two INDEPENDENT `IsZoomedIn` bools synced only through a nullable animator argument on `ZoomIn`/`ZoomOut` — a null pass, or a direct `ChargenPreviewAnimator.SetZoomedIn` call bypassing the controller, could desync the camera target from the animation pose. Retail's `m_bZoomedIn` is a SINGLE field gating both, so `ChargenPreviewZoomController` now takes its `ChargenPreviewAnimator` as a required constructor dependency and `IsZoomedIn` reads straight through to the animator's own flag — one owner, matching retail's own shape, with no second bool left to disagree. **`m_alternateSetupID` (MUST-COVER item 1) — RESEARCH CORRECTION, not a straight port:** re-reading the decomp function-by-function (not just address-by-address) found that ALL FIVE `m_alternateSetupID` write sites — including the two the CC6a review fix round cited, Penumbraen crown `@0x004DFB3F` and Undead no-flame `@0x004E0C54` — belong to `gmBarberUI`, not `gmCGAppearancePage`. Enclosing-function table (every write site, confirmed by scanning each site's containing function body for sibling calls that only make sense in one class): `@0x004DFB5B` sits inside `gmBarberUI::ListenToElementMessage` (sibling evidence: `gmBarberUI::SetSelection`/`gmBarberUI::Rotate` calls in the same body, which ends in a `CM_Character::Event_FinishBarber` wire call — a barber-shop-only message); `@0x004E0C54` (Penumbraen crown), `@0x004E0D42`, and `@0x004E0DB1` all sit inside the SAME `gmBarberUI::InitializePage` (sibling evidence: `m_pOption1Checkbox` reads and `UIElement_Text::SetStringInfoWithFont` calls on barber-specific string ids in that body); the ONLY thing `gmCGAppearancePage` itself ever does with the field is READ it generically through the shared `gmCG3DView` ctor/`::Update` (every `gmCG3DView` owner does this) — `gmCGAppearancePage`'s own field list (`acclient.h:56373-56428`, checked exhaustively) has NO `m_pOption1Checkbox`-equivalent member and none of its own methods write `m_alternateSetupID`. `gmBarberUI` is the POST-CREATION barber-shop appearance-editing screen — a wholly separate UI class from character creation's `gmCGAppearancePage`. **For character creation, `m_alternateSetupID` is therefore ALWAYS `INVALID_DID` in retail — the barber shop's crown/flame variant checkbox is not reachable during chargen at all**, and is out of this campaign's scope entirely. **Directive for CC6b-mount: do NOT build an option checkbox for Penumbraen-crown/Undead-no-flame variants on the Appearance page — retail has no such control there.** `ChargenAppearanceFactory.TryCompose` still gained a real, decomp-cited `alternateSetupIdOverride` parameter (default `InvalidDid`, i.e. no-op for every existing caller) implementing `gmCG3DView::Update`'s own generic precedence exactly (`~0x004EEA46-0x004EEA53`: the override, when present, REPLACES the hairstyle/gender-resolved setup outright, not additively) — a real mechanism reserved for a hypothetical future non-chargen (barber-shop) consumer of this same factory, not a fabricated chargen feature; 5 new hand-built tests prove the precedence chain and the `INVALID_DID` sentinel discipline. **RetailHeldPose extraction (MUST-COVER item 2) — DONE, clean mechanical extraction:** new `src/AcDream.App/Rendering/RetailHeldPose.cs` shares `ResolvePoseDid` (master-map-slot-7 DID lookup) and `ComposePartTransform` (`Scale*Rotate*Translate`) between `RetailPaperdollPoseApplicator.Apply` (paperdoll, refactored to call the shared helper, behavior byte-identical) and `ChargenPreviewEntityBuilder` (both the pre-existing rest-pose path and the new idle-frame path) — the two sites' surrounding per-index LOOP shapes stayed separate (paperdoll walks an already-filtered `WorldEntity.MeshRefs`; chargen walks the pre-filter Setup-part-indexed scratch list), matching the MUST-COVER's own "only if it stays clean" bar. **Bookkeeping:** TS-83 retired in `docs/architecture/retail-divergence-register.md` (§4 count 50→49, row removed, RETIRED clause added to the header narrative); the CC6a ledger row above now cites its real commit SHAs (`55bfd9ca`, `1774d8b2`) instead of "HEAD of `campaign-cc6a`". **Tests:** `RetailAnimationCyclePlaybackTests` (10, Core), `ChargenAppearanceFactoryTests` (+4, the override precedence/sentinel), `ChargenPreviewRotationControllerTests` (10, +1 this fix round — F7's clockwise-past-360 clamp case), `ChargenPreviewZoomControllerTests` (9, +2 this fix round — F2's null-ctor-throws and read-through-no-independent-state cases; every pre-existing case rewritten for the now-required-animator constructor), `ChargenPreviewAnimatorTests` (7, hand-built fixtures — no dat needed since a `ChargenPreviewAnimatedBuild` is constructible entirely in memory), `ChargenPreviewEntityBuilderTests` (+5, installed-DAT-gated — `TryBuildAnimated` resolves a real idle cycle for Aluvian AND Olthoi, the unknown-setup null path, both Olthoi/OlthoiAcid shared enum keys resolve to a real installed DID). Counts: Core.Tests 4786/1 skip (unchanged this fix round — F1-F7 were doc/API-shape/allocation fixes, no new Core tests), Content.Tests 147/0 skips (unchanged), App.Tests 5152/6 skips (+3 from 5149/6, the F2/F7 additions) — zero failures, full solution Release build green. Two PRE-EXISTING flakes noted across repeated full-solution runs, neither caused by this round and neither reproducing in isolation: `AcDream.Core.Net.Tests.Transport.NakEmissionTests.LossSoak_TwoPercentBidirectional_ZeroMessageLoss_LedgersConverge` (randomized-loss-injection timing, zero files under `src/AcDream.Core.Net/` touched) and `AcDream.Content.Tests.DecodedTextureCacheTests.GetOrCreate_ConcurrentMissRunsFactoryOnce` (a concurrency race under full-solution parallel load, zero files under `src/AcDream.Content/` touched this round either) — both pass 100% run standalone; both projects' full suites otherwise pass clean. **OWED (CC6b page-mount half, separate follow-up):** the Appearance/Summary viewport mount (`0x100003bb`/`0x10000406`), binding the Zoom In/Out and Rotate Clockwise/Counter-Clockwise buttons to `ChargenPreviewZoomController.ZoomIn`/`ZoomOut` (now parameterless — F2 made the animator a required constructor dependency, not a per-call argument) and `ChargenPreviewRotationController.Toggle`/`Tick`, spin controls, color wheels, and the INITIAL HEADING: `gmCGAppearancePage::InitializePage @0x0047FDD0` sets `m_fCurHeading = 180f` at `0x00480235` and pushes it via `SetPlayerHeading` at `0x0048023F` (overriding the ctor’s 0°; cross-confirmed at `gmBarberUI::PostInit @0x004DE330` and the summary page’s `0x0047BD54`) — the mount half must seed `ChargenPreviewRotationController.HeadingDegrees = 180f` or the character faces AWAY from the camera at the user gate. **Explicitly NOT owed:** an option checkbox for Penumbraen-crown/Undead-no-flame variants — see item 4's enclosing-function table above; `gmCGAppearancePage` never had one, so CC6b-mount must not invent one. | | CC7 | — | | | | -| CC6b-MOUNT | CODE-COMPLETE 2026-08-15 (the page-mount half CC6b-PRE deferred — Appearance page, spin controls, color-wheel family, viewport wiring — landing after CC4 merged, closing out Campaign CC's CC6 slice) | `34c6fceab0bc300ab638339b88c5e5f98ae4d724`, (this commit — the review fix round) | fix round landed F1-F13, narrow re-review pending | **Appearance page** (`CharacterCreationAppearancePage`, `src/AcDream.App/UI/Layout/`, wired into `CharacterCreationUiController` beside the four sibling pages): gender buttons (`0x100003a7`/`a8` -> `SelectGender(2)`/`SelectGender(1)`, decomp `ListenToElementMessage` cases `0x9d`/`0x9e`); Face/Clothes sub-tabs (`0x100003a9`/`aa`, cases `0x9f`/`0xa0`) toggling the `0x100003ae`/`b4` choice containers and defaulting the "current part" to Hair/Headgear respectively; nine spin controls (hair/eyes/nose/mouth/skin `0x100003af-b3`, headgear/shirt/trousers/footwear `0x100003b5-b8`) reproducing retail's two-arrow-plus-body-click composite through `UiButton.OnClickAt`'s local x coordinate — decrement zone x=[80,127), increment zone x=[127,174), else selects the part with no index change (cases `0xa5-0xa9` and their headgear/shirt/trousers/footwear mirrors) — since `DatWidgetFactory` consumes each spin's two locally-reused arrow children (`0x1000030a`/`0x1000030b`) into ONE flat `UiButton` with no separate addressable arrow widget; nine color swatches (`0x1000030f-0x10000317` -> `SetColor(0..8)`, gated on the current part's own color-list length exactly like retail's `iNumColors > N` check); the shade scrollbar (`0x10000321`) bound via `ScalarChanged`; zoom/rotate buttons delegating to a late-bound `IChargenPreviewControl` seam. **Per-part routing table** (`StyleSlotFor`/`ColorSlotFor`/`ShadeSlotFor`), decomp-derived from `SetColor @0x0047DD50` and `SetShade @0x0047C860`: Hair has its own color AND shade; Eyes has color but NO shade (retail's `SetShade` switch has no case 1 — independently confirmed against CC6a's own "eye color has no shade indirection" finding); Nose/Mouth/Skin have NO color and ALL route their shade to SKIN shade (cases 2/3/4 share one decompiled body — a genuine retail quirk, not a porting shortcut); Headgear/Shirt/Trousers/Footwear each have their own color and shade. **Wrap semantics** (`CharacterCreationAppearancePage.CycleIndex`, internal static, unit-tested via 10 `[Theory]` cases): plain `[0,count)` modulo wrap for every style spin except Headgear; Headgear alone gets the decomp-derived `(count+1)`-position RING including the `Unset` ("no headgear") position — `CharGenState::SetHeadgearStyle`'s literal signed-int32 comparison shape (`0x0047F4B5`-`0x0047F530` decrement, `0x0047F7D8` increment): decrementing FROM style 0 lands on Unset, incrementing FROM Unset lands on style 0, decrementing FROM Unset wraps to the LAST style, incrementing past the last style lands on Unset — a real closed ring of `count+1` positions, not a plain wrap. **Review fix round F1 correction (2026-08-15):** every OTHER style spin ALSO has a decomp-observable Unset-cycling case, in the SAME switch the headgear ring was ported from — the shared decrement tail (`label_47f065`/`label_47f6d9`, reached from Hair's own decrement case `@0x0047f465-0x0047f486` and inlined per-part for Eyes/Nose/Mouth/Shirt/Trousers/Footwear) computes `new = cur - 1` on the raw signed int32 (Unset = -1), giving `new = -2`, which wraps to `count - 1` — the SAME "wrap to the last index" shape headgear's own ring uses. Incrementing from Unset (`new = -1 + 1 = 0`) was already correct in acdream. The original claim here ("no decomp-observable Unset-cycling case... starts at style 0 for BOTH directions") is WRONG for decrement; fixed in `CharacterCreationAppearancePage.CycleIndex` and its own corrected doc comment. **Heritage 6/0xc/0xd gate** (`gmCGAppearancePage::Update @~0x0047EB46-0x0047EE95`): Gearknight/Olthoi/OlthoiAcid hide the Clothes sub-tab (making all four clothing spins unreachable, matching the OWED item's "four clothing spins hidden" framing through retail's OWN mechanism — hiding the tab, not each spin individually) plus the Nose/Mouth spins directly, and disable the Eyes spin's arrows (`_eyesArrowsDisabled`, since Olthoi/Gearknight forms have fixed eyes); **review fix round F3 correction (2026-08-15):** forces `SetChoice(FACE)`/`SetSelection(HAIR)` UNCONDITIONALLY whenever the gate engages (`@0x0047eac6/0x0047eacf` Gearknight, `@0x0047ee32/0x0047ee3b` Olthoi/OlthoiAcid) — NOT only when Clothes happened to be showing, the original (wrong) framing here. A conditional gate left Nose/Mouth as the current part when the Face tab was already active, stranding the shade control on a now-hidden part; retail always snaps back to Hair. **Preview wiring** (`ChargenPreviewController`, `src/AcDream.App/Rendering/`, new): bridges a real architectural gap the CC6a/CC6b-PRE foundation left open — `ChargenPreviewRenderer` only ever built its OWN private `ChargenPreviewCamera` with no injection seam, but `ChargenPreviewZoomController` needs a SETTABLE camera to tween. Fixed at the root: `ChargenPreviewViewportCamera` gained a `ChargenPreviewCamera`-accepting constructor overload, `ChargenPreviewRenderer` gained an optional `camera` parameter using it, and `ChargenPreviewController` owns the ONE shared `ChargenPreviewCamera` instance handed to both. `ChargenPreviewController` consolidates the per-frame `IPrivateEntityViewportFrame` owner role (mirrors `PaperdollFramePresenter`, self-timing via `Stopwatch` rather than touching the shared frame-phase interface) with the `IChargenPreviewControl` seam the page's buttons bind against (constructed before the graphics backend exists, so the page cannot receive the real renderer at construction time — assigned late by `LivePresentationComposition`, exactly mirroring the paperdoll's own late `viewport.Renderer = ...` assignment). `Rebuild` recomposes via `ChargenAppearanceFactory.TryCompose` + `ChargenPreviewEntityBuilder.TryBuildAnimated` on ANY heritage/gender/appearance-selection change (no-op if identical to the last composed selection) but only SNAPS the camera to the heritage's default eye on a HERITAGE OR GENDER change (decomp-cited: `gmCGAppearancePage::Update`'s only two confirmed direct call sites are `InitializePage` and the two gender-button handlers; spin/color/shade changes call the narrower `SetSelection`/`SetColor`/`SetShade`, none of which touch `m_vectCurPosition`) — a fresh `ChargenPreviewAnimator` is unavoidable on every rebuild (it owns the resolved drawable-part list, which changes with the mesh) but is immediately restored to the PREVIOUS zoom state via `SetZoomedIn`, and the CURRENT accumulated rotation heading (not the retail default) is threaded into the rebuild, matching retail's `m_bZoomedIn`/`m_fCurHeading` both living on the PAGE and surviving `Update`. Mounted as the THIRD private creature viewport beside paperdoll/creature-appraisal: `RetailUiRuntime` gained `ChargenPreviewViewportWidget`/`ChargenPreviewControl`/`IsChargenPreviewPageVisible` (computed through `CharacterCreationUiController`'s new `AppearanceViewport`/`AppearancePreviewControl`/`IsAppearancePageVisible`, the last one gating on BOTH the page root's own Visible AND the whole screen's `Root.Visible` since `Close()` only ever hides the latter); `LivePresentationComposition` constructs the renderer+catalog+controller and wires `viewport.Renderer`/`page.PreviewControl` through the same lease/`AdoptRelease` pattern paperdoll uses; `FrameRootComposition`'s `PrivateEntityViewportFrameGroup` gained the controller as its third member; `GameWindow`/`GameWindowLifetime` gained the matching guard fields and `RenderShutdownRoots` disposal entries. **Testability seam:** `IChargenPreviewRenderer`/`IChargenPreviewFrameView` (mirroring `IPaperdollDollRenderer`/`IPaperdollFrameView`) let `ChargenPreviewControllerTests` (6 cases, installed-DAT-gated, fake renderer/view — no live GPU) exercise the REAL `ChargenAppearanceFactory`/`ChargenPreviewEntityBuilder` composition path against the installed EoR dat: same-selection no-op, heritage-change camera reset, appearance-only-change camera preservation, zoom-state preservation across an appearance rebuild, the 180° heading actually reaching the built entity's `Rotation` after `Render()`, and the invisible-page render skip. **Color-wheel scouting (campaign plan risk item 4, RESOLVED via live-DAT probe against the installed EoR dat — `CharacterCreationLiveDatTests.AppearancePage_HasGenderChoiceSpinsSwatchesShadeAndViewport`/`AppearancePage_SpinArrowGeometryIsUniformAcrossAllNineSpins`):** NO new `DatWidgetFactory` widget type was needed anywhere on this page. The nine swatch buttons author Type 1 -> `UiButton`; their nine Type-3 companion "selected"-ring overlays (`0x10000318-0x10000320`) and the GradCircle (`0x1000030e`) author Type 3 -> the generic `UiDatElement` fallback; the shade scrollbar (`0x10000321`) authors Type 0xB -> `UiScrollbar`, matching the decomp's own `DynamicCast(0xb)`. The nine spin containers and their two locally-reused arrow children all author Type 1 -> `UiButton`. Two narrow, DECIDED visual substitutions from this finding are filed as AP-215: swatches use their own `.Selected` highlight instead of toggling the separate companion overlay (retail's `SetColor`'s `m_tColorWheel[...]->SetVisible` mechanism), and the four icon-only style spins (hair/eyes/nose/mouth — CC1's `ChargenHairStyle`/`ChargenEyeStrip`/`ChargenFaceStrip` carry only an `IconId`, no name) show a 1-based ordinal instead of retail's icon thumbnail; the four clothing spins DO show their real `ChargenGearOption.Name`. **The `@140355` gender-flip-on-init oddity (campaign plan risk item 5, RESOLVED via decomp alone — no live cdb needed):** `gmCGAppearancePage::InitializePage`'s own gender-read-then-FLIP-to-the-opposite code (`~0x004802DA-0x00480303`) is real and ALWAYS fires, because `gmCharGenMainUI`'s own constructor (`~0x004e81f5-0x004e8218`, BEFORE any page constructs) calls `CharGenState::RandomizeCharacter(state, hasToD) @0x005c6d80` — retail's chargen screen is NEVER actually blank on open; it always starts with a fully random heritage/gender/appearance/clothing/template/start-area already rolled, which the Appearance page's own init code then immediately flips to the opposite gender. Filed as AP-214, the same unported-primitive gap AP-212 already tracks for the Random button (`RandomizeHeritageGroup`/`RandomizeAppearance`/`RandomizeClothing`/`RandomizeTemplate`/`RandomizeStartArea` are the SAME six primitives `RandomizeCharacter` calls) — acdream's chargen screen opens honestly blank instead, by design, this round. **AD-101 RETIRED** (register §2, 79->78 active rows): `CharacterCreationHeritagePage.Select` no longer auto-selects a gender after a heritage click — the Appearance page's real gender buttons are now the only gender-selection path, matching the review fix round's own retirement-sequencing correction (must land no later than CC5's Finish un-ghosting, which it does — CC5 has not yet un-ghosted Finish). Retail's own default is verified NOT blank (AP-214, above) but acdream's honest-blank choice is deliberate, not an oversight. Updated `CharacterCreationUiControllerTests`'s shared fixture (`FakeRuntime`/`BuildOptions`) with real non-empty Hair/Eyes/Nose/Mouth/Headgear/Shirt/Trousers/Footwear/ClothingColors lists (previously all empty placeholders — no existing test depended on the empty state) and a real `BuildAppearancePage()` layout fixture (uniform spin geometry matching the live-DAT-measured 80/127/174 zone boundaries) so the new dispatch tests exercise the SAME `OnClickAt` zone math production code uses; the one pre-existing gender-side-effect assertion (`HeritageButton_SelectsHeritage_AndAutoSelectsFirstGender`) is renamed/corrected to assert NO gender side effect. **TS-82 NARROWED** (register §4): closed out for the Appearance page specifically (now real, not content-inert) — the row now covers Summary only, CC5's remaining scope. **Register bookkeeping this commit:** AD-101 retired (row deleted, count 79->78); AP-214 filed (the `RandomizeCharacter`-at-ctor / gender-flip finding, count 149->150); AP-215 filed (the two Appearance-page visual substitutions, count 150->151); TS-82 narrowed (Summary-only, count unchanged). **Scope-addendum work (folded into this same commit, not a separate round):** `ChargenPreviewRotationController.HeadingDegrees`'s doc comment corrected to name BOTH the ctor's `0f` (`gmCGAppearancePage::gmCGAppearancePage @0x0047CDAC`) and `InitializePage`'s override to `180f` (`@0x0047FDD0`, write at `0x00480235`, pushed via `SetPlayerHeading` at `0x0048023F`) as retail's OPERATIVE starting heading; DECIDED to change the controller's own parameterless-constructor default from `0f` to a new `RetailDefaultHeadingDegrees = 180f` constant (option (b) of the two offered) rather than requiring every future mount site to remember a separate "seed to 180" call at construction — every real `gmCG3DView` owner (Appearance, Summary `@0x0047BD54` — confirmed a SEPARATE `gmCG3DView` instance/page, CC5's own scope, not touched here — and `gmBarberUI`) converges on 180° before its first visible frame, so a controller whose default silently faces the character away from the camera is exactly the trap the addendum warned about; existing pure-math tests updated to pass `0f` explicitly (keeps their relative-delta assertions simple and unchanged in meaning) plus one new test pinning the parameterless-constructor 180° default at the seam a real consumer experiences, and a second, end-to-end confirmation inside `ChargenPreviewControllerTests` that `Render()` actually applies that heading to the built entity's `Rotation`. **Tests:** `CharacterCreationLiveDatTests` (+2 permanent structural/geometry tests replacing the temporary scouting probe), `CharacterCreationUiControllerTests` (+23: gender/spin/wrap/swatch/shade/zoom-rotate dispatch, the Olthoi clothing-hide gate, the 10-case `CycleIndex` wrap-semantics theory, the renamed AD-101 test), `ChargenPreviewControllerTests` (+6, new file, installed-DAT-gated), `ChargenPreviewRotationControllerTests` (+1, the 180°-default pin). Counts (Release, full solution, `ACDREAM_PROBE_LIVE_MOUNT=1` + `ACDREAM_DAT_DIR` set so every installed-DAT-gated test in this round actually runs rather than skip-gating): Runtime 1713/0 (unchanged — `SetAppearanceIndex`/`SetShade` command plumbing already existed in `IRuntimeCharacterCreationCommands`/`GameRuntimeCommands.cs` from CC3, nothing new needed there), Core 4786/1 skip (unchanged), Content 147/0 (unchanged), App 5220/3 skips (5208/15 skips without the probe env vars — the 12-skip delta is exactly the installed-DAT-gated tests this round adds/exercises), Headless 166/0 (unchanged) — zero failures across two consecutive full-solution runs; one transient failure in `AcDream.Core.Net.Tests.Transport.NakEmissionTests.LossSoak_TwoPercentBidirectional_ZeroMessageLoss_LedgersConverge` reproduced on the FIRST full-solution run and passed clean both in isolation and on an immediate full-solution re-run — the SAME pre-existing, previously-documented flake CC6b-PRE's own ledger row already names (randomized-loss-injection timing, zero files under `src/AcDream.Core.Net/` touched this round either). **OWED for CC5+ / future:** the actual retail-icon rendering pipeline for hair/eyes/nose/mouth style spins (AP-215's own icon-label half) and the GradCircle's own interactive click-to-hue behavior (review fix round correction 2026-08-15: AP-215 does NOT name the GradCircle — that was this ledger row's own false claim; the GradCircle gap is filed separately as AP-217 — the GradCircle is currently a non-interactive static container this round, since its own click-to-color-position mapping has no decomp citation yet and the nine swatch buttons already provide a full, decomp-cited color-selection path); a real `RandomizeCharacter` port (AP-214/AP-212's shared landing site) if a future connected gate wants retail's true randomized-on-open default instead of acdream's honest-blank one; the exact pixel-identical companion-overlay swatch highlight (AP-215) if a future visual gate demands it. | +| CC6b-MOUNT | CODE-COMPLETE 2026-08-15 (the page-mount half CC6b-PRE deferred — Appearance page, spin controls, color-wheel family, viewport wiring — landing after CC4 merged, closing out Campaign CC's CC6 slice); REVIEW-CLOSED 2026-08-15 (dual-lens re-review of the F1-F13 fix round returned NOT CLOSED with residuals R1-R3 + 2 nits, all fixed this round, re-reviewer pre-authorized a diff-check-only close) | `34c6fceab0bc300ab638339b88c5e5f98ae4d724`, `d2a71152`, (this commit — the R1-R3+nits closeout) | CLOSED (dual-lens: architectural PASS-with-items, retail-fidelity FAIL → F1-F13 fix round `d2a71152` → narrow re-review: F1-F13 verified against the decomp, residuals R1-R3 + 2 nits → this commit; re-reviewer pre-authorized diff-check-only close) | **Appearance page** (`CharacterCreationAppearancePage`, `src/AcDream.App/UI/Layout/`, wired into `CharacterCreationUiController` beside the four sibling pages): gender buttons (`0x100003a7`/`a8` -> `SelectGender(2)`/`SelectGender(1)`, decomp `ListenToElementMessage` cases `0x9d`/`0x9e`); Face/Clothes sub-tabs (`0x100003a9`/`aa`, cases `0x9f`/`0xa0`) toggling the `0x100003ae`/`b4` choice containers and defaulting the "current part" to Hair/Headgear respectively; nine spin controls (hair/eyes/nose/mouth/skin `0x100003af-b3`, headgear/shirt/trousers/footwear `0x100003b5-b8`) reproducing retail's two-arrow-plus-body-click composite through `UiButton.OnClickAt`'s local x coordinate — decrement zone x=[80,127), increment zone x=[127,174), else selects the part with no index change (cases `0xa5-0xa9` and their headgear/shirt/trousers/footwear mirrors) — since `DatWidgetFactory` consumes each spin's two locally-reused arrow children (`0x1000030a`/`0x1000030b`) into ONE flat `UiButton` with no separate addressable arrow widget; nine color swatches (`0x1000030f-0x10000317` -> `SetColor(0..8)`, gated on the current part's own color-list length exactly like retail's `iNumColors > N` check); the shade scrollbar (`0x10000321`) bound via `ScalarChanged`; zoom/rotate buttons delegating to a late-bound `IChargenPreviewControl` seam. **Per-part routing table** (`StyleSlotFor`/`ColorSlotFor`/`ShadeSlotFor`), decomp-derived from `SetColor @0x0047DD50` and `SetShade @0x0047C860`: Hair has its own color AND shade; Eyes has color but NO shade (retail's `SetShade` switch has no case 1 — independently confirmed against CC6a's own "eye color has no shade indirection" finding); Nose/Mouth/Skin have NO color and ALL route their shade to SKIN shade (cases 2/3/4 share one decompiled body — a genuine retail quirk, not a porting shortcut); Headgear/Shirt/Trousers/Footwear each have their own color and shade. **Wrap semantics** (`CharacterCreationAppearancePage.CycleIndex`, internal static, unit-tested via 10 `[Theory]` cases): plain `[0,count)` modulo wrap for every style spin except Headgear; Headgear alone gets the decomp-derived `(count+1)`-position RING including the `Unset` ("no headgear") position — `CharGenState::SetHeadgearStyle`'s literal signed-int32 comparison shape (`0x0047F4B5`-`0x0047F530` decrement, `0x0047F7D8` increment): decrementing FROM style 0 lands on Unset, incrementing FROM Unset lands on style 0, decrementing FROM Unset wraps to the LAST style, incrementing past the last style lands on Unset — a real closed ring of `count+1` positions, not a plain wrap. **Review fix round F1 correction (2026-08-15):** every OTHER style spin ALSO has a decomp-observable Unset-cycling case, in the SAME switch the headgear ring was ported from — the shared decrement tail (`label_47f065`/`label_47f6d9`, reached from Hair's own decrement case `@0x0047f465-0x0047f486` and inlined per-part for Eyes/Nose/Mouth/Shirt/Trousers/Footwear) computes `new = cur - 1` on the raw signed int32 (Unset = -1), giving `new = -2`, which wraps to `count - 1` — the SAME "wrap to the last index" shape headgear's own ring uses. Incrementing from Unset (`new = -1 + 1 = 0`) was already correct in acdream. The original claim here ("no decomp-observable Unset-cycling case... starts at style 0 for BOTH directions") is WRONG for decrement; fixed in `CharacterCreationAppearancePage.CycleIndex` and its own corrected doc comment. **Heritage 6/0xc/0xd gate** (`gmCGAppearancePage::Update @~0x0047EB46-0x0047EE95`): Gearknight/Olthoi/OlthoiAcid hide the Clothes sub-tab (making all four clothing spins unreachable, matching the OWED item's "four clothing spins hidden" framing through retail's OWN mechanism — hiding the tab, not each spin individually) plus the Nose/Mouth spins directly, and disable the Eyes spin's arrows (`_eyesArrowsDisabled`, since Olthoi/Gearknight forms have fixed eyes); **review fix round F3 correction (2026-08-15):** forces `SetChoice(FACE)`/`SetSelection(HAIR)` UNCONDITIONALLY whenever the gate engages (`@0x0047eac6/0x0047eacf` Gearknight, `@0x0047ee32/0x0047ee3b` Olthoi/OlthoiAcid) — NOT only when Clothes happened to be showing, the original (wrong) framing here. A conditional gate left Nose/Mouth as the current part when the Face tab was already active, stranding the shade control on a now-hidden part; retail always snaps back to Hair. **Preview wiring** (`ChargenPreviewController`, `src/AcDream.App/Rendering/`, new): bridges a real architectural gap the CC6a/CC6b-PRE foundation left open — `ChargenPreviewRenderer` only ever built its OWN private `ChargenPreviewCamera` with no injection seam, but `ChargenPreviewZoomController` needs a SETTABLE camera to tween. Fixed at the root: `ChargenPreviewViewportCamera` gained a `ChargenPreviewCamera`-accepting constructor overload, `ChargenPreviewRenderer` gained an optional `camera` parameter using it, and `ChargenPreviewController` owns the ONE shared `ChargenPreviewCamera` instance handed to both. `ChargenPreviewController` consolidates the per-frame `IPrivateEntityViewportFrame` owner role (mirrors `PaperdollFramePresenter`, self-timing via `Stopwatch` rather than touching the shared frame-phase interface) with the `IChargenPreviewControl` seam the page's buttons bind against (constructed before the graphics backend exists, so the page cannot receive the real renderer at construction time — assigned late by `LivePresentationComposition`, exactly mirroring the paperdoll's own late `viewport.Renderer = ...` assignment). `Rebuild` recomposes via `ChargenAppearanceFactory.TryCompose` + `ChargenPreviewEntityBuilder.TryBuildAnimated` on ANY heritage/gender/appearance-selection change (no-op if identical to the last composed selection) but only SNAPS the camera to the heritage's default eye on a HERITAGE OR GENDER change (decomp-cited: `gmCGAppearancePage::Update`'s only two confirmed direct call sites are `InitializePage` and the two gender-button handlers; spin/color/shade changes call the narrower `SetSelection`/`SetColor`/`SetShade`, none of which touch `m_vectCurPosition`) — a fresh `ChargenPreviewAnimator` is unavoidable on every rebuild (it owns the resolved drawable-part list, which changes with the mesh) but is immediately restored to the PREVIOUS zoom state via `SetZoomedIn`, and the CURRENT accumulated rotation heading (not the retail default) is threaded into the rebuild, matching retail's `m_bZoomedIn`/`m_fCurHeading` both living on the PAGE and surviving `Update`. Mounted as the THIRD private creature viewport beside paperdoll/creature-appraisal: `RetailUiRuntime` gained `ChargenPreviewViewportWidget`/`ChargenPreviewControl`/`IsChargenPreviewPageVisible` (computed through `CharacterCreationUiController`'s new `AppearanceViewport`/`AppearancePreviewControl`/`IsAppearancePageVisible`, the last one gating on BOTH the page root's own Visible AND the whole screen's `Root.Visible` since `Close()` only ever hides the latter); `LivePresentationComposition` constructs the renderer+catalog+controller and wires `viewport.Renderer`/`page.PreviewControl` through the same lease/`AdoptRelease` pattern paperdoll uses; `FrameRootComposition`'s `PrivateEntityViewportFrameGroup` gained the controller as its third member; `GameWindow`/`GameWindowLifetime` gained the matching guard fields and `RenderShutdownRoots` disposal entries. **Testability seam:** `IChargenPreviewRenderer`/`IChargenPreviewFrameView` (mirroring `IPaperdollDollRenderer`/`IPaperdollFrameView`) let `ChargenPreviewControllerTests` (6 cases, installed-DAT-gated, fake renderer/view — no live GPU) exercise the REAL `ChargenAppearanceFactory`/`ChargenPreviewEntityBuilder` composition path against the installed EoR dat: same-selection no-op, heritage-change camera reset, appearance-only-change camera preservation, zoom-state preservation across an appearance rebuild, the 180° heading actually reaching the built entity's `Rotation` after `Render()`, and the invisible-page render skip. **Color-wheel scouting (campaign plan risk item 4, RESOLVED via live-DAT probe against the installed EoR dat — `CharacterCreationLiveDatTests.AppearancePage_HasGenderChoiceSpinsSwatchesShadeAndViewport`/`AppearancePage_SpinArrowGeometryIsUniformAcrossAllNineSpins`):** NO new `DatWidgetFactory` widget type was needed anywhere on this page. The nine swatch buttons author Type 1 -> `UiButton`; their nine Type-3 companion "selected"-ring overlays (`0x10000318-0x10000320`) and the GradCircle (`0x1000030e`) author Type 3 -> the generic `UiDatElement` fallback; the shade scrollbar (`0x10000321`) authors Type 0xB -> `UiScrollbar`, matching the decomp's own `DynamicCast(0xb)`. The nine spin containers and their two locally-reused arrow children all author Type 1 -> `UiButton`. Two narrow, DECIDED visual substitutions from this finding are filed as AP-215: swatches use their own `.Selected` highlight instead of toggling the separate companion overlay (retail's `SetColor`'s `m_tColorWheel[...]->SetVisible` mechanism), and the four icon-only style spins (hair/eyes/nose/mouth — CC1's `ChargenHairStyle`/`ChargenEyeStrip`/`ChargenFaceStrip` carry only an `IconId`, no name) show a 1-based ordinal instead of retail's icon thumbnail; the four clothing spins DO show their real `ChargenGearOption.Name`. **The `@140355` gender-flip-on-init oddity (campaign plan risk item 5, RESOLVED via decomp alone — no live cdb needed):** `gmCGAppearancePage::InitializePage`'s own gender-read-then-FLIP-to-the-opposite code (`~0x004802DA-0x00480303`) is real and ALWAYS fires, because `gmCharGenMainUI`'s own constructor (`~0x004e81f5-0x004e8218`, BEFORE any page constructs) calls `CharGenState::RandomizeCharacter(state, hasToD) @0x005c6d80` — retail's chargen screen is NEVER actually blank on open; it always starts with a fully random heritage/gender/appearance/clothing/template/start-area already rolled, which the Appearance page's own init code then immediately flips to the opposite gender. Filed as AP-214, the same unported-primitive gap AP-212 already tracks for the Random button (`RandomizeHeritageGroup`/`RandomizeAppearance`/`RandomizeClothing`/`RandomizeTemplate`/`RandomizeStartArea` are the SAME six primitives `RandomizeCharacter` calls) — acdream's chargen screen opens honestly blank instead, by design, this round. **AD-101 RETIRED** (register §2, 79->78 active rows): `CharacterCreationHeritagePage.Select` no longer auto-selects a gender after a heritage click — the Appearance page's real gender buttons are now the only gender-selection path, matching the review fix round's own retirement-sequencing correction (must land no later than CC5's Finish un-ghosting, which it does — CC5 has not yet un-ghosted Finish). Retail's own default is verified NOT blank (AP-214, above) but acdream's honest-blank choice is deliberate, not an oversight. Updated `CharacterCreationUiControllerTests`'s shared fixture (`FakeRuntime`/`BuildOptions`) with real non-empty Hair/Eyes/Nose/Mouth/Headgear/Shirt/Trousers/Footwear/ClothingColors lists (previously all empty placeholders — no existing test depended on the empty state) and a real `BuildAppearancePage()` layout fixture (uniform spin geometry matching the live-DAT-measured 80/127/174 zone boundaries) so the new dispatch tests exercise the SAME `OnClickAt` zone math production code uses; the one pre-existing gender-side-effect assertion (`HeritageButton_SelectsHeritage_AndAutoSelectsFirstGender`) is renamed/corrected to assert NO gender side effect. **TS-82 NARROWED** (register §4): closed out for the Appearance page specifically (now real, not content-inert) — the row now covers Summary only, CC5's remaining scope. **Register bookkeeping this commit:** AD-101 retired (row deleted, count 79->78); AP-214 filed (the `RandomizeCharacter`-at-ctor / gender-flip finding, count 149->150); AP-215 filed (the two Appearance-page visual substitutions, count 150->151); TS-82 narrowed (Summary-only, count unchanged). **Scope-addendum work (folded into this same commit, not a separate round):** `ChargenPreviewRotationController.HeadingDegrees`'s doc comment corrected to name BOTH the ctor's `0f` (`gmCGAppearancePage::gmCGAppearancePage @0x0047CDAC`) and `InitializePage`'s override to `180f` (`@0x0047FDD0`, write at `0x00480235`, pushed via `SetPlayerHeading` at `0x0048023F`) as retail's OPERATIVE starting heading; DECIDED to change the controller's own parameterless-constructor default from `0f` to a new `RetailDefaultHeadingDegrees = 180f` constant (option (b) of the two offered) rather than requiring every future mount site to remember a separate "seed to 180" call at construction — every real `gmCG3DView` owner (Appearance, Summary `@0x0047BD54` — confirmed a SEPARATE `gmCG3DView` instance/page, CC5's own scope, not touched here — and `gmBarberUI`) converges on 180° before its first visible frame, so a controller whose default silently faces the character away from the camera is exactly the trap the addendum warned about; existing pure-math tests updated to pass `0f` explicitly (keeps their relative-delta assertions simple and unchanged in meaning) plus one new test pinning the parameterless-constructor 180° default at the seam a real consumer experiences, and a second, end-to-end confirmation inside `ChargenPreviewControllerTests` that `Render()` actually applies that heading to the built entity's `Rotation`. **Tests:** `CharacterCreationLiveDatTests` (+2 permanent structural/geometry tests replacing the temporary scouting probe), `CharacterCreationUiControllerTests` (+23: gender/spin/wrap/swatch/shade/zoom-rotate dispatch, the Olthoi clothing-hide gate, the 10-case `CycleIndex` wrap-semantics theory, the renamed AD-101 test), `ChargenPreviewControllerTests` (+6, new file, installed-DAT-gated), `ChargenPreviewRotationControllerTests` (+1, the 180°-default pin). Counts (Release, full solution, `ACDREAM_PROBE_LIVE_MOUNT=1` + `ACDREAM_DAT_DIR` set so every installed-DAT-gated test in this round actually runs rather than skip-gating): Runtime 1713/0 (unchanged — `SetAppearanceIndex`/`SetShade` command plumbing already existed in `IRuntimeCharacterCreationCommands`/`GameRuntimeCommands.cs` from CC3, nothing new needed there), Core 4786/1 skip (unchanged), Content 147/0 (unchanged), App 5220/3 skips (5208/15 skips without the probe env vars — the 12-skip delta is exactly the installed-DAT-gated tests this round adds/exercises), Headless 166/0 (unchanged) — zero failures across two consecutive full-solution runs; one transient failure in `AcDream.Core.Net.Tests.Transport.NakEmissionTests.LossSoak_TwoPercentBidirectional_ZeroMessageLoss_LedgersConverge` reproduced on the FIRST full-solution run and passed clean both in isolation and on an immediate full-solution re-run — the SAME pre-existing, previously-documented flake CC6b-PRE's own ledger row already names (randomized-loss-injection timing, zero files under `src/AcDream.Core.Net/` touched this round either). **OWED for CC5+ / future:** the actual retail-icon rendering pipeline for hair/eyes/nose/mouth style spins (AP-215's own icon-label half) and the GradCircle's own retail-driven repaint (review fix round correction 2026-08-15: AP-215 does NOT name the GradCircle — that was this ledger row's own false claim; the GradCircle gap is filed separately as AP-217, REWRITTEN 2026-08-15 at the re-review of `d2a71152` (R3) after re-deriving from the decomp: `gmCGAppearancePage::ListenToElementMessage`'s own dispatch switch has NO case for the GradCircle's offset at all, so it is not a click target in retail either — `DoGradDisk` is a PAINT-only routine that blits the gradient art tinted with the current part's color (or blanks it for Eyes) whenever `SetColor`/`SetSelection` run; acdream's gap is that it never repaints the GradCircle at all, a cosmetic paint gap rather than a dead click target, and the nine swatch buttons already provide the full, decomp-cited color-selection INPUT path); a real `RandomizeCharacter` port (AP-214/AP-212's shared landing site) if a future connected gate wants retail's true randomized-on-open default instead of acdream's honest-blank one; the exact pixel-identical companion-overlay swatch highlight (AP-215) if a future visual gate demands it; **the current-part spin highlight itself, newly measured DEAD for all nine spins (AP-222, filed at the re-review of `d2a71152`, N2)** — none of the nine spins author Highlight-state media, so `RefreshColorAndShadeControls`'s `TrySetRetailState(Highlight)` call silently never changes what's drawn; unresolved whether retail's own spin art has the same gap or uses a different mechanism entirely, needs a decomp read of the real per-frame spin-face renderer before deciding a fix. | diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index c9c4d8db..a598663e 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -1078,13 +1078,25 @@ internal sealed class LivePresentationCompositionPhase } }); } - else if (dispatcherLease.Resource is not null) + else if (dispatcherLease.Resource is not null && interaction.RetainedUi is not null) { // Fix round F8: dispatcher is available but the mount coordinator // hadn't resolved ChargenPreviewViewportWidget by this one-shot // pass — loud instead of silent, since the coordinator's own // later per-frame retries cannot recover this GPU-side binding // (see this block's own disposition comment above). + // + // Re-review R1: the retained UI arm (`interaction.RetainedUi`) + // is null in the default configuration (ACDREAM_RETAIL_UI + // unset — see InteractionRetainedUiComposition.cs's own gate on + // RuntimeOptions.RetailUi), and in that configuration there is + // no Appearance page at all. The dispatcher lease is + // acquired unconditionally regardless of retained-UI presence, + // so without this second guard every ordinary launch printed + // this diagnostic even though nothing was actually broken. + // Narrowed to fire only in the one configuration it is meant to + // diagnose: retained UI mounted, dispatcher ready, but the + // coordinator's widget resolution missed this one-shot pass. Console.WriteLine( "[UI] chargen preview viewport unavailable at composition " + "time — the Appearance page's zoom/rotate controls and " diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs index 0c9c6cd8..347b092f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs @@ -370,6 +370,36 @@ public sealed class CharacterCreationLiveDatTests // drops it shows up here instead of as a silently-dead // highlight. Assert.True(spin.ToggleBehavior, $"spin 0x{spinId:X8} must author ToggleBehavior for the current-part highlight to work."); + // Re-review nit N2 (2026-08-15): ToggleBehavior alone is necessary + // but not sufficient — UiButton.UpdateVisualState only COMMITS the + // requested state when _availableStates actually contains it + // (UiButton.cs's TrySetRetailState -> Selected setter -> + // UpdateVisualState chain). MEASURED (not assumed) against the + // installed EoR dat: TrySetRetailState(Highlight) itself always + // reports success (the ToggleBehavior branch commits + // unconditionally, matching TrySetRetailState's own contract), + // but NONE of the nine spins actually carries Highlight / + // Highlight_rollover / Highlight_pressed media on either of + // their two consumed arrow face segments — every one of them + // authors only Normal / Normal_rollover / Ghosted. So F2 item 2's + // current-part highlight is CURRENTLY A NO-OP for every spin: + // ActiveState silently stays at its prior value ("Normal") + // instead of ever becoming "Highlight". The pre-existing + // ToggleBehavior pin above never caught this because it only + // checks the PROPERTY that gates the state-machine branch, not + // whether that branch has anything to actually draw. Filed as + // AP-222 (retail-vs-acdream status unresolved — retail's own + // gmCGAppearancePage::SetSelection call sites are cited for + // the SetState(1)/SetState(6) calls, not for whether retail's + // OWN spin art authors Highlight media either). Pinned to + // "Normal" so a future DAT revision that adds real Highlight + // media is what makes this assertion start failing — the + // correct trigger to update it to "Highlight" instead of a + // silently-reintroduced dead highlight going unnoticed either way. + Assert.True( + spin.TrySetRetailState(UiButtonStateMachine.Highlight), + $"spin 0x{spinId:X8} must accept a Highlight state request."); + Assert.Equal("Normal", spin.ActiveState); } // Every color-wheel-family id resolves through EXISTING