From 98de4f5ab3674d5b5947212d57180b4845d2309b Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 10 Aug 2026 10:40:19 +0200 Subject: [PATCH] =?UTF-8?q?fix(chat):=20Campaign=20CH=20round=203=20?= =?UTF-8?q?=E2=80=94=20SpewBox=20flush-top/font,=20/help=20exact=20print?= =?UTF-8?q?=20sequence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-gate round 3 findings (a)-(c): (a) SpewBox: TopOffset moves from the round-1 60px placeholder to 0 (flush to the viewport top). SpewBoxController never wired DatFont/Font at all before this round, so it silently rendered through the 15px debug BitmapFont fallback; it now resolves retail dat Font 0x40000025 (MaxCharHeight=11px) through a new RetailUiRuntime.Assets accessor — the smallest font id confirmed in use by any currently-imported retail LayoutDesc fixture, cross-referenced against every tests/AcDream.App.Tests/UI/Layout/fixtures/*.json dump and confirmed against the installed DAT via AcDream.Cli dump-font-atlas. It is also the chat window's own smallest font (the 0x2100006F floating-window 1/2/3/4 indicator badges), so both selection criteria the brief offered agree. Both remain best-available approximations, not resolved retail values — register row AP-178 updated accordingly. (b)/(c) /help and /help death: round 2 extracted the individual retail strings byte-exact but never traced ClientCommunicationSystem::DoHelp's complete print sequence. Byte-swept DoHelp's own range plus the five Summary-branch functions it calls into (HelpEmote/HelpSquelch/ HelpStatusGroup/HelpTextGroup/HelpAllGroup) against the PDB-paired acclient.exe. Retail's real shape: bare /help prints exactly TWO scroll entries (HelpPrefixNote, then the 13-item AvailableHelpListing built from DoHelp's own literals and each group's Summary_HelpType branch, in exact source order) — not the acdream-invented cheat sheet BuildHelpText() built before. Any resolved /help gets the SAME two-entry shape: HelpPrefixNote, then ForMoreInformationPrefix concatenated directly onto the verb's own Detail text (retail's own unsubstituted "" literal, ported verbatim). ChatCommandRouter.EmitVerbHelp applies this uniformly to every resolved verb, not just death. An unresolved verb now shows retail's real "Unknown command" fallback text; that fallback types 0x1A (ClientLocal), which retail routes to the SpewBox exclusively — a gap ChatVM's UI.Abstractions layer can't yet reach, filed as ISSUES #367 / register AP-186 rather than left silently unregistered. Jump-in-air (round 2's open item 1) was root-caused and fixed separately at a5a7eb4f between rounds — recorded in the campaign ledger. Debug suite (all projects): 12,329 passed / 4 skipped / 1 failed — the one failure is issue #351, a pre-existing Debug-only streaming flake confirmed reproducing identically on the pristine pre-round-3 commit via git stash, not a regression. Release verification covers every project reachable without rebuilding AcDream.App: a live client process (PID 15064) held its own Release binaries locked for the session and was not killed per project policy — AcDream.UI.Abstractions.Tests (867/867, the layer both /help fixes live in) plus every other non-App-dependent project, all 0 failed. AcDream.App/AcDream.App.Tests/AcDream.Core.Tests (the SpewBox fix's layer) are green in Debug only this session. Co-Authored-By: Claude Opus 5 --- docs/ISSUES.md | 43 +++++++ .../retail-divergence-register.md | 5 +- docs/plans/2026-08-09-chat-parity-campaign.md | 104 +++++++++++++++- .../2026-08-09-campaign-ch-test-script.md | 38 ++++-- .../LivePresentationComposition.cs | 13 +- src/AcDream.App/UI/RetailUiRuntime.cs | 11 ++ src/AcDream.App/UI/SpewBoxController.cs | 91 ++++++++++++-- .../Panels/Chat/ChatCommandRouter.cs | 65 +++++++--- .../Panels/Chat/RetailClientCommandCatalog.cs | 48 +------- .../Panels/Chat/RetailCommandHelpTable.cs | 114 +++++++++++++++++- .../UI/SpewBoxControllerTests.cs | 77 ++++++++++++ .../Panels/Chat/ChatCommandRouterTests.cs | 40 +++++- .../Panels/Chat/ChatPanelInputTests.cs | 26 ++-- .../Chat/RetailCommandHelpTableTests.cs | 95 +++++++++++++++ 14 files changed, 665 insertions(+), 105 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index d5f2d57b..202e501b 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -192,6 +192,49 @@ click — not decoded by CH6a. `src/AcDream.App/UI/Layout/LayoutImporter.cs` (`BuildWidget`/`ConsumesDatChildren` handling); `src/AcDream.App/UI/UiText.cs`. +## #367 — ChatCommandRouter's local-presentation fallbacks type-0x1A text still lands in the chat scroll, never the SpewBox + +**Status:** OPEN — filed 2026-08-10, Campaign CH user-gate round 3, while +tracing `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete print +sequence for findings (b)/(c). Retail's DoHelp fallback for an unresolved +`/help ` is `AddTextToScroll(u"Unknown command", 0x1A, 1, 0)` — type +`0x1A` (`ClientLocal`) is HARDCODED to the SpewBox, never the chat window +(`docs/research/2026-08-09-chat-retail-interface-text.md` §2.1/§2.2, the +same routing rule Campaign CH user-gate round 2 item 2 already ported for +the portal-space notice). `RuntimeCommunicationState.AddText` (Runtime +layer) implements this rule correctly — `type == ClientLocal` routes to +`SpewBox` only. But `ChatCommandRouter`/`ChatVM` live in +`AcDream.UI.Abstractions`, a layer beneath Runtime that must stay +presentation/Runtime-independent (Code Structure Rules), so they have no +path to the SpewBox at all — every local-presentation fallback +(`RetailCommandHelpTable.UnknownCommand` now; also the pre-existing +"Unknown command: {verb}." command-shaped-input refusal in +`ChatCommandRouter.Submit`'s main body) still writes through +`ChatVM.ShowSystemMessage`, which only ever reaches `ChatLog`. Not a +regression this round — the fallback text was already wrong AND +already routed to the chat window before this round's fix; this round +corrected the TEXT ("Unknown command", byte-exact) and traced the +routing divergence clearly enough to file it. Register row AP-186. + +**Fix shape:** either (a) give `ChatVM` (or a sibling in UI.Abstractions) +an optional `Action? OnClientLocalText` hook the App-layer host +wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)` +the same way `ChatWindowController`/other retained-UI controllers already +receive delegates from the composition layer, or (b) accept the +divergence permanently as an acdream simplification (all local-presentation +refusals show in the chat window instead of splitting across two surfaces) +and retire AP-186 as an accepted Intentional Architecture row instead. Needs +a product decision, not just an implementation — small either way once +decided. + +**Where:** `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; +`src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs`; +`src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs` (`AddText`, the +correctly-implemented Runtime-layer oracle this should eventually reach). + +**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH, +user gate round 3). + ## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved **Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3. diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index e4ca40d2..c2d776a4 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) — 131 active rows (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 filed 2026-08-09 at Campaign CH user-gate round 2, item 3 — three of the seven retail `/help` group-topic listings (channels/chatting/commands) remain PARTIAL, not fully verbatim: their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which constructs its output from three BN-mislabeled data fragments (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc. are NOT real vtable dispatch) concatenated around a live `ChannelSystem::GetChannelName` lookup, not decodable with confidence from a static string sweep; each partial group keeps its own verbatim summary line and an explicit UNVERIFIED note instead of the fully-fabricated meta-message the user caught on `/help death` (that group, plus status/text/allegiances, are now COMPLETE verbatim listings); tracked as ISSUES.md #364. 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 filed 2026-08-09 at the CH4 REJECT-review, item 9 — roughly 10 chat refusal/usage call sites this campaign added route through `ChatVM.ShowSystemMessage`'s single `ClientLocal 0x00` sink where retail types several of them `0x1A`: `DoStupidChannelHack`, `DoChannelList`/`On`/`Off`, `DoAllegiance`, `DoHouseAvailableList`, `DoReply`; three sites (`DoSpeaker`/`DoEndurance`/`DoTitle`) are already correct at `0x00`, matching retail. Retail's own bad-args fallback (`DoCommand @0x0057E46D`) also answers with `HandleFailureEvent(0x26)`, not a local "Usage:" line, which acdream's `ChatCommandRouter.Submit` synthesizes instead. Deliberately NOT re-plumbed this session — filed as issue #363, marked for CH5-or-later; 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) — 132 active rows (AP-186 filed 2026-08-10 at Campaign CH user-gate round 3, while tracing DoHelp's print sequence — ChatCommandRouter's type-0x1A local-presentation fallbacks (now including the byte-exact "Unknown command" text) still render via the chat scroll instead of the SpewBox, an architectural layering gap (UI.Abstractions cannot see Runtime's `RuntimeCommunicationState.AddText`), tracked as 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 filed 2026-08-09 at Campaign CH user-gate round 2, item 3 — three of the seven retail `/help` group-topic listings (channels/chatting/commands) remain PARTIAL, not fully verbatim: their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which constructs its output from three BN-mislabeled data fragments (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc. are NOT real vtable dispatch) concatenated around a live `ChannelSystem::GetChannelName` lookup, not decodable with confidence from a static string sweep; each partial group keeps its own verbatim summary line and an explicit UNVERIFIED note instead of the fully-fabricated meta-message the user caught on `/help death` (that group, plus status/text/allegiances, are now COMPLETE verbatim listings); tracked as ISSUES.md #364. 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 filed 2026-08-09 at the CH4 REJECT-review, item 9 — roughly 10 chat refusal/usage call sites this campaign added route through `ChatVM.ShowSystemMessage`'s single `ClientLocal 0x00` sink where retail types several of them `0x1A`: `DoStupidChannelHack`, `DoChannelList`/`On`/`Off`, `DoAllegiance`, `DoHouseAvailableList`, `DoReply`; three sites (`DoSpeaker`/`DoEndurance`/`DoTitle`) are already correct at `0x00`, matching retail. Retail's own bad-args fallback (`DoCommand @0x0057E46D`) also answers with `HandleFailureEvent(0x26)`, not a local "Usage:" line, which acdream's `ChatCommandRouter.Submit` synthesizes instead. Deliberately NOT re-plumbed this session — filed as issue #363, marked for CH5-or-later; 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 @@ -233,7 +233,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`), 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`). **CH USER-GATE ROUND 1 (2026-08-09):** colour PINS — the user tested live, side-by-side against retail, and confirmed the on-screen SpewBox text is the same bright yellow as an incoming Tell (`0x81C4C8`, `RetailChatColorTable.Yellow` = `(1, 1, 0.247, 1)`); `SpewBoxController.SpewBoxColor` now uses that exact value. The user's SAME live pass also reported that SIZE, POSITION, and FONT still visibly differ from retail — so despite extent's earlier AUTHORED status above, size is user-gate round 1: differs, iterating (re-opened pending a follow-up measurement pass, not yet root-caused). Three sub-claims therefore 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, and the user confirms this is visibly wrong; (2) size/font — the AUTHORED `450×72` extent and whatever font this renders with still do not match what the user sees live; unmeasured which of extent, the unresolved parent scale, or font metrics is the actual cause; (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`) Colour is CONFIRMED, not a placeholder — CH user-gate round 1 (2026-08-09) pinned it against the user's own live side-by-side retail observation, not a recollection. A live cdb capture of `gmSpewBoxUI`'s runtime rect/state (or walking the `States` dictionary this pass skipped, or identifying the C++-assigned parent) remains the resolution path for the three still-open sub-claims (position, size/font, vertical flow) | SpewBox text may render in the wrong absolute screen location, size/font, or vertical flow versus retail — all three CONFIRMED wrong by the user's CH round-1 live pass, not merely suspected; colour is CLOSED and no longer a risk. 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, though the box's overall size/font still visibly differs from retail per the user | `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`). **CH USER-GATE ROUND 1 (2026-08-09):** colour PINS — the user tested live, side-by-side against retail, and confirmed the on-screen SpewBox text is the same bright yellow as an incoming Tell (`0x81C4C8`, `RetailChatColorTable.Yellow` = `(1, 1, 0.247, 1)`); `SpewBoxController.SpewBoxColor` now uses that exact value. The user's SAME live pass also reported that SIZE, POSITION, and FONT still visibly differ from retail — so despite extent's earlier AUTHORED status above, size is user-gate round 1: differs, iterating (re-opened pending a follow-up measurement pass, not yet root-caused). Three sub-claims therefore 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, and the user confirms this is visibly wrong; (2) size/font — the AUTHORED `450×72` extent and whatever font this renders with still do not match what the user sees live; unmeasured which of extent, the unresolved parent scale, or font metrics is the actual cause; (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). **CH USER-GATE ROUND 3 (2026-08-10):** the user's finding (a) confirmed POSITION and FONT still read wrong live — "not aligned all the way to the top" and "not the correct font and size (retail's is SMALLER)." Both sub-claims close as best-available APPROXIMATIONS, not resolved retail values (a re-run of `SpewBoxLayoutDumpDiagnostic` this round still finds no `FontDid`/colour property on element `0x10000048` or its ListBox child `0x10000049`, confirming the true retail values remain genuinely unmeasurable statically): (1) position — `TopOffset` moves from the round-1 60px placeholder to `0` (flush to the viewport top), per the user's explicit direction; the true retail PARENT remains unidentified. (2) font — `SpewBoxController` now resolves retail dat Font `0x40000025` (`MaxCharHeight=11px`, `Baseline=9px`; confirmed via `AcDream.Cli dump-font-atlas` sweeping every populated font id `0x40000000`-`0x40000032` in the installed DAT) instead of silently falling through to the unwired 15px debug `BitmapFont` every prior round shipped with (no `DatFont`/`Font` was ever set on this element before). `0x40000025` is the SMALLEST font id confirmed in use by any of acdream's currently-imported retail LayoutDesc fixtures (cross-referenced across all `tests/AcDream.App.Tests/UI/Layout/fixtures/*.json` dumps) — it is ALSO the chat window's own smallest font (the `0x2100006F` floating-window 1/2/3/4 indicator badges), so both selection criteria the round-3 brief offered agree on the same id, with no tie to break. Vertical content flow remains OPEN, unchanged from round 1. | `src/AcDream.App/UI/SpewBoxController.cs`; `src/AcDream.Core/Chat/SpewBoxState.cs` (`MaxConcurrentItems`); `src/AcDream.App/UI/UiText.cs` (`HonorVerticalJustification`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`Assets` accessor, round 3) | Colour is CONFIRMED, not a placeholder — CH user-gate round 1 (2026-08-09) pinned it against the user's own live side-by-side retail observation, not a recollection. A live cdb capture of `gmSpewBoxUI`'s runtime rect/state (or walking the `States` dictionary this pass skipped, or identifying the C++-assigned parent) remains the resolution path for the three still-open sub-claims (position, size/font, vertical flow) | SpewBox text may render in the wrong absolute screen location, size/font, or vertical flow versus retail — all three CONFIRMED wrong by the user's CH round-1 live pass, not merely suspected; colour is CLOSED and no longer a risk. 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, though the box's overall size still visibly differs from retail per the user. Round 3 (2026-08-10) closes the position/font risks as best-available approximations (flush-top mount, smallest confirmed-used retail font) rather than resolved retail values — the box may still not sit at retail's true pixel position/size, and the exact retail font remains genuinely unmeasurable; only vertical content flow remains fully OPEN, unchanged from round 1 | `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` | @@ -339,6 +339,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-183 | **Filed 2026-08-09 at the CH4 REJECT-review, item 9.** Roughly 10 chat refusal/usage call sites Campaign CH slice CH4 added route through `ChatVM.ShowSystemMessage`'s single `LogTextType 0x00` (ClientLocal-informational) sink; retail types several of them `0x1A` (bright red / genuine refusal) instead: `DoStupidChannelHack` (the "You must specify the text you wish to say!" family, registered channel verbs), `DoChannelList`/`DoChannelOn`/`DoChannelOff` ("Please specify the channel name."), `DoAllegiance` (the "Please see @help Allegiance..." refusal this session's Blocker 1 added), `DoHouseAvailableList`, and `DoReply` ("Someone must @tell you first!"). Three CH4 sites are already correct at `0x00` because retail itself types them informational: `DoSpeaker`, `DoEndurance`, `DoTitle`. Separately, retail's own bad-args fallback (`ClientCommunicationSystem::DoCommand @0x0057E46D`) answers a registered handler that returns 0 with `HandleFailureEvent(0x26)`, not a local "Usage: " line — `ChatCommandRouter.Submit` synthesizes a `"Usage: {clientCommand.Usage}"` string instead whenever a catalog command's `InvalidArgumentsText` is null. Filed as issue #363; deliberately NOT re-plumbed this session (re-typing every call site is larger than a REJECT-review fix batch). `src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs` (`ShowSystemMessage`); `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs` (`Submit`'s `Usage:` fallback) | Every one of these sites shows correctly-worded text in the correctly-shaped chat log entry, just at the wrong color/destination classification — a real but low-severity divergence from retail's exact on-screen presentation | A user comparing acdream's chat window side-by-side with retail for one of these specific refusals sees the wrong color (informational white/default instead of bright red) or, for the generic "Usage:" fallback, different WORDING than retail's `HandleFailureEvent(0x26)` text entirely | `ClientCommunicationSystem::DoStupidChannelHack @ 0x0057B144`; `DoChannelList @ 0x0057A9B0`; `DoChannelOn @ 0x0057AA80`; `DoChannelOff @ 0x0057AB50`; `DoAllegiance @ 0x0057D5A0`; `DoHouseAvailableList @ 0x00570510`; `DoReply @ 0x00577910`; `DoCommand @ 0x0057E46D` (`HandleFailureEvent(0x26)`) | | AP-182 | **Filed 2026-08-09 (Campaign CH slice CH4); corrected 2026-08-09 at the CH4 REJECT-review (nit 11).** `@title ` is wired to a pure no-op — `LiveSessionRuntimeFactory`'s `SetChatTitle` binding is `_ => { }`; the requested title is neither stored nor consumed anywhere (the original filing's "stores the value locally" claim was false). This matches retail's own silent success (no confirmation text was recovered at the `DoTitle` success site, so a no-visible-effect accept is exactly as faithful as a stored-but-unread value would be). Also omitted: `DoTitle`'s three local failure messages — no title given, "You must provide a new title for the window."; length over 99 characters, "Window title length cannot exceed 100 characters."; and wrong source window (`m_idCurrentCommandSource` 1 or 8), "This command must be issued from a popup chat window." — acdream's catalog validator (`ClientCommandId.SetChatTitle`, `AnyArguments`) accepts any argument shape and never raises any of the three. `src/AcDream.App/Net/LiveSessionRuntimeFactory.cs` (`SetChatTitle`) | Retail's chat window presumably re-renders its title bar text; acdream's chat window has no title bar at all under the current retained-UI import, so there is nothing to visually diverge from yet | Once a titled chat-window chrome is built, `@title` needs to be re-wired to it — today it is a pure no-op, and the three failure messages above are silently absent | `ClientCommunicationSystem::DoTitle @ 0x0057A640` | | AP-185 | **Filed 2026-08-10 (Campaign CH slice CH6a — retail chat-window layout + 8-grip resize).** The main chat window's 8 cosmetic `_Locked` border-art twins (`0x10000693`-`0x1000069A`) are retail's `PlayerModule::LockUI`-driven alternate skin — `gmFloatyMainChatUI::UpdateLockedStatus @0x004D23D0` swaps them in for the 8 live Resizebar/Dragbar grips (`0x1000069B`-`0x100006A2`) when the UI is locked, and swaps them back out when unlocked. `ChatWindowController.Bind` always hides the twins and always shows the live set — i.e. it renders only retail's UNLOCKED skin, regardless of `UiRoot.UiLocked`. `src/AcDream.App/UI/Layout/ChatWindowController.cs` (`LockedTwinIds`) | `UiRoot.UiLocked` already gates the underlying move/resize INTERACTION generically and correctly in both states (locked ⇒ no move, no resize, regardless of which border art is drawn); the two art sets occupy identical rects, so always showing the interactive-grip skin is a cosmetic simplification, not a functional one, and the default matches `UiLocked`'s own `false` default | A user who locks the UI (`PlayerModule::LockUI`) sees the interactive-grip chat-window border art unchanged instead of retail's inert locked variant — cosmetic only; the window still correctly refuses to move or resize while locked | `gmFloatyMainChatUI::UpdateLockedStatus @0x004D23D0`; `PlayerModule::LockUI`; `docs/research/2026-08-09-chat-retail-window-shell.md` §1.6 | +| AP-186 | **Filed 2026-08-10 at Campaign CH user-gate round 3, while tracing `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete print sequence for findings (b)/(c).** Retail's DoHelp fallback for an unresolved `/help ` is `AddTextToScroll(u"Unknown command", 0x1A, 1, 0)` — type `0x1A` (`ClientLocal`) is HARDCODED to the SpewBox, never the chat window (same routing rule round 2 item 2 already ported for the portal-space notice; `docs/research/2026-08-09-chat-retail-interface-text.md` §2.1/§2.2). `RuntimeCommunicationState.AddText` (Runtime layer) implements this rule correctly. But `ChatCommandRouter`/`ChatVM` live in `AcDream.UI.Abstractions`, a layer beneath Runtime that must stay presentation/Runtime-independent (Code Structure Rules) — they have no path to the SpewBox, so every local-presentation fallback typed `ClientLocal` (now `RetailCommandHelpTable.UnknownCommand`; also the pre-existing "Unknown command: {verb}." command-shaped-input refusal in `ChatCommandRouter.Submit`'s main body) still writes through `ChatVM.ShowSystemMessage`, which only ever reaches `ChatLog`. Not a regression this round — the fallback already showed in the chat window before this round's fix (with fabricated text); this round corrected the TEXT and traced the routing divergence clearly enough to register it. Filed as ISSUES.md #367. `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs` | This is an architectural layering gap (UI.Abstractions cannot see Runtime), not a data-availability gap — `RuntimeCommunicationState.AddText` already implements the correct routing rule one layer up, so fixing it is a plumbing exercise (an optional delegate hook), not new research | A user typing an unresolvable `/help ` sees "Unknown command" in the chat scroll instead of the top-of-screen SpewBox flash retail shows — text content is now byte-exact, only the SURFACE differs | `ClientSystem::AddTextToScroll @0x00563C50`; `docs/research/2026-08-09-chat-retail-interface-text.md` §2.1/§2.2; `src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs` (`AddText`, the correctly-implemented oracle) | ## 4. Temporary stopgap (TS) — 39 active rows (TS-70 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item E (#362) — `ClientCommandResponses.cs` now parses and renders all four named inbound GameEvents (`ChannelIndex 0x0149`, `ChannelList 0x0148`, `AvailableHouses 0x0271`, `AllegianceInfoResponse 0x027C`), each wired into `GameEventWiring.cs` and rendering retail-shaped `LogTextType 0x00` lines ported from the named-retail decomp (`Handle_Communication__ChannelIndex`/`ChannelList` @0x0057d0c0/@0x0057d230, `Handle_House__Recv_AvailableHouses` + `DisplayListOfCoords` @0x00585d50/@0x00585c20, `Handle_Allegiance__AllegianceInfoResponseEvent` @0x0056a1d0); the row's `@on`/`@off` mention was never itself missing a handler (both already resolve through the pre-existing `WeenieErrorWithString` registration) so nothing there needed a fix; TS-68/TS-69 filed 2026-08-09, Campaign CH slice CH4 — the deferred allegiance/house subcommand dispatchers, the three unported pure-local commands (day/log/render); TS-66/TS-67 filed and TS-29 retired 2026-08-08, Campaign A slice A5 — the region ambient system landed, so TS-29's ambient half is ported and its music half turned out to have nothing to port; TS-66 is the omitted `seen_outside` interior case and TS-67 the in-plane contribution weight. TS-64/TS-65 filed 2026-08-08, Campaign A slice A2 — TS-64 the two unimplemented retail sound preferences (unfocused-app silence, pan disable) plus the three enable bools; TS-65 the volume-squared quirk, applied on the ambient path where two lanes byte-confirmed it and deliberately NOT on the hook path where the pre-multiplying overload is unpinned. TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState` — **narrative corrected 2026-08-03 (#297): "real" only became true at #297. Until then the bits existed but the source `PublicWeenieBitfield` was frozen at CreateObject, so every one of those sites read a stale value for the whole session. The site enumeration is also incomplete: `RuntimeSetPositionMoverPreparation.cs:183-188` is a SEVENTH mover-flags site that decodes `record.Snapshot.ObjectDescriptionFlags` directly rather than calling `ResolveMoverPvpState`, and it also derives `ObjectInfoState.IsPlayer` from the PWD bit, contradicting `EntityCollisionFlags.cs:119-123`'s claim that every site uses a GUID-prefix heuristic. See AP-134.** — and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) diff --git a/docs/plans/2026-08-09-chat-parity-campaign.md b/docs/plans/2026-08-09-chat-parity-campaign.md index b2444554..8c373a2c 100644 --- a/docs/plans/2026-08-09-chat-parity-campaign.md +++ b/docs/plans/2026-08-09-chat-parity-campaign.md @@ -33,10 +33,20 @@ character on the probe account), and two temporary graphical-only probes are left behind for the next round. **CH6a (main-window layout + 8-grip resize) landed CODE-COMPLETE the same day** — see its ledger row and the updated round-1 items H/I and round-2 item 6 dispositions below; CH6b -(floating windows 1–4) and CH6c (opacity) remain not started. Status -stays CODE-COMPLETE pending the next user gate round (still needed for -item 1, CH6a's own visual confirmation, CH6b/CH6c, and a final in-client -visual pass on everything fixed so far). +(floating windows 1–4) and CH6c (opacity) remain not started. Round 2's +open item 1 (jump-in-air) was root-caused between rounds from live probe +evidence and FIXED at `a5a7eb4f` — a production-controller install path +never wired `OnInterfaceText`. **User gate round 3 ran 2026-08-10 and +found three more findings, all presentation; see "User gate — round 3" +below.** All three are fixed in this round's commit: the SpewBox now +mounts flush to the viewport top and resolves a real (smaller) retail dat +font instead of the unwired 15px debug fallback; bare `/help` and +`/help ` (including `/help death`) now print retail's exact +`DoHelp` shape — two scroll entries in the right order, not one +acdream-invented blob. Status stays CODE-COMPLETE pending the next user +gate round (still needed for CH6a's own visual confirmation, CH6b/CH6c, +round 3's fixes, and a final in-client visual pass on everything fixed so +far). **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. @@ -201,6 +211,9 @@ implementer per slice against a pinned contract (per | CH6a main-window layout + 8-grip resize | (this commit) | 12,317 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only) | pending — needs the next in-client round (items H/I round 1, item 6 round 2) | | CH6b/CH6c floating windows + opacity | not started | — | — | not started | | User gate round 2 | (this commit) | 12,267 passed / 4 skipped / 0 failed | — | items 2/4/5 fixed this commit, item 3 confirmed-fixed, item 6 folded into CH6a's spec, item 1 NOT reproduced (see "User gate — round 2" below) | +| CH6a main-window layout + 8-grip resize | `1fd51543` | 12,317 passed / 4 skipped / 0 failed | pending | pending — landed same day as round 2 | +| Jump-in-air root cause (round-2 item 1, resolved) | `a5a7eb4f` | Runtime tests 1,323/0 | — | round-3 probe evidence pinpointed a missing `OnInterfaceText` wire on the production controller-commit path (`RuntimeLocalPlayerMovementState.CommitRuntimeOwnedController`); FIXED, regression test added | +| User gate round 3 | (this commit) | Debug (all projects): 12,329 passed / 4 skipped / 1 failed (pre-existing #351 Debug-only flake — reproduces identically on the pristine pre-round-3 commit, not a regression); Release (every project reachable while a live `AcDream.App.exe` client — PID 15064, must not be killed per project policy — holds its own Release binaries locked, blocking `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` specifically): `AcDream.UI.Abstractions.Tests` (the layer this round's `/help` fix lives in) 867/867, plus `Core.Net.Tests` 823/823, `Runtime.Tests` 1,323/1,323, `Content.Tests` 130/130, `Headless.Tests` 89/89, `Bake.Tests` 15/15, `Cli.Tests` 4/4 — all 0 failed | — | findings (a)-(c) fixed this commit — SpewBox flush-top + retail dat font, `/help`/`/help death` exact retail print sequence (see "User gate — round 3" below) | ### CH4 closeout (2026-08-09) @@ -590,3 +603,86 @@ chrome mount), plus new/updated tests in `tests/AcDream.App.Tests/UI/UiRootInputTests.cs`, `tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs`, and `tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs`. + +## User gate — round 3 (2026-08-10) + +The user tested round 2's fixes live and reported three more findings, all +presentation. Between rounds, the main session also root-caused round 2's +open item 1 (jump-in-air silence) from live probe evidence and landed the +fix at `a5a7eb4f` — see the standalone ledger row above; not repeated here +since it needed no further work this round. + +| # | User finding (condensed) | Disposition | +|---|---|---| +| (a) | The SpewBox yellow text is "still not aligned all the way to the top" and not "the correct font and size" (retail's is SMALLER than ours). | **FIXED this SHA.** Two independent sub-fixes, both best-available APPROXIMATIONS (the true retail values remain unmeasurable — `SpewBoxLayoutDumpDiagnostic` re-run this round still finds no `FontDid`/colour property on element `0x10000048` or its ListBox child): **position** — `SpewBoxController.TopOffset` moves from round 1's 60px placeholder to `0` (flush to the viewport top), per the user's explicit direction. **Font** — the controller never wired `DatFont`/`Font` at all before this round, so it silently fell through to the retained-UI host's 15px debug `BitmapFont`; it now resolves retail dat Font `0x40000025` (`MaxCharHeight=11px`) through `RetailUiRuntime.Assets.ResolveFont` (a new public accessor), the SAME memoized resolver the rest of the retained UI uses. `0x40000025` was picked by cross-referencing every currently-imported retail LayoutDesc fixture (`tests/AcDream.App.Tests/UI/Layout/fixtures/*.json`) for the smallest FontDid actually in use — it is 11px, smaller than every other font found (`0x40000000`=16px, `0x40000002`=14px, etc.), confirmed against the installed DAT via `AcDream.Cli dump-font-atlas` sweeping every populated font id `0x40000000`-`0x40000032`. It is ALSO the chat window's own smallest font (the `0x2100006F` floating-window 1/2/3/4 indicator badges use the same id) — both selection criteria the round-3 brief offered landed on the same answer, no tie to break. Register row AP-178 updated (not retired — position/font remain approximations, not resolved retail values; only vertical content flow stays fully OPEN). | +| (b) | `/help` output is "still not what retail displays". | **FIXED this SHA.** Round 2 extracted the individual STRINGS byte-exact but never traced `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete PRINT SEQUENCE, so the bare `/help` listing was still an acdream-invented cheat sheet ("Chat: /say...", "Channels: /general...", etc. — none of it retail text). Traced this round via a byte-sweep of DoHelp's own range (`0x57f9e0`-`0x57fe7e`) plus the five Summary-branch functions it calls into, against the PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH). Retail's real bare-`/help` output is exactly TWO scroll entries — `RetailCommandHelpTable.HelpPrefixNote` (now re-swept with its leading blank line and trailing double newline, previously dropped) then `RetailCommandHelpTable.AvailableHelpListing`, a 13-item straight-line concatenation of retail's real topic-group one-liners (allegiances/channels/chatting/death/emote/fillcomps/friends/house/squelch/status/text/commands) in DoHelp's exact source order — never one concatenated blob. `ChatCommandRouter.EmitBareHelp` now emits two `ShowSystemMessage` calls, matching. The acdream-only `RetailClientCommandCatalog.BuildHelpText()`/router `BuildHelpText()` methods that built the old fabricated listing are deleted outright (dead code once nothing calls them). | +| (c) | `/help death` is "still not formatted correctly". | **FIXED this SHA.** The CONTENT (`DeathGroupDetail`'s 8 lines) was already byte-exact from round 2 — what was still wrong was the SHAPE. DoHelp wraps EVERY successfully-resolved `/help ` in the SAME two-entry shape as the bare listing: `HelpPrefixNote` as its own entry, then a SECOND entry that is `ForMoreInformationPrefix` ("For more information, type @help .\n" — retail's own literal; "" is NOT a substituted placeholder, confirmed by the absence of any sprintf/substitution call in the decomp) concatenated DIRECTLY onto the verb's own detail text — no blank line, no third entry, because retail's own handler call appends into the SAME string accumulator the prefix was built into. `ChatCommandRouter.EmitVerbHelp` now applies this wrap UNIFORMLY to every resolved verb (both `RetailClientCommandCatalog` and `RetailCommandHelpTable` lookups), not just death — the general fix, not a death-specific patch, per CLAUDE.md's root-cause discipline. An unresolved verb now shows retail's real fallback text, `RetailCommandHelpTable.UnknownCommand` ("Unknown command", swept verbatim) instead of the acdream-invented "No help available for '{verb}'." — this surfaced a SEPARATE finding: retail types this fallback `0x1A` (`ClientLocal`), which routes to the SpewBox exclusively, never the chat window; `ChatCommandRouter`/`ChatVM` live in `AcDream.UI.Abstractions`, a layer beneath Runtime with no SpewBox access, so the fallback still renders in the chat scroll — not a regression (it was already there, just with fabricated text), now tracked as ISSUES.md #367 / register row AP-186 instead of silently continuing unregistered. | + +Evidence: this commit's diff + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs` +(new pinning tests for `HelpPrefixNote`, `ForMoreInformationPrefix`, +`UnknownCommand`, `AvailableHelpListing`, and the router-level `/help death` +two-entry shape) + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs` +(updated `/help` tests for the new shape) + +`tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs` +(updated entry-count assertions) + `tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs` +(new flush-top / resize / font-wiring tests). Debug suite (all projects) +green — 12,329 passed / 4 skipped / 1 failed, the single failure being +issue #351 (a pre-existing, load-sensitive Debug-only flake in an +unrelated streaming test, confirmed reproducing identically on the +pristine pre-round-3 commit via `git stash`, not a regression from this +work). Release verification was possible for every project reachable +without rebuilding `AcDream.App` — a live client process (PID 15064) held +its own Release binaries locked for the whole session and was not killed +per project policy (`feedback_dont_kill_clients_before_launch`) — +including `AcDream.UI.Abstractions.Tests` (867/867, the layer both `/help` +fixes live in) and every other non-App-dependent test project, all 0 +failed. `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` (which the +SpewBox fix and the SpewBox tests live in) could not be Release-verified +this session; they are green in Debug. + +### Byte-sweep method for findings (b)/(c) + +`ClientCommunicationSystem::DoHelp @0x0057f9e0` has two branches +(`arg2 > 0` = verb specified, `arg2 <= 0` = bare `/help`). Both ALWAYS +print via exactly two `AddTextToScroll` calls, both typed `0` +(informational) — never a single concatenated string: + +1. The SAME `HelpPrefixNote` line, unconditionally, in both branches. +2. Bare: `AvailableHelpListing` — 13 items, 8 inline literals DoHelp + builds itself (allegiances/channels/chatting/death/fillcomps/friends/ + house, plus the header) interleaved with 5 items DoHelp gets by calling + each group's own `Summary_HelpType` branch (`HelpEmote`/`HelpSquelch`/ + `HelpStatusGroup`/`HelpTextGroup`/`HelpAllGroup` — every one of those 5 + functions has the identical `if (arg2 != Summary_HelpType) {Detail} + else {"@help X - ..."}` shape `HelpEmote` makes explicit at + `acclient_2013_pseudo_c.txt:388664`). Verb-specified (verb resolves): + `ForMoreInformationPrefix` immediately followed (string concatenation, + no separator) by the verb's own Detail-branch text. + +When the verb does NOT resolve, DoHelp instead prints ONE entry, +`UnknownCommand`, typed `0x1A` — the SpewBox-exclusive routing type CH +user-gate round 2 item 2 already ported for the portal-space notice. + +Tool: `py tools/pdb-extract/sweep_weenie_strings.py +C:/Users/erikn/Downloads/acclient.exe --range 0x57f9e0 0x57fe7e +--ascii-only --min-len 3` (DoHelp's own range) plus the same tool against +each of the five Summary-branch functions' own ranges (`HelpEmote` +`0x578b80`-`0x578c60`, `HelpSquelch` `0x57c190`-`0x57c2d0`, +`HelpStatusGroup` `0x57c410`-`0x57c5a0`, `HelpTextGroup` +`0x57c6c0`-`0x57c860`, `HelpAllGroup` `0x57e7f0`-`0x57eba0`), each range +read from the pseudo-C's own function-header addresses. `check_exe_pdb.py` +confirmed the candidate binary MATCH before any of this. + +### SpewBox font selection method for finding (a) + +`AcDream.Cli dump-font-atlas 0x400000XX` (already-existing +tooling, extended nowhere — used as-is) against every populated font id +from `0x40000000` to `0x40000032` (38 of 50 candidate ids populated in the +installed `client_portal.dat`), reading each `Font` DBObj's own +`MaxCharHeight`/`BaselineOffset`/glyph-table-size fields. Cross-referenced +against every `FontDid` value appearing in +`tests/AcDream.App.Tests/UI/Layout/fixtures/*.json` (24 already-imported +retail LayoutDesc dumps) to find the smallest font id actually CONFIRMED +in use by any retail UI import acdream has ported, rather than merely +present in the DAT. `0x40000025` (11px) was both the global minimum across +those fixtures AND the chat window's own minimum — no tie-break needed. diff --git a/docs/research/2026-08-09-campaign-ch-test-script.md b/docs/research/2026-08-09-campaign-ch-test-script.md index e272e7bd..efe9c4b7 100644 --- a/docs/research/2026-08-09-campaign-ch-test-script.md +++ b/docs/research/2026-08-09-campaign-ch-test-script.md @@ -8,14 +8,18 @@ FEEL right". ## 1. On-screen interface text (CH2 — the SpewBox) - Jump, and press jump again while airborne → **"You can't jump while in - the air"** appears as transient text near the top of the viewport, NOT - in the chat window. Chat gets no line at all. + the air"** appears as transient text flush to the very TOP of the + viewport (round 3, 2026-08-10 — moved off the earlier 60px-down + placeholder), NOT in the chat window. Chat gets no line at all. - Spam it 5+ times fast: the line refreshes in place (no stacking of identical text); distinct refusals stack newest-on-top, max 4 lines. - Try `@version` → its output goes to CHAT (green), not the SpewBox. -- **Report presentation impressions**: position, color, and how long lines - linger are placeholders pending measurement (register AP-177/AP-178) — - say what looks wrong vs your retail memory. +- The SpewBox text should now read visibly SMALLER than round 2 (retail + dat Font `0x40000025`, 11px, replacing the earlier 15px debug font). +- **Report presentation impressions**: position and font are now + best-available approximations, not resolved retail values (register + AP-178); line lifetime is still a placeholder (AP-177) — say what looks + wrong vs your retail memory. ## 2. Chat colors (CH1) @@ -58,11 +62,31 @@ Side-by-side vs retail if possible: - An unknown verb like `@somenonsense` → passes through to ACE (server answers, client does not swallow it). +## 5. Help text (round 3, 2026-08-10) + +- `/help` (no args) → TWO lines in the chat window: a "Note: You may + substitute a forward slash..." line, then "Available help:" followed by + 13 real retail topic one-liners (allegiances/channels/chatting/death/ + emote/fillcomps/friends/house/squelch/status/text/commands). This + replaced an acdream-invented cheat sheet ("Chat: /say...", "Client: + /help...") — that text should no longer appear at all. +- `/help death` → the SAME "Note:" line first, then a SECOND line starting + "For more information, type @help ." immediately followed by + the 8-line corpse/death command listing — not just the 8 lines alone. +- `/help somenonsenseverb` → "Unknown command" (no "Note:" line before it; + retail's fallback skips the wrapper). This still shows in the CHAT + window rather than the SpewBox — a known, tracked gap (#367/AP-186), not + a new bug to report. + ## Known-open, do not report as new - Ctrl+M mute chord (#358) — still broken, separate from this campaign. -- SpewBox lifetime/position/color are registered placeholders - (AP-177/AP-178) pending a retail measurement session. +- SpewBox line lifetime is still a placeholder (AP-177) pending a retail + measurement session; position/font are now best-available + approximations (AP-178), not confirmed retail pixel values. +- `/help ` fallback ("Unknown command") shows in the chat window, + not the SpewBox — retail types it for the SpewBox exclusively, but + `ChatCommandRouter` has no path there yet (#367/AP-186). - Allegiance management subcommands print the help refusal instead of executing (#360); `@day`/`@log`/`@render` deferred (#361); four request commands send but responses aren't rendered yet (#362). diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index 9598d47e..55617bb2 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -1091,11 +1091,22 @@ internal sealed class LivePresentationCompositionPhase SpewBoxController>? spewBoxLease = null; if (interaction.RetainedUi is { } spewBoxRetainedUi) { + // Campaign CH user-gate round 3: resolve the SpewBox's own + // retail dat font through the SAME memoized resolver the rest + // of the retained UI uses (RetailUiRuntime.Assets), rather than + // silently falling back to the debug bitmap font every prior + // round shipped with. See SpewBoxController's class remarks + // and register row AP-178. + RetailUiAssets spewBoxAssets = spewBoxRetainedUi.Runtime.Assets; + UiDatFont? spewBoxFont = + spewBoxAssets.ResolveFont(SpewBoxController.RetailFontId); spewBoxLease = scope.Acquire( "spew box", () => new SpewBoxController( spewBoxRetainedUi.Host.Root, - new SpewBoxVM(d.Runtime.CommunicationOwner.SpewBox)), + new SpewBoxVM(d.Runtime.CommunicationOwner.SpewBox), + spewBoxFont, + spewBoxAssets.DebugFont), static value => value.Dispose()); } CompositionAcquisitionScope.CompositionAcquisitionLease< diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 0eec62fa..97692b2d 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -321,6 +321,17 @@ public sealed class RetailUiRuntime : IDisposable } public UiHost Host => _bindings.Host; + + /// + /// Shared dat/sprite/font resolvers this runtime was built with. + /// Campaign CH user-gate round 3: lets a controller built OUTSIDE this + /// runtime's own Mount* graph (e.g. SpewBoxController, + /// composed directly in LivePresentationComposition) resolve the + /// SAME memoized instances instead of loading + /// its own copy. + /// + public RetailUiAssets Assets => _bindings.Assets; + public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction; public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider; public ToolbarController? ToolbarController { get; private set; } diff --git a/src/AcDream.App/UI/SpewBoxController.cs b/src/AcDream.App/UI/SpewBoxController.cs index 61a8a541..b25204e9 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.Rendering; using AcDream.App.UI.Layout; using AcDream.UI.Abstractions.Panels.SpewBox; @@ -67,19 +68,73 @@ namespace AcDream.App.UI; /// the divergence register rows this class cites for each remaining /// placeholder. /// +/// +/// Campaign CH user-gate round 3 (2026-08-10), finding (a) — position +/// and font. The user reported live: "still not aligned all the way to +/// the top" and "not the correct font and size (retail's is SMALLER than +/// ours)". Two changes, both still user-DIRECTED approximations (not +/// resolved retail values — the true absolute position/parent and the true +/// retail font remain unmeasurable statically; SpewBoxLayoutDumpDiagnostic +/// re-run this round still finds no FontDid/colour property on +/// element 0x10000048 or its ListBox child): +/// +/// Position: is now 0 — flush +/// to the viewport top, per the user's explicit direction ("mount at +/// viewport top-center, exactly"). The centered-X, recompute-every-frame +/// behavior from CH2 nit 1 is unchanged. +/// Font: now accepts a +/// resolved (retail font id +/// , 0x40000025) instead of silently +/// falling back to the debug at its ad hoc 15px +/// pixel height (the pre-round-3 behavior — no DatFont/Font +/// was ever wired here at all). 0x40000025 is MaxCharHeight=11 +/// px (confirmed via AcDream.Cli dump-font-atlas against the +/// installed DAT, sweeping every populated font id +/// 0x40000000-0x40000032) — the SMALLEST font id actually +/// confirmed IN USE by any of acdream's currently-imported retail +/// LayoutDesc fixtures (cross-referenced across every +/// tests/AcDream.App.Tests/UI/Layout/fixtures/*.json dump), and +/// it is ALSO the chat window's own smallest font — the same +/// 0x2100006F floating-window 1/2/3/4 indicator badges +/// (ChatWindowController.Indicator1-4Id) use it. Both selection +/// criteria from the round-3 brief ("smallest DAT font used by retail UI +/// imports" vs "the chat window's own font, whichever is smaller") land +/// on the SAME id, so there was no tension to resolve. This is visibly +/// smaller than the previous 15px debug font, matching the user's +/// report. Falls back to the debug font only if the dat resolve fails +/// (matching every other retained-UI controller's pattern, e.g. +/// ChatWindowController.Bind). +/// +/// Register row AP-178 updated to record both dispositions. +/// internal sealed class SpewBoxController : IDisposable { + /// + /// Retail dat Font id this controller resolves for its text + /// (Campaign CH user-gate round 3 — see the class remarks). Not + /// retail's own measured SpewBox font (unmeasurable — no FontDid + /// property was found on the authored element); the smallest DAT font + /// confirmed in use by any currently-imported retail LayoutDesc, + /// chosen so the rendered text is visibly smaller than the prior debug + /// fallback, per the user's report. + /// + internal const uint RetailFontId = 0x40000025u; + /// /// Register row AP-178 (screen position): retail's authored ABSOLUTE /// screen position is still unknown — the LayoutDesc dump (see class /// remarks) recovered the element's position as (0,0) relative - /// to a PARENT this sweep could not identify, so this centered-top - /// placement remains acdream's own choice, not a resolved retail value. - /// (The SIBLING row AP-177 — the invented line-lifetime timeout — lives - /// in 's own doc comment, not + /// to a PARENT this sweep could not identify. Campaign CH user-gate + /// round 3 (2026-08-10): the user reported the box was not flush to + /// the very top of the screen; mounted at 0 now, per explicit + /// user direction — still acdream's own placement choice pending the + /// true retail parent/offset, but now matching the user's live report + /// instead of an arbitrary 60px placeholder. (The SIBLING row AP-177 — + /// the invented line-lifetime timeout — lives in + /// 's own doc comment, not /// here; this controller does not own that concern.) /// - private const float TopOffset = 60f; + private const float TopOffset = 0f; /// /// Register row AP-178 (extent): AUTHORED, not a placeholder — the @@ -121,9 +176,10 @@ internal sealed class SpewBoxController : IDisposable /// (research doc §3.2.3); the LayoutDesc dump (see class remarks) also /// never surfaced a colour property for this element. The exact retail /// value simply happens to coincide with the Tell colour, per the user's - /// live observation. SIZE/POSITION/FONT remain OPEN — the user reports - /// all three still differ from retail; user gate round 1: differs, - /// iterating. + /// live observation. POSITION and FONT were re-addressed at Campaign CH + /// user-gate round 3 (2026-08-10) — see the class remarks and the + /// / comments; both + /// remain acdream-directed approximations, not resolved retail values. /// private static readonly Vector4 SpewBoxColor = new(1f, 1f, 0.247f, 1f); @@ -134,7 +190,18 @@ internal sealed class SpewBoxController : IDisposable private UiText.Line[] _lines = Array.Empty(); private bool _disposed; - public SpewBoxController(UiRoot root, SpewBoxVM vm) + /// Retained-UI root this element mounts under. + /// SpewBox view-model (bounded, newest-on-top queue). + /// + /// Resolved retail dat font () — Campaign CH + /// user-gate round 3. Null falls back to , + /// matching every other retained-UI controller's dat-font pattern (e.g. + /// ChatWindowController.Bind). + /// + /// Fallback bitmap font, used only when + /// is null. + public SpewBoxController( + UiRoot root, SpewBoxVM vm, UiDatFont? font = null, BitmapFont? debugFont = null) { _root = root ?? throw new ArgumentNullException(nameof(root)); _vm = vm ?? throw new ArgumentNullException(nameof(vm)); @@ -152,6 +219,12 @@ internal sealed class SpewBoxController : IDisposable Height = SpewBoxHeight, Anchors = AnchorEdges.None, Centered = true, + // Campaign CH user-gate round 3: retail dat font (RetailFontId) + // when resolved, falling back to the debug bitmap font exactly + // like every other retained-UI controller (ChatWindowController + // et al.) — see the class remarks. + DatFont = font, + Font = debugFont, // 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 diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs b/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs index 08128295..f681fd87 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs @@ -184,7 +184,7 @@ public static class ChatCommandRouter { if (EqAny(trimmed, "/help", "/?", "@help", "@?")) { - vm.ShowSystemMessage(BuildHelpText()); + EmitBareHelp(vm); return true; } @@ -195,25 +195,64 @@ public static class ChatCommandRouter if (StartsWithAny(trimmed, "/help ", "@help ", "/? ", "@? ")) { string verb = trimmed[(trimmed.IndexOf(' ') + 1)..].Trim(); - vm.ShowSystemMessage(BuildVerbHelpText(verb)); + EmitVerbHelp(verb, vm); return true; } return false; } - private static string BuildVerbHelpText(string verb) + /// + /// Bare /help — retail's DoHelp arg2<=0 branch. TWO + /// separate scroll entries, never one concatenated blob (Campaign CH + /// user-gate round 3, finding (b) — see 's + /// class remarks for the full print-sequence trace). + /// + private static void EmitBareHelp(ChatVM vm) + { + vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote); + vm.ShowSystemMessage(RetailCommandHelpTable.AvailableHelpListing); + } + + /// + /// /help <verb> — retail's DoHelp arg2>0 branch. + /// Campaign CH user-gate round 3, finding (c): a resolved verb gets the + /// SAME two-entry shape as the bare listing (Note, then + /// + /// immediately concatenated — NOT a third entry — with the verb's own + /// Detail text); an unresolved verb gets retail's real + /// text instead of + /// an acdream-invented "No help available" message. + /// + private static void EmitVerbHelp(string verb, ChatVM vm) { if (verb.Length == 0) - return BuildHelpText(); + { + EmitBareHelp(vm); + return; + } string normalized = verb.TrimStart('/', '@'); if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText)) - return catalogText; - if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText)) - return tableText; + { + vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote); + vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + catalogText); + return; + } - return $"No help available for '{verb}'."; + if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText)) + { + vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote); + vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + tableText); + return; + } + + // Retail types this 0x1A (ClientLocal) -> SpewBox-only; ChatVM has + // no SpewBox routing capability yet, so this still renders via the + // chat scroll — a pre-existing gap, not new this round. See the + // class remarks on RetailCommandHelpTable.UnknownCommand and + // ISSUES.md #367. + vm.ShowSystemMessage(RetailCommandHelpTable.UnknownCommand); } private static bool EqAny(string value, params string[] options) @@ -237,14 +276,4 @@ public static class ChatCommandRouter return false; } - - private static string BuildHelpText() => - $"{RetailCommandHelpTable.HelpPrefixNote}\n" + - "Chat: /say (default), /tell , , /reply, /retell\n" + - "Channels: /general /trade /fellowship /a (allegiance room)\n" + - " /patron /vassals /monarch /covassals\n" + - " /lfg /roleplay /society /olthoi\n" + - "Client: /help [command] (this) /clear /framerate /loc\n" + - $" {RetailClientCommandCatalog.BuildHelpText()}\n" + - "Server: type @acehelp or @acecommands for ACE's full list."; } diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs b/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs index 74faba5e..aa76c0ed 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs @@ -221,7 +221,7 @@ public static class RetailClientCommandCatalog // (data_7de2f8) — the first paragraph only; the full multi-paragraph // block is reproduced verbatim by DoEndurance itself and is long // enough that only the opening line is duplicated here as a teaser — - // BuildHelpText below uses the SAME text callers already see through + // this is the SAME text callers already see through // ClientCommandController. private static readonly Definition Endurance = NoArguments( ClientCommandId.Endurance, @@ -668,52 +668,6 @@ public static class RetailClientCommandCatalog return true; } - /// Help line generated from the same definition routing uses. - public static string BuildHelpText() => string.Join("\n ", - Lifestone.HelpText, - Marketplace.HelpText, - PkArena.HelpText, - PkLiteArena.HelpText, - PkLite.HelpText, - HouseRecall.HelpText, - MansionRecall.HelpText, - HouseAbandon.HelpText, - QueryAge.HelpText, - QueryBirth.HelpText, - FrameRate.HelpText, - LockUi.HelpText, - Version.HelpText, - Location.HelpText, - Corpse.HelpText, - Die.HelpText, - Clear.HelpText, - SaveUi.HelpText, - LoadUi.HelpText, - SaveAutoUi.HelpText, - LoadAutoUi.HelpText, - Away.HelpText, - Consent.HelpText, - Emote.HelpText, - Emotes.HelpText, - Friends.HelpText, - Squelch.HelpText, - Unsquelch.HelpText, - Filter.HelpText, - Unfilter.HelpText, - MessageTypes.HelpText, - FillComponents.HelpText, - Endurance.HelpText, - Speaker.HelpText, - SetTitle.HelpText, - ChatToggle.HelpText, - NoTellToggle.HelpText, - JoinChannel.HelpText, - LeaveChannel.HelpText, - Permit.HelpText, - HouseAvailableList.HelpText, - AllegianceHometown.HelpText, - AllegianceInfo.HelpText); - /// /// Every verb string this catalog dispatches, INCLUDING the /// specially-parsed "house"/"hou"/"allegiance"/"all" verbs (which are diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs b/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs index fb8863dc..949a272a 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs @@ -58,6 +58,48 @@ namespace AcDream.UI.Abstractions.Panels.Chat; /// unresolved mechanism and remain acdream summaries. See the remarks on /// for the full extraction method. /// +/// +/// +/// Campaign CH user-gate round 3 (2026-08-10), findings (b)/(c): the +/// round 2 pass extracted the individual STRINGS byte-exact but never +/// traced ClientCommunicationSystem::DoHelp @0x0057f9e0's complete +/// PRINT SEQUENCE — so the CONTENT was right while the SHAPE (how many +/// transcript entries, in what order, with what wrapping) was still +/// acdream-invented. Traced this round via a byte-sweep of DoHelp's own +/// range (0x57f9e0-0x57fe7e) plus every Summary-branch it calls into +/// (HelpEmote/HelpSquelch/HelpStatusGroup/HelpTextGroup/HelpAllGroup), +/// against the PDB-paired C:\Users\erikn\Downloads\acclient.exe +/// (verified MATCH). DoHelp ALWAYS prints via exactly two +/// AddTextToScroll calls (never one concatenated blob), both typed +/// 0 (informational): +/// +/// the SAME line, unconditionally — +/// for the bare listing AND for every successful /help <verb> +/// lookup; +/// bare /help: (13 items, +/// straight-line concatenation of 8 inline literals plus 5 delegated to +/// each group's own Summary_HelpType branch, in DoHelp's exact source +/// order). /help <verb> when the verb resolves: +/// immediately followed by the +/// verb's own Detail-branch text (e.g. ) — +/// ONE string, no blank line between the prefix and the listing, because +/// retail's own handler call APPENDS into the same accumulator the +/// prefix was built into (ClientCommunicationSystem::DoHelp, +/// arg2>0 branch, the eax_35(2, var_18, &var_10) call). +/// +/// When the verb does NOT resolve, DoHelp prints ONE entry, +/// , typed 0x1A (ClientLocal) — +/// retail routes that type to the SpewBox exclusively, never the chat +/// window (docs/research/2026-08-09-chat-retail-interface-text.md +/// §2.1/§2.2). ChatCommandRouter operates on ChatVM +/// (AcDream.UI.Abstractions), which has no SpewBox routing +/// capability — wiring that would mean threading a +/// RuntimeCommunicationState-shaped dependency down into a layer +/// that must stay presentation/Runtime-independent, out of this round's +/// scope. The unknown-verb fallback therefore still renders via the chat +/// scroll, a pre-existing (not newly introduced) gap now tracked at +/// ISSUES.md #367 instead of silently continuing unregistered. +/// /// public static class RetailCommandHelpTable { @@ -73,10 +115,42 @@ public static class RetailCommandHelpTable public const string Retell = "@retell - Sends the text to the last person you @tell'd. You may also use @rt."; - // acclient_2013_pseudo_c.txt:1031564 (data_7e11e0), the "Note:" line - // DoHelp @0x0057F9E0 prints alongside the bare group index. + // acclient_2013_pseudo_c.txt:394980 (data_0x7e11e0). RE-SWEPT byte-exact + // Campaign CH user-gate round 3 (2026-08-10) via sweep_weenie_strings.py + // --range 0x57f9e0 0x57fe7e --ascii-only against the PDB-paired + // C:\Users\erikn\Downloads\acclient.exe (verified MATCH). The ORIGINAL + // extraction dropped the leading blank line and the trailing double + // newline that are part of retail's own literal — this is DoHelp's + // OWN, always-first scroll entry (a dedicated AddTextToScroll call, + // never concatenated with what follows), printed unconditionally both + // for the bare /help listing and for every successful /help + // lookup. See the class remarks for the complete print-sequence trace. public const string HelpPrefixNote = - "Note: You may substitute a forward slash (/) for the at symbol (@)."; + "\nNote: You may substitute a forward slash (/) for the at symbol (@).\n\n"; + + // acclient_2013_pseudo_c.txt:395087 (data_0x7e1178), swept alongside + // HelpPrefixNote above. DoHelp's arg2>0 (verb-specified) branch builds + // this as the START of its second scroll entry, then the resolved + // verb's own Detail-branch handler APPENDS its listing directly onto + // the SAME accumulator (ClientCommunicationSystem::DoHelp, the + // `eax_35(2, var_18, &var_10)` call) -- so the retail-faithful port is + // string concatenation with NO separator, not two entries. The literal + // text "" is NOT a format placeholder -- the decomp shows a + // straight PStringBase construction with no sprintf/substitution call + // between this literal and its use, so retail never actually inserts + // the real verb name here. Ported verbatim per CLAUDE.md's "do not fix + // the decompiled code" rule, even though it reads like an unfinished + // dev message. + public const string ForMoreInformationPrefix = + "For more information, type @help .\n"; + + // acclient_2013_pseudo_c.txt:395052 (u"Unknown command", UTF-16LE) -- + // DoHelp's fallback when the verb hash lookup fails, or resolves to an + // entry with no registered help callback. Retail types this 0x1A + // (ClientLocal) -- SpewBox-only; see the class remarks' routing note + // and ISSUES.md #367 for why ChatCommandRouter still shows it in the + // chat scroll. + public const string UnknownCommand = "Unknown command"; // @mr/@pr are registered with a NULL function pointer in the 2013 // build (verified at 0x00583041/0x005830C1 — arg3 is 0), so they never @@ -305,6 +379,40 @@ public static class RetailCommandHelpTable public const string CommandsGroupDetail = CommandsGroupSummary + GroupDetailUnverifiedSuffix; + // Campaign CH user-gate round 3 (2026-08-10), finding (b): DoHelp's + // bare-/help "else" branch (arg2<=0, acclient_2013_pseudo_c.txt:395089- + // 395268), swept whole against C:\Users\erikn\Downloads\acclient.exe + // (--range 0x57f9e0 0x57fe7e --ascii-only, verified MATCH). ONE + // straight-line concatenation of 13 items, in this exact source order: + // the "Available help:\n" header, then 8 items DoHelp builds from its + // OWN inline literals (allegiances/channels/chatting/death/fillcomps/ + // friends/house — swept directly), interleaved with 5 items DoHelp + // builds by calling each group's OWN Summary_HelpType branch + // (HelpEmote/HelpSquelch/HelpStatusGroup/HelpTextGroup/HelpAllGroup — + // each independently swept from its own function range; every one of + // those 5 functions has the identical + // `if (arg2 != Summary_HelpType) {Detail} else {"@help X - ..."}` + // shape HelpEmote makes explicit at acclient_2013_pseudo_c.txt:388664). + // channels/chatting/commands' summary lines are reused from the + // consts above (independently cross-validated: both extractions agree + // byte-for-byte). Prints as DoHelp's SECOND scroll entry, right after + // HelpPrefixNote's own — see the class remarks and + // ChatCommandRouter's bare-/help handling. + public const string AvailableHelpListing = + "Available help:\n" + + "@help allegiances - Commands to help you deal with your Allegiance.\n" + + ChannelsGroupSummary + "\n" + + ChattingGroupSummaryVerbatim + "\n" + + "@help death - Commands for making, finding, and looting corpses.\n" + + "@help emote - How to perform text and action emotes.\n" + + "@help fillcomps - A command to help you buy components in bulk.\n" + + "@help friends - Commands to help you manage your friends list.\n" + + "@help house - Commands that help you manage your house, including guest and storage management.\n" + + "@help squelch - Commands that let you block out messages from other players.\n" + + "@help status - Commands that display useful information.\n" + + "@help text - Commands that help you manage your text window.\n" + + CommandsGroupSummary + "\n"; + private static readonly FrozenDictionary ByVerb = new Dictionary(StringComparer.OrdinalIgnoreCase) { diff --git a/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs b/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs index f431d487..182d6b62 100644 --- a/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using AcDream.App.UI; using AcDream.Core.Chat; using AcDream.UI.Abstractions.Panels.SpewBox; +using DatReaderWriter.Types; namespace AcDream.App.Tests.UI; @@ -175,4 +177,79 @@ public sealed class SpewBoxControllerTests Assert.Empty(root.Children); } + + // ── Campaign CH user-gate round 3 (2026-08-10), finding (a) ───────── + + [Fact] + public void Construction_MountsFlushToTheViewportTop() + { + // The user reported the box "still not aligned all the way to the + // top" — TopOffset moved from the round-1 60px placeholder to 0. + var root = new UiRoot { Width = 1280f, Height = 720f }; + using var controller = new SpewBoxController(root, new SpewBoxVM(new SpewBoxState())); + + UiText text = Assert.IsType(root.Children.OfType().Single()); + Assert.Equal(0f, text.Top); + } + + [Fact] + public void Tick_KeepsTopFlushAndRecentersX_AcrossAResize() + { + var root = new UiRoot { Width = 1280f, Height = 720f }; + var state = new SpewBoxState(); + using var controller = new SpewBoxController(root, new SpewBoxVM(state)); + + state.Enqueue("resize me"); + root.Tick(dt: 0d, nowMs: 1000L); + + UiText text = Assert.IsType(root.Children.OfType().Single()); + float widthBefore = root.Width; + Assert.Equal((widthBefore - 450f) / 2f, text.Left); + Assert.Equal(0f, text.Top); + + // Simulate a window resize, then the next per-frame tick. + root.Width = 1920f; + root.Tick(dt: 0d, nowMs: 1016L); + + Assert.Equal((1920f - 450f) / 2f, text.Left); + Assert.Equal(0f, text.Top); // top offset never depends on width + } + + [Fact] + public void Construction_WithResolvedRetailFont_WiresDatFontOntoTheText() + { + // Campaign CH user-gate round 3: the retail dat font (id + // SpewBoxController.RetailFontId) is now actually WIRED, where + // before the controller never set DatFont/Font at all and silently + // fell through to the render context's default debug font. + var root = new UiRoot { Width = 1280f, Height = 720f }; + var font = new UiDatFont( + fgTex: 1, fgW: 64, fgH: 64, + bgTex: 0, bgW: 0, bgH: 0, + lineHeight: 11f, baselineOffset: 9f, + glyphs: new Dictionary()); + + using var controller = new SpewBoxController( + root, new SpewBoxVM(new SpewBoxState()), font, debugFont: null); + + UiText text = Assert.IsType(root.Children.OfType().Single()); + Assert.Same(font, text.DatFont); + Assert.Equal(11f, text.DatFont!.LineHeight); + } + + [Fact] + public void Construction_WithoutAResolvedFont_FallsBackToTheSuppliedDebugFont() + { + // No installed-DAT font available (e.g. headless) -- the debug + // bitmap font parameter is still wired through, matching every + // other retained-UI controller's dat-font/debug-font pattern. + var root = new UiRoot { Width = 1280f, Height = 720f }; + + using var controller = new SpewBoxController( + root, new SpewBoxVM(new SpewBoxState()), font: null, debugFont: null); + + UiText text = Assert.IsType(root.Children.OfType().Single()); + Assert.Null(text.DatFont); + Assert.Null(text.Font); + } } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs index 8bdbb29a..087437a5 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs @@ -237,6 +237,11 @@ public class ChatCommandRouterTests Assert.Contains(log.Snapshot(), entry => entry.Text.Contains("Returns you to the last lifestone")); } + // Campaign CH user-gate round 3 (2026-08-10), finding (c): a resolved + // verb prints retail's DoHelp SHAPE, not just its content — the SAME + // HelpPrefixNote entry every /help path prints, then a SECOND entry + // that is ForMoreInformationPrefix concatenated directly onto the + // verb's own detail text (no blank line, no separate third entry). [Theory] [InlineData("/help mr", "@mr - Sends the text to the last person who used @m to send you a message. This only works for monarchs.")] [InlineData("/help pr", "@pr - Sends the text to the last vassal who used @p to send you a message.")] @@ -251,18 +256,47 @@ public class ChatCommandRouterTests var outcome = ChatCommandRouter.Submit(input, vm, bus, ChatChannelKind.Say); Assert.Equal(SubmitOutcome.ClientHandled, outcome); - Assert.Contains(log.Snapshot(), entry => entry.Text == expected); + var entries = log.Snapshot(); + Assert.Equal(2, entries.Length); + Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text); + Assert.Equal(RetailCommandHelpTable.ForMoreInformationPrefix + expected, entries[1].Text); } [Fact] - public void HelpVerb_UnknownVerb_ShowsFallbackMessage() + public void HelpVerb_UnknownVerb_ShowsRetailUnknownCommandText() { + // Campaign CH user-gate round 3 (2026-08-10): retail's own DoHelp + // fallback text is "Unknown command" (swept verbatim), not an + // acdream-invented "No help available" message. Retail types this + // 0x1A (ClientLocal / SpewBox-only); ChatVM has no SpewBox routing + // capability yet (ISSUES.md #367), so it still lands in the chat + // scroll here as ONE entry (no HelpPrefixNote wrapper — DoHelp's + // fallback bypasses the two-entry shape entirely). var (vm, log, bus) = Fixture(); var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say); Assert.Equal(SubmitOutcome.ClientHandled, outcome); Assert.Empty(bus.Published); - Assert.Contains(log.Snapshot(), entry => entry.Text.Contains("No help available")); + var entry = Assert.Single(log.Snapshot()); + Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text); + } + + [Fact] + public void HelpBare_ShowsRetailTwoEntryShape() + { + // Campaign CH user-gate round 3 (2026-08-10), finding (b): bare + // /help must emit retail's real two-entry sequence (Note, then the + // 13-item "Available help:" listing) — not the previous + // acdream-invented single-blob cheat sheet. + var (vm, log, bus) = Fixture(); + + var outcome = ChatCommandRouter.Submit("/help", vm, bus, ChatChannelKind.Say); + + Assert.Equal(SubmitOutcome.ClientHandled, outcome); + var entries = log.Snapshot(); + Assert.Equal(2, entries.Length); + Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text); + Assert.Equal(RetailCommandHelpTable.AvailableHelpListing, entries[1].Text); } } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs index 590f2500..8e3a95be 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs @@ -22,10 +22,15 @@ public sealed class ChatPanelInputTests public void Submit_HelpCommand_RendersLocalHelpAndDoesNotPublish() { // Phase J follow-up: client-side commands (/help, /?, /h) are - // intercepted before the parser. They render a local cheat-sheet - // via ChatLog.OnSystemMessage and do NOT round-trip the server - // — that's what prevented the "Unknown command: help" duplicate - // ACE was firing back. + // intercepted before the parser. They render local text via + // ChatLog.OnSystemMessage and do NOT round-trip the server — that's + // what prevented the "Unknown command: help" duplicate ACE was + // firing back. + // + // Campaign CH user-gate round 3 (2026-08-10): retail's DoHelp + // prints via exactly TWO scroll entries (Note, then the 13-item + // "Available help:" listing), never one acdream-invented blob — see + // RetailCommandHelpTable's class remarks for the full trace. var log = new ChatLog(); var vm = new ChatVM(log); var panel = new ChatPanel(vm); @@ -39,12 +44,11 @@ public sealed class ChatPanelInputTests panel.Render(new PanelContext(0.016f, bus), renderer); Assert.Empty(bus.Published); - var entry = Assert.Single(log.Snapshot()); - Assert.Equal(ChatKind.System, entry.Kind); - // Help text mentions / and @ equivalence and points at @acehelp - // for the server's full command list. - Assert.Contains("/tell", entry.Text); - Assert.Contains("@acehelp", entry.Text); + var entries = log.Snapshot(); + Assert.Equal(2, entries.Length); + Assert.All(entries, entry => Assert.Equal(ChatKind.System, entry.Kind)); + Assert.Equal(AcDream.UI.Abstractions.Panels.Chat.RetailCommandHelpTable.HelpPrefixNote, entries[0].Text); + Assert.Equal(AcDream.UI.Abstractions.Panels.Chat.RetailCommandHelpTable.AvailableHelpListing, entries[1].Text); } [Theory] @@ -67,7 +71,7 @@ public sealed class ChatPanelInputTests panel.Render(new PanelContext(0.016f, bus), renderer); Assert.Empty(bus.Published); - Assert.Single(log.Snapshot()); + Assert.Equal(2, log.Snapshot().Length); } [Fact] diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs index ef24493a..37f0279d 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs @@ -165,4 +165,99 @@ public sealed class RetailCommandHelpTableTests Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text)); Assert.Equal(expected, text); } + + // ── Campaign CH user-gate round 3 (2026-08-10) ────────────────────── + // The round 2 pass extracted the individual STRINGS byte-exact; this + // round traces DoHelp's complete PRINT SEQUENCE — see the class + // remarks. These pin the newly-swept literals byte-exact against + // tools/pdb-extract/sweep_weenie_strings.py's output. + + [Fact] + public void HelpPrefixNote_HasRetailsLeadingAndTrailingBlankLines() + { + // acclient_2013_pseudo_c.txt:394980 (data_0x7e11e0) -- the original + // round-2 extraction dropped the leading "\n" and the trailing + // "\n\n" that are part of retail's own literal. + Assert.Equal( + "\nNote: You may substitute a forward slash (/) for the at symbol (@).\n\n", + RetailCommandHelpTable.HelpPrefixNote); + } + + [Fact] + public void ForMoreInformationPrefix_KeepsRetailsUnsubstitutedPlaceholderVerbatim() + { + // acclient_2013_pseudo_c.txt:395087 (data_0x7e1178) -- "" + // is retail's own literal text, not a format placeholder acdream + // failed to substitute; the decomp shows no sprintf/substitution + // call between this literal and its use. + Assert.Equal( + "For more information, type @help .\n", + RetailCommandHelpTable.ForMoreInformationPrefix); + } + + [Fact] + public void UnknownCommand_MatchesRetailsExactFallbackText() + { + Assert.Equal("Unknown command", RetailCommandHelpTable.UnknownCommand); + } + + [Fact] + public void AvailableHelpListing_MatchesDoHelpsCompleteThirteenItemSequence() + { + // acclient_2013_pseudo_c.txt:395091-395249 (DoHelp bare-arg "else" + // branch), swept whole via sweep_weenie_strings.py --range + // 0x57f9e0 0x57fe7e --ascii-only against the PDB-paired + // C:\Users\erikn\Downloads\acclient.exe (verified MATCH). Exact + // source order: header, then allegiances/channels/chatting/death/ + // emote/fillcomps/friends/house/squelch/status/text/commands. + Assert.Equal( + "Available help:\n" + + "@help allegiances - Commands to help you deal with your Allegiance.\n" + + "@help channels - How to communicate with people in your allegiance or fellowship.\n" + + "@help chatting - How to chat publically and privately.\n" + + "@help death - Commands for making, finding, and looting corpses.\n" + + "@help emote - How to perform text and action emotes.\n" + + "@help fillcomps - A command to help you buy components in bulk.\n" + + "@help friends - Commands to help you manage your friends list.\n" + + "@help house - Commands that help you manage your house, including guest and storage management.\n" + + "@help squelch - Commands that let you block out messages from other players.\n" + + "@help status - Commands that display useful information.\n" + + "@help text - Commands that help you manage your text window.\n" + + "@help commands - Lists all commands.\n", + RetailCommandHelpTable.AvailableHelpListing); + } + + [Fact] + public void DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape() + { + // Campaign CH user-gate round 3, finding (c): "/help death"'s + // CONTENT was already byte-exact (round 2); what was still wrong + // was the SHAPE. Retail's DoHelp prints two scroll entries for any + // resolved verb: HelpPrefixNote, then ForMoreInformationPrefix + // concatenated directly onto the verb's own Detail text (no blank + // line, no third entry). Exercised at the router level (not just + // the table) so this is the actual /help death user experience, + // not merely the table's stored string. + var log = new AcDream.Core.Chat.ChatLog(); + var vm = new ChatVM(log, displayLimit: 50); + var bus = new RecordingCommandBus(); + + var outcome = ChatCommandRouter.Submit( + "/help death", vm, bus, ChatChannelKind.Say); + + Assert.Equal(SubmitOutcome.ClientHandled, outcome); + var entries = log.Snapshot(); + Assert.Equal(2, entries.Length); + Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text); + Assert.Equal( + RetailCommandHelpTable.ForMoreInformationPrefix + + RetailCommandHelpTable.DeathGroupDetail, + entries[1].Text); + } + + private sealed class RecordingCommandBus : ICommandBus + { + public List Published { get; } = new(); + public void Publish(T command) where T : notnull => Published.Add(command); + } }