From f7a6f46ba05a037de5152e5d193d3bdaea6e1de0 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 10 Aug 2026 16:22:36 +0200 Subject: [PATCH] =?UTF-8?q?fix(chat):=20consolidated-review=20fixes=20?= =?UTF-8?q?=E2=80=94=20retail=20/help=20Detail=20extraction,=20seam=20wiri?= =?UTF-8?q?ng=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SHOULD-FIX 1: RetailClientCommandCatalog's ~45 catalog leaf verbs were showing acdream-authored Summary text for /help instead of retail's own Detail_HelpType(2) text. Byte-swept every Help* handler against the PDB-paired acclient.exe (verified MATCH), confirmed each Detail/Summary branch by reading the actual decompiled if/else shape (address order and string length both proved unreliable alone), and fixed a sweep_weenie_strings.py 800-char truncation bug that silently dropped several longer Detail branches. Resolved every ambiguous CmdHashData-registered verb (hor/hr/hom/hoa/alh/ah/friends_add/ friends_remove/squelch/unsquelch) by reading for Binary Ninja's nullptr-4th-arg decompiler artifact instead of trusting it. Coverage: 42 of 47 distinct catalog Definitions verbatim-extracted, 4 confirmed-null (index/clist/on/off register with a genuinely null help pointer — DoHelp falls to UnknownCommand for these, now reproduced), 1 honest UNVERIFIED (messagetypes builds its text from a runtime enum table, not a static string). ChatCommandRouter now prefers retail Detail text over the catalog summary; RetailCommandHelpTable's class doc no longer overclaims its own scope. SHOULD-FIX 2: extracted the a5a7eb4f-class OnInterfaceText wiring into a testable CreateChatViewModel method and added ComposedChatViewModelWiresOnInterfaceTextToSpewBox, which the prior FakeFactory-based test suite could never exercise. SHOULD-FIX 3: retires register row AP-113. DoLifestone/DoMarketplace print their own 0x1A refusal text (byte-recovered, UTF-16LE) instead of falling through to the generic 0x26 fallback; ChatCommandRouter's comment corrected to state the fallback's real scope. SHOULD-FIX 4: corrected the divergence register's stale AP section header sentence about AP-190's opacity default (refuted by cc582899). NITs: (a) HeadlessStaticStateAudit routes through the injected HeadlessDiagnosticWriter instead of Console.WriteLine; (b) a bounded 300-pump liveness diagnostic on the IsQuiescent conductor gate (no retry, no behavior change); (c) fixed the #365 hydration test's doc comment contradiction against diagnosis §8; (d) the 0x26 fallback dispatches on WeenieErrorMessages' own Type instead of hardcoding ClientLocal. Full Release suite: 12,553 passed / 4 skipped / 0 failed (baseline 03404b71: 12,542/4/0; net +11 tests, zero regressions). Co-Authored-By: Claude Opus 5 --- .../retail-divergence-register.md | 4 +- docs/plans/2026-08-09-chat-parity-campaign.md | 131 ++++++++ .../InteractionRetainedUiComposition.cs | 40 ++- .../Hosting/HeadlessProcessHost.cs | 10 +- .../Hosting/HeadlessSessionHost.cs | 9 +- .../Hosting/HeadlessSessionWorldProjection.cs | 40 ++- .../Hosting/HeadlessStaticStateAudit.cs | 19 +- .../Panels/Chat/ChatCommandRouter.cs | 86 ++++- .../Panels/Chat/RetailClientCommandCatalog.cs | 28 +- .../Panels/Chat/RetailCommandHelpTable.cs | 313 +++++++++++++++++- .../InteractionRetainedUiCompositionTests.cs | 38 +++ .../HeadlessSessionHostTests.cs | 32 +- .../HeadlessStaticStateAuditTests.cs | 46 ++- .../Panels/Chat/ChatCommandRouterTests.cs | 60 +++- .../Chat/RetailCommandHelpTableTests.cs | 176 ++++++++++ 15 files changed, 947 insertions(+), 85 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 4b0e07c6..368e3cc0 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) — 133 active rows (AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships one shared 0.5/1.0 default rather than gmMainChatUI's per-class 1.0/1.0 override, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 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 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) +## 3. Documented approximation (AP) — 132 active rows (AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 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 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered @@ -307,7 +307,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-174 | **Volume-knob taxonomy differs from retail's, filed 2026-08-08 (Campaign A slice A2).** Retail has exactly three float knobs — `effect_sound_volume`, `ambient_sound_volume`, `interface_sound_volume` — **no master and no music knob**, and the interface one is registered and then never read (interface sounds are scaled by the EFFECT knob). acdream keeps an extra `MasterVolume` on top of `SfxVolume`, which A2 folds into the mixer's single master multiply (`EffectMaster = MasterVolume * SfxVolume`) rather than publishing as an AL listener gain — so the −50 dB no-allocate floor, the audible radius, and the whole-decibel quantisation all move with the slider the way they would if retail had one. `MusicVolume` is dead (retail has no music system at all; slice A6 deletes it) and `AmbientVolume` is unread until slice A5 wires the ambient path. No Interface knob exists yet; slice A4 adds the UI bus and will scale it by the effect knob, matching retail's dead-knob behaviour rather than implementing a working one. | `src/AcDream.App/Audio/OpenAlAudioEngine.cs` (`EffectMaster`); `src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs` | A master slider is a modern nicety users expect and costs nothing once it is inside the one retail multiply; implementing retail's dead interface knob as a working control would be a divergence in the other direction, so it stays dead. | At Master 1.0 (the default) behaviour is bit-identical to a retail single-knob mix. Below 1.0 the mix is quieter than retail's would be at the same effect setting, because retail has no such knob to turn down. | `SoundManager::InitPrefs @ 0x005503F0`; `SoundManager::GetAttenuation @ 0x00550020`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` §3 D11/D13 | | ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` | | AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.Runtime/Gameplay/RuntimeCombatAttackState.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` | -| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` | +| ~~AP-113~~ | **RETIRED 2026-08-10 (consolidated-review round, SHOULD-FIX 3/1 byproduct).** The exact wording IS now recovered — Binary Ninja's misidentification was the same pooled-string/mislabeled-vtable-slot artifact this file already documents elsewhere (`ClientCommunicationSystem::\`vftable'.RecvNotice_AddItemToTrade`), not a genuinely unrecoverable address. Read directly from the raw `push imm32` operand at `0x0056fc84` and decoded as UTF-16LE against the PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH): `"Please see @help lifestone for more information on how to use this command."` `RetailClientCommandCatalog.Lifestone` now carries it as `InvalidArgumentsText`, so `/ls now` prints retail's own exact sentence, byte-exact, not a diagnostic approximation. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs` (`Lifestone`) | — | — | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` | | ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` | | AP-115 | **NARROWED 2026-08-08 (Campaign A slice A4) — the sound half is landed; only the notice's presentation remains.** The enter/exit cues now play: `LocalPlayerTeleportPresentation.EnterTunnel`/`ExitTunnel` fire `UI_EnterPortal`/`UI_ExitPortal` through the resolved interface sound bank, which is where retail plays them (`0x004D638E` / `0x004D7405`, inside the teleport-animation boundary rather than the tunnel renderer). The DAT-authored portal-space viewport, animation `SoundTweakedHook`, and centered repeating `"In Portal Space - Please Wait..."` display string are live. **Scope note (2026-08-06):** this row covers the cue's PRESENTATION only. Its five-second arming threshold is a separate, unregistered divergence now filed as AP-150 — retail emits the notice unconditionally per tunnel rotation segment (0.6-1.8 s) and has no such threshold. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/Streaming/LocalPlayerTeleportController.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs` | acdream has no ClientUISystem sound-table-enum resolver yet; inventing direct wave IDs would be less faithful. The notice uses the retained fullscreen UI rather than chat and remains tied to the portal presentation lifetime. | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, animation-authored sound, centered wait notice, and (as of A4) retail's short UI enter/exit cue sounds. The residual is that the notice uses the retained fullscreen UI rather than chat, and its five-second arming is AP-150. | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` | diff --git a/docs/plans/2026-08-09-chat-parity-campaign.md b/docs/plans/2026-08-09-chat-parity-campaign.md index 686d1527..bf4d9b39 100644 --- a/docs/plans/2026-08-09-chat-parity-campaign.md +++ b/docs/plans/2026-08-09-chat-parity-campaign.md @@ -289,6 +289,7 @@ implementer per slice against a pinned contract (per | CH6b floating windows 1–4 | `22020ef2`, reworked `1aa77099` | 12,420 passed / 4 skipped / 0 failed | REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked `1aa77099` — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions | | CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — BLOCKER (out-of-box `DefaultOpacity` 0.5→1.0), AP-190 reworded + two new decomp-verified clauses (retail's per-tick ease, retail's entry-field-specific focus predicate), NITs (`UiElement.cs` stale comment, `WindowUnregistered` detach, post-Dispose `Set*` guards, `DrawString`/outline-pass alpha tests) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) | | Goal-window #363/#367 interface-text seam | `09453eca` | 12,542 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm the SpewBox now flashes for the reclassified refusals (see "Goal-window follow-up" below) | +| Consolidated review — retail `/help` Detail extraction, seam wiring test | this commit | 12,553 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — SHOULD-FIX 1 (42 of 47 catalog leaf verbs given retail Detail_HelpType text, 4 confirmed-null, 1 honest UNVERIFIED, retiring the class doc's overclaim), SHOULD-FIX 2 (`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` — the a5a7eb4f defect class had no test), SHOULD-FIX 3 (AP-113 RETIRED — Lifestone's and Marketplace's own bespoke bad-args refusal text recovered byte-exact), SHOULD-FIX 4 (register header's stale 0.5/1.0 sentence corrected), NITs (a)-(d) (`HeadlessDiagnosticWriter` instead of `Console.WriteLine`, bounded non-quiescent-pump liveness diagnostic, hydration test doc-comment contradiction, 0x26 fallback dispatches on its own `Type`) | pending — needs the next connected round to confirm `/help ` now shows retail's exact wording (see "Consolidated review" below) | ### CH4 closeout (2026-08-09) @@ -940,3 +941,133 @@ red instead of doing nothing; `/hslist badtype` and a bad-args catalog command (e.g. `/ls now`) now flash red SpewBox text instead of showing a green "Usage:" line in chat; `/help nonsenseverb` and a bare `/`/`@` now flash on the SpewBox instead of appearing in the chat scroll. + +## Consolidated review — goal-window round (2026-08-10) + +Applied the consolidated-review findings against the `09453eca` goal +window under the same hard constraints (no subagents, no client launches, +one commit). Baseline `03404b71`, full Release suite 12,542/4/0. + +**SHOULD-FIX 1 — retail `/help ` Detail extraction.** Retail's +`DoHelp @0x0057F9E0` calls a resolved verb's OWN registered help callback +with `Detail_HelpType(2)`, never `Summary_HelpType(1)` — +`RetailClientCommandCatalog`'s ~45 leaf Definitions were all showing their +acdream-authored Summary-shaped `HelpText` for `/help ` instead. +Located every `Help*` handler by name in `acclient_2013_pseudo_c.txt`, +swept its exact byte extent for `push imm32` string literals against the +PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH), and +confirmed each Detail-vs-Summary branch assignment by reading the actual +decompiled if/else shape — address order and string length both proved +UNRELIABLE indicators on their own (HelpDie's Detail sits at the HIGHER +address, HelpCorpse's at the LOWER; the "longest text" heuristic held for +every simple 2/3-branch case but AFK/Emote/Squelch turned out to +CONCATENATE multiple literals within one branch instead). Found and fixed +a real tool gap along the way: `sweep_weenie_strings.py`'s stock 800-char +cap silently dropped several longer Detail branches as a bare `None` +(HelpConsent 978 chars, HelpFillComponents 1036, HelpEndurance 2459) — a +custom unbounded pass recovered them. + +`InitializeCommands @0x00581970`'s registration blocks always render the +CmdHashData help-pointer 4th argument as a literal `nullptr` in Binary +Ninja's pseudo-C — a confirmed, systematic decompiler artifact (the real +pointer is stored to a throwaway local immediately before the constructor +call, which BN fails to thread through). Reading for the PRESENCE of that +typed local (vs. a plain `int32_t = 0`) resolved every ambiguous verb: +hor/hr/hom/hoa and alh/ah share their Detail text with `HelpHouse`/ +`HelpAllegiance` (i.e. `HouseOverview`/`AllegianceOverview`, already +extracted); friends_add/friends_remove share `HelpFriends`; +squelch/unsquelch share `HelpSquelch`. Four verbs +(index/clist/on/off) genuinely have no help pointer — retail's own +`DoHelp` falls straight to `UnknownCommand` for these, a CONFIRMED +behavior now reproduced (`RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp`), +not a guess. `messagetypes`/its 3 aliases stay an honest acdream summary — +`HelpMessageTypes` builds its text from a live enum table at runtime, not +a static string. Final coverage (measured by test, not hand-counted): 42 +of 47 distinct catalog leaf Definitions verbatim-extracted, 4 +confirmed-null, 1 unverified. + +`ChatCommandRouter.EmitVerbHelp` now checks +`CatalogVerbsWithNoRetailHelp` → `RetailCommandHelpTable.TryGetCatalogVerbDetailText` +→ `RetailClientCommandCatalog.TryGetHelpText` (the summary fallback, +unchanged) → the pre-existing `RetailCommandHelpTable.TryGetHelpText` +(chat-alias/channel verbs, a disjoint key space). `RetailCommandHelpTable`'s +class doc, which claimed to cover only verbs the catalog "doesn't dispatch +directly," is corrected — it now overlaps the catalog's own leaf verbs by +design. A fresh full-length byte-sweep of `HelpAllegiance @0x0057ae10` (the same +function `AllegianceOverview` already cites) also turned up one +previously-dropped line (" WARNING! Officers banning...") the earlier +extraction missed — now split into `AllegianceWarningLine` and restored +in place at its correct position (between "ban list" and "info"). + +**SHOULD-FIX 2 — `OnInterfaceText` production wiring had no test.** The +a5a7eb4f defect class (a composed hook wired but never transferred) went +undetected because `InteractionRetainedUiCompositionTests`' `FakeFactory` +substitutes a stub `ChatVM` for every composition-order test and never +exercises `RetailInteractionRetainedUiCompositionFactory`'s real wiring. +Extracted the wiring (`InteractionRetainedUiComposition.cs`, previously +inline at the line the review cited) into its own testable +`CreateChatViewModel` method — no GPU/dat/UiHost dependency, so it is +callable directly against the existing null-heavy test `Fixture` without +a full `Compose()`. New test +`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` asserts the hook is +non-null AND that a `ShowInterfaceText` call actually lands in +`RuntimeCommunicationState.SpewBox` after a `Tick`. + +**SHOULD-FIX 3 — AP-113 RETIRED.** `DoLifestone`/`DoMarketplace +@0x0056FC70`/`@0x0056FCE0` print their OWN 0x1A string for bad args and +return 1 — retail never reaches the generic `HandleFailureEvent(0x26)` +fallback for either. Both literals were mis-attributed by Binary Ninja to +unrelated vtable-slot symbols (the same pooled-string artifact class this +campaign has hit before); read directly off the raw `push imm32` operand +and decoded as UTF-16LE against the PDB-paired binary: `"Please see @help +lifestone for more information on how to use this command."` and the +`marketplace` analogue. `RetailClientCommandCatalog.Lifestone`/`Marketplace` +now carry them as `InvalidArgumentsText`. AP-113 is RETIRED (its only +named verb, lifestone, now has its exact wording); Marketplace was never a +filed divergence, so its fix is a plain accuracy improvement alongside it. +`ChatCommandRouter.cs`'s bad-args comment is corrected to name both +verbs and state the 0x26 fallback's actual scope (verbs with NO bespoke +retail refusal, not every bad-args case). + +**SHOULD-FIX 4 — register header sentence fixed.** `retail-divergence- +register.md`'s AP section header still said AP-190 "ships one shared +0.5/1.0 default rather than gmMainChatUI's per-class 1.0/1.0 override" — +refuted by `cc582899`, which fixed the shipped default TO 1.0/1.0 (the +row's own REWORDED (2) already said so; only the header summary hadn't +caught up). Corrected in place; the active-row count drops to 132 with +AP-113's retirement. + +**NITs (a)-(d).** (a) `HeadlessStaticStateAudit`'s single-session log line +now routes through the injected `HeadlessDiagnosticWriter` (constructed +before the audit call in `HeadlessProcessHost`, not after) instead of a +bare `Console.WriteLine` that bypassed the structured stream every other +headless diagnostic uses. (b) `HeadlessSessionWorldProjection.PumpFirstEntry` +gained a bounded, diagnostic-only trip-wire: 300 consecutive non-quiescent +pumps (generous — many seconds at the host's tick cadence) emit ONE +message naming the stuck landblock; no retry, no behavior change, the gate +itself is untouched. (c) The #365 hydration test's doc comment is +corrected — it claimed reverting the `IsQuiescent` gates makes the +conductor reach "`PublicationCommitted` — a non-null dormant controller," +which contradicts the diagnosis doc's own §8 finding (the controller is +ALREADY built AND published, `CanExecuteLiveMovement = True`, when the +assertion actually fails); the comment now also states plainly that this +bounded xunit test proves the GATE, not the #365 stall's closure — that +broader claim's evidence is the separate live-ACE run §8 records. (d) +`ChatCommandRouter`'s 0x26 fallback now resolves +`WeenieErrorMessages.Resolve(0x026u, null)` once and dispatches on its own +`Type` field instead of assuming `ShowInterfaceText`'s hardcoded +`ClientLocal` is correct for it (it is, today — this just stops relying on +that being an unstated invariant). + +Full Release suite: 12,553 passed / 4 skipped / 0 failed (net +11 tests +from this round, zero regressions; one `LandblockPresentationPipelineTests` +failure observed on one parallel full-solution run reproduced as PASS in +isolation and on two subsequent full-solution reruns — pre-existing +run-order flakiness in an unrelated streaming test, not caused by this +round's changes). No subagent review this session (hard constraint); no +connected user gate (hard constraint — no client launches). The next +connected round should confirm `/help die`, `/help lifestone`, +`/help endurance`, and a handful of the other 42 newly-extracted verbs +show retail's exact wording instead of the old acdream summaries, and that +`/help index`/`/clist`/`/on`/`/off` now show "Unknown command" via the +SpewBox. diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index de1d5a2d..d46ff129 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -419,6 +419,33 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory incrementBusy: itemInteraction.IncrementBusyCount, canSend: () => late.Session.IsInWorld); + /// + /// Consolidated-review round (2026-08-10), SHOULD-FIX 2: extracted out + /// of so this specific wiring — the + /// composed hook that routes + /// ChatCommandRouter's 0x1A refusals to + /// /SpewBox — is + /// independently testable without the rest of 's + /// GPU/dat/UiHost dependencies (all null-safe in a pure unit test; this + /// method only touches ). + /// The a5a7eb4f defect class (a composed hook wired but never + /// transferred) is exactly what this seam existing untested let slip + /// through — see InteractionRetainedUiCompositionTests.ComposedChatViewModelWiresOnInterfaceTextToSpewBox. + /// + internal static ChatVM CreateChatViewModel(InteractionRetainedUiDependencies d) => + new ChatVM( + d.Communication.Chat, + displayLimit: 200, + commandTargets: d.Communication.CommandTargets) + { + // Issue #363 / #367: routes ChatCommandRouter's 0x1A + // (ClientLocal) command refusals to the same SpewBox + // chokepoint every other interface-text producer uses, + // instead of the chat scroll. + OnInterfaceText = text => + d.Communication.AddText(text, RetailLogTextType.ClientLocal), + }; + public RetainedUiComposition CreateRetainedUi( InteractionRetainedUiDependencies d, InteractionUiLateBindings late, @@ -559,18 +586,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory host.Root.Width = d.Window.Size.X; host.Root.Height = d.Window.Size.Y; - var chat = new ChatVM( - d.Communication.Chat, - displayLimit: 200, - commandTargets: d.Communication.CommandTargets) - { - // Issue #363 / #367: routes ChatCommandRouter's 0x1A - // (ClientLocal) command refusals to the same SpewBox - // chokepoint every other interface-text producer uses, - // instead of the chat scroll. - OnInterfaceText = text => - d.Communication.AddText(text, RetailLogTextType.ClientLocal), - }; + var chat = CreateChatViewModel(d); AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore = d.Settings.LayoutStore; RetailUiPersistenceBindings? persistence = layoutStore is null diff --git a/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs b/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs index 3f4eec94..db260f7d 100644 --- a/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs +++ b/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs @@ -42,10 +42,14 @@ internal sealed class HeadlessProcessHost : IDisposable throw new HeadlessConfigurationException( "Direct credentials require exactly one configured session."); } - HeadlessStaticStateAudit.ValidateProcessIsolation( - configuration.Sessions.Count); - + // Consolidated-review round (2026-08-10), NIT (a): construct the + // diagnostics writer BEFORE the audit call so its single-session + // "probes enabled" line routes through the same structured stream + // every other headless diagnostic uses, instead of a bare + // Console.WriteLine that bypassed it. _diagnostics = new HeadlessDiagnosticWriter(diagnostics); + HeadlessStaticStateAudit.ValidateProcessIsolation( + configuration.Sessions.Count, _diagnostics); var credentials = new HeadlessCredentialResolver( standardInput, paths.ConfigDirectory); diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs b/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs index 9c2ece5b..31b22a53 100644 --- a/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs +++ b/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs @@ -675,7 +675,14 @@ internal sealed class HeadlessSessionHost : IDisposable Runtime, content, _firstEntryDrive, - _acceptedPositionDrive); + _acceptedPositionDrive, + // Consolidated-review round (2026-08-10), NIT (b): the + // SAME session-labelled diagnostics stream every other + // producer in this class writes into. + onNonQuiescentStall: message => _diagnostics.Message( + _descriptor.Id, + message, + Runtime.Generation.Value)); _worldProjection = projection; worldProjection = projection; } diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs b/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs index 5665ffa6..f6f4b532 100644 --- a/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs +++ b/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs @@ -617,18 +617,34 @@ internal sealed class HeadlessSessionWorldProjection /// resolves promptly. /// private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive; + private readonly Action? _onNonQuiescentStall; private uint _requestedLocalPlayerCell; + /// + /// Consolidated-review round (2026-08-10), NIT (b): a purely + /// diagnostic trip-wire, not a behavior change. At the host's per-tick + /// pump cadence this is on the order of many seconds — generous enough + /// that no legitimate multi-tick collision-generation sequence should + /// ever cross it, so crossing it means something is genuinely stuck + /// (a publication that never seals, an admission that never clears). + /// + private const int NonQuiescentStallPumpThreshold = 300; + + private int _nonQuiescentPumpCount; + private bool _reportedNonQuiescentStall; + internal HeadlessSessionWorldProjection( GameRuntime runtime, HeadlessProcessContentOwner.HeadlessProcessContentLease content, RuntimeFirstEntryDriveController? firstEntry = null, - RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null) + RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null, + Action? onNonQuiescentStall = null) : this( runtime, new HeadlessCollisionNeighborhood(runtime, content), firstEntry, - acceptedPositionDrive) + acceptedPositionDrive, + onNonQuiescentStall) { } @@ -636,7 +652,8 @@ internal sealed class HeadlessSessionWorldProjection GameRuntime runtime, IHeadlessCollisionNeighborhood collision, RuntimeFirstEntryDriveController? firstEntry = null, - RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null) + RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null, + Action? onNonQuiescentStall = null) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); @@ -644,6 +661,7 @@ internal sealed class HeadlessSessionWorldProjection ?? throw new ArgumentNullException(nameof(collision)); _firstEntry = firstEntry; _acceptedPositionDrive = acceptedPositionDrive; + _onNonQuiescentStall = onNonQuiescentStall; } public void ProjectSpawn( @@ -776,7 +794,23 @@ internal sealed class HeadlessSessionWorldProjection // publication work each tick (its own mutating side effect), so // gating it too would make the neighborhood itself never converge. if (!_collision.IsQuiescent) + { + // NIT (b): diagnostic-only trip-wire, no retry and no behavior + // change -- the gate above keeps refusing to drive the + // conductor exactly as it always has. Fires ONCE per stall + // episode so a hang shows up in the diagnostics stream instead + // of reading as "still working" indefinitely. + if (++_nonQuiescentPumpCount >= NonQuiescentStallPumpThreshold + && !_reportedNonQuiescentStall) + { + _reportedNonQuiescentStall = true; + _onNonQuiescentStall?.Invoke(FormattableString.Invariant( + $"collision neighborhood non-quiescent for {_nonQuiescentPumpCount} pumps, requested landblock=0x{_requestedLocalPlayerCell:X8}")); + } return; + } + _nonQuiescentPumpCount = 0; + _reportedNonQuiescentStall = false; _firstEntry?.DriveAll(); _acceptedPositionDrive?.Advance(); } diff --git a/src/AcDream.Headless/Hosting/HeadlessStaticStateAudit.cs b/src/AcDream.Headless/Hosting/HeadlessStaticStateAudit.cs index e056fac5..9e8d4a1d 100644 --- a/src/AcDream.Headless/Hosting/HeadlessStaticStateAudit.cs +++ b/src/AcDream.Headless/Hosting/HeadlessStaticStateAudit.cs @@ -1,6 +1,7 @@ using System.Reflection; using AcDream.Core.Physics; using AcDream.Headless.Configuration; +using AcDream.Headless.Diagnostics; namespace AcDream.Headless.Hosting; @@ -22,8 +23,18 @@ namespace AcDream.Headless.Hosting; /// internal static class HeadlessStaticStateAudit { - internal static void ValidateProcessIsolation(int sessionCount) + /// + /// Consolidated-review round (2026-08-10), NIT (a): + /// is the SAME structured writer every other headless diagnostic uses + /// ('s _diagnostics field, now + /// constructed before this call rather than after it) — the raw + /// Console.WriteLine this replaced bypassed the session-labelled + /// JSON stream every other producer writes into. + /// + internal static void ValidateProcessIsolation( + int sessionCount, HeadlessDiagnosticWriter diagnostics) { + ArgumentNullException.ThrowIfNull(diagnostics); var enabled = new List(); foreach (PropertyInfo property in typeof(PhysicsDiagnostics) .GetProperties(BindingFlags.Public | BindingFlags.Static) @@ -53,8 +64,10 @@ internal static class HeadlessStaticStateAudit if (sessionCount == 1) { - Console.WriteLine(FormattableString.Invariant( - $"[headless-audit] single-session process — process-global physics probes enabled: {string.Join(", ", enabled)}")); + diagnostics.Message( + sessionId: "process", + eventName: FormattableString.Invariant( + $"headless-audit: single-session process — process-global physics probes enabled: {string.Join(", ", enabled)}")); return; } diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs b/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs index d5d6c2ba..9b3c090b 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs @@ -54,21 +54,44 @@ public static class ChatCommandRouter { // #363 / register row AP-183: retail's bad-args refusal is // ALWAYS 0x1A (ClientLocal / SpewBox-only) — verified - // against five decompiled handlers (DoDie, DoChannelList/ - // On/Off, DoAllegiance, DoHouseAvailableList). A Definition - // with its own InvalidArgumentsText is the handler's own - // bespoke refusal string, printed before it returns - // "handled" (1) so retail's generic fallback never fires - // for it. A Definition with none falls to that generic - // fallback: ClientCommunicationSystem::DoCommand - // @0x0057E46D calls HandleFailureEvent(0x26) when a - // registered handler returns 0 (bad args) — + // against seven decompiled handlers (DoDie, DoChannelList/ + // On/Off, DoAllegiance, DoHouseAvailableList, and — added at + // the consolidated-review round 2026-08-10, retiring + // register row AP-113 — DoLifestone/DoMarketplace). A + // Definition with its own InvalidArgumentsText is the + // handler's own bespoke refusal string (byte-recovered from + // the PDB-paired acclient.exe; Binary Ninja mis-attributes + // both DoLifestone's and DoMarketplace's own literal to an + // unrelated vtable-slot symbol, the same class of artifact + // this file already documents for the Help* family), + // printed before the handler returns "handled" (1) so + // retail's generic fallback never fires for it. The 0x26 + // fallback below therefore covers ONLY verbs with no + // bespoke retail refusal of their own — not every bad-args + // case, and not a placeholder pending more extraction: + // ClientCommunicationSystem::DoCommand @0x0057E46D calls + // HandleFailureEvent(0x26) when a registered handler + // returns 0 (bad args) with no bespoke string, resolving to // "That is not a valid command." (WeenieErrorMessages // [0x026]) — never the acdream-invented "Usage: {Usage}" - // line this branch used to synthesize. - vm.ShowInterfaceText(clientCommand.InvalidArgumentsText - ?? WeenieErrorMessages.Resolve(0x026u, null).Text - ?? "That is not a valid command."); + // line this branch used to synthesize. Consolidated-review + // NIT (d): dispatches on the resolved entry's own Type + // rather than assuming ShowInterfaceText's hardcoded + // ClientLocal is correct for it. + if (clientCommand.InvalidArgumentsText is { } bespokeRefusal) + { + vm.ShowInterfaceText(bespokeRefusal); + } + else + { + (string? text, RetailLogTextType type) fallback = + WeenieErrorMessages.Resolve(0x026u, null); + string fallbackText = fallback.text ?? "That is not a valid command."; + if (fallback.type == RetailLogTextType.ClientLocal) + vm.ShowInterfaceText(fallbackText); + else + vm.ShowSystemMessage(fallbackText); + } return SubmitOutcome.ClientHandled; } @@ -261,6 +284,24 @@ public static class ChatCommandRouter /// text instead of /// an acdream-invented "No help available" message. /// + /// + /// Consolidated-review round (2026-08-10), SHOULD-FIX 1: lookup order + /// is now (1) + /// — a catalog leaf verb retail itself registers with a NULL help + /// pointer (index/clist/on/off) goes straight to + /// , matching + /// retail's own DoHelp exactly, and must never reach either + /// text source below; (2) + /// — + /// retail's OWN Detail_HelpType text, byte-swept from the retail + /// binary, for the 42 catalog leaf verbs it covers; (3) + /// — the + /// catalog's acdream-authored summary, now purely the fallback for + /// catalog verbs not yet extracted (currently only messagetypes and + /// its 3 aliases); (4) + /// — chat-alias/channel verbs and the group-topic nodes, a disjoint key + /// space from the catalog so this reordering changes nothing for them. + /// private static void EmitVerbHelp(string verb, ChatVM vm) { if (verb.Length == 0) @@ -270,6 +311,25 @@ public static class ChatCommandRouter } string normalized = verb.TrimStart('/', '@'); + + if (RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp.Contains( + normalized.TrimEnd(','))) + { + // Retail types this 0x1A (ClientLocal) -> SpewBox-only, the + // SAME fallback an unregistered verb gets — DoHelp's help- + // pointer-null guard skips its callback branch entirely. See + // RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp's remarks. + vm.ShowInterfaceText(RetailCommandHelpTable.UnknownCommand); + return; + } + + if (RetailCommandHelpTable.TryGetCatalogVerbDetailText(normalized, out string retailDetailText)) + { + vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote); + vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + retailDetailText); + return; + } + if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText)) { vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote); diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs b/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs index c72247f8..6b24283c 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs @@ -29,16 +29,36 @@ public static class RetailClientCommandCatalog bool HasValidArguments, string? InvalidArgumentsText); + // ClientCommunicationSystem::DoLifestone @ 0x0056FC70. Consolidated + // review round (2026-08-10), SHOULD-FIX 3: retires register row + // AP-113. DoLifestone's own bad-args branch prints ITS OWN 0x1A string + // and returns 1 — retail never reaches the generic HandleFailureEvent + // (0x26) fallback for it. Binary Ninja mis-attributes the literal to + // an unrelated vtable-slot symbol + // (ClientCommunicationSystem::`vftable'.RecvNotice_AddItemToTrade, + // the classic pooled-string artifact); recovered byte-exact by reading + // the raw `push imm32` operand at 0x0056fc84 and decoding the UTF-16LE + // string at data VA 0x007d0578 directly against the PDB-paired + // C:\Users\erikn\Downloads\acclient.exe (verified MATCH). private static readonly Definition Lifestone = new( ClientCommandId.LifestoneRecall, Usage: "/lifestone", HelpText: "/lifestone (/lif, /ls) - Returns you to the last lifestone you used without killing you.", - ValidateArguments: static arguments => arguments.Length == 0); + ValidateArguments: static arguments => arguments.Length == 0, + InvalidArgumentsText: "Please see @help lifestone for more information on how to use this command."); - private static readonly Definition Marketplace = NoArguments( + // ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0. Same + // methodology and same vtable-mislabeling artifact as Lifestone above + // (mis-attributed to RecvNotice_UpdateToolbarSelectionDisplay); + // operand at 0x0056fcf4, data VA 0x007d0610. Not a previously-filed + // register row — Marketplace was never flagged as diverging — this is + // a plain accuracy improvement alongside Lifestone's AP-113 retirement. + private static readonly Definition Marketplace = new( ClientCommandId.MarketplaceRecall, - "/marketplace", - "/marketplace (/mar, /mp) - Teleports you to the Marketplace of Dereth."); + Usage: "/marketplace", + HelpText: "/marketplace (/mar, /mp) - Teleports you to the Marketplace of Dereth.", + ValidateArguments: static arguments => arguments.Length == 0, + InvalidArgumentsText: "Please see @help marketplace for more information on how to use this command."); private static readonly Definition PkArena = NoArguments( ClientCommandId.PkArenaRecall, diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs b/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs index 956690d2..00f1bf10 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs @@ -4,14 +4,24 @@ namespace AcDream.UI.Abstractions.Panels.Chat; /// /// Campaign CH slice CH4 (2026-08-09): /help <verb> text for -/// every retail-registry verb -/// doesn't dispatch directly — chat aliases and channel verbs +/// every retail-registry verb — originally chat aliases and channel verbs /// (), the null-func help-only nodes (retail /// registers these with NO handler; typing them bare reaches the server, /// only @help <verb> shows anything locally), and the /// allegiance/house command overviews (the per-subcommand detail lives /// here too, even though most subcommands are not yet locally executed — -/// see TS-68). +/// see TS-68). Corrected at the consolidated-review round +/// (2026-08-10), SHOULD-FIX 1: the sentence above previously claimed +/// this table covers only verbs +/// "doesn't dispatch directly" — that framing is now FALSE and was itself +/// an overclaim-by-omission: (see +/// its own remarks, and the "Catalog leaf-verb Detail extraction" region +/// below) supplies retail Detail_HelpType text for 42 of the catalog's OWN +/// dispatched leaf verbs too, which ChatCommandRouter now prefers +/// over the catalog's acdream-authored summaries. This table is the single +/// /help <verb> retail-text source for BOTH catalog-dispatched +/// and non-catalog verbs; +/// is now purely the fallback for the catalog verbs not yet extracted. /// /// /// The named constants above (, @@ -193,13 +203,27 @@ public static class RetailCommandHelpTable public const string Motd = "@allegiance motd - Displays the message of the day for your allegiance. @allegiance motd set - Sets the MOTD. Can only be used by monarchs. @allegiance motd clear - Clears the MOTD. Can only be used by monarchs. NOT YET IMPLEMENTED in acdream (TS-68) — the command reaches the server as literal text."; + // Split out of AllegianceOverview's own data block (data_0x7dfb58) at + // the consolidated review round (2026-08-10) so the const declaration + // order matches use; see the class remarks on AllegianceOverview below. + public const string AllegianceWarningLine = + " WARNING! Officers banning or booting a character by account could wind up in a situation where they are no longer in the allegiance if they boot a character that is above them in the hierarchy.\n"; + // acclient_2013_pseudo_c.txt:1031234 (data_7e03d4) plus the full - // per-subcommand block at 1031210-1031230 (data_7dfb58). + // per-subcommand block at 1031210-1031230 (data_7dfb58). Consolidated + // review round (2026-08-10), SHOULD-FIX 1 byproduct: a fresh full-length + // byte sweep of HelpAllegiance @0x0057ae10 (0x57ae10-0x57aef0, no + // max_chars truncation this time) turned up the " WARNING! Officers + // banning..." line between "ban list" and "info" that the original + // extraction dropped — confirmed present in the SAME data block + // (data_0x7dfb58) this const already cites, not a new source. Restored + // verbatim, including the retail leading space. public const string AllegianceOverview = "@allegiance - Commands to help manage your allegiance.\n" + "@allegiance boot [-account] - Removes a character from your allegiance.\n" + "@allegiance ban - Bans all characters on the given character's account from your allegiance (and boots them too!)\n" + "@allegiance ban list - List the characters whose accounts are banned from your allegiance.\n" + + AllegianceWarningLine + "@allegiance info - Requests information on a member of your allegiance. [IMPLEMENTED]\n" + "@allegiance chat - Turn allegiance chat on and off.\n" + "@allegiance chat kick [, ] - Kick a player temporarily from the allegiance chat room.\n" @@ -416,6 +440,287 @@ public static class RetailCommandHelpTable + "@help text - Commands that help you manage your text window.\n" + CommandsGroupSummary + "\n"; + // ── Catalog leaf-verb Detail extraction (consolidated review round, + // 2026-08-10, SHOULD-FIX 1) ───────────────────────────────────────── + // + // RetailClientCommandCatalog's 47 top-level Definitions each carry an + // acdream-authored HelpText — a SUMMARY-shaped one-liner, not retail's + // Detail_HelpType(2) text. DoHelp @0x0057F9E0 calls a resolved verb's + // OWN registered help callback with Detail_HelpType for /help + // (the class remarks above trace this call precisely) — retail never + // shows the Summary text there. Every Help* handler below was located + // by name in acclient_2013_pseudo_c.txt, its exact byte extent (this + // function's start VA to the next function's start VA, read from the + // pseudo-C's own function-header addresses) swept for `push imm32` + // string literals against the PDB-paired + // C:\Users\erikn\Downloads\acclient.exe (verified MATCH via + // check_exe_pdb.py), and the resulting branch assignment confirmed by + // reading the actual decompiled if/else shape rather than guessing + // from string length or address order — those two heuristics + // DISAGREED between HelpDie (Detail at the higher address) and + // HelpCorpse (Detail at the LOWER address), so neither is trustworthy + // alone. sweep_weenie_strings.py's stock 800-char cap silently dropped + // several longer Detail branches (HelpConsent 978 chars, HelpFillComponents + // 1036, HelpEndurance 2459) as a `None` result with no error — a custom + // unbounded-length pass (same PE-parsing logic, no cap) recovered them; + // this is a tool limitation worth fixing generally, not specific to + // this sweep. + // + // CmdHashData help-pointer verification. Several verbs the + // catalog dispatches (hor/hr, hom/hoa, friends_add, friends_remove, + // squelch, unsquelch, alh/ah) have no distinctly-NAMED Help* function of + // their own. `ClientCommunicationSystem::InitializeCommands + // @0x00581970`'s per-verb block ALWAYS shows the registered help + // pointer as a literal `nullptr` 4th argument to + // `CmdHashData::CmdHashData(...)` in Binary Ninja's rendering — a + // confirmed, systematic decompiler artifact, not per-verb truth: the + // REAL help pointer is instead stored to a throwaway local + // (`uint8_t (__stdcall* var_3c_NN)(...) = ClientCommunicationSystem::HelpXxx;`) + // immediately before the constructor call, which Binary Ninja fails to + // thread through as the actual argument. Presence of that typed + // function-pointer local (not a plain `int32_t var_3c_NN = 0;`) is the + // ground truth: hor/hr/hom/hoa share `HelpHouse` (the SAME function + // "house"/"hou" use — already covers it), + // friends_add/friends_remove share `HelpFriends` (same as "friends"), + // squelch/unsquelch share `HelpSquelch`, and alh/ah share + // `HelpAllegiance` (the SAME function "allegiance"/"all" use — already covers it). Four verbs + // (index/clist/on/off) genuinely DO have a plain `int32_t var_3c_NN = 0;` + // local with no preceding Help-pointer assignment — CONFIRMED NULL, + // not merely undecoded: retail's own `DoHelp` skips straight to its + // "Unknown command" fallback for these (the `if (eax_35 != 0)` guard + // around the help-pointer call), even though the verb itself dispatches + // fine. names them so + // ChatCommandRouter can reproduce that exact behavior instead of + // falling back to the catalog's invented summary. + // + // messagetypes/message_types/msgtypes/msg_types + // are the one leaf verb NOT extracted this round: + // `HelpMessageTypes @0x0056e5d0` calls + // `gmCCommunicationSystem::GetListofSquelchChannels(arg4)` to build its + // text from a live enum table at runtime — there is no static string to + // sweep. It keeps its acdream summary, honestly UNVERIFIED rather than + // guessed at. + // + // Coverage this round: 42 of the 47 catalog Definitions got a verbatim + // Detail override (, several + // sharing one extraction per the CmdHashData note above), 4 are + // CONFIRMED-NULL (), and 1 + // (messagetypes and its 3 aliases) remains an honest acdream summary — + // RetailClientCommandHelpCoverageTests pins these counts so a + // future extraction pass (or an accidental regression) is caught. + public const string LifestoneDetail = + "@lifestone - Returns you to the last lifestone you used without killing you.\n"; + + public const string MarketplaceDetail = + "@marketplace - Teleports you to the Marketplace of Dereth.\n"; + + public const string PkArenaDetail = + "@pkarena - Teleports you to the PK Arena. You must be PK to use this command.\n"; + + public const string PkLiteArenaDetail = + "@pklarena - Teleports you to the PKL Arena. You must be PKL to use this command.\n"; + + public const string PkLiteDetail = + "@pklite - Sets your status to Player Killer Lite (PK Lite). PK Lite characters can attack other PK Lite characters. They cannot, however, attack Player Killer (PK) characters. PK Lite characters operate under the same combat rules as PK characters, except that if you are killed in a PK Lite battle, you will not accrue vitae and you will not drop any coins or items. Only Non-Player Killers may use this command to enter PK Lite. Dying in a PK Lite battle and logging off will restore your status to Non-Player Killer.\n"; + + public const string AgeDetail = + "@age - Displays your total gameplay time.\n"; + + public const string BirthDetail = + "@birth - Displays when your character was created.\n"; + + public const string FrameRateDetail = + "@framerate - Toggles the framerate display.\n"; + + public const string LockUiDetail = + "@lockui - Toggles the locked state of the UI layout.\n"; + + public const string VersionDetail = + "@version - Tells you what version of the software you are using.\n"; + + public const string LocDetail = + "@loc - Displays your current position in your chat window. Use this information when you wish to submit a bug report.\n"; + + public const string CorpseDetail = + "@corpse - Displays the location of your last outdoor death. Even if your corpse has disappeared or if you have subsequently died indoors, typing this command will display your last outdoor corpse location.\n"; + + public const string DieDetail = + "@die - If you wish to kill your character and leave a corpse, you may use the @die command. This will result in your character's death, you will leave behind a corpse with some of your items, and you will appear at your lifestone. If you wish to travel to your lifestone without leaving behind a corpse, you may use the @lifestone command.\n"; + + public const string ClearDetail = + "@clear - Clears the chat box of all text.\n"; + + public const string SaveUiDetail = + "@saveui - Saves the current user interface layout to disk using the provided file name. If no file name is provided the layout is saved with a name that is unique for your server, character and resolution.\n"; + + public const string LoadUiDetail = + "@loadui - Loads a previously saved user interface layout from disk using the provided file name"; + + public const string SaveAutoUiDetail = + "@saveautoui - Stores the current layout to a character and resolution specific file. This layout will automatically be used when the resolution changes for this character to the current size.\n"; + + public const string LoadAutoUiDetail = + "@loadautoui - Forces a previously saved layout to load for this user and resolution."; + + public const string AfkDetail = + "@afk - Turns on AFK (away-from-keyboard) mode. When set to AFK, other players that send you directed chatyou will receive a customizable message that your are not currently at the keyboard.\n@afk on - Turns on AFK mode. When set to AFK, other players that send you directed chatyou will receive a customizable message that your are not currently at the keyboard.\n@afk off - Turn off AFK mode.\n@afk msg - Set the message that will be sent to players that send you directed chat while you are in AFK mode. Issuing \"@afk msg\" with no message will set your AFK message back to the default. Your custom AFK message is limited to 192 characters.\n"; + + public const string ConsentDetail = + "The @consent commands allow you to display and manage your corpse-looting consent list. This list lets you control whether others may permit you to loot their corpse and also allows you to monitor who has given you permission. You may have a maximum of 20 separate permissions at any given time. You will not be able to loot a corpse that was the victim of a player killer, even if its owner has given you permission. Also, players who have squelched you are not able to permit you to loot their corpse. Note that you can toggle your consent on/off via the Character Options panel as well as through these commands.\n@consent on - Turns on your ability to accept permissions from other players.\n@consent off - Turns off your ability to accept permissions from other players.\n@consent who - Lists those who have given you permission to loot their corpses.\n@consent remove - Removes the permission a player granted to you.\n@consent clear - Clears your entire consent list.\n\n"; + + public const string EmoteDetail = + "The @emote command causes your character to emote some text, by performing an action in the third person. For example, if you typed the following while logged in as a character named Arville:\n @emote looks around the town curiously.\nthen the chat windows of everyone around you would display:\n Arville looks around the town curiously.\nYou can use any of these shorter forms of the command as well:\n @e \n @em \n ; \n : \n\nYou can also use a variety of standard emotes. These emotes come with special animations as well as text. Type @emotes to see a list.\n\n"; + + public const string EmoteListDetail = + "Standard Emotes:\nNote: These commands should be bound on either side by asterisks. (Example: *wave*)\nShakeFist; Beckon; BeSeeingYou; BlowKiss; BowDeep; ClapHands; Cry; Laugh; Nod; Point; Shrug; Wave; Akimbo; HeartyLaugh; Salute; TapFoot; WaveHigh; WaveLow; Yawn; Stretch; Cringe; Kneel; Plead; Shiver; Shoo; Slouch; Spit; Surrender; Woah; Winded; YMCA; Eat; Drink; Teapot; Pray; Mock; Cheer; Helper; Warm Hands; Scratch Head; Shake Head\n\n"; + + public const string FriendsDetail = + "Every time someone on your friends list logs in or out, you will receive notification. In addition, you can query the online status of your friends list at any time. Your friends list can contain up to 50 characters.\n@friends - Shows all your current friends and indicates if any of them are online.\n@friends online - Shows your current online friends.\n@friends add - Adds a character to your friends list.\n@friends remove - Removes a character from your friends list.\n@friends remove -all - Clears your friends list.\n@friends old - Shows the characters who were on your old-style friends list prior to the January 2006 update, so you can move them to your new-style friends list if necessary.\n"; + + public const string SquelchDetail = + "The @squelch commands let you block out messages from specific characters or players. The @unsquelch commands lets squelched messages reach you again. Use the options on these commands to squelch all message types or just some types of messages; one character or an entire account. You may have up to 32 players squelched at once. Note that NPCs cannot be permanently squelched.\n\n@squelch - Shows the current list of squelched characters.\n@squelch [-account] - Squelches all messages from a character. With the account flag, this command also stops everything except normal chat coming from the target's other characters.\n@squelch [-message_type] - This will filter out all text messages of a certain type from a specific character. For example, the following will filter out all tell messages from Oswald:\n Example: @squelch -tell Oswald.\n@squelch -reply [-account] [-message_type] - This filters out all text messages from whoever last tell'd you. You may also use the -account flag and/or limit the squelch by indicating specific message types. For example, this will filter out all tell messages from the account of Oswald, assuming that Oswald was the last person who sent you an @tell:\n Example: @squelch -reply -account -tell\n\n@unsquelch - Shows the current list of squelched characters.\n@unsquelch - Removes all squelches from a character, including account squelch.\n@unsquelch [-message_type] : This allows text messages of type message_type to come from a squelched character. For example the following allows assessment messages from a character name Oswald:\n Example: @unsquelch -assessment Oswald\n@unsquelch -reply [-account] [-message_type] : This allows text messages of type message_type from whoever last sent you an @tell. For example, the following will allow any character on Oswald's account to once again send you @tells, assuming that Oswald was the last person who sent you an @tell:\n Example: @squelch -reply -account -tell\n\nType @messagetypes for a complete list of message types.\n"; + + public const string FilterDetail = + "The @filter commands filter out all incoming messages of a certain type. Type @messagetypes to see a list of the message types that you can filter.\n@filter - List all the filters currently in place.\n@filter <-message_type> - Filters out all incoming messages of a specific type. For example, the following will filter out all spellcasting text: \n Example: @filter -spellcasting\n@filter -all - Filters out all incoming messages of all types.\n\n"; + + public const string UnfilterDetail = + "The @unfilter commands remove specific filters from your incoming messages. For a complete list of message types that you can filter, type @help messagetypes.\n@unfilter <-message_type> - Removes filters on incoming messages of a specific type. For example, the following allows spellcasting text to resume:\n Example: @unfilter -spellcasting\n@unfilter -all - Removes all filters on incoming messages of all types.\n"; + + public const string FillCompsDetail = + "The @fillcomps command assists in the bulk purchase of spell components. It is the sole interface for filling the buy list, which is the column of red zeros to the right in your components panel. To designate which components you would like to buy, change the zeros to the number of each component you would like to buy. The types of components you can buy are scarabs, herbs, powders, potions, and talismans.\n\nThis is the proper syntax: @fillcomps \n\n@fillcomps - Fills the buy list with all of the components that are desired.\n@fillcomps - Fills the buy list with all of the components of the given type.\n@fillcomps - Fills the buy list with all of the components until the total price of the components exceeds the given value.\n@fillcomps - Fills the buy list with all of the components of the given type until the total price of components exceeds the given value.\n@fillcomps clear - Sets the requested amount for all components to zero.\n"; + + public const string EnduranceDetail = + "The endurance attribute has a number of abilities tied to it.\nFirst, some combination of strength and endurance (with endurance being more important) now allows one to regenerate hit points at a faster rate the higher one's endurance is. This bonus is in addition to any regeneration spells one may have placed upon themselves. This endurance regeneration bonus caps at around 110%.\nSecond, the higher a player's Endurance, the less stamina one uses while attacking. This benefit is tied to Endurance only, and it caps out at around 50% less stamina used per attack. The minimum stamina used per attack remains one.\nThird, the higher a player's Endurance, the more likely they are not to use a point of stamina to successfully evade a missile or melee attack. A player is required to have Melee Defense for melee attacks or Missile Defense for missile attacks trained or specialized in order for this specific ability to work. This benefit is tied to Endurance only, and it caps out at around a 75% chance to avoid losing a point of stamina per successful evasion.\nFourth, some combination of strength and endurance (the two are roughly of equivalent importance) now allows one to partially resist drain and harm attacks, up to a maximum of roughly 50%.\nFifth, some combination of strength and endurance (the two are roughly of equivalent importance) now allows one to have a level of \"natural resistances\" to the 7 damage types, the same as a certain level of life protections. This caps out at a 50% resistance (the equivalent to level 5 life prots) to these damage types. This resistance is not additive to life protections: higher level life protections will overwrite these natural resistances, although life vulns will take these natural resistances into account, if the player does not have a higher level life protection cast upon him.\nThe natural resistances, drain resistances, and regeneration rate info are now visible on the Character Information Panel, in what was once the Burden panel. This panel now displays the above three Endurance benefits, the burden info, as well as information about your age, birth date, and number of deaths.\nThe 5 categories for the endurance benefits are, in order from lowest benefit to highest: Poor, Mediocre, Hardy, Resilient, and Indomitable, with each range of benefits divided up equally amongst the 5 (e.g. Poor describes having anywhere from 1-10% resistance against drain health attacks, etc.).\n"; + + public const string SpeakerDetail = + "@speaker - No longer used, see @allegiance officer for a similar command.\n"; + + public const string TitleDetail = + "@title - Sets the title of the popup chat window.\n"; + + public const string ChatToggleDetail = + "@chat - Sets whether or not you receive normal chat. When set to \"off\", you will no longer receive any spoken speech (normal chat). However, you will still receive tells.\n"; + + public const string NoTellDetail = + "@notell - Sets whether or not you receive @tells. When set to \"on\", you will not receive any tells.\n"; + + public const string JoinChatDetail = + "@join - Allows you to hear and speak on the given channel.\n"; + + public const string LeaveChatDetail = + "@leave - Prevents you from hearing or speaking on the given channel.\n"; + + public const string PermitDetail = + "The @permit command gives or revokes corpse-looting permissions to other players. You can permit other players to loot any one of your corpses. You may not @permit a player again until he or she has looted your corpse. Permissions expire either after one hour or when the permitted player logs off. If you were killed by a player killer, no one can loot your corpse except you or your killer, even if you give someone else permission.\n@permit add - Allows another player to loot your corpse.\n@permit remove - Removes permission to access your corpse from the named character.\nType @help consent for more details on corpse looting.\n"; + + public const string HslistDetail = + "@hslist - Lists the number and, if appropriate, positions of houses currently available for purchase. Types include: Apartment, Cottage, Villa, Mansion\n"; + + private static readonly FrozenDictionary CatalogVerbDetailByVerb = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["lifestone"] = LifestoneDetail, + ["lif"] = LifestoneDetail, + ["ls"] = LifestoneDetail, + ["marketplace"] = MarketplaceDetail, + ["mar"] = MarketplaceDetail, + ["mp"] = MarketplaceDetail, + ["pkarena"] = PkArenaDetail, + ["pka"] = PkArenaDetail, + ["pklarena"] = PkLiteArenaDetail, + ["pla"] = PkLiteArenaDetail, + ["pklite"] = PkLiteDetail, + ["pkl"] = PkLiteDetail, + // hor/hr/hom/hoa share HelpHouse with "house"/"hou" — see the + // CmdHashData note in the class remarks above. + ["hor"] = HouseOverview, + ["hr"] = HouseOverview, + ["hom"] = HouseOverview, + ["hoa"] = HouseOverview, + ["age"] = AgeDetail, + ["birth"] = BirthDetail, + ["framerate"] = FrameRateDetail, + ["lockui"] = LockUiDetail, + ["version"] = VersionDetail, + ["loc"] = LocDetail, + ["corpse"] = CorpseDetail, + ["cor"] = CorpseDetail, + ["die"] = DieDetail, + ["clear"] = ClearDetail, + ["saveui"] = SaveUiDetail, + ["loadui"] = LoadUiDetail, + ["saveautoui"] = SaveAutoUiDetail, + ["loadautoui"] = LoadAutoUiDetail, + ["afk"] = AfkDetail, + ["consent"] = ConsentDetail, + ["e"] = EmoteDetail, + ["em"] = EmoteDetail, + ["emote"] = EmoteDetail, + ["me"] = EmoteDetail, + ["emotes"] = EmoteListDetail, + // friends/friends_add/friends_remove share HelpFriends — see the + // CmdHashData note in the class remarks above. + ["friends"] = FriendsDetail, + ["friends_add"] = FriendsDetail, + ["friends_remove"] = FriendsDetail, + // squelch/unsquelch share HelpSquelch (and its own concatenated + // HelpAdvancedSquelch/HelpAdvancedUnSquelch delegation) — see + // the CmdHashData note in the class remarks above. + ["squelch"] = SquelchDetail, + ["unsquelch"] = SquelchDetail, + ["filter"] = FilterDetail, + ["unfilter"] = UnfilterDetail, + // messagetypes/message_types/msgtypes/msg_types intentionally + // absent — HelpMessageTypes builds its text from a live enum + // table at runtime (GetListofSquelchChannels), not a static + // string; see the class remarks above. + ["fillcomps"] = FillCompsDetail, + ["endurance"] = EnduranceDetail, + ["speaker"] = SpeakerDetail, + ["title"] = TitleDetail, + ["chat"] = ChatToggleDetail, + ["notell"] = NoTellDetail, + ["join"] = JoinChatDetail, + ["leave"] = LeaveChatDetail, + ["permit"] = PermitDetail, + ["hslist"] = HslistDetail, + // alh/ah share HelpAllegiance with "allegiance"/"all" — see the + // CmdHashData note in the class remarks above. + ["alh"] = AllegianceOverview, + ["ah"] = AllegianceOverview, + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Retail-registered catalog leaf verbs confirmed (via the CmdHashData + /// help-pointer note in the class remarks above) to register with a + /// NULL help function pointer. Retail's own DoHelp skips its + /// help-callback branch entirely for these and falls straight to the + /// generic text — the SAME text an + /// unregistered verb gets — even though the verb dispatches normally + /// for ordinary (non-help) use. ChatCommandRouter must check + /// this BEFORE consulting either + /// or the catalog's own summary, or it would show acdream-invented + /// help text retail never displays. + /// + public static readonly FrozenSet CatalogVerbsWithNoRetailHelp = + new HashSet(StringComparer.OrdinalIgnoreCase) { "index", "clist", "on", "off" } + .ToFrozenSet(StringComparer.OrdinalIgnoreCase); + + /// + /// Retail Detail_HelpType(2) text for a + /// leaf verb — see the class remarks' "Catalog leaf-verb Detail + /// extraction" section for the recovery method and coverage count. + /// Callers should check + /// FIRST (a confirmed-null verb must never reach this lookup, since a + /// stale entry here would silently show text retail never displays for + /// it), then this method, then fall back to + /// for the + /// verbs not yet extracted. + /// + public static bool TryGetCatalogVerbDetailText(string verb, out string detailText) => + CatalogVerbDetailByVerb.TryGetValue(verb.TrimEnd(','), out detailText!); + private static readonly FrozenDictionary ByVerb = new Dictionary(StringComparer.OrdinalIgnoreCase) { diff --git a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs index 1c8e2c92..37a08346 100644 --- a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs @@ -13,6 +13,7 @@ using AcDream.Core.Items; using AcDream.Core.Spells; using AcDream.Runtime; using AcDream.Runtime.Gameplay; +using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.App.Tests.Composition; @@ -149,6 +150,43 @@ public sealed class InteractionRetainedUiCompositionTests ], fixture.Factory.Releases); } + /// + /// Consolidated-review round (2026-08-10), SHOULD-FIX 2: the a5a7eb4f + /// defect class — a composed hook + /// wired but never transferred to production — had no test, because + /// above substitutes a + /// for the composition-order + /// tests and never exercises the REAL wiring in + /// . + /// This test calls that production method directly (it only touches + /// , no + /// GPU/dat/UiHost dependencies, so the existing null-heavy + /// dependencies are sufficient without a full + /// ) and proves the hook both EXISTS and + /// actually reaches , + /// not merely that it was assigned. + /// + [Fact] + public void ComposedChatViewModelWiresOnInterfaceTextToSpewBox() + { + using var fixture = new Fixture(retailUi: true); + + ChatVM chat = RetailInteractionRetainedUiCompositionFactory + .CreateChatViewModel(fixture.Dependencies); + + Assert.NotNull(chat.OnInterfaceText); + + const string probeText = "consolidated-review SHOULD-FIX 2 probe"; + chat.ShowInterfaceText(probeText); + // ShowInterfaceText only enqueues; SpewBoxState decouples enqueue + // from visibility (see its own class remarks) -- Tick drains it. + fixture.Dependencies.Runtime.CommunicationOwner.SpewBox.Tick(0); + + Assert.Contains( + fixture.Dependencies.Runtime.CommunicationOwner.SpewBox.Snapshot(), + entry => entry.Text == probeText); + } + [Fact] public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody() { diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs index 1a326e9c..c823e0cc 100644 --- a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs @@ -1558,14 +1558,32 @@ public sealed class HeadlessSessionHostTests /// a DIFFERENT landblock, the same shape as one of the OTHER eight /// landblocks in production's real 3x3 publication plan — proving Step /// 3a's gate covers "an admission is open ANYWHERE in the plan", not - /// just the center. Must FAIL on the pre-Step-3a tree — verified by - /// temporarily reverting the three IsQuiescent gates in - /// HeadlessSessionWorldProjection.ProjectSpawn/ - /// ProjectPosition/PumpFirstEntry and confirming this test - /// fails (the conductor reaches PublicationCommitted — a non-null - /// dormant controller — while the unrelated admission is still held - /// open, since driving is unconditional pre-fix). + /// just the center. /// + /// + /// Consolidated-review round (2026-08-10), NIT (c): what THIS bounded + /// xunit test proves is the GATE, not the #365 hydration stall's + /// closure — that broader claim's evidence is the separate live-ACE + /// end-to-end run recorded in + /// docs/research/2026-08-10-365-headless-hydration-diagnosis.md + /// §8 ("hydration now succeeds ... entityCount reaches 136"). This + /// test's own scope is narrower and fully mechanical: the conductor + /// stays undriven (runtime.MovementOwner.Controller stays null) + /// for as long as the unrelated admission is held open, and drives and + /// reaches IsRuntimePublished within a bounded tick budget once + /// it releases. Temporarily reverting the three IsQuiescent + /// gates in HeadlessSessionWorldProjection.ProjectSpawn/ + /// ProjectPosition/PumpFirstEntry (never committed — see + /// §8's "Verification that the new test... actually discriminates") + /// makes this test fail the SAME way §8 itself observed: the + /// Assert.Null(runtime.MovementOwner.Controller) checks below + /// fail because the controller is ALREADY built AND published + /// (CanExecuteLiveMovement = True) while the unrelated admission + /// is still open — not merely "reached PublicationCommitted, + /// still dormant" as an earlier draft of this comment claimed; that + /// wording described a weaker intermediate state than what reverting + /// the gate actually produces. + /// [Fact] public void RealAdmissionNeverDrivesTheConductorWhileOpenAndHydratesOnceReleased() { diff --git a/tests/AcDream.Headless.Tests/HeadlessStaticStateAuditTests.cs b/tests/AcDream.Headless.Tests/HeadlessStaticStateAuditTests.cs index 19d3dba1..fbbe8785 100644 --- a/tests/AcDream.Headless.Tests/HeadlessStaticStateAuditTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessStaticStateAuditTests.cs @@ -1,5 +1,6 @@ using AcDream.Core.Physics; using AcDream.Headless.Configuration; +using AcDream.Headless.Diagnostics; using AcDream.Headless.Hosting; namespace AcDream.Headless.Tests; @@ -8,9 +9,13 @@ namespace AcDream.Headless.Tests; /// #365 Step 1: the audit's refusal rationale is multi-root attribution /// ambiguity, which does not hold for a process that owns exactly one /// session. These tests mutate process- -/// global probe flags and swap , so they run in -/// their own non-parallel collection — see -/// . +/// global probe flags, so they run in their own non-parallel collection — +/// see . Consolidated- +/// review round (2026-08-10), NIT (a): the single-session log line now +/// routes through the injected +/// instead of , so these tests capture its +/// backing directly rather than swapping the +/// process-global console. /// [CollectionDefinition( HeadlessStaticStateAuditCollection.Name, @@ -31,17 +36,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable public void SingleSessionWithProbeEnabledIsAllowedAndLoggedLoudly() { PhysicsDiagnostics.ProbeParkEnabled = true; - var originalOut = Console.Out; using var captured = new StringWriter(); - Console.SetOut(captured); - try - { - HeadlessStaticStateAudit.ValidateProcessIsolation(sessionCount: 1); - } - finally - { - Console.SetOut(originalOut); - } + var diagnostics = new HeadlessDiagnosticWriter(captured); + + HeadlessStaticStateAudit.ValidateProcessIsolation( + sessionCount: 1, diagnostics); Assert.Contains( nameof(PhysicsDiagnostics.ProbeParkEnabled), @@ -52,17 +51,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable [Fact] public void SingleSessionWithNoProbesEnabledIsSilent() { - var originalOut = Console.Out; using var captured = new StringWriter(); - Console.SetOut(captured); - try - { - HeadlessStaticStateAudit.ValidateProcessIsolation(sessionCount: 1); - } - finally - { - Console.SetOut(originalOut); - } + var diagnostics = new HeadlessDiagnosticWriter(captured); + + HeadlessStaticStateAudit.ValidateProcessIsolation( + sessionCount: 1, diagnostics); Assert.Equal(string.Empty, captured.ToString()); } @@ -71,11 +64,12 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable public void MultiSessionWithProbeEnabledStillThrowsNamingTheProbe() { PhysicsDiagnostics.ProbeParkEnabled = true; + var diagnostics = new HeadlessDiagnosticWriter(TextWriter.Null); HeadlessConfigurationException exception = Assert.Throws< HeadlessConfigurationException>( () => HeadlessStaticStateAudit.ValidateProcessIsolation( - sessionCount: 2)); + sessionCount: 2, diagnostics)); Assert.Contains( nameof(PhysicsDiagnostics.ProbeParkEnabled), @@ -86,9 +80,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable [Fact] public void MultiSessionWithNoProbesEnabledIsAllowed() { + var diagnostics = new HeadlessDiagnosticWriter(TextWriter.Null); + Exception? exception = Record.Exception( () => HeadlessStaticStateAudit.ValidateProcessIsolation( - sessionCount: 3)); + sessionCount: 3, diagnostics)); Assert.Null(exception); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs index d7d66450..6e71fc89 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs @@ -93,20 +93,23 @@ public class ChatCommandRouterTests } [Fact] - public void LifestoneWithArguments_ShowsRetailBadArgsRefusal_ViaInterfaceTextSeam() + public void LifestoneWithArguments_ShowsRetailsBespokeRefusal_ViaInterfaceTextSeam() { - // #363 / register row AP-183: a Definition with no bespoke - // InvalidArgumentsText falls to retail's own generic bad-args - // fallback (HandleFailureEvent(0x26), "That is not a valid - // command.") at 0x1A ClientLocal — never the acdream-invented - // "Usage: /lifestone" line this used to synthesize. + // Consolidated-review round (2026-08-10), SHOULD-FIX 3: retires + // register row AP-113. DoLifestone prints ITS OWN 0x1A string for + // bad args and returns 1 -- retail never reaches the generic + // HandleFailureEvent(0x26) fallback for it. Byte-recovered from the + // PDB-paired acclient.exe (Binary Ninja mis-attributes the literal + // to an unrelated vtable-slot symbol). var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink(); var outcome = ChatCommandRouter.Submit("/ls now", vm, bus, ChatChannelKind.Say); Assert.Equal(SubmitOutcome.ClientHandled, outcome); Assert.Empty(bus.Published); - Assert.Equal("That is not a valid command.", Assert.Single(interfaceTexts)); + Assert.Equal( + "Please see @help lifestone for more information on how to use this command.", + Assert.Single(interfaceTexts)); Assert.Empty(log.Snapshot()); } @@ -122,10 +125,51 @@ public class ChatCommandRouterTests Assert.Equal(SubmitOutcome.ClientHandled, outcome); var entry = Assert.Single(log.Snapshot()); - Assert.Equal("That is not a valid command.", entry.Text); + Assert.Equal( + "Please see @help lifestone for more information on how to use this command.", + entry.Text); Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType); } + [Fact] + public void PkArenaWithArguments_ShowsRetailsGenericBadArgsFallback_ViaInterfaceTextSeam() + { + // #363 / register row AP-183: a Definition with no bespoke + // InvalidArgumentsText (PkArena never had one, unlike Lifestone/ + // Marketplace above) falls to retail's own generic bad-args + // fallback (HandleFailureEvent(0x26), "That is not a valid + // command.") at 0x1A ClientLocal -- never the acdream-invented + // "Usage: /pkarena" line this used to synthesize. Carries the + // generic-fallback coverage the two Lifestone tests above used to + // own before Lifestone got its own bespoke text. + var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink(); + + var outcome = ChatCommandRouter.Submit("/pka now", vm, bus, ChatChannelKind.Say); + + Assert.Equal(SubmitOutcome.ClientHandled, outcome); + Assert.Empty(bus.Published); + Assert.Equal("That is not a valid command.", Assert.Single(interfaceTexts)); + Assert.Empty(log.Snapshot()); + } + + [Fact] + public void MarketplaceWithArguments_ShowsRetailsBespokeRefusal_ViaInterfaceTextSeam() + { + // Same methodology and same vtable-mislabeling artifact as + // Lifestone above -- see RetailClientCommandCatalog.Marketplace's + // own citation. + var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink(); + + var outcome = ChatCommandRouter.Submit("/mar now", vm, bus, ChatChannelKind.Say); + + Assert.Equal(SubmitOutcome.ClientHandled, outcome); + Assert.Empty(bus.Published); + Assert.Equal( + "Please see @help marketplace for more information on how to use this command.", + Assert.Single(interfaceTexts)); + Assert.Empty(log.Snapshot()); + } + [Fact] public void PkLiteAlias_ResolvesAsClientHandled_NotTheServerTextPath() { diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs index 37f0279d..0a5e9945 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs @@ -255,6 +255,182 @@ public sealed class RetailCommandHelpTableTests entries[1].Text); } + // ── Consolidated-review round (2026-08-10), SHOULD-FIX 1 ──────────── + // Catalog leaf-verb Detail extraction: /help die and /help lifestone + // pinned byte-exact through the router (the actual user-visible + // outcome, not just the table's stored string — same pattern as + // DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape above), + // plus a coverage-count assertion so a future extraction pass (or an + // accidental regression that silently drops an override) is caught. + + [Fact] + public void HelpDie_ThroughRouter_PrintsRetailsExactDetailText() + { + // HelpDie @0x005784a0: Summary @0x005784ad ("Kills your character + // and leaves a corpse..."), Detail @0x005784b4 (the long paragraph + // below) -- DoHelp always calls Detail_HelpType(2) for a resolved + // verb, never Summary. Byte-swept against the PDB-paired + // C:\Users\erikn\Downloads\acclient.exe (verified MATCH). + var log = new AcDream.Core.Chat.ChatLog(); + var vm = new ChatVM(log, displayLimit: 50); + var bus = new RecordingCommandBus(); + + var outcome = ChatCommandRouter.Submit( + "/help die", 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.DieDetail, + entries[1].Text); + Assert.Equal( + "@die - If you wish to kill your character and leave a corpse, " + + "you may use the @die command. This will result in your " + + "character's death, you will leave behind a corpse with some " + + "of your items, and you will appear at your lifestone. If " + + "you wish to travel to your lifestone without leaving behind " + + "a corpse, you may use the @lifestone command.\n", + RetailCommandHelpTable.DieDetail); + } + + [Fact] + public void HelpLifestone_ThroughRouter_PrintsRetailsExactDetailText() + { + // HelpLifestone @0x00578500 is UNCONDITIONAL (no Summary/Detail + // branch at all) -- the same string prints for every HelpType, + // confirmed by reading the decompiled body directly (no if/else). + var log = new AcDream.Core.Chat.ChatLog(); + var vm = new ChatVM(log, displayLimit: 50); + var bus = new RecordingCommandBus(); + + var outcome = ChatCommandRouter.Submit( + "/help lifestone", 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.LifestoneDetail, + entries[1].Text); + Assert.Equal( + "@lifestone - Returns you to the last lifestone you used without killing you.\n", + RetailCommandHelpTable.LifestoneDetail); + } + + [Fact] + public void HelpAlh_RoutesToTheSameHelpAllegianceTextAsHelpAllegiance() + { + // alh/ah are registered with HelpAllegiance as their OWN help + // pointer (confirmed via InitializeCommands's field-store + // registration pattern, not the CmdHashData-constructor pattern) -- + // the exact same function "allegiance"/"all" use, so /help alh must + // show byte-identical text to /help allegiance, not a separate + // (possibly stale) copy. + Assert.True(RetailCommandHelpTable.TryGetCatalogVerbDetailText( + "alh", out string alhText)); + Assert.Equal(RetailCommandHelpTable.AllegianceOverview, alhText); + } + + [Theory] + [InlineData("index")] + [InlineData("clist")] + [InlineData("on")] + [InlineData("off")] + public void ConfirmedNullHelpVerb_ThroughRouter_ShowsUnknownCommandNotCatalogSummary( + string verb) + { + // index/clist/on/off are registered in InitializeCommands with a + // plain `int32_t var_3c_NN = 0;` local -- no preceding Help-pointer + // assignment, unlike every extracted verb above -- confirming a + // genuinely NULL help function pointer. Retail's own DoHelp skips + // its help-callback branch entirely for these and falls to the + // SAME "Unknown command" 0x1A text an unregistered verb gets, even + // though the verb dispatches fine for ordinary (non-help) use. + // Showing the catalog's own invented summary here would be + // retail-inaccurate. + var log = new AcDream.Core.Chat.ChatLog(); + var vm = new ChatVM(log, displayLimit: 50); + var bus = new RecordingCommandBus(); + + var outcome = ChatCommandRouter.Submit( + $"/help {verb}", vm, bus, ChatChannelKind.Say); + + Assert.Equal(SubmitOutcome.ClientHandled, outcome); + var entries = log.Snapshot(); + Assert.Single(entries); + Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text); + Assert.Equal( + (uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal, + entries[0].LogTextType); + } + + [Fact] + public void CatalogLeafVerbCoverage_ExtractedVsConfirmedNullVsUnverified_MatchesConsolidatedReviewCount() + { + // Coverage-count assertion (SHOULD-FIX 1's explicit acceptance + // criterion): dedupe RetailClientCommandCatalog.KnownVerbs down to + // one entry per distinct Definition via its (unique-per-Definition) + // catalog summary text, excluding the two meta-dispatchers + // ("house"/"hou" and "allegiance"/"all" resolve subcommand-by- + // subcommand, not as a single leaf Definition), then classify each + // distinct Definition into exactly one bucket. A future extraction + // pass should bump ExtractedCount down UnverifiedCount and update + // this pin in the same commit; an accidental regression that drops + // an override without updating CatalogVerbsWithNoRetailHelp would + // fail this test instead of silently reverting to a summary. + var seenSummaries = new HashSet(StringComparer.Ordinal); + int extractedCount = 0; + int confirmedNullCount = 0; + int unverifiedCount = 0; + + foreach (string verb in RetailClientCommandCatalog.KnownVerbs) + { + if (verb is "house" or "hou" or "allegiance" or "all") + continue; + + Assert.True( + RetailClientCommandCatalog.TryGetHelpText(verb, out string summary), + $"catalog verb '{verb}' has no summary text"); + if (!seenSummaries.Add(summary)) + continue; // an alias of a Definition already counted + + bool confirmedNull = RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp + .Contains(verb); + bool extracted = RetailCommandHelpTable.TryGetCatalogVerbDetailText( + verb, out _); + + // A verb must never be BOTH confirmed-null AND carry an + // extracted override -- that would mean ChatCommandRouter's + // ordering (confirmed-null checked first) is hiding a real + // Detail text, or the two tables disagree about the same verb. + Assert.False( + confirmedNull && extracted, + $"'{verb}' is both confirmed-null and has an extracted Detail override"); + + if (confirmedNull) + confirmedNullCount++; + else if (extracted) + extractedCount++; + else + unverifiedCount++; + } + + // Measured, not hand-counted -- 47 distinct leaf Definitions total + // (squelch/unsquelch and friends/friends_add/friends_remove each + // count separately here since they are DISTINCT Definitions with + // their own summary text, even though several share ONE extracted + // Detail text -- see the CmdHashData note in RetailCommandHelpTable's + // class remarks). + Assert.Equal(42, extractedCount); + Assert.Equal(4, confirmedNullCount); + Assert.Equal(1, unverifiedCount); + } + private sealed class RecordingCommandBus : ICommandBus { public List Published { get; } = new();