diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index b402baf3..ab9871c9 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -170,7 +170,7 @@ readiness/requeue adaptation. See --- -## 3. Documented approximation (AP) — 128 active rows (AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3 — extending `SpewBoxLayoutDumpDiagnostic`'s sweep from `dats.Portal` (zero hits) to `dats.Local` FOUND the SpewBox element; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving only absolute screen position and colour 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) — 128 active rows (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 @@ -234,7 +234,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-40 | Chat uses one fixed `0.75` outer opacity and has no descendant-focus-driven active/default opacity transition | `src/AcDream.App/Rendering/GameWindow.cs` chat mount; `ChatWindowController.cs` | Font resolution is now live and per-element; only the opacity behavior remains deferred to the shared window/focus runtime | Focused chat remains too translucent and idle chat never restores the configured default alpha | `ChatInterface::SetOpacity @ 0x004F3120`; `SetDefaultOpacity @ 0x004F3BC0`; `SetActiveOpacity @ 0x004F3C40` | | AP-175 | PopUpString (`GameEvent 0x0004`) renders as an ordinary chat-log line (`ChatKind.Popup`) instead of retail's MODAL DIALOG. Filed 2026-08-09, Campaign CH slice CH1 (color table) — the color-table work routes this entry through the new 34-value `LogTextType` table (fixed at `0x00` Default/green, unchanged from the entry's pre-existing color) but does not change WHERE it renders; a modal-dialog port is out of this slice's scope | `src/AcDream.Core/Chat/ChatLog.cs` (`OnPopup`); `src/AcDream.Core.Net/GameEventWiring.cs:126` | Informational popup text still reaches the player via the chat transcript; a full modal-dialog port is deferred work, not a color-table concern | Any retail-specific PopUpString behavior contingent on being a blocking modal (e.g. must-acknowledge) is not reproduced; acdream's chat-log line can be missed or scrolled past instead | `ClientCommunicationSystem::Handle_Communication__PopUpString @0x0057FE80`; `docs/research/2026-08-09-chat-retail-color-table.md` §5.1 | | AP-177 | SpewBox line lifetime is an INVENTED 5-second placeholder. Retail's `gmSpewBoxUI` never raises the expiry element message (`0x10000003`) anywhere in its own compiled Sept 2013 EoR code — the real per-line timeout/fade curve is owned by keystone.dll's authored behaviour for layout `0x10000012` element `0x1000004A`, which this slice did not measure (a live cdb capture on `gmSpewBoxUI::ListenToElementMessage @0x004D57C0` against a real retail client would resolve it). Filed 2026-08-09, Campaign CH slice CH2 | `src/AcDream.Core/Chat/SpewBoxState.cs` (`DefaultLifetime`) | A round, conservative placeholder was chosen over guessing a retail-matching curve; no fade is modeled at all (the line pops on and off) | SpewBox lines may linger noticeably longer or shorter than retail's actual timing, and pop instead of fading | `docs/research/2026-08-09-chat-retail-interface-text.md` §3.2.1 | -| AP-178 | **NARROWED 2026-08-09 at the CH2 REJECT-review rework (NIT 3, `docs/research/2026-08-09-ch2-review-findings.md`):** the original filing swept only `dats.Portal` and found ZERO elements of class `0x10000016` (`gmSpewBoxUI`), so it recorded position, extent, font, colour, AND `MaxConcurrentItems` as all-invented. Extending the identical sweep to `dats.Local` (`client_local_English.dat`) FOUND it: LayoutDesc `0x21000011`, element `0x10000048`, whose sole child (ListBox `0x10000049`, matching `gmSpewBoxUI::PostInit`'s `GetChildRecursive(0x10000049)` verbatim) carries ListBox property `0x10000028` = the integer `4`. Two sub-claims RETIRE: extent is now AUTHORED (`450×72`, not a placeholder size) and `MaxConcurrentItems` is now AUTHORED (`4`, not retail's code-default `1`). Two sub-claims REMAIN open: (1) absolute screen position — the recovered position is `(0,0)` RELATIVE TO A PARENT this sweep could not identify (the element is presumably still mounted via the C++ `gmClient` HUD registration block the research doc's §1.1 describes, just parented under something dat-authored rather than the root view directly), so `TopOffset=60px` + a one-time-computed centered `Left` remain acdream's own placeholder, not a resolved retail value; (2) colour — the element/ListBox's direct-state `StateDesc.Properties` dump found only a bool at `0x3B` and the `MaxConcurrentItems` integer, no colour property, and the per-`UIStateId` `States` dictionary (hover/pressed/etc. variants) was not walked this pass. Retail's edge codes (`leftEdge=3`/`rightEdge=3`, "centered" per `ElementReader.ToAnchors`'s own doc comment; `topEdge=1`, top-anchored) confirm the box is a fixed-width centered block, not a full-viewport stretch — `SpewBoxController`'s anchor shape was corrected to match (`AnchorEdges.Top` only + computed centered `Left`, `OneLine=false` since 4 concurrent lines can now actually be visible instead of collapsing to 1) | `src/AcDream.App/UI/SpewBoxController.cs`; `src/AcDream.Core/Chat/SpewBoxState.cs` (`MaxConcurrentItems`) | The colour placeholder follows the user's own (unconfirmed) recollection that retail's SpewBox is yellow rather than an arbitrary pick; a live cdb capture of `gmSpewBoxUI`'s runtime rect/state (or walking the `States` dictionary this pass skipped, or identifying the C++-assigned parent) would be the next resolution path for the two still-open sub-claims | SpewBox text may render in the wrong absolute screen location or colour versus retail; the size/max-items risk this row originally recorded ("bursts of refusals collapse to one visible line where retail's authored ListBox may show more") is RETIRED — up to 4 now render, matching the authored value | `docs/research/2026-08-09-chat-retail-interface-text.md` §3.2.2-§3.2.4; `tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs`; `src/AcDream.App/UI/Layout/ElementReader.cs` (`ToAnchors`) | +| AP-178 | **NARROWED 2026-08-09 at the CH2 REJECT-review rework (NIT 3, `docs/research/2026-08-09-ch2-review-findings.md`), WORDING CORRECTED at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6):** the original filing's `dats.Portal` pass used an id source (`DatCollection`'s top-level AGGREGATE `GetAllIdsOfType()`) that is NOT `dats.Portal`'s own id space (`dats.Portal.GetAllIdsOfType()` reports a count of ZERO for this type), so querying those ids against `dats.Portal.TryGet` established nothing about Portal either way — the "swept only dats.Portal and found ZERO... all-invented" framing overclaimed a search that never meaningfully happened. Extending the sweep to `dats.Local` (`client_local_English.dat`), this time correctly paired, FOUND it: LayoutDesc `0x21000011`, element `0x10000048`, whose sole child (ListBox `0x10000049`, matching `gmSpewBoxUI::PostInit`'s `GetChildRecursive(0x10000049)` verbatim) carries ListBox property `0x10000028` = the integer `4`. Whether `dats.Portal` ALSO carries a copy remains UNESTABLISHED, not ruled out. Two sub-claims RETIRE: extent is now AUTHORED (`450×72`, not a placeholder size) and `MaxConcurrentItems` is now AUTHORED (`4`, not retail's code-default `1`). Three sub-claims REMAIN open: (1) absolute screen position — the recovered position is `(0,0)` RELATIVE TO A PARENT this sweep could not identify (the element is presumably still mounted via the C++ `gmClient` HUD registration block the research doc's §1.1 describes, just parented under something dat-authored rather than the root view directly), so `TopOffset=60px` + a centered `Left` recomputed every frame (corrected from a one-time computation at nit 1 — see `SpewBoxController.Tick`) remain acdream's own placeholder, not a resolved retail value; (2) colour — the element/ListBox's direct-state `StateDesc.Properties` dump found only a bool at `0x3B` and the `MaxConcurrentItems` integer, no colour property, and the per-`UIStateId` `States` dictionary (hover/pressed/etc. variants) was not walked this pass; (3) vertical content flow — the block now renders TOP-aligned (newest line at the top, via `UiText.VerticalJustify`/`HonorVerticalJustification`, nit 2) because that is the only placement consistent with "newest on top," but retail's own authored vertical justification for this element is unmeasured, so this is also acdream's invention pending measurement, not a resolved retail value. Retail's edge codes (`leftEdge=3`/`rightEdge=3`, "centered" per `ElementReader.ToAnchors`'s own doc comment; `topEdge=1`, top-anchored) confirm the box is a fixed-width centered block, not a full-viewport stretch — `SpewBoxController`'s anchor shape was corrected to match (`AnchorEdges.None` + a centered `Left` recomputed every frame against the current root width, `OneLine=false` since 4 concurrent lines can now actually be visible instead of collapsing to 1) | `src/AcDream.App/UI/SpewBoxController.cs`; `src/AcDream.Core/Chat/SpewBoxState.cs` (`MaxConcurrentItems`); `src/AcDream.App/UI/UiText.cs` (`HonorVerticalJustification`) | The colour placeholder follows the user's own (unconfirmed) recollection that retail's SpewBox is yellow rather than an arbitrary pick; a live cdb capture of `gmSpewBoxUI`'s runtime rect/state (or walking the `States` dictionary this pass skipped, or identifying the C++-assigned parent) would be the next resolution path for the three still-open sub-claims | SpewBox text may render in the wrong absolute screen location, colour, or vertical flow versus retail; the size/max-items risk this row originally recorded ("bursts of refusals collapse to one visible line where retail's authored ListBox may show more") is RETIRED — up to 4 now render, matching the authored value | `docs/research/2026-08-09-chat-retail-interface-text.md` §3.2.2-§3.2.4; `tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs`; `src/AcDream.App/UI/Layout/ElementReader.cs` (`ToAnchors`) | | AP-179 | `ChatLog.OnCombatLine`'s generic `0x06` Combat fallback types combat-feedback lines with a single stand-in `LogTextType` for callers with no more specific hit/miss/evade classification in hand, instead of retail's per-message dispatch. Split out of AP-176 (RETIRED 2026-08-09, Campaign CH slice CH2 — the WeenieError half of that bundled row is now the full 344-row `HandleFailureEvent` port, `WeenieErrorMessages.Resolve`); this combat-line half was never in CH2's scope and keeps its own row so the divergence is not silently dropped | `src/AcDream.Core/Chat/ChatLog.cs` (`OnCombatLine`) | `0x06` matches the switch's majority combat-line behavior and is a safe baseline; a full per-combat-message dispatch port is out of Campaign CH's scope | Wrong chat color for the combat-line kinds retail types distinctly (hit/miss/evade variants) | `ClientCommunicationSystem::HandleFailureEvent @0x00571990`; originally filed at the CH1 Opus review 2026-08-09 as part of AP-176, split out at CH2 | | AP-180 | `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed — retail's `ClientSystem::AddTextToScroll(text, type, allowPluginFilter, windowId)` delivers a `type == 0x1A` message with a non-zero `windowId` to BOTH the SpewBox and that specific chat window (research doc §2.3), the shape ~40 slash-command-output sites depend on. acdream's chokepoint routes on `type` alone; every current production caller passes `windowId = 0`, so the gap is latent, not yet visibly wrong. Filed 2026-08-09 at the CH2 REJECT-review rework (NIT 2, `docs/research/2026-08-09-ch2-review-findings.md`) | `src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs` (`AddText`) | No production caller passes a non-zero `windowId` yet, so nothing observably diverges today; implementing the dual-destination echo is CH4/CH5 scope at the earliest | A future slash-command-output caller that passes a non-zero `windowId` expecting it to echo into its originating chat window (matching retail) will silently land in the SpewBox only | `ClientSystem::AddTextToScroll @0x00563C50`; `docs/research/2026-08-09-chat-retail-interface-text.md` §2.3 | | AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (`0x06004C63`) used only when `ThumbTopSprite`/`ThumbBotSprite` are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | `src/AcDream.App/UI/UiScrollbar.cs:35` | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | `UIElement_Scrollbar::UpdateLayout @0x4710d0`; cap sprites `0x06004C60` (top) + `0x06004C66` (bottom) from base layout `0x2100003E` | diff --git a/docs/plans/2026-08-09-chat-parity-campaign.md b/docs/plans/2026-08-09-chat-parity-campaign.md index 2989577d..b6bcc814 100644 --- a/docs/plans/2026-08-09-chat-parity-campaign.md +++ b/docs/plans/2026-08-09-chat-parity-campaign.md @@ -1,10 +1,12 @@ # Campaign CH — chat & interface-text retail parity **Status:** ACTIVE 2026-08-09 — CH1 complete (implementation `172c6f9a` + -review fixes `34d8a3c0`); CH2 code-complete (`77c8296e`), REJECT-reviewed -(`docs/research/2026-08-09-ch2-review-findings.md`) and reworked -(`e0e78883`), pending the in-client user gate (jump-in-air / jump-loaded -refusals showing on-screen, not in chat). +review fixes `34d8a3c0`); CH2 CODE-COMPLETE and CLOSED pending the user +gate (`77c8296e`, REJECT-reviewed at +`docs/research/2026-08-09-ch2-review-findings.md`, reworked `e0e78883`, +re-reviewed APPROVE-WITH-FIXES with nits applied this commit) — the sole +outstanding item is the in-client user gate (jump-in-air / jump-loaded +refusals showing on-screen, not in chat). CH3 (side channels) next. **Why now:** first track of the alpha-release program (chat is the most visible daily surface for the friend-alpha). User-directed 2026-08-09. @@ -103,7 +105,7 @@ implementer per slice against a pinned contract (per |---|---|---|---|---| | R1–R4 research | `see docs/research/2026-08-09-chat-retail-*` | — | — | — | | CH1 colors | `172c6f9a` | 11,835 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed `34d8a3c0` | pending | -| CH2 interface text | `77c8296e`, reworked `e0e78883` | 11,914 passed / 4 skipped / 0 failed | REJECT; reworked `e0e78883` (`docs/research/2026-08-09-ch2-review-findings.md`) | pending | +| CH2 interface text | `77c8296e`, reworked `e0e78883` | 11,916 passed / 4 skipped / 0 failed | REJECT → reworked `e0e78883` → re-review APPROVE-WITH-FIXES → nits (this commit) | pending | | CH3 side channels | — | — | — | — | | CH4 commands | — | — | — | — | | CH5 closeout | — | — | — | — | diff --git a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs index 70914fec..6ca43560 100644 --- a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs +++ b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs @@ -359,6 +359,15 @@ internal sealed class LiveSessionRuntimeFactory (string? text, RetailLogTextType type) = WeenieErrorMessages.Resolve(code, null); if (text is not null) _domain.Communication.AddText(text, type); + else + // CH2 re-review nit 5 (docs/plans/2026-08-09-chat-parity-campaign.md): + // matches GameEventWiring's WeenieError/ + // WeenieErrorWithString handlers, which already log + // this same case. An unmapped id resolves to a null + // Text — retail's switch has no default case, so it + // produces no player-facing text — but the raw id + // stays visible to us instead of silently vanishing. + Console.WriteLine($"[weenie-error] unmapped code=0x{code:X4}"); }, PlayerPublicWeenieBitfield: () => _domain.EntityObjects.Objects.Get(_player.Identity.ServerGuid)? diff --git a/src/AcDream.App/UI/SpewBoxController.cs b/src/AcDream.App/UI/SpewBoxController.cs index b5fc6250..1fbcf331 100644 --- a/src/AcDream.App/UI/SpewBoxController.cs +++ b/src/AcDream.App/UI/SpewBoxController.cs @@ -1,4 +1,5 @@ using System.Numerics; +using AcDream.App.UI.Layout; using AcDream.UI.Abstractions.Panels.SpewBox; namespace AcDream.App.UI; @@ -83,11 +84,23 @@ internal sealed class SpewBoxController : IDisposable /// (leftEdge=3/rightEdge=3, "centered" per /// ElementReader.ToAnchors's doc comment) mean the box is a /// FIXED-width block horizontally centered in its parent, not a - /// full-viewport stretch — the constructor below anchors it that way - /// (a one-time centered computed against - /// , only) since - /// has no "centered, fixed-width" flag - /// combination to express retail's mode 3 directly. + /// full-viewport stretch. has no "centered, + /// fixed-width" flag combination to express retail's mode 3 directly, so + /// recomputes a centered + /// against the CURRENT every frame instead of + /// anchoring. + /// + /// CH2 re-review nit 1 + /// (docs/plans/2026-08-09-chat-parity-campaign.md): the original + /// rework anchored with only, meaning + /// captured the constructor's + /// one-time centered Left as a fixed left MARGIN + /// (ComputeAnchoredRect's Left/Right-both-false branch) and + /// replayed that absolute pixel position forever — a window resize left + /// the box off-center. is now + /// (so ApplyAnchor is a no-op) and + /// centering is owned entirely by the per-frame recompute below. + /// /// private const float SpewBoxWidth = 450f; private const float SpewBoxHeight = 72f; @@ -127,17 +140,32 @@ internal sealed class SpewBoxController : IDisposable Name = "SpewBox", // Centered fixed-width block (retail's "mode 3" edge code on // both left and right) — see the AP-178 extent comment above. + // This is only the FIRST frame's value; Tick recomputes it + // every frame against the current root width (CH2 re-review + // nit 1 — see the extent comment's nit-1 paragraph). Left = (root.Width - SpewBoxWidth) / 2f, Top = TopOffset, Width = SpewBoxWidth, Height = SpewBoxHeight, - Anchors = AnchorEdges.Top, + Anchors = AnchorEdges.None, Centered = true, // AUTHORED MaxConcurrentItems is 4, not retail's code-default 1 // (see SpewBoxState.MaxConcurrentItems) — OneLine=true would // silently collapse the box back down to showing only the // newest of up to 4 concurrent lines. OneLine = false, + // CH2 re-review nit 2 (docs/plans/2026-08-09-chat-parity-campaign.md): + // retail's own authored vertical justification for THIS element + // is unknown (register row AP-178 covers presentation + // unknowns) — top-aligned flow is acdream's own choice, made + // because it is the only placement consistent with "lines flow + // from the top, newest on top" (see Tick's ordering comment). + // HonorVerticalJustification opts the scrollable multi-line + // path into VerticalJustify without a full ConfigureDatState + // LayoutDesc binding, which this synthesized element does not + // have. + VerticalJustify = VJustify.Top, + HonorVerticalJustification = true, ClickThrough = true, ZOrder = int.MaxValue, DefaultColor = SpewBoxColor, @@ -166,10 +194,29 @@ internal sealed class SpewBoxController : IDisposable /// private void Tick(double nowSeconds) { - // SpewBoxVM.Lines returns newest-first, matching retail's - // InsertItem(item, 0) — with OneLine now false and the AUTHORED - // MaxConcurrentItems == 4 (see SpewBoxState.MaxConcurrentItems), - // up to 4 lines render, newest on top. + // CH2 re-review nit 1 (docs/plans/2026-08-09-chat-parity-campaign.md): + // Anchors is AnchorEdges.None (see the AP-178 extent comment on + // SpewBoxWidth), so nothing else recenters this element on a + // window resize — recompute every frame against the CURRENT root + // width rather than the width captured once at construction. + _text.Left = (_root.Width - SpewBoxWidth) / 2f; + + // CH2 re-review nit 3 (docs/plans/2026-08-09-chat-parity-campaign.md): + // deliberate inversion of UiText.LinesProvider's documented + // contract ("Provider of the lines to show, oldest-first" — + // UiText.cs). SpewBoxVM.Lines returns newest-first, matching + // retail's InsertItem(item, 0), and this controller feeds that + // order straight through WITHOUT reversing it. That is correct + // here specifically because the box is top-aligned + // (VerticalJustify.Top + HonorVerticalJustification above): index 0 + // of the lines array draws at the TOP of the box, so feeding + // newest-first into a slot documented as oldest-first is exactly + // what makes "newest line on top" true. Reversing the feed to + // satisfy the doc comment literally would flip the visible order to + // oldest-on-top, which is wrong for this element — see + // SpewBoxControllerTests' rendered-order test. With OneLine now + // false and the AUTHORED MaxConcurrentItems == 4 (see + // SpewBoxState.MaxConcurrentItems), up to 4 lines render. IReadOnlyList lines = _vm.Lines(nowSeconds); _text.Visible = lines.Count > 0; if (lines.Count == 0) diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index 9057e95b..06714e47 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -171,6 +171,20 @@ public sealed class UiText : UiElement, IUiDatStateful /// public VJustify VerticalJustify { get; set; } = VJustify.Center; + /// + /// Opts the SCROLLABLE multi-line path (i.e. == + /// ) into without + /// requiring a full LayoutDesc binding. + /// sets the equivalent internal flag + /// (_honorDatVerticalJustification) for DAT-imported text such as + /// spellbook tabs; synthesized (non-DAT) controllers that still want + /// top/bottom/center content flow instead of the historical bottom- + /// pinned transcript behavior set this directly. CH2 re-review nit 2 + /// (docs/plans/2026-08-09-chat-parity-campaign.md): added for + /// SpewBoxController's top-aligned, newest-line-on-top flow. + /// + public bool HonorVerticalJustification { get; set; } + /// The scroll model — also read by the linked UiScrollbar. public UiScrollable Scroll { get; } = new(); @@ -504,7 +518,7 @@ public sealed class UiText : UiElement, IUiDatStateful Scroll.MaxScroll, Scroll.ScrollY, VerticalJustify, - _honorDatVerticalJustification); + _honorDatVerticalJustification || HonorVerticalJustification); _lastBaseY = baseY; // Normalised selection span (start <= end), if any. diff --git a/src/AcDream.Core/Chat/SpewBoxState.cs b/src/AcDream.Core/Chat/SpewBoxState.cs index 5c90067f..e7808d7e 100644 --- a/src/AcDream.Core/Chat/SpewBoxState.cs +++ b/src/AcDream.Core/Chat/SpewBoxState.cs @@ -54,18 +54,26 @@ public sealed class SpewBoxState /// /// The shipped LayoutDesc's AUTHORED value — no longer a placeholder. /// CH2 REJECT-review rework, NIT 3 - /// (docs/research/2026-08-09-ch2-review-findings.md): the - /// original C.7 sweep only searched dats.Portal, which has no - /// 0x10000016 (gmSpewBoxUI) element anywhere; extending the same - /// sweep to dats.Local (client_local_English.dat) found - /// it — LayoutDesc 0x21000011, element 0x10000048, whose - /// sole child (ListBox 0x10000049, matching - /// gmSpewBoxUI::PostInit's GetChildRecursive(0x10000049) - /// call verbatim) carries ListBox property 0x10000028 = the - /// integer 4. Retail's own code default - /// (gmSpewBoxUI::PostInit @0x004D5AB0), used only when this - /// property is absent or unreadable, was 1 — the shipped layout - /// overrides it with 4. + /// (docs/research/2026-08-09-ch2-review-findings.md), wording + /// corrected at the CH2 re-review nits pass + /// (docs/plans/2026-08-09-chat-parity-campaign.md, nit 6): the + /// original C.7 sweep's dats.Portal pass was not a meaningful + /// search — its id source was DatCollection's top-level + /// AGGREGATE GetAllIdsOfType<LayoutDesc>(), not + /// dats.Portal's own (which reports a count of ZERO for this + /// type), so querying those ids against dats.Portal.TryGet + /// established nothing about Portal either way. Extending the sweep + /// to dats.Local (client_local_English.dat) — this time + /// with a correctly-paired id source — found it: LayoutDesc + /// 0x21000011, element 0x10000048, whose sole child + /// (ListBox 0x10000049, matching gmSpewBoxUI::PostInit's + /// GetChildRecursive(0x10000049) call verbatim) carries ListBox + /// property 0x10000028 = the integer 4. Retail's own + /// code default (gmSpewBoxUI::PostInit @0x004D5AB0), used only + /// when this property is absent or unreadable, was 1 — the + /// shipped layout overrides it with 4. See + /// SpewBoxLayoutDumpDiagnostic's own corrected RESULT comment + /// for the full id-source analysis. /// public const int MaxConcurrentItems = 4; diff --git a/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs b/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs index d2fd1978..162d8c1b 100644 --- a/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs +++ b/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs @@ -4,6 +4,7 @@ using AcDream.App.UI; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Core.Social; +using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions; namespace AcDream.App.Tests.Net; @@ -250,6 +251,54 @@ public sealed class LiveSessionCommandRouterTests Assert.Equal([0x50C4A54Au], options); } + [Fact] + public void ShowWeenieErrorFriendsFull_ResolvesThroughAddText_AndLandsInSpewBoxNotChat() + { + // CH2 re-review nit 7 (docs/plans/2026-08-09-chat-parity-campaign.md): + // exercises the FULL retail path for id 0x0561 (the 50-friends-cap + // refusal): ClientCommandController.AddFriend's cap guard calls + // ShowWeenieError(0x0561u); the binding below mirrors + // LiveSessionRuntimeFactory.CreateCommandBindings' ShowWeenieError + // closure exactly (WeenieErrorMessages.Resolve then + // RuntimeCommunicationState.AddText) — the same chokepoint that + // fans a ClientLocal line out to SpewBox instead of chat + // (RuntimeCommunicationStateTests pins that half in isolation + // already). This is "the closest seam" per the review: every other + // LiveSessionRuntimeFactory test in this tree is a source-text + // conformance grep (ReadSource), not an instantiation — the real + // factory needs a live WorldSession/domain graph no test builds. + var communication = new RuntimeCommunicationState(); + ClientCommandController.Bindings bindings = NewClientBindings() with + { + Friends = communication.Friends, + ShowWeenieError = code => + { + (string? text, RetailLogTextType type) = WeenieErrorMessages.Resolve(code, null); + if (text is not null) + communication.AddText(text, type); + }, + }; + LiveSessionCommandRouter router = NewRouter(clientBindings: bindings); + router.Activate(); + + // Seed 50 friends so AddFriend's cap guard fires + // (ClientCommandController.cs's AddFriend). + var seeded = new List(); + for (uint i = 0; i < 50; i++) + seeded.Add(new FriendEntry(i + 1, $"Friend{i}", true, false, [], [])); + communication.Friends.Apply(new FriendsUpdate(FriendsUpdateType.Full, seeded)); + + router.Publish(new ExecuteClientCommandCmd(ClientCommandId.FriendsAdd, "OneTooMany")); + + communication.SpewBox.Tick(0d); + Assert.Equal(1, communication.SpewBox.Count); + Assert.Equal( + "You may only have a maximum of 50 friends at once. If you wish to add more friends, you must first remove some.", + communication.SpewBox.Snapshot()[0].Text); + Assert.Equal(0, communication.Chat.Count); + Assert.Equal(50, communication.Friends.Count); + } + private static LiveSessionCommandRouter NewRouter( ChatLog? chat = null, TurbineChatState? turbine = null, diff --git a/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs b/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs index 6e4bb7a9..f431d487 100644 --- a/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs @@ -111,9 +111,14 @@ public sealed class SpewBoxControllerTests public void Controller_QueueStaysBounded_AcrossManyTicksWithoutADrawPass() { // BLOCKER 1 acceptance (c): repeated enqueue+tick cycles must never - // let the visible set grow past SpewBoxState.MaxConcurrentItems - // (retail's code default, 1) — this is what "unbounded per-session - // leak" would have looked like if the drain never ran at all. + // let the visible set grow past SpewBoxState.MaxConcurrentItems. + // CH2 re-review nit 4 (docs/plans/2026-08-09-chat-parity-campaign.md): + // corrected — MaxConcurrentItems is the shipped LayoutDesc's + // AUTHORED value, 4, not retail's code-default of 1 (the fallback + // gmSpewBoxUI::PostInit uses only when the property is absent; see + // SpewBoxState.MaxConcurrentItems). This is what "unbounded + // per-session leak" would have looked like if the drain never ran + // at all. var root = new UiRoot { Width = 1280f, Height = 720f }; var state = new SpewBoxState(); using var controller = new SpewBoxController(root, new SpewBoxVM(state)); @@ -131,6 +136,33 @@ public sealed class SpewBoxControllerTests Assert.Equal("line 49", text.LinesProvider!()[0].Text); } + [Fact] + public void Controller_RenderedOrder_NewerMessageIsTheTopmostLine() + { + // CH2 re-review nit 3 (docs/plans/2026-08-09-chat-parity-campaign.md): + // pins the RENDERED order, not UiText.LinesProvider's documented + // contract (oldest-first) — SpewBoxController deliberately inverts + // that (see Tick's ordering comment) because the box is now + // top-aligned and retail shows the newest interface-text line on + // top. Drives root.Tick, not the provider, matching every other + // test in this file. + var root = new UiRoot { Width = 1280f, Height = 720f }; + var state = new SpewBoxState(); + using var controller = new SpewBoxController(root, new SpewBoxVM(state)); + + state.Enqueue("older message"); + root.Tick(dt: 0d, nowMs: 1000L); + + state.Enqueue("newer message"); + root.Tick(dt: 0d, nowMs: 1001L); + + UiText text = Assert.IsType(root.Children.OfType().Single()); + IReadOnlyList rendered = text.LinesProvider!(); + Assert.Equal(2, rendered.Count); + Assert.Equal("newer message", rendered[0].Text); + Assert.Equal("older message", rendered[1].Text); + } + [Fact] public void Dispose_RemovesBothTheTextAndTheGlobalTimeSinkFromTheRoot() { diff --git a/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs b/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs index 061fea95..e7b5f2ef 100644 --- a/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs +++ b/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs @@ -21,14 +21,28 @@ namespace AcDream.App.Tests.UI; /// tree. /// /// -/// RESULT (2026-08-09, run against the installed client_portal.dat): -/// the installed DAT's entire LayoutDesc id space is the CLOSED range -/// 0x21000000-0x21000075 (118 possible ids, 101 populated — -/// sanity-checked by confirming 3 independently-known ids, 0x2100002E -/// character window / 0x21000023 inventory / 0x21000016 toolbar prototype, -/// are all present). The sweep is therefore EXHAUSTIVE, and it finds -/// ZERO elements of class 0x10000016 anywhere in any of the -/// 101 layouts, and LayoutDesc 0x10000012 — the id the decompiled +/// RESULT (2026-08-09, run against the installed client_portal.dat), +/// WORDING CORRECTED at the CH2 re-review nits pass +/// (docs/plans/2026-08-09-chat-parity-campaign.md, nit 6): this +/// pass's id source, allIds below, is +/// dats.GetAllIdsOfType<LayoutDesc>() — the top-level AGGREGATE +/// accessor — NOT dats.Portal.GetAllIdsOfType<LayoutDesc>(), +/// which the very next line prints and which reports a count of +/// ZERO. The loop below then queries those aggregate ids against +/// dats.Portal.TryGet, a database whose own id enumeration for this +/// type is empty — so the original "the sweep is therefore EXHAUSTIVE... +/// finds ZERO elements... anywhere" conclusion never actually established +/// anything about dats.Portal: it iterated ids sourced from +/// elsewhere against a database that cannot be enumerated the same way. +/// The id-space/sanity-check numbers below (CLOSED range +/// 0x21000000-0x21000075, 118 possible ids, 101 populated, +/// 3 known ids present) describe THAT aggregate list, not Portal's own. +/// What IS established — from the separate dats.Local sweep, see +/// RESULT 2 below — is that client_local_English.dat hosts the +/// SpewBox layout at LayoutDesc 0x21000011. Whether +/// dats.Portal ALSO carries a 0x10000016 element remains +/// UNESTABLISHED; this sweep did not meaningfully search it. LayoutDesc +/// 0x10000012 — the id the decompiled /// CreateChildElementByEnum(null, 0x10000012, 0x1000004A) call /// appears to reference — exists but has zero top-level elements (it is /// not the per-line template catalog; that id must resolve through a