diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index b1db5639..480cc674 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -1159,10 +1159,14 @@ therefore remains an OH6 portal-fan approximation. The current ordinary-part classifier is a materially aligned candidate and OH4 must protect it while removing only contradictory view/admission residue. -The walk's visited EnvCells and landscape land cells still form different -typed inputs. Outdoor particle, light, and shadow consumers must borrow the -landscape half rather than infer visibility from aggregate owner ids; OH8 owns -that final consumer cutover. +The walk's visited EnvCells and landscape land cells form different typed +inputs. Campaign OVERHAUL S5-c1 made particle update borrow the exact completed +landscape half for `CLandCell::IsInView`, while EnvCell owners use retail's +constant-true `CEnvCell::IsInView` virtual. Point lights remain a separate +resident-registry product and consume neither walk half. Null-root safety +frames publish no cell-visibility answer; the former terrain frustum/AABB +reconstruction and last-frame point-light feedback seam are deleted. +Directional shadows remain the next typed landscape consumer (S5-c2). WorldBuilder contributes DAT decoding, mesh preparation, residency, and Vulkan batch mechanics only; it makes no visibility or ordering decision. Forced merge diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index bf875870..dc1d1693 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -150,7 +150,7 @@ readiness/requeue adaptation. See | AD-18 | Aperture far-Z punch is two-pass stencil-gated with an invented mark bias: 0.0005 NDC capped to a 0.5 m EYE-SPACE span (`MarkBiasNdc`); retail's single DEPTHTEST_ALWAYS punch is safe only under painter's far→near order we don't have | `src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs:149` | **#117** (2026-06-11): the unconditional punch erased nearer occluders, painting interiors through them; the two-pass form is the z-buffered equivalent of retail's ordering safety. **#129** (2026-06-12): the constant-NDC bias spanned ~190 m of eye depth at a landblock (non-linear depth) → distant occluders punched; the eye-space cap bounds the reach (`Issue129PunchBiasTests`). DO-NOT-RETRY: punch must stay depth-gated (ISSUES #108) | Door-plane-hugging geometry beyond the 0.5 m cap re-occludes the aperture (a **#108**-class regression at >10 m viewing range); an occluder within the cap in front of a distant aperture still punches through | `D3DPolyRender::DrawPortalPolyInternal` 0x0059bc90 (maxZ1=7 / maxZ2=6) | | AD-19 | Under outdoor roots, ALL dynamics draw in one z-buffered final pass; retail draws objects painter-ordered per landcell inside the landscape pass (interior roots route per **#118**) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs:126` | The dynamics-drawn-LAST invariant is what makes the aperture depth punch safe (first BR-2 attempt punched after dynamics and erased the player, reverted `88be519`); z-buffer substitutes for painter's order on opaque geometry | Punch/seal correctness hinges on an ordering invariant — any pass added after DrawDynamicsLast, or alpha content needing painter order, gets erased inside apertures or composites wrong | `LScape::draw` → `DrawBlock` 0x005a17c0 → DrawSortCell pc:430124; `PView::DrawCells` 0x005a4840 | | AD-20 | Camera sweep fallback seeds the eye's `AdjustPosition` from the PLAYER's cell; retail re-seats at the sought eye's own tracked cell (rest of function is a verbatim `update_viewer` port) | `src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:97` | acdream's camera doesn't track the sought-eye's cell separately; the eye is near the player so the player-cell stab list is assumed to cover it | An eye outside the player cell's stab-list coverage (boundary corners, cross-landblock pull-back) seats in the wrong cell — and the viewer cell roots the whole render: one-frame wrong root (flap-class flash) | `SmartBox::update_viewer` 0x00453ce0, pc:92878-92883 | -| AD-21 | Null-clipRoot legacy outdoor safety path (no portal visibility, no punches/seals, no-clip terrain) for pre-spawn / login / legacy cameras; in-world retail always has a viewer_cell root | `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameRootSource`, `WorldRenderFrame.ClipRoot`); `src/AcDream.App/Rendering/WorldSceneRenderer.cs` (null-root safety draw) | Result is null ONLY when neither an interior root nor the synthetic outdoor node exists; kept so the login screen shows the live sky | If viewer-root resolution ever returns null in-world (membership bug, fly-camera edge), the frame silently degrades — interiors stop drawing through doorways; the old two-branch FLAP reappears for those frames | `SmartBox::RenderNormalMode` decomp:92635 | +| AD-21 | Null-clipRoot legacy outdoor safety path (no portal visibility, no punches/seals, no-clip terrain) for debug/streaming-gap/legacy cameras; in-world retail always has a `viewer_cell` root | `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameRootSource`, `WorldRenderFrame.ClipRoot`); `src/AcDream.App/Rendering/WorldSceneRenderer.cs` (null-root safety draw) | Result is null ONLY when neither an interior root nor the synthetic outdoor node exists. Login is portal-covered and draws no live sky/world; a safety draw may render resident flat terrain/entities but publishes no cell-visibility answer | If viewer-root resolution ever returns null in-world (membership bug, fly-camera edge), the frame silently degrades — interiors stop drawing through doorways, outdoor particles fail closed, and the old two-branch FLAP reappears for that frame | `SmartBox::RenderNormalMode` decomp:92635; Campaign OVERHAUL S5-c1 | | AD-22 | Async streamed mesh loading with bounded CPU replay residency, per-frame upload budgets, and point-of-use self-heal (`EnsureLoaded` re-request in the dispatcher's mesh-missing path, **#128**); retail loads synchronously — geometry is never absent | `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadCaches.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadFrameBudget.cs` | Immutable preparation descriptors and the bounded CPU cache can re-stage an evicted mesh; dispatcher self-heal makes absence transient while upload budgets prevent a portal arrival from monopolizing a frame | A future consumer that neither retains an owner nor reaches the self-heal/replay path can remain invisible; under heavy admission pressure a valid mesh can pop in later than retail's synchronous path | retail synchronous content load; `docs/architecture/worldbuilder-inventory.md` portal-readiness and bounded-residency seams | | ~~AD-23~~ | **RETIRED 2026-09-03 at Campaign OVERHAUL v2 S3 review fix round 1 (`6575cfcee`).** The mechanism this row described — `ClipSlotCull` of live entities with `ServerGuid != 0` and null `ParentCellId` "while indoor clip routing is active" — is deleted with the per-cell clip routing itself (`SetClipRouting`, `_clipRoutingActive`, `ResolveSlotForFrame`'s routing arm): the routing had ZERO callers since the walk cutover, so the culling never ran, and the walk admits every part by retail's sphere test (`DrawMeshInternal` @0x0059f360) with cell membership owned by the S2 graph. Historical text: live entities with `ServerGuid != 0` and null `ParentCellId` were culled while indoor clip routing was active; retail objects are always cell-resident (synchronous add-to-cell at creation) | (deleted) formerly `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` `ResolveSlotForFrame`/`ClipSlotCull` | Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, *given membership resolves promptly* | An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight | retail per-cell object lists in PView traversal | | AD-24 | EnvCell shell geometry content-deduplicated and instanced; retail draws each CEnvCell's own structure directly | `src/AcDream.Core/Rendering/Wb/EnvCellGeometryIdentity.cs` | Phase A8 retained WB's 31× hash; the 2026-07-24 full-DAT gate proved a real collision (`0x00030175`/`0x01BC0105`), so App+Bake now share a namespaced FNV-1a tuple identity and the bake rejects any full-tuple collision | A future collision outside the installed full-DAT gate could still merge different shells at runtime; the stronger 59-bit payload makes this extremely unlikely, and every bake fails loudly rather than publishing it | retail `PView::DrawCells` → per-cell drawing_bsp (cited at the former renderer `:319`) | @@ -220,7 +220,7 @@ readiness/requeue adaptation. See --- -## 3. Documented approximation (AP) — 162 active rows (AP-241/AP-242/AP-243 filed 2026-09-04 at S4-c3a; recounted honestly 2026-09-04 at the S4-c2 fix round 1 — the prior "161" was stale; AP-240 filed 2026-09-04 at the S4-c2 fix round 1 (M2) — clip-mapped GfxObj/scenery/building subsets draw immediately outside retail's CLIP FIFO, `WbDrawDispatcher.IsOpaque` filtering them out before they ever reach the alpha queue; AP-34 RETIRED 2026-09-04 at the S4-c2 fix round 1 — its residuals continue as the renumbered AP-238/AP-239 (both were filed as the colliding/dangling "AP-236"/"AP-237"/"AP-236b" ids the S4-c2 chunk 2 commit introduced; ids are never reused, so the fix round renumbered them and restored AP-34's own row as struck-through rather than deleted outright — see A2/M8c); AP-234 RETIRED 2026-09-02 at OH2/S1 chunk B — retail's exact CellStruct surface/subset construction (`CellStructSideCandidates`, source-surface-index subsets, and the built-EnvCell `(Surface.Type & 6) != 0` admission test) replaces the NoPos-based approximation, ported from `D3DPolyRender::ConstructMesh` @0x0059DFA0, `RenderDeviceD3D::DrawEnvCell` @0x0059F170, and `D3DPolyRender::DrawMesh` @0x0059D4A0 (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md), with chunk-A/B test evidence; AP-202 RETIRED 2026-08-26 by #446 — retail PFile `.keymap` Load/Save/startup/shutdown persistence is now live; AP-235 filed 2026-08-25 at the Campaign CT4 fix round — `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# switches instead of a live `EnumMapper` read; AP-233 filed 2026-08-23 at the Holtburg windmill fix — the render-side inter-frame animation blend, now holding the boundary frame at every seam; AP-232 filed 2026-08-22 at Campaign VM VM1 — the #226 two-draw detail blend weight on TRANSLUCENT subsets versus retail's single stage-1 output alpha; AP-185 RETIRED 2026-08-20 — `RetailWindowLockPresentationController` now swaps all eight imported locked/live chrome blocks, hides live-only floating-chat and SmartBox grips, suppresses only the nine-slice grip overlay, and applies the current lock before a late-mounted window's first `OnShown`; the radar's persistent B7/B8 semantic face is pinned against pointer-state clobber and covered by a real-fixture draw cycle; AP-231 filed 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the Skills page formula-connector-text approximation in `ComposeFormula`, see the row's own text for the full disclosure of what is byte-verified versus best-derived; AP-213 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the remaining flat-list-vs-four-bucket-sorted-model half is now ported: `ChargenSkillDetail`/`ChargenSkillFormula` (Core) thread `SkillBase.MinLevel`/`Description`/`Formula` from the global SkillTable through `ChargenOptions.TryGetSkillDetail` (`ChargenTableReader.Project` populates it, live-DAT-pinned at 38 entries — 23 MinLevel<=1/15 MinLevel==2, matching the Batch F investigation's own recorded finding exactly), and `CharacterCreationSkillsPage` now groups every costable skill into `SkillBucket` (Specialized/Trained/UseableUntrained/UnuseableUntrained, `UpdateSkillEntry`'s own `iMinlevel <= 1` test), sorts each bucket alphabetically (`InsertEntrySorted`'s `wcscmp`, ported as `string.CompareOrdinal`), and builds one `Templates[0]` header row per bucket ahead of that bucket's `Templates[1]` skill rows — `DoSkillRecords`'s own unconditional 4-header-then-populate build order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild — the observable placement matches retail's incremental single-row `InsertEntrySorted` move without reproducing its internal mechanism, a documented and harmless substitution). 3 new fixture tests (`SkillsPage_BucketHeaders_AlwaysBuildAllFour_InRetailOrder`, `SkillsPage_UntrainedSkill_BucketsByMinLevel`, `SkillsPage_AdvancingASkill_MovesItsRowIntoTheNewBucket`) plus 1 new live-DAT test (`InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage`); AP-216/AP-217 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 1 — both rows' STOPPED items are now landed: `CharacterCreationUiController.AppearancePalSetSource`/`AppearanceClothingTableSource`/`AppearancePaletteColorSource` wire a DAT-backed `ChargenAppearanceCatalog` into the Appearance page from `LivePresentationComposition` (mirroring the existing `AppearancePreviewControl` seam), and `UiButton`/`UiDatElement` both gained a per-instance `Tint` property threaded into every existing `DrawSprite` call they make; `CharacterCreationAppearancePage` now sets `Tint` directly on each swatch button and the GradCircle element instead of layering a flat-fill `ChargenSwatchColorTile` overlay on top (that class is deleted) — a genuine multiplicative sprite tint on the widget's OWN authored art, matching retail's `SurfaceWindow::BlitAndColor(..., Blit_Multiply, color)` exactly rather than approximating it with an opaque rectangle. Both fixture test suites (`CharacterCreationAppearancePageSwatchColorTests`, 8 tests) and the live-DAT color pins (`ChargenAppearanceCatalogColorTests`) pass unchanged against the new mechanism; AP-218 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-6) — `gmCGAppearancePage::Update`'s heritage-flavored static Hair/Eyes/Skin spin caption (`ID_CharGen_HairStyle`/`_Eyes`/`_Skin`, Gearknight `GearText_*`, Olthoi/OlthoiAcid `OlthoiText_*`) is now ported verbatim by `RefreshSpinCaptions`, replacing the prior ordinal substitution outright — see AP-215's own rewritten row for what remains open (the icon-thumbnail gap, restated); recount at this same edit: the row count this header carried before Batch B was already one LOW relative to the physical table (Batch A's own ending state: header said 164, the physical table already held 165 rows — verified by direct count against that commit) — a pre-existing drift this edit corrects to the counted total, not an artifact of Batch B's own net change (F12 correction, gate round 1 closeout, 2026-08-16: this note originally said "one high", the inverted direction — the header was UNDER-counting, not over-counting); AP-222 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch B fix (GF-11b) — the Appearance spins' current-part highlight and the Town buttons' Normal-to-white caption swap both port retail's actual mechanism (per-state label color/outline commit off the REQUESTED retail state id, independent of art-media availability — `UiButton.SetPerStateLabelStyle`/`ComputeRequestedStateId`), closing the row's own "not yet resolved which side is wrong" question: NEITHER client's spin ART changes (no Highlight media exists on either), but BOTH clients' spin TEXT does, matching retail's `SetState(1)`/`SetState(6)` property commit exactly (live-DAT-measured 218,167,85 -> 255,221,131, outline off -> on); AP-215 NARROWED the same batch (GF-9) — item 1 (the swatch-selection substitution) is RETIRED now that the real companion-overlay mechanism (`SetColor`'s `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible`) is ported (`CharacterCreationAppearancePage`'s nine `SwatchOverlayIds`), leaving only item 2 (the icon-less style-spin ordinal label) open; AP-230 filed 2026-08-16 at the Campaign CC gate round 1 Batch A fix (GF-13) — the chargen-scoped-vs-general-importer-wide honor split for dat property 0x3B (Invisible: `UIElement::OnSetAttribute` case 8 hides an element), with the general client-wide honor deferred as its own visual gate (docs/ISSUES.md #408, 1,083 elements affected); AP-213 NARROWED the same gate round (GF-5) — the Skills page's click-to-advance/double-click-retreat single-button substitution is RETIRED now that the real per-row `pSkillUpButton`/`pSkillDownButton` arrows are wired to retail's own plain-click dispatch, leaving open only the flat-list-vs-four-bucket-sorted-model half; AP-229 filed 2026-08-16 at the Campaign CC CC7 review-fix round, F1 — the screen-layering divergence: retail's `UIFlow::UseNewMode` destroys/reconstructs the current UI framework on every mode switch where acdream's CC7 keeps both `CharacterManagementUiController` and `CharacterCreationUiController` mounted for the whole lifetime and only reveals/occludes them; AP-228 filed 2026-08-16 at the CC5 re-review residual round (R4) — the Summary listbox's skill-row KEY source, same divergence class as AP-226 filed the same round, a few retail lines away; AP-227 filed 2026-08-16 at the same review-fix round, F9 — an empty Summary name-field commit calls `SetName("")` (clearing the state), where retail's own NUL-inclusive length gate leaves `CharGenState.name` UNCHANGED for that specific case; AP-226 filed 2026-08-16 at the Campaign CC CC5 review-fix round, F11 — the Summary page's DAT-sourced labels versus retail's static `pcProfessions`/`pcGender`/`pcHeritage`/`pcTown` tables, including the non-human-heritage-renders-bare-"Heritage: " retail quirk; AP-225 RETIRED the same round, F6 — the reviewer re-derived `gmCGSummaryPage::ListenToElementMessage @0x0047bf40`'s length check and proved the 32-vs-33 threshold this row flagged as "not fully certain" does NOT exist: the compared length is NUL-inclusive (an empty field's length is 1, matching AP-226's own F11/F9 finding), so `length > 0x21` is EXACTLY `visibleChars > 32` — acdream's `MaxNameLength = 32` was always byte-correct, not merely internally-consistent; AP-223/AP-224 filed 2026-08-15 at Campaign CC slice CC5 — the acdream-only `HeritageOrGenderUnset` Finish refusal and the Summary listbox's two-bucket (Specialized/Trained only) skill-list narrowing (AP-224 corrected 2026-08-16 at the same review-fix round, F3 — its "template mechanism ported exactly" claim was FALSE as shipped, now fixed and true again, see its own row); AP-214 RETIRED the same slice — `RandomizeCharacter` is now ported and wired at the screen-open edge, closing the honest-blank-open gap it recorded; AP-212 NARROWED the same slice — the Appearance/Summary Random-button primitives are now real faithful ports, not uniform-pick approximations, leaving only Heritage/Profession/Town (still uniform-pick) and Skills (still unported) open; AP-222 filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — the current-part spin highlight is a measured no-op for all nine spins, no Highlight media authored on any of them; AP-221 filed the same re-review (R2) — the chargen preview's one-shot-composition-vs-retryable-coordinator binding gap; AP-217 rewritten and AP-220 tightened the same re-review (R3 corrects the GradCircle from a dead click target to unported paint-art; N1 narrows the Gearknight-exit wording to non-Olthoi); AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; AP-203 RETIRED 2026-08-26 by #446 — all 306 installed-DAT rows now have distinct identities and concrete consumers; AP-204 corrected and RETIRED the same day — exact capture/conflict/button semantics now follow named retail; AP-202 RETIRED 2026-08-26 by #446 — retail `.keymap` file interchange and profile lifetime now ship; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) +## 3. Documented approximation (AP) — 161 active rows (AP-117 retired 2026-09-04 at Campaign OVERHAUL S5-c1; AP-241/AP-242/AP-243 filed 2026-09-04 at S4-c3a; recounted honestly 2026-09-04 at the S4-c2 fix round 1 — the prior "161" was stale; AP-240 filed 2026-09-04 at the S4-c2 fix round 1 (M2) — clip-mapped GfxObj/scenery/building subsets draw immediately outside retail's CLIP FIFO, `WbDrawDispatcher.IsOpaque` filtering them out before they ever reach the alpha queue; AP-34 RETIRED 2026-09-04 at the S4-c2 fix round 1 — its residuals continue as the renumbered AP-238/AP-239 (both were filed as the colliding/dangling "AP-236"/"AP-237"/"AP-236b" ids the S4-c2 chunk 2 commit introduced; ids are never reused, so the fix round renumbered them and restored AP-34's own row as struck-through rather than deleted outright — see A2/M8c); AP-234 RETIRED 2026-09-02 at OH2/S1 chunk B — retail's exact CellStruct surface/subset construction (`CellStructSideCandidates`, source-surface-index subsets, and the built-EnvCell `(Surface.Type & 6) != 0` admission test) replaces the NoPos-based approximation, ported from `D3DPolyRender::ConstructMesh` @0x0059DFA0, `RenderDeviceD3D::DrawEnvCell` @0x0059F170, and `D3DPolyRender::DrawMesh` @0x0059D4A0 (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md), with chunk-A/B test evidence; AP-202 RETIRED 2026-08-26 by #446 — retail PFile `.keymap` Load/Save/startup/shutdown persistence is now live; AP-235 filed 2026-08-25 at the Campaign CT4 fix round — `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# switches instead of a live `EnumMapper` read; AP-233 filed 2026-08-23 at the Holtburg windmill fix — the render-side inter-frame animation blend, now holding the boundary frame at every seam; AP-232 filed 2026-08-22 at Campaign VM VM1 — the #226 two-draw detail blend weight on TRANSLUCENT subsets versus retail's single stage-1 output alpha; AP-185 RETIRED 2026-08-20 — `RetailWindowLockPresentationController` now swaps all eight imported locked/live chrome blocks, hides live-only floating-chat and SmartBox grips, suppresses only the nine-slice grip overlay, and applies the current lock before a late-mounted window's first `OnShown`; the radar's persistent B7/B8 semantic face is pinned against pointer-state clobber and covered by a real-fixture draw cycle; AP-231 filed 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the Skills page formula-connector-text approximation in `ComposeFormula`, see the row's own text for the full disclosure of what is byte-verified versus best-derived; AP-213 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the remaining flat-list-vs-four-bucket-sorted-model half is now ported: `ChargenSkillDetail`/`ChargenSkillFormula` (Core) thread `SkillBase.MinLevel`/`Description`/`Formula` from the global SkillTable through `ChargenOptions.TryGetSkillDetail` (`ChargenTableReader.Project` populates it, live-DAT-pinned at 38 entries — 23 MinLevel<=1/15 MinLevel==2, matching the Batch F investigation's own recorded finding exactly), and `CharacterCreationSkillsPage` now groups every costable skill into `SkillBucket` (Specialized/Trained/UseableUntrained/UnuseableUntrained, `UpdateSkillEntry`'s own `iMinlevel <= 1` test), sorts each bucket alphabetically (`InsertEntrySorted`'s `wcscmp`, ported as `string.CompareOrdinal`), and builds one `Templates[0]` header row per bucket ahead of that bucket's `Templates[1]` skill rows — `DoSkillRecords`'s own unconditional 4-header-then-populate build order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild — the observable placement matches retail's incremental single-row `InsertEntrySorted` move without reproducing its internal mechanism, a documented and harmless substitution). 3 new fixture tests (`SkillsPage_BucketHeaders_AlwaysBuildAllFour_InRetailOrder`, `SkillsPage_UntrainedSkill_BucketsByMinLevel`, `SkillsPage_AdvancingASkill_MovesItsRowIntoTheNewBucket`) plus 1 new live-DAT test (`InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage`); AP-216/AP-217 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 1 — both rows' STOPPED items are now landed: `CharacterCreationUiController.AppearancePalSetSource`/`AppearanceClothingTableSource`/`AppearancePaletteColorSource` wire a DAT-backed `ChargenAppearanceCatalog` into the Appearance page from `LivePresentationComposition` (mirroring the existing `AppearancePreviewControl` seam), and `UiButton`/`UiDatElement` both gained a per-instance `Tint` property threaded into every existing `DrawSprite` call they make; `CharacterCreationAppearancePage` now sets `Tint` directly on each swatch button and the GradCircle element instead of layering a flat-fill `ChargenSwatchColorTile` overlay on top (that class is deleted) — a genuine multiplicative sprite tint on the widget's OWN authored art, matching retail's `SurfaceWindow::BlitAndColor(..., Blit_Multiply, color)` exactly rather than approximating it with an opaque rectangle. Both fixture test suites (`CharacterCreationAppearancePageSwatchColorTests`, 8 tests) and the live-DAT color pins (`ChargenAppearanceCatalogColorTests`) pass unchanged against the new mechanism; AP-218 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-6) — `gmCGAppearancePage::Update`'s heritage-flavored static Hair/Eyes/Skin spin caption (`ID_CharGen_HairStyle`/`_Eyes`/`_Skin`, Gearknight `GearText_*`, Olthoi/OlthoiAcid `OlthoiText_*`) is now ported verbatim by `RefreshSpinCaptions`, replacing the prior ordinal substitution outright — see AP-215's own rewritten row for what remains open (the icon-thumbnail gap, restated); recount at this same edit: the row count this header carried before Batch B was already one LOW relative to the physical table (Batch A's own ending state: header said 164, the physical table already held 165 rows — verified by direct count against that commit) — a pre-existing drift this edit corrects to the counted total, not an artifact of Batch B's own net change (F12 correction, gate round 1 closeout, 2026-08-16: this note originally said "one high", the inverted direction — the header was UNDER-counting, not over-counting); AP-222 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch B fix (GF-11b) — the Appearance spins' current-part highlight and the Town buttons' Normal-to-white caption swap both port retail's actual mechanism (per-state label color/outline commit off the REQUESTED retail state id, independent of art-media availability — `UiButton.SetPerStateLabelStyle`/`ComputeRequestedStateId`), closing the row's own "not yet resolved which side is wrong" question: NEITHER client's spin ART changes (no Highlight media exists on either), but BOTH clients' spin TEXT does, matching retail's `SetState(1)`/`SetState(6)` property commit exactly (live-DAT-measured 218,167,85 -> 255,221,131, outline off -> on); AP-215 NARROWED the same batch (GF-9) — item 1 (the swatch-selection substitution) is RETIRED now that the real companion-overlay mechanism (`SetColor`'s `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible`) is ported (`CharacterCreationAppearancePage`'s nine `SwatchOverlayIds`), leaving only item 2 (the icon-less style-spin ordinal label) open; AP-230 filed 2026-08-16 at the Campaign CC gate round 1 Batch A fix (GF-13) — the chargen-scoped-vs-general-importer-wide honor split for dat property 0x3B (Invisible: `UIElement::OnSetAttribute` case 8 hides an element), with the general client-wide honor deferred as its own visual gate (docs/ISSUES.md #408, 1,083 elements affected); AP-213 NARROWED the same gate round (GF-5) — the Skills page's click-to-advance/double-click-retreat single-button substitution is RETIRED now that the real per-row `pSkillUpButton`/`pSkillDownButton` arrows are wired to retail's own plain-click dispatch, leaving open only the flat-list-vs-four-bucket-sorted-model half; AP-229 filed 2026-08-16 at the Campaign CC CC7 review-fix round, F1 — the screen-layering divergence: retail's `UIFlow::UseNewMode` destroys/reconstructs the current UI framework on every mode switch where acdream's CC7 keeps both `CharacterManagementUiController` and `CharacterCreationUiController` mounted for the whole lifetime and only reveals/occludes them; AP-228 filed 2026-08-16 at the CC5 re-review residual round (R4) — the Summary listbox's skill-row KEY source, same divergence class as AP-226 filed the same round, a few retail lines away; AP-227 filed 2026-08-16 at the same review-fix round, F9 — an empty Summary name-field commit calls `SetName("")` (clearing the state), where retail's own NUL-inclusive length gate leaves `CharGenState.name` UNCHANGED for that specific case; AP-226 filed 2026-08-16 at the Campaign CC CC5 review-fix round, F11 — the Summary page's DAT-sourced labels versus retail's static `pcProfessions`/`pcGender`/`pcHeritage`/`pcTown` tables, including the non-human-heritage-renders-bare-"Heritage: " retail quirk; AP-225 RETIRED the same round, F6 — the reviewer re-derived `gmCGSummaryPage::ListenToElementMessage @0x0047bf40`'s length check and proved the 32-vs-33 threshold this row flagged as "not fully certain" does NOT exist: the compared length is NUL-inclusive (an empty field's length is 1, matching AP-226's own F11/F9 finding), so `length > 0x21` is EXACTLY `visibleChars > 32` — acdream's `MaxNameLength = 32` was always byte-correct, not merely internally-consistent; AP-223/AP-224 filed 2026-08-15 at Campaign CC slice CC5 — the acdream-only `HeritageOrGenderUnset` Finish refusal and the Summary listbox's two-bucket (Specialized/Trained only) skill-list narrowing (AP-224 corrected 2026-08-16 at the same review-fix round, F3 — its "template mechanism ported exactly" claim was FALSE as shipped, now fixed and true again, see its own row); AP-214 RETIRED the same slice — `RandomizeCharacter` is now ported and wired at the screen-open edge, closing the honest-blank-open gap it recorded; AP-212 NARROWED the same slice — the Appearance/Summary Random-button primitives are now real faithful ports, not uniform-pick approximations, leaving only Heritage/Profession/Town (still uniform-pick) and Skills (still unported) open; AP-222 filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — the current-part spin highlight is a measured no-op for all nine spins, no Highlight media authored on any of them; AP-221 filed the same re-review (R2) — the chargen preview's one-shot-composition-vs-retryable-coordinator binding gap; AP-217 rewritten and AP-220 tightened the same re-review (R3 corrects the GradCircle from a dead click target to unported paint-art; N1 narrows the Gearknight-exit wording to non-Olthoi); AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; AP-203 RETIRED 2026-08-26 by #446 — all 306 installed-DAT rows now have distinct identities and concrete consumers; AP-204 corrected and RETIRED the same day — exact capture/conflict/button semantics now follow named retail; AP-202 RETIRED 2026-08-26 by #446 — retail `.keymap` file interchange and profile lifetime now ship; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) **2026-08-28 count correction:** #408 retired AP-230 by moving property `0x3B` visibility into the shared importer and every retained stateful widget; @@ -380,7 +380,7 @@ research and is no longer active. | AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables. The adaptation is now structurally limited to replacing Ready/Walk/Run-family states, so authoritative actions/substates (especially Dead) always win. | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`, `CanApplyVelocityCycle`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) | | AP-81 | **NARROWED 2026-08-04 (Bug B). The remote VectorUpdate handler still pre-clears the two ground transients and seeds the client Airborne flag one frame ahead of the sweep.** The GRAVITY half of this row is RETIRED: the handler no longer writes `Body.State |= Gravity`, and neither landing block clears it, so GRAVITY_PS is wire-owned for the object's whole life exactly as retail has it (`CPhysicsObj` constructor state `0x400C08` @0x00512508; `set_state` @0x00514DD0 post-processes only lighting/nodraw/hidden and never masks GRAVITY). The per-tick force this row's sibling sites used to apply is also gone — see the Bug B entry in `docs/ISSUES.md` #32. What remains is the handler's `TransientState &= ~(Contact | OnWalkable)` plus `rm.Airborne = true` on a `Velocity.Z > 0.5f` vector. Retail reaches the identical state one frame later: `check_contact` (0x0050F5B0) fails on the ascending velocity, the transition runs contact-free, `SetPositionInternal` clears CONTACT_TS and `set_on_walkable(0)` fires LeaveGround. The pre-clear is deliberately KEPT because it is what makes the per-tick `set_on_walkable` edge observe `previousOnWalkable == false` and therefore NOT fire a second LeaveGround for the same departure, and because `CMotionInterp::LeaveGround` (0x00528B00) writes `set_local_velocity(GetLeaveGroundVelocity(), autonomous)` — relocating it into the tick would overwrite the authoritative launch vector mid-arc | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`ApplyOrdinaryVector`, the `Velocity.Z > 0.5f` branch) | One frame of head start on a state the sweep derives anyway. Both landing blocks now derive Contact/OnWalkable from the committed contact plane and neither touches the Gravity state bit, so the flag dance no longer decides whether gravity is delivered | A VectorUpdate whose vertical component clears the 0.5 m/s threshold on a body the sweep would still find in contact marks that body airborne one frame early. Retire when the remote departure edge is owned solely by the per-tick `set_on_walkable` commit and LeaveGround's velocity write is ordered after the authoritative vector | `SmartBox::DoVectorUpdate @ 0x004521C0`; `CPhysicsObj::check_contact @ 0x0050F5B0`; `CPhysicsObj::calc_acceleration @ 0x00510950`; `CPhysicsObj::SetPositionInternal @ 0x00515330`; `set_on_walkable @ 0x00511310`; `CMotionInterp::LeaveGround @ 0x00528B00` | | AP-82 | **StickyManager deep-overlap back-off sign pin**: when the stick-gap overlap exceeds one tick's step (`speed×quantum < \|dist\|`, `dist < 0`), acdream applies `delta = −(speed×quantum)` (rate-limited back-off); ACE's literal port keeps `+delta` there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | `src/AcDream.Core/Physics/Motion/StickyManager.cs` (`AdjustOffset` delta clamp; conformance `StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited`) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb `StickyManager::adjust_offset` trace with a forced overlap when convenient | `StickyManager::adjust_offset` 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) | -| AP-85 | **Point-light pool = single 128-cap player-nearest list, optionally FILTERED by LAST FRAME's rendered visible-cell set, vs retail's dual pools (7 dynamic + 40 static, degrade-scaled) collected from a DBObj-load/flush-bounded resident registry** (A7.L1, 2026-07-09 — third revision, Town Network starvation fix #79/#93/#176/#177): retail's `CEnvCell::visible_cell_table` (`add_visible_cell` 0x0052de40) is populated ON DEMAND as cells are approached/seen (`DBObj::Get`-loads) and pruned by `flush_cells` — so a real dungeon's per-frame candidate set stays small (naturally proximity-bounded) even though the collection walk itself (`add_dynamic_lights` 0x0052d410) is "the whole resident table, not a re-flood." acdream's `_all` list instead registers at LANDBLOCK-granularity load/unload (a whole single-landblock dungeon streams as ONE unit), so for the Town Network (463 registered fixtures, one landblock) `_all` is effectively "everything ever loaded in this dungeon," not a proximity-bounded set — wide enough that the player-nearest-128 cap alone let a straight-line-closer-but-wall-disconnected corridor's fixtures out-rank the player's own room, starving it. Fix: `BuildPointLightSnapshot(playerWorldPos, visibleCells)` takes an optional candidacy FILTER — a light joins the pool iff `CellId==0` (cell-less, always in) or `visibleCells.Contains(CellId)` — narrowing candidates to the frame's actual visible cells BEFORE the existing dynamics-first player-nearest cap runs; `GameWindow` feeds LAST FRAME's already-rendered `RetailPViewFrameResult.DrawableCells` back to `WorldRenderFrameBuilder` (one frame / ~16 ms latency, chosen specifically to avoid re-threading a mid-`DrawInside` callback — the exact mechanism, `c500912b`, that caused the #176 seam-floor flicker regression when it re-flooded an independent CAMERA-seeded set mid-frame). The distance-sort anchor stays the PLAYER (unchanged from the prior revision) — only candidacy narrows. Remaining deviation: this is a RENDER-visibility approximation of retail's true on-demand-load/flush RESIDENCY bound, with one frame of latency, not a port of the DBObj-load/flush mechanism itself; and the pool is still ONE 128-cap list vs retail's separate 7-dynamic/40-static degrade-scaled pools | `src/AcDream.Core/Lighting/LightManager.cs` (`BuildPointLightSnapshot`, `MaxGlobalLights`); `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.ObserveDrawableCells`, `ClearDrawableCells`, `Prepare`); pins `PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`, `PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics`, `PointSnapshot_ResidentCollection_CellTagDoesNotFilter`, `BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell`, `BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded` | The render already computes a visible-cell set every frame for drawing (single source of truth, no duplicate flood) — reusing it as a candidacy filter approximates retail's proximity-bounded residency without porting DBObj on-demand load/flush; one-frame latency is imperceptible at normal camera speeds and structurally differs from the reverted mechanism (no independent re-flood mid-frame) | On a portal crossing, the FIRST indoor frame after re-entry (or after any outdoor-only frame) is unscoped (fail-open) — one frame may show slightly wider pool composition than steady-state; a room with >7 resident dynamics still shows them all (retail trims to 7 player-nearest) — slightly purpler wedge than retail; adopt the dual pools + degrade caps + true DBObj-bounded residency in later A7-arc work | `insert_light` 0x0054d1b0 (player-sorted, capped); `add_visible_cell` 0x0052de40 (on-demand-load resident registry + flush); `add_dynamic_lights` 0x0052d410 (whole-table walk); caller 0x00452d30; `calc_point_light` 0x0059c8b0 (static 1/d³ curve — A7 fix #2) | +| AP-85 | **CORRECTED 2026-09-04 at Campaign OVERHAUL S5-c1. Point-light pool = one resident, player-nearest 128-cap list vs retail's separate 7-dynamic/40-static degrade-scaled pools and DBObj-granular residency.** Retail walks `CEnvCell::visible_cell_table`, populated on demand by `add_visible_cell`/`DBObj::Get` and pruned by `flush_cells`. acdream registers lights at broader landblock publication lifetime, then `BuildPointLightSnapshot(playerWorldPos)` selects from that complete resident registry with dynamics-first/player-distance ordering. The stale last-frame drawable-cell feedback description was false: that filter had already become a no-op and S5-c1 deleted its interfaces/calls. | `src/AcDream.Core/Lighting/LightManager.cs` (`BuildPointLightSnapshot`, `MaxGlobalLights`); `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.Prepare`); pins `PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`, `PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics`, `PointSnapshot_ResidentCollection_CellTagDoesNotFilter`, `BuildPointLightSnapshot_UsesAllResidentLights`, `Runtime_environment_uses_all_resident_lights` | The one retained registry avoids a second camera flood and matches retail's resident-table ownership, but acdream's streaming publication granularity does not reproduce DBObj load/flush residency and its RHI contract exposes one 128-entry list | A large one-landblock dungeon may admit lights from farther or wall-disconnected resident rooms that retail's DBObj-granular table would not retain; a room with more than seven resident dynamic lights can keep more than retail | `insert_light` 0x0054d1b0; `add_visible_cell` 0x0052de40; `add_dynamic_lights` 0x0052d410; caller 0x00452d30; `calc_point_light` 0x0059c8b0; Campaign OVERHAUL S5-c1 | | AP-84 | **BSP shadow-shape part poses = motion-table default-state frame snapshot at registration, not retail's live CPhysicsPart pose** (#175): server entities with a wire MotionTableId register their BSP part shapes at the default style's first-cycle LowFrame pose through `LiveEntityDefaultPoseResolver`; retail collision reads each part's CURRENT pose every test. Equivalent for the door lifecycle (closed = default pose; open = ETHEREAL bypasses collision entirely, #150) and for idle statics | `src/AcDream.App/Physics/LiveEntityDefaultPoseResolver.cs`; `src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs`; `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` (`partPoseOverride`) | Registration is one-shot in acdream (retail re-poses parts per frame); the default-state pose is the correct idle pose and the only non-ethereal pose doors ever collide in | An entity whose server-driven motion state materially MOVES a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case — doors are the dominant BSP-part weenies); revisit if animated non-ethereal BSP movers appear | `CPhysicsPart` live pose (see #150 notes); motion-table default state = CPartArray init; ShadowShapeBuilder placement-frame fallback for table-less entities | | AP-83 | **CONTAINED, not dormant (Campaign S S6, 2026-08-07) — the row's 'no current mover sets PerfectClip' premise was FALSE.** The camera (`PhysicsCameraCollisionProbe.SweepEye`, the sole production PerfectClip setter) reaches this tail LIVE: neither `CollisionExemption.ShouldSkip` (creature-only viewer exemption) nor `FindObjCollisionsInCell` (unconditional shadow-list walk) cuts the chain for a non-creature Cyl-shaped shadow entry — a real population (static scenery with an authored primitive and no physics BSP). The tail head now records every reach (`PhysicsDiagnostics.RecordCylPerfectClipTailReach`): viewer movers count camera-live silently; any NON-viewer mover reaching it logs loudly one-shot, so a future flag change cannot exercise this ACE-derived math unreviewed. Four containment tests drive the camera's exact call shape both ways, sabotage-verified on the creature-exemption axis the proof depends on. **Severity narrowed to camera-feel only**: the probe never commits a PhysicsBody, so a wrong TOI can only mispull the spring-arm camera. The math itself remains ACE-derived and the row stays ACTIVE for that reason alone. Original text: **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. The non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified. Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail remains dormant unless a future mover explicitly enables PerfectClip | **Risk restated at S6:** the camera ALREADY reaches this tail — an ACE/retail TOI delta here is a live, currently-unverified camera-feel risk (a prop the camera pulls in slightly off), not a dormant one. If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` | | AP-91 | **CONTAINED, not dormant (Campaign S S6, 2026-08-07) — the row's 'no current mover sets PerfectClip' premise was FALSE.** The camera (`PhysicsCameraCollisionProbe.SweepEye`, the sole production PerfectClip setter) reaches this tail LIVE: neither `CollisionExemption.ShouldSkip` (creature-only viewer exemption) nor `FindObjCollisionsInCell` (unconditional shadow-list walk) cuts the chain for a non-creature Sphere-shaped shadow entry — a real population (static scenery with an authored primitive and no physics BSP). The tail head now records every reach (`PhysicsDiagnostics.RecordSpherePerfectClipTailReach`): viewer movers count camera-live silently; any NON-viewer mover reaching it logs loudly one-shot, so a future flag change cannot exercise this ACE-derived math unreviewed. Four containment tests drive the camera's exact call shape both ways, sabotage-verified on the creature-exemption axis the proof depends on. **Severity narrowed to camera-feel only**: the probe never commits a PhysicsBody, so a wrong TOI can only mispull the spring-arm camera. The math itself remains ACE-derived and the row stays ACTIVE for that reason alone. Original text: **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | **Risk restated at S6:** the camera ALREADY reaches this tail — an ACE/retail TOI delta here is a live, currently-unverified camera-feel risk (a prop the camera pulls in slightly off), not a dormant one. If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` | @@ -423,7 +423,7 @@ research and is no longer active. | ~~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` | | AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` | -| AP-117 | **NARROWED 2026-09-03 at Campaign OVERHAUL v2 S3 chunks 3–4.** In the production walk path the outdoor land-cell in-view state now comes from `WalkLandscape.CheckBlocks`/`LandCellCheck` — the ported `LScape::draw_check_blocks` + `landcell_check` corner-interval tests (`Render::get_clip_height` + `Render::block_check`) — handed to `TerrainModernRenderer` as `inViewLandcells`, so `CollectVisibleCells`' reconstruction (per-cell frustum + `IsAabbVisibleThroughClipRegion` clip-plane test) runs ONLY when no walk product exists (the non-walk path). The scissor-AABB half of that reconstruction's input is gone with chunk 4's scissor deletion (`ndcClipAabb` is no longer produced by any walk consumer). Remaining deviation: the non-walk fallback still reconstructs rather than ports. | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`, the `inViewLandcells is null` branch); `src/AcDream.App/Rendering/Walk/WalkLandscape.cs` (`LandCellCheck`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` | +| ~~AP-117~~ | **RETIRED 2026-09-04 at Campaign OVERHAUL S5-c1.** `RetailPViewFrameResult.VisibleLandscapeCells` now publishes `WalkFrameDriver.VisitedLandscapeCellIds` directly. Outdoor particle update consumes that exact completed product through `CLandCell::IsInView` semantics; EnvCells use the constant-true `CEnvCell::IsInView` virtual. The complete `TerrainModernRenderer.CollectVisibleCells`/`TerrainVisibleCellIds` null-root frustum reconstruction is deleted, so a frame without a walk product publishes no outdoor visibility answer. | `src/AcDream.App/Rendering/RetailPViewRenderer.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs`; `src/AcDream.App/Rendering/TerrainModernRenderer.cs` | — | — | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; CEnvCell ICF virtual body `0x005269F0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` | | AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, a mode that settled without a blocker transition clears immediately; local ACE's observed pre-wield transition plus `ready -> NonCombat`, or post-wield `NonCombat -> ready -> NonCombat`, causes one normal `ChangeCombatMode` request from the trailing notice. Explicit user combat input cancels settlement. Retail's client does not need this extra request against the retail server. | `src/AcDream.App/UI/AutoWieldController.cs`; production binding in `GameWindow.cs` | Local ACE queues a trailing NonCombat callback during primary-weapon replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later explicit combat request, replacement, or logout clears it; peace-mode equips send none | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`; ACE `Player_Inventory.TryShuffleStance` / `TryDequipObjectWithNetworking` | | AP-119 | Equal-generation CreateObject refresh applies the packet's complete `PhysicsDesc` to the existing `EntityEffectProfile`, including replacing its network sound/PES-table/default-script description. Retail's equal-`INSTANCE_TS` branch applies the individual ObjDesc/Parent-or-Position/Movement/State/Vector/WeenieDesc tail and does not call `CPhysicsObj::set_description` again. | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`ILiveEntitySameGenerationUpdateSink.OnDescription`); `LiveEntitySameGenerationUpdateRouter.cs` | Existing spell/projectile/portal VFX tests and connected behavior were accepted with this refresh. Slice 4 records and isolates it rather than silently changing DAT-effect ownership during an architecture extraction. | A same-generation CreateObject whose PeTable/sound/default-script fields differ from the original can replace effect lookup state where retail would retain the original table, producing a different later typed effect. | `SmartBox::HandleCreateObject @ 0x00454C80`; `CPhysicsObj::set_description @ 0x00514F40`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` | | AP-120 | `ObjectTableWiring.ApplyEntitySpawn` publishes the CreateObject's WeenieDesc/item state before the same-generation physics update tail. Retail applies WeenieDesc after ObjDesc, Parent-or-Position/Pickup, Movement, State, and Vector. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (`ApplyEntitySpawn`); `src/AcDream.App/World/LiveEntitySameGenerationUpdateRouter.cs` | Core.Net owns item-table ingestion before App callbacks and the current single-thread FIFO prevents a second network packet from interleaving; changing publication order crosses the Core.Net/App ownership boundary and requires a separately tested event transaction. | A synchronous item-table observer can see the refreshed WeenieDesc while the same object's physics/parent/state still reflects the prior snapshot; retail observers see the completed physics tail first. | `SmartBox::HandleCreateObject @ 0x00454C80`; `ACCObjectMaint::CreateObject @ 0x00558870` | diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md index 07756b9f..a970c575 100644 --- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md +++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md @@ -680,8 +680,8 @@ Update immediately when a slice changes state. Chat is not the ledger. | S4-c2 attempts 1–5 | **ATTEMPT 1/5 CONSUMED; ATTEMPT 2/5 PASSED AND LANDED 2026-09-04** — reviewed implementation `daaeab0ba`; campaign stack `89f1e2676` → `9ccb61a8e` → `0aa166aa0` → `252886e84`; packet §17 | G3/G4 UNPASSED | Sequential retail/evidence then production/gate-honesty PASS. Fresh: Release 0W/0E; shader 32/32; affected 37/37; real allocation 2/2 at 0 B; production 239/239. Official no-retry hermetic 16,734/1: sole unchanged global `Console.Out` capture race, exact theory 2/2 isolated. InstalledDat 255/10 documented/1. Validation routes `selfgate-20260904-115818-s4c2-landed-route` and `selfgate-20260904-120025-s4c2-landed-g3b` PASS provisional. First soak failed on known #461/#462; retry and S4-c1 control both 9/9 graceful; stationary averages CPU p95 −0.01 ms, GPU p95 +0.16 ms, alloc p50 +1.2 KiB, update p95 −0.12 ms. Three attempts remain unused. | | S4-c3a | **LANDED 2026-09-04 after the owner-authorized §21 evidence exception.** Reviewed scratch stack `44e2bc227b` → `b6bf6c131` → `eea5793d2` → `14397b14c` → `359061b82` → `255194e90` → `9cfddf301`; campaign cherry-picks `a86ec73ec` → `8e0c6fb14` → `01674bcc7` → `8bd75ba31` → `b6b015604` → `316193043` → `67c76026e`. The earlier stop remains in history at `06b986622`. | G3/G4 UNPASSED | Final retail lens PASS; production behavior/lifecycle/allocation/scope clean. The narrow independent gate-honesty re-review PASS reproduced the exact first failure (AP-table-boundary `Assert.True`, `rowIndex=-1`, before `Assert.Single`), restored AP-241 byte-identically, and passed the pin 1/1. Fresh campaign: Release 0W/0E; focused class lane 96/96; no-surface 1/1; AP pin 1/1; real allocation 2/2 at 0 B; shader 32/32; `git diff --check` clean. No client launched. NEXT: write c3b's deletion-only contract from the landed code; G3 stays locked until c3b lands. | | S4-c3b | **LANDED 2026-09-04** — implementation `26e97ba41`, provenance contract `5110bf676`, packet-only correction `3f2f00c9f`; packet §22–§25. Dead classic-group `LocalSortCenters`/`CachedBatch.LocalSortCenter` storage and the alpha camera-parameter/digest chain are deleted. Live per-cell/particle CYpt keys, opaque `SortDistance`, building/private/portal distances, two FIFO lists, all state/barriers, and AP/AD rows remain. | **G3 LEAD SELF-GATE PROVISIONAL PASS; owner acceptance/owner-only rows pending. G4 UNPASSED.** | Retail/deletion lens PASS. Production lens found no code defect and one artifact-provenance omission; packet-only fix round 1 passed its narrow re-review. Fresh campaign: Release 0W/0E; App 132/132; Core 29/29; allocation 2/2 at 0 B; shader 32/32. G3: route 1 13/13, route 2 4/4, route 3 retry 3/3, all exit 0/graceful. The first route-3 attempt stopped before Nanto on registered #462. Exact PNG paths: packet §25. | -| S5-c1 | **STOPPED 2026-09-04 after fix round 2; scratch stack not landed.** Packet §9. Implementation `9aadc917a`; comment-only fix rounds `74a2e4c48` and `a28384d80`. Retail lens PASS; production lens found no code/test defect but FAILed on two documentation/evidence items: null-root really completes an empty `hasCompletedView=true` transaction while architecture/AD-21 say no answer, and the dirty-base official artifacts are not bound to the reviewed implementation source. | **MANDATORY THIRD-ROUND STOP; G4 UNPASSED.** | Reproduced scratch evidence: Release 0W/0E; Core 111/111; App 146/146; allocation 1/1 at 0 B; corrected hermetic 16,760/16,760; InstalledDat 385/10 documented/1; three 30-entry manifests verify. No client/self-gate and no campaign cherry-pick. Explicit owner direction is required for any documentation/evidence-only exception; history remains intact. | -| S5 | STOPPED at c1 after the two allowed fix rounds; c2–c5 and closeout remain undispatched | G4 UNPASSED | Packet §9 is authoritative. Never merge main before G4. | +| S5-c1 | **IMPLEMENTED 2026-09-04; sequential review pending.** Packet `s5-consumers-material-closeout-packet.md` §§1–9. The exact walk landscape set is now the typed retained particle product; land cells require membership while non-null EnvCells retain their constant-true virtual. AP-117's null-root reconstruction and the dead point-light feedback seam are deleted; AP-117 is retired and AP-85/AD-21 are corrected. | G4 UNPASSED | Focused Core 111/111, App 146/146, real warmed allocation 1/1 at 0 B, Release 0W/0E. The first official artifact is preserved at 16,759/16,760 with only the stale AP-row-count pin; §8 corrected 162→161 and the exact pin passes 1/1. Fresh official hermetic 16,760/16,760; InstalledDat 385/10 documented/1 with no new identity. Directional shadows, building degrade, AP-232, probes, and closeout remain later chunks. | +| S5 | IN FLIGHT — c1 implemented/review pending; c2–c5 and closeout remain | G4 | Packet §7 is the bounded decomposition; never merge main before G4. | --- diff --git a/docs/research/2026-09-01-overhaul/oh1-construction-landscape-contract.md b/docs/research/2026-09-01-overhaul/oh1-construction-landscape-contract.md index 7ee07515..5a8d9d12 100644 --- a/docs/research/2026-09-01-overhaul/oh1-construction-landscape-contract.md +++ b/docs/research/2026-09-01-overhaul/oh1-construction-landscape-contract.md @@ -307,6 +307,7 @@ weather/sky post-pass | Outside cells reached by the static bbox transit remain in the CELLARRAY. | Dirty `CellTransit` removed an outdoor prune globally, including collision callers. | Correct retail fact, wrong ownership boundary risk. OH3 must port the exact bbox route without mutating unrelated sphere/collision routes. | | Dynamics and attached children inherit the root's exact ordered CELLARRAY and publish exact per-part entries. | `WalkProductionWorldData` buckets whole `RenderProjectionRecord`s by `ShadowObjectRegistry.GetOwnerCells`, resolving equipped children by a parent callback. The draw classifier later tries to restore per-part stamps. | Membership is reconstructed from the collision registry, loses the canonical `CShadowPart` transaction/order, and creates a second model. | | `LScape::draw` runs once and visibility unions all views. | `RetailFrameWalk.DrawLandscape` correctly installs one view set and walks blocks/cells once. | Keep. | +| `CLandCell::IsInView` consumes the previous completed landscape stamp; `CEnvCell::IsInView` is constant true. | `RetailPViewFrameResult.VisibleLandscapeCells` publishes the walk's exact landscape half; `ParticleSystem.ApplyRetailView` branches by cell-id family. Frames without a completed walk product publish no outdoor answer. | **FIXED by S5-c1 (2026-09-04).** The competing null-root terrain reconstruction is deleted; point lights remain resident-registry based. | | Per landscape cell: terrain -> building -> ordinary objects. | `RetailFrameWalk.DrawLandscape` now emits one `OnLandCellTurn` per admitted land cell BEFORE that cell's own building/object turn (`RenderDeviceD3D::DrawBlock`'s real per-cell interleave), and `WalkFrameDriver.Replay` submits each cell's terrain through a deferred, order-preserving batch rather than one whole pre-stage draw. | FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round 1) — the whole-stage `TerrainSlice(0)`/`DrawTerrainSlice` this row described is deleted outright; row retired. | | Cell object draw consumes the cell's exact stable far-to-near `shadow_part_list`. | The driver materializes per-cell record buckets, classifies/batches them, and uses a scope-level ordered stream plus separate alpha collection. | OH3/OH7 must replace reconstructed membership/order with exact part entries and exact per-cell alpha barriers. | | Building is alpha drain -> portal pass -> shell, then cell objects. | `RetailFrameWalk.DrawBuilding` and the driver's building events model this order. | Mechanism broadly matches, but its correctness depends on the sibling OH1 built-mesh/view and depth-lifecycle findings. | diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md index 43aa408e..32acc50d 100644 --- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md +++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md @@ -1,6 +1,6 @@ # Campaign OVERHAUL v2 — S5 consumers, material, and closeout packet -**Status:** S5-c1 STOPPED 2026-09-04 after fix round 2; not landed. +**Status:** S5-c1 IMPLEMENTED 2026-09-04; sequential review pending. **Branch:** `claude/campaign-w-retail-frame-walk`. **Gate:** G4 remains unpassed. Nothing merges to `main` before G4. @@ -275,45 +275,56 @@ failed test first, then a fresh official hermetic lane under a new artifact name and the required InstalledDat lane. Preserve the original failed artifact and record both identities in the implementer return. -## 9. S5-c1 mandatory stop after fix round 2 — 2026-09-04 +## 9. S5-c1 implementation result — 2026-09-04 -The reviewed scratch stack is intentionally **not landed**: +The bounded implementation publishes `WalkFrameDriver.VisitedLandscapeCellIds` +as the typed landscape product, retains it transactionally in the particle +visibility owner, and applies the retail cell-family virtual: membership for +land cells, constant true for non-null EnvCells. The null-root terrain +reconstruction and the dead point-light drawable-cell feedback chain are +deleted. AP-117 is retired; AP-85 and AD-21 now describe the surviving code. -- gate-correction contract `2bb565b0b`; -- implementation `9aadc917a`; -- retail fix-round-1 contract/implementation `afaebcad6` / `74a2e4c48`; -- retail fix-round-2 contract/implementation `ed8200664` / `a28384d80`. +The real warmed allocation pin initially exposed that enumerating the retained +`SortedSet` of world-simulation handles allocated on every `Apply`. The +owner is now a sorted `List` maintained at lifecycle mutation time, so the +production Apply loop preserves stable handle order and measures 0 B without +changing queue, router, draw, or particle tick behavior. This is an +implementation-mechanism correction, not a retail behavior deviation. -The retail lens ultimately passed. It verified the paired executable/PDB bytes, -the exact land-cell versus EnvCell virtual behavior, previous-completed-frame -timing, x87/AP-116 edges, typed landscape handoff, and unchanged particle -draw/queue/tick behavior. The production lens found no production-code or test -defect and reproduced Release 0 warnings / 0 errors, Core VFX 111/111, App -focused 146/146, allocation 1/1 at 0 B, AP-116 3/3, all six non-vacuous -mutations, all source guards, and all three 30-entry artifact manifests with -zero mismatch. Official artifacts remain in the scratch worktree: +Focused returns before the official lanes: Core VFX 111/111, App particle / +frame / renderer / terrain 146/146, and the real warmed production allocation +pin 1/1 at 0 B. Release built with 0 warnings / 0 errors. The first official +hermetic artifact, `artifacts/s5-c1-hermetic-20260904`, is deliberately +preserved at 16,759/16,760: its sole failure was the stale 162-row test literal +after AP-117's retirement. Section 8 authorized only that coupled test +correction; the exact failed pin then passed 1/1 with the truthful 161-row +header and physical count. The one-shot corrected official lane at +`artifacts/s5-c1-hermetic-corrected-20260904` then passed 16,760/16,760, +0 skipped, across 14 assemblies. The inclusive InstalledDat lane at +`artifacts/s5-c1-installed-20260904` produced the documented global set: +385 passed, 10 failed, 1 skipped. The failures are `TowerAscent`, the two +#383 layout sweeps, #458 `Oh_doorway_still_first_frame_diff`, and the six +`AlphaFlushCounts_*` transcript Facts; the skip is +`Sweep_the_lookin_gate_decodes_against_the_street_fixture`. No new failure or +skip identity appeared. -- `artifacts/s5-c1-hermetic-20260904`: 16,759 passed / 1 stale-row failure; -- `artifacts/s5-c1-hermetic-corrected-20260904`: 16,760/16,760; -- `artifacts/s5-c1-installed-20260904`: 385 passed / 10 documented failed / - 1 documented skipped, with no new identity. +Independent mutation ledger (each mutation restored exactly before the next): -The final production lens nevertheless failed on two correction-worthy -documentation/evidence findings after both allowed fix rounds: - -1. `acdream-architecture.md` and AD-21 say a null-root frame publishes no - cell-visibility answer. Production actually completes - `hasCompletedView=true` with an empty landscape set; only login/portal carry - false. Scratch packet §11.1 states this correctly, so the implementation's - claim that architecture/register prose is truthful is false. -2. The packet reports the official lanes but does not reconcile their recorded - `WorktreeDirty=true` bases (`00f134f9e` for the first lane and `2bb565b0b` - for the corrected/InstalledDat lanes) to implementation commit `9aadc917a`, - nor state that later commits are prose/comments only. The manifests prove - their 30 recorded evidence files, not the dirty source content. - -Correcting either finding would be fix round 3. The campaign rule therefore -requires this stop: do not cherry-pick the scratch stack, do not run a graphical -gate on it, and do not dispatch S5-c2. Resumption requires explicit owner -direction on whether to authorize an evidence/documentation-only exception; -history must remain intact either way. G3 stays provisional and G4 unpassed. +1. EnvCell eligibility made set-dependent: `ApplyRetailView_UsesLandscapeMembershipButEnvCellConstantVirtual` + first failed `Assert.True`, expected true / actual false, at + `ParticleSystemTests.cs:553`. +2. Outdoor eligibility made constant true: the same test first failed + `Assert.False`, expected false / actual true, at `ParticleSystemTests.cs:559`. +3. Renderer fed the diagnostic union: `PViewWorld_PublishesOnlyLandscapeCellsToParticleVisibility` + first failed the HashSet equality at `WorldSceneRendererTests.cs:279`, + expected `[16842755]`, actual `[16843008, 16842755]`. +4. A production `CollectVisibleCells` symbol was restored: + `Production_source_has_no_reconstructed_particle_visibility_feedback` + first failed at `TerrainParticleCellVisibilityTests.cs:37`, naming + `TerrainModernRenderer.cs`. +5. A production `ObserveDrawableCells` symbol was restored: the same source + guard first failed at line 37, naming `WorldRenderFrameBuilder.cs`. +6. The inclusive boundary was changed from `<=` to `<`: + `ApplyRetailView_UsesOwnerVisibilityAndInclusiveAuthoredDistance` first + failed `Assert.True`, expected true / actual false, at + `ParticleSystemTests.cs:519`. diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index d47641e7..bae4029f 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -23,6 +23,7 @@ internal sealed class RetailPViewRenderer // call. Every walk consumer reads it synchronously in this frame. private readonly HashSet _drawableCellsScratch = new(); private readonly HashSet _visibleCellsScratch = new(); + private readonly HashSet _visibleLandscapeCellsScratch = new(); // FW3 visual-gate fix: the interior root's dynamics phase, invoked by // the driver's clearInteriorDepth closure at the walk's pre-clear @@ -294,6 +295,8 @@ internal sealed class RetailPViewRenderer _drawableCellsScratch.Clear(); _drawableCellsScratch.UnionWith(walkDriver.VisitedCells); walkDriver.CopyVisibleCellsTo(_visibleCellsScratch); + _visibleLandscapeCellsScratch.Clear(); + _visibleLandscapeCellsScratch.UnionWith(walkDriver.VisitedLandscapeCellIds); if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled) { @@ -437,6 +440,7 @@ internal sealed class RetailPViewRenderer clipAssembly, drawableCells, _visibleCellsScratch, + _visibleLandscapeCellsScratch, counts, sourceCounts, diagnosticPartition: null); @@ -865,10 +869,17 @@ public sealed class RetailPViewFrameResult public ClipFrameAssembly ClipAssembly { get; private set; } = null!; public HashSet DrawableCells { get; private set; } = null!; + /// + /// The production retail landscape walk's exact outdoor land-cell + /// in_view set. Particle consumers borrow this typed half rather + /// than the diagnostic union. + /// + public HashSet VisibleLandscapeCells { get; private set; } = null!; + /// /// The production retail walk's exact visited-cell set. This is the one - /// visibility answer consumed by EnvCell preparation, particles, lights, - /// and directional-shadow filtering. + /// diagnostic visibility union. Typed consumers must use + /// or . /// public HashSet VisibleCells { get; private set; } = null!; @@ -881,6 +892,7 @@ public sealed class RetailPViewFrameResult ClipFrameAssembly clipAssembly, HashSet drawableCells, HashSet visibleCells, + HashSet visibleLandscapeCells, RenderFrameDiagnosticCounts diagnosticCounts, RenderProjectionCounts sourceCounts, InteriorEntityPartition.Result? diagnosticPartition) @@ -888,6 +900,7 @@ public sealed class RetailPViewFrameResult ClipAssembly = clipAssembly; DrawableCells = drawableCells; VisibleCells = visibleCells; + VisibleLandscapeCells = visibleLandscapeCells; DiagnosticCounts = diagnosticCounts; SourceCounts = sourceCounts; DiagnosticPartition = diagnosticPartition; diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs index 29da7261..aa84796c 100644 --- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs @@ -67,7 +67,6 @@ public sealed partial class TerrainModernRenderer : IDisposable // Reusable per-frame buffers. private readonly List _visibleSlots = new(); - private readonly HashSet _visibleCellIds = new(); private readonly HashSet _walkVisibleLandblocks = new(); private DrawElementsIndirectCommand[] _deicScratch = Array.Empty(); @@ -108,15 +107,6 @@ public sealed partial class TerrainModernRenderer : IDisposable /// cref="DrawLandCells"/> batches (indirect draws) submitted THIS FRAME. internal int WalkDrawCount => _walkDrawsThisFrame; - /// - /// Outdoor landcells admitted by the current landscape view. The set is - /// accumulated across doorway landscape slices and consumed after the - /// completed render frame by particle visibility. - /// - internal HashSet VisibleCellIds => _visibleCellIds; - - public void BeginVisibilityFrame() => _visibleCellIds.Clear(); - /// /// Resets the per-GPU-fenced-frame-slot draw state. A retail outside view /// may draw terrain more than once in a frame. @@ -249,16 +239,15 @@ public sealed partial class TerrainModernRenderer : IDisposable // Campaign FW4: RetailFrameWalk is the sole visibility authority. // Terrain remains one full-landblock MDI draw, but only landblocks - // containing a walk-admitted landcell participate, and the published - // landcell set is the walk's exact in_view set rather than a second - // frustum-derived approximation. + // containing a walk-admitted landcell participate. The walk publishes + // its exact in_view set directly to typed consumers; this draw path + // does not reconstruct or retain a second visibility answer. _walkVisibleLandblocks.Clear(); if (inViewLandcells is not null) { foreach (uint cellId in inViewLandcells) { _walkVisibleLandblocks.Add(cellId & 0xFFFF0000u); - _visibleCellIds.Add(cellId); } } @@ -279,16 +268,6 @@ public sealed partial class TerrainModernRenderer : IDisposable continue; } _visibleSlots.Add(slot); - if (inViewLandcells is null) - { - CollectVisibleCells( - _visibleCellIds, - data.LandblockId, - data.WorldOrigin, - data.AabbMin.Z, - data.AabbMax.Z, - frustum); - } } if (_visibleSlots.Count == 0) return; @@ -480,53 +459,6 @@ public sealed partial class TerrainModernRenderer : IDisposable // Private helpers // ---------------------------------------------------------------- - /// - /// S3 landing hygiene (H2): frustum-only now. Retail never view-clips - /// terrain (see 's doc); the CPU/GPU - /// clip-region equivalence check this used to also run (a doorway - /// slice's clip-space planes plus its NDC-AABB scissor fallback) had - /// exactly one caller — — and that caller's own two - /// clip-region parameters were themselves dead (grep: no production - /// caller ever passed either), so the equivalence helper and its - /// homogeneous-plane subroutine are deleted along with them. - /// - internal static void CollectVisibleCells( - HashSet destination, - uint landblockId, - Vector3 worldOrigin, - float zMin, - float zMax, - FrustumPlanes? frustum) - { - ArgumentNullException.ThrowIfNull(destination); - const float cellSize = AcDream.Core.Physics.TerrainSurface.CellSize; - const int cellsPerSide = AcDream.Core.Physics.TerrainSurface.CellsPerSide; - uint prefix = landblockId & 0xFFFF0000u; - - for (int cellX = 0; cellX < cellsPerSide; cellX++) - { - float minX = worldOrigin.X + cellX * cellSize; - float maxX = minX + cellSize; - for (int cellY = 0; cellY < cellsPerSide; cellY++) - { - float minY = worldOrigin.Y + cellY * cellSize; - float maxY = minY + cellSize; - var cellMin = new Vector3(minX, minY, zMin); - var cellMax = new Vector3(maxX, maxY, zMax); - if (frustum is not null - && !FrustumCuller.IsAabbVisible(frustum.Value, cellMin, cellMax)) - { - continue; - } - - uint low = AcDream.Core.Physics.TerrainSurface.ComputeOutdoorCellLowId( - cellX * cellSize, - cellY * cellSize); - destination.Add(prefix | low); - } - } - } - private void EnsureCapacity(int newCapacity) { if (newCapacity <= _alloc.Capacity) diff --git a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs index f922dd29..1ba84a27 100644 --- a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs +++ b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs @@ -5,7 +5,7 @@ namespace AcDream.App.Rendering.Vfx; internal interface IWorldSceneParticleVisibility { - void MarkVisibleCells(HashSet cellIds); + void MarkVisibleLandscapeCells(HashSet cellIds); void CompleteFrame(); @@ -15,8 +15,10 @@ internal interface IWorldSceneParticleVisibility /// /// Bridges the retained retail PView result into the next physics update's /// CObjCell::IsInView particle gate. The controller owns only immutable -/// frame meaning: one completed viewer position plus the AC cells admitted by -/// that completed view. It neither creates emitters nor performs rendering. +/// frame meaning: one completed viewer position plus the outdoor landscape +/// cells admitted by that completed view. EnvCells use retail's constant-true +/// virtual and therefore never enter this retained set. The controller neither +/// creates emitters nor performs rendering. /// public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility { @@ -50,12 +52,24 @@ public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility _frameUsesWorldView = true; } - public void MarkVisibleCells(HashSet cellIds) + public void MarkVisibleLandscapeCells(HashSet cellIds) { ArgumentNullException.ThrowIfNull(cellIds); if (!_frameOpen || !_frameUsesWorldView) return; + foreach (uint cellId in cellIds) + { + uint low = cellId & 0xFFFFu; + if (low == 0u || low >= 0x0100u) + { + throw new ArgumentException( + $"Landscape visibility accepts only outdoor land cells; " + + $"0x{cellId:X8} is not one.", + nameof(cellIds)); + } + } + _buildingCellIds.UnionWith(cellIds); } diff --git a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs index ef9cc364..f5a36b18 100644 --- a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs +++ b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs @@ -100,9 +100,6 @@ internal interface IWorldRenderFrameBuilder bool waitingForLogin, DayGroupData? activeDayGroup); - void ObserveDrawableCells(IReadOnlySet drawableCells); - - void ClearDrawableCells(); } internal interface IWorldFrameCameraSource @@ -135,9 +132,6 @@ internal interface IWorldFrameEnvironmentPreparation in RenderFrameFoundation foundation, DayGroupData? activeDayGroup); - void ObserveDrawableCells(IReadOnlySet drawableCells); - - void ClearDrawableCells(); } internal interface IWorldFrameAnimatedEntitySource @@ -205,10 +199,6 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder return new WorldRenderFrame(camera, roots, buildings, animated); } - public void ObserveDrawableCells(IReadOnlySet drawableCells) => - _environment.ObserveDrawableCells(drawableCells); - - public void ClearDrawableCells() => _environment.ClearDrawableCells(); } internal sealed class RuntimeWorldFrameCameraSource : IWorldFrameCameraSource @@ -343,7 +333,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation { private readonly RetailSelectionScene? _selection; private readonly ParticleVisibilityController _particles; - private readonly TerrainModernRenderer? _terrain; private readonly WorldRevealCoordinator? _reveal; private readonly WbFrustum? _environmentFrustum; @@ -356,7 +345,7 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation { _selection = selection; _particles = particles ?? throw new ArgumentNullException(nameof(particles)); - _terrain = terrain; + _ = terrain; _reveal = reveal; _environmentFrustum = environmentFrustum; } @@ -365,7 +354,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation { _selection?.SetViewFrustum(camera.Frustum); _particles.BeginFrame(camera.Position); - _terrain?.BeginVisibilityFrame(); if (waitingForLogin) return; @@ -541,13 +529,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation _lightingUbo?.Upload(ubo); } - public void ObserveDrawableCells(IReadOnlySet drawableCells) => - ArgumentNullException.ThrowIfNull(drawableCells); - - public void ClearDrawableCells() - { - } - private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell) { // CellManager::ChangePosition @ 0x004559B0 keys this on the player diff --git a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs index c758fe92..25a0704e 100644 --- a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs +++ b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs @@ -8,8 +8,6 @@ namespace AcDream.App.Rendering; internal interface IWorldScenePassExecutor { - HashSet? TerrainVisibleCellIds { get; } - void BeginFrame(); void PrepareFlatWorldClip(); @@ -100,8 +98,6 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor _particleRenderer = particleRenderer; } - public HashSet? TerrainVisibleCellIds => _terrain?.VisibleCellIds; - public void BeginFrame() { _visibleParticleOwners.Clear(); diff --git a/src/AcDream.App/Rendering/WorldSceneRenderer.cs b/src/AcDream.App/Rendering/WorldSceneRenderer.cs index 6a79838f..c66b3788 100644 --- a/src/AcDream.App/Rendering/WorldSceneRenderer.cs +++ b/src/AcDream.App/Rendering/WorldSceneRenderer.cs @@ -209,10 +209,8 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase // so the portal-visible return above already covers every // waiting frame. One gate computes the frame's visibility; // this phase only enforces it. - _passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId); - terrainDrawn = true; - if (_passes.TerrainVisibleCellIds is { } flatTerrainCells) - _particleVisibility.MarkVisibleCells(flatTerrainCells); + _passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId); + terrainDrawn = true; } else { @@ -250,11 +248,11 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase camera.Camera.View, _diagnostics.CameraCellResolution)); - // One visibility answer: the walk's exact visited-cell set - // feeds effects and the frame environment (point lights and - // directional-shadow filtering). - _particleVisibility.MarkVisibleCells(pviewResult.VisibleCells); - _frames.ObserveDrawableCells(pviewResult.VisibleCells); + // CLandCell::IsInView consumes only the walk's exact landscape + // half. EnvCells use CEnvCell::IsInView's constant-true result; + // VisibleCells remains a diagnostics-only union. + _particleVisibility.MarkVisibleLandscapeCells( + pviewResult.VisibleLandscapeCells); _diagnostics.EmitPViewInput( pviewResult.VisibleCells, pviewResult.ClipAssembly.OutsideViewSlices.Length, @@ -287,7 +285,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase _entities.LandblockEntries, roots.PlayerLandblockId, world.AnimatedEntityIds); - _frames.ClearDrawableCells(); } _passes.DisableClipDistances(); diff --git a/src/AcDream.Core/Vfx/ParticleSystem.cs b/src/AcDream.Core/Vfx/ParticleSystem.cs index d8c66b44..3bbae610 100644 --- a/src/AcDream.Core/Vfx/ParticleSystem.cs +++ b/src/AcDream.Core/Vfx/ParticleSystem.cs @@ -20,7 +20,11 @@ public sealed class ParticleSystem : IParticleSystem // thousands of finite/fading emitters. private readonly SortedSet _allHandles = []; private readonly SortedSet _simulationHandles = []; - private readonly SortedSet _worldSimulationHandles = []; + // ApplyRetailView is a per-frame production path. SortedSet's enumerator + // allocates its traversal stack, so retain this subset as a sorted list; + // lifecycle mutations pay the binary insertion/removal cost and the + // warmed view walk stays allocation-free in emitter-spawn order. + private readonly List _worldSimulationHandles = []; private readonly SortedSet[] _renderableHandlesByPass = [[], [], []]; private readonly SortedSet[] _renderableUnattachedHandlesByPass = @@ -147,7 +151,7 @@ public sealed class ParticleSystem : IParticleSystem _allHandles.Add(handle); _simulationHandles.Add(handle); if (visibilityPolicy == ParticleVisibilityPolicy.World) - _worldSimulationHandles.Add(handle); + AddWorldSimulationHandle(handle); AddEmitterToRenderIndexes(emitter); for (int i = 0; i < desc.InitialParticles; i++) @@ -330,7 +334,7 @@ public sealed class ParticleSystem : IParticleSystem if (visibilityPolicy == ParticleVisibilityPolicy.World) { if (emitter.SimulationEnabled) - _worldSimulationHandles.Add(handle); + AddWorldSimulationHandle(handle); } else { @@ -343,22 +347,25 @@ public sealed class ParticleSystem : IParticleSystem /// /// Applies CPhysicsObj::ShouldDrawParticles (0x0050FE60) to every /// live emitter. The App layer supplies the previous completed retail - /// PView's cell set, equivalent to CObjCell::IsInView when the next - /// physics update runs. + /// PView's exact landscape-cell set. The cell-family branch below invokes + /// the matching retail virtual when the next physics update runs: + /// CLandCell::IsInView reads that completed set, while + /// CEnvCell::IsInView is constant true. /// public void ApplyRetailView( Vector3 viewerPosition, - IReadOnlySet visibleCellIds, + IReadOnlySet visibleLandscapeCellIds, bool hasCompletedView, float rangeMultiplier = 1f) { - ArgumentNullException.ThrowIfNull(visibleCellIds); + ArgumentNullException.ThrowIfNull(visibleLandscapeCellIds); if (!float.IsFinite(rangeMultiplier) || rangeMultiplier <= 0f) rangeMultiplier = 1f; LastRetailViewEmitterVisitCount = 0; - foreach (int handle in _worldSimulationHandles) + for (int i = 0; i < _worldSimulationHandles.Count; i++) { + int handle = _worldSimulationHandles[i]; if (!_byHandle.TryGetValue(handle, out ParticleEmitter? emitter)) continue; LastRetailViewEmitterVisitCount++; @@ -374,8 +381,12 @@ public sealed class ParticleSystem : IParticleSystem float maxDistance = emitter.Desc.MaxDegradeDistance * rangeMultiplier; float distance = RetailDistance(emitter.OwnerPosition, viewerPosition); + uint ownerCellLow = emitter.OwnerCellId & 0xFFFFu; + bool ownerCellInView = ownerCellLow >= 0x0100u + || (ownerCellLow > 0u + && visibleLandscapeCellIds.Contains(emitter.OwnerCellId)); emitter.ViewEligible = emitter.OwnerCellId != 0 - && visibleCellIds.Contains(emitter.OwnerCellId) + && ownerCellInView // The x87 comparison in ShouldDrawParticles admits unordered // comparisons (NaN) and reject a negative authored range. && (float.IsNaN(distance) @@ -442,7 +453,7 @@ public sealed class ParticleSystem : IParticleSystem emitter.SimulationEnabled = true; _simulationHandles.Add(handle); if (emitter.VisibilityPolicy == ParticleVisibilityPolicy.World) - _worldSimulationHandles.Add(handle); + AddWorldSimulationHandle(handle); else emitter.ViewEligible = true; } @@ -1007,6 +1018,13 @@ public sealed class ParticleSystem : IParticleSystem } } + private void AddWorldSimulationHandle(int handle) + { + int index = _worldSimulationHandles.BinarySearch(handle); + if (index < 0) + _worldSimulationHandles.Insert(~index, handle); + } + private static bool IsRenderable(ParticleEmitter emitter) => emitter.PresentationVisible && emitter.ViewEligible; diff --git a/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs b/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs index e51bdcec..924a404c 100644 --- a/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs +++ b/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs @@ -1,58 +1,42 @@ -using System.Numerics; -using AcDream.App.Rendering; +using AcDream.App.Tests.Rendering.Walk; namespace AcDream.App.Tests.Rendering; /// -/// S3 landing hygiene (H2): -/// is frustum-only now — its clip-plane-list and NDC-AABB scissor -/// parameters and the CPU/GPU clip-region equivalence check they fed had -/// exactly one production caller (), -/// and that caller never actually passed either (grep). The doorway-clip/scissor -/// rejection cases this file used to pin (RejectsCellsOutsideDoorwayClipPlanes, -/// RejectsCellsOutsideDoorwayScissorAabb, UnionsCellsFromEveryLandscapeSlice) -/// pinned a mechanism nothing in production ever fed — deleted with the -/// parameters. What remains still pins the one thing this method's callers -/// actually rely on: it publishes the visible terrain cell set per landblock, -/// respecting an explicit frustum when one is given. +/// Campaign OVERHAUL S5-c1: the null-root terrain path remains a safety draw, +/// but it may not reconstruct or publish a competing cell-visibility answer. +/// The same architecture pin protects the deleted point-light feedback seam. /// public sealed class TerrainParticleCellVisibilityTests { [Fact] - public void CollectVisibleCells_PublishesLandscapeCellsWithoutEntitySurvivors() + public void ProductionRetainsNoReconstructedVisibilityOrLightFeedbackSymbols() { - var cells = new HashSet(); + string sourceRoot = Path.Combine(WalkOracleTraceRepoRoot.Find(), "src"); + string[] production = Directory.GetFiles( + sourceRoot, + "*.cs", + SearchOption.AllDirectories); + string[] forbidden = + [ + "CollectVisibleCells", + "TerrainVisibleCellIds", + "ObserveDrawableCells", + "ClearDrawableCells", + ]; - TerrainModernRenderer.CollectVisibleCells( - cells, - 0xA9B4FFFFu, - Vector3.Zero, - zMin: 0f, - zMax: 20f, - frustum: null); - - Assert.Equal(64, cells.Count); - Assert.Contains(0xA9B40001u, cells); - Assert.Contains(0xA9B40040u, cells); - } - - [Fact] - public void CollectVisibleCells_ForceDrawnParentStillUsesCellFrustum() - { - var cells = new HashSet(); - FrustumPlanes frustum = FrustumPlanes.FromViewProjection(Matrix4x4.Identity); - - // The parent landblock may have entered TerrainModernRenderer's draw - // list through neverCullLandblockId. The cell collector deliberately - // receives no such bypass and still rejects its off-frustum cells. - TerrainModernRenderer.CollectVisibleCells( - cells, - 0xA9B4FFFFu, - new Vector3(100f, 100f, 100f), - zMin: 100f, - zMax: 120f, - frustum); - - Assert.Empty(cells); + foreach (string symbol in forbidden) + { + string[] owners = production + .Where(path => File.ReadAllText(path).Contains( + symbol, + StringComparison.Ordinal)) + .Select(path => Path.GetRelativePath(sourceRoot, path)) + .OrderBy(path => path, StringComparer.Ordinal) + .ToArray(); + Assert.True( + owners.Length == 0, + $"Production symbol {symbol} remains in: {string.Join(", ", owners)}"); + } } } diff --git a/tests/AcDream.App.Tests/Rendering/Vfx/ParticleVisibilityControllerTests.cs b/tests/AcDream.App.Tests/Rendering/Vfx/ParticleVisibilityControllerTests.cs index 8d17b725..4c56fb71 100644 --- a/tests/AcDream.App.Tests/Rendering/Vfx/ParticleVisibilityControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Vfx/ParticleVisibilityControllerTests.cs @@ -28,7 +28,7 @@ public sealed class ParticleVisibilityControllerTests controller.BeginFrame(Vector3.Zero); controller.UseWorldView(); - controller.MarkVisibleCells(new HashSet { 0x01010001u }); + controller.MarkVisibleLandscapeCells(new HashSet { 0x01010001u }); controller.CompleteFrame(); controller.Apply(particles, 1f); @@ -84,15 +84,125 @@ public sealed class ParticleVisibilityControllerTests controller.BeginFrame(Vector3.Zero); controller.UseWorldView(); - controller.MarkVisibleCells([0x01010001u]); + controller.MarkVisibleLandscapeCells([0x01010001u]); controller.CompleteFrame(); controller.BeginFrame(new Vector3(1000f, 1000f, 0f)); controller.UseWorldView(); - controller.MarkVisibleCells([0x02020001u]); + controller.MarkVisibleLandscapeCells([0x02020001u]); controller.AbortFrame(); controller.Apply(particles, 1f); Assert.True(Assert.Single(particles.EnumerateEmitters()).ViewEligible); } + + [Fact] + public void CompleteAbortAndResetPublishOnlyCompleteLandscapeTransactions() + { + var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(1)); + int first = particles.SpawnEmitter( + new EmitterDesc + { + DatId = 0x32000004u, + Type = ParticleType.Still, + MaxDegradeDistance = 100f, + MaxParticles = 1, + }, + Vector3.Zero, + attachedObjectId: 43u); + int second = particles.SpawnEmitter( + new EmitterDesc + { + DatId = 0x32000005u, + Type = ParticleType.Still, + MaxDegradeDistance = 100f, + MaxParticles = 1, + }, + Vector3.Zero, + attachedObjectId: 44u); + particles.UpdateEmitterOwnerCell(first, 0x0101_0001u); + particles.UpdateEmitterOwnerCell(second, 0x0101_0002u); + var controller = new ParticleVisibilityController(); + + controller.BeginFrame(Vector3.Zero); + controller.UseWorldView(); + controller.MarkVisibleLandscapeCells([0x0101_0001u]); + controller.CompleteFrame(); + + controller.BeginFrame(new Vector3(1000f, 0f, 0f)); + controller.UseWorldView(); + controller.MarkVisibleLandscapeCells([0x0101_0002u]); + controller.AbortFrame(); + controller.Apply(particles, 1f); + Assert.True(particles.EnumerateEmitters().Single(e => e.Handle == first).ViewEligible); + Assert.False(particles.EnumerateEmitters().Single(e => e.Handle == second).ViewEligible); + + controller.BeginFrame(Vector3.Zero); + controller.UseWorldView(); + controller.MarkVisibleLandscapeCells([0x0101_0002u]); + controller.CompleteFrame(); + controller.Apply(particles, 1f); + Assert.False(particles.EnumerateEmitters().Single(e => e.Handle == first).ViewEligible); + Assert.True(particles.EnumerateEmitters().Single(e => e.Handle == second).ViewEligible); + + controller.Reset(); + controller.Apply(particles, 1f); + Assert.All(particles.EnumerateEmitters(), emitter => Assert.False(emitter.ViewEligible)); + } + + [Theory] + [InlineData(0u)] + [InlineData(0x0101_0100u)] + public void MarkVisibleLandscapeCells_RejectsNonLandscapeIds(uint cellId) + { + var controller = new ParticleVisibilityController(); + controller.BeginFrame(Vector3.Zero); + controller.UseWorldView(); + + ArgumentException error = Assert.Throws( + () => controller.MarkVisibleLandscapeCells([cellId])); + + Assert.Contains($"0x{cellId:X8}", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void ProductionApply_WarmedParticleViewPathDoesNotAllocate() + { + var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(1)); + int outdoor = particles.SpawnEmitter( + new EmitterDesc + { + DatId = 0x32000006u, + Type = ParticleType.Still, + MaxDegradeDistance = 100f, + MaxParticles = 1, + }, + Vector3.Zero, + attachedObjectId: 45u); + int environment = particles.SpawnEmitter( + new EmitterDesc + { + DatId = 0x32000007u, + Type = ParticleType.Still, + MaxDegradeDistance = 100f, + MaxParticles = 1, + }, + Vector3.Zero, + attachedObjectId: 46u); + particles.UpdateEmitterOwnerCell(outdoor, 0x0101_0001u); + particles.UpdateEmitterOwnerCell(environment, 0x0101_0100u); + var controller = new ParticleVisibilityController(); + controller.BeginFrame(Vector3.Zero); + controller.UseWorldView(); + controller.MarkVisibleLandscapeCells([0x0101_0001u]); + controller.CompleteFrame(); + + long allocated = ZeroAllocationProbe.MeasureWarmed( + () => controller.Apply(particles, 1f), + batchSize: 256, + warmupBatches: 2, + samples: 4); + + Assert.True(allocated == 0, $"Particle view allocated {allocated} bytes."); + } } diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs index d6ab130e..127c7695 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs @@ -316,7 +316,7 @@ public sealed class WalkStaticStreamPopulatorTests Assert.DoesNotContain( "one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase); - Assert.Contains("162 active rows", register, StringComparison.Ordinal); + Assert.Contains("161 active rows", register, StringComparison.Ordinal); int apSectionStart = register.IndexOf( "## 3. Documented approximation (AP)", StringComparison.Ordinal); int apSectionEnd = register.IndexOf( @@ -338,7 +338,7 @@ public sealed class WalkStaticStreamPopulatorTests Assert.Single(File.ReadLines(registerPath), static line => line.StartsWith("| AP-243 |", StringComparison.Ordinal)); Assert.Equal( - 162, + 161, File.ReadLines(registerPath).Count(static line => line.StartsWith("| AP-", StringComparison.Ordinal))); Assert.Contains("greater than 50 m", register, StringComparison.Ordinal); diff --git a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs index 4aed26cf..d0cb0e5e 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs @@ -52,9 +52,6 @@ public sealed class WorldRenderFrameBuilderTests in foundation, waitingForLogin: true, activeDayGroup: null); - var drawableCells = new HashSet { 0x01010001u }; - builder.ObserveDrawableCells(drawableCells); - builder.ClearDrawableCells(); Assert.Equal( [ @@ -74,8 +71,6 @@ public sealed class WorldRenderFrameBuilderTests Assert.Null(result.ClipRoot); Assert.True(visibility.WaitingForLogin); Assert.Equal(foundation, environment.Foundation); - Assert.Same(drawableCells, environment.DrawableCells); - Assert.Equal(1, environment.ClearCount); } [Fact] @@ -254,7 +249,7 @@ public sealed class WorldRenderFrameBuilderTests } [Fact] - public void Runtime_environment_uses_resident_lights_independent_of_drawable_cells() + public void Runtime_environment_uses_all_resident_lights() { const uint visibleCell = 0x01010100u; const uint hiddenCell = 0x01010101u; @@ -280,10 +275,6 @@ public sealed class WorldRenderFrameBuilderTests lightingUbo: null, new WorldRenderRangeState(4, 12), skyPes: null); - var borrowed = new HashSet { visibleCell }; - environment.ObserveDrawableCells(borrowed); - borrowed.Clear(); - borrowed.Add(hiddenCell); WorldCameraFrame camera = CameraFrame(new FlyCamera()); WorldRootFrame roots = default; RenderFrameFoundation foundation = default; @@ -293,7 +284,6 @@ public sealed class WorldRenderFrameBuilderTests Assert.Contains(visibleLight, lighting.PointSnapshot); Assert.Contains(hiddenLight, lighting.PointSnapshot); - environment.ClearDrawableCells(); environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null); Assert.Contains(visibleLight, lighting.PointSnapshot); @@ -544,10 +534,6 @@ public sealed class WorldRenderFrameBuilderTests { public RenderFrameFoundation Foundation { get; private set; } - public IReadOnlySet? DrawableCells { get; private set; } - - public int ClearCount { get; private set; } - public void Prepare( in WorldCameraFrame camera, in WorldRootFrame roots, @@ -558,10 +544,6 @@ public sealed class WorldRenderFrameBuilderTests Foundation = foundation; } - public void ObserveDrawableCells(IReadOnlySet drawableCells) => - DrawableCells = drawableCells; - - public void ClearDrawableCells() => ClearCount++; } private sealed class RecordingAnimated( diff --git a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs index ba85ee06..e2374d64 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs @@ -194,9 +194,7 @@ public sealed class WorldSceneRendererTests "flat:clip", "flat:sky", "flat:terrain", - "visibility:mark", "flat:entities", - "frame:clear-cells", "passes:disable-clip", "particles:global", "flat:weather", @@ -220,7 +218,7 @@ public sealed class WorldSceneRendererTests } [Fact] - public void PViewWorld_PublishesTheWalkVisibleCellsOnce() + public void PViewWorld_PublishesTheWalkLandscapeCellsOnce() { var root = new LoadedCell { @@ -240,7 +238,6 @@ public sealed class WorldSceneRendererTests "passes:begin", "pview:draw", "visibility:mark", - "frame:observe-cells", "diagnostics:pview", "passes:disable-clip", "particles:none", @@ -265,7 +262,7 @@ public sealed class WorldSceneRendererTests } [Fact] - public void PViewWorld_PublishesLookInCellsToParticleAndLightVisibility() + public void PViewWorld_PublishesOnlyLandscapeCellsToParticleVisibility() { var root = new LoadedCell { @@ -276,15 +273,11 @@ public sealed class WorldSceneRendererTests rig.Renderer.Render(default); - // Retail gates effects on cell->IsInView() (ShouldDrawParticles - // @0x0050FE60), and a cell entered through a building portal - // (DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0) is - // drawn by the same traversal as a flooded cell. The particle gate and - // light-candidate scope must therefore receive the walk's one visited - // set, including look-ins. - Assert.Contains(0x01010003u, rig.Visibility.MarkedCells); - Assert.NotNull(rig.Frames.ObservedCells); - Assert.Contains(0x01010003u, rig.Frames.ObservedCells!); + // The borrowed result deliberately has three distinct products: + // DrawableCells contains the EnvCell, VisibleCells is their diagnostic + // union, and VisibleLandscapeCells contains only this outdoor id. + Assert.Equal([0x0101_0003u], rig.Visibility.MarkedCells); + Assert.DoesNotContain(0x0101_0100u, rig.Visibility.MarkedCells); } [Fact] @@ -925,15 +918,6 @@ public sealed class WorldSceneRendererTests return frame; } - public IReadOnlySet? ObservedCells { get; private set; } - - public void ObserveDrawableCells(IReadOnlySet drawableCells) - { - calls.Add("frame:observe-cells"); - ObservedCells = new HashSet(drawableCells); - } - - public void ClearDrawableCells() => calls.Add("frame:clear-cells"); } private sealed class EntitySource : IWorldSceneEntitySource @@ -995,7 +979,7 @@ public sealed class WorldSceneRendererTests { public HashSet MarkedCells { get; } = []; - public void MarkVisibleCells(HashSet cellIds) + public void MarkVisibleLandscapeCells(HashSet cellIds) { calls.Add("visibility:mark"); MarkedCells.UnionWith(cellIds); @@ -1015,13 +999,14 @@ public sealed class WorldSceneRendererTests public PViewRenderer(List calls) { _calls = calls; - // Distinct flood-only vs in-view sets: 0x01010003 is a look-in - // cell that is drawn but never part of the main flood. + // Distinct typed products: EnvCell shell preparation, diagnostic + // union, and the outdoor landscape IsInView answer. _interiorResult = new RetailPViewFrameResult().Reset( ClipFrameAssembler.BeginWalkFrame( ClipFrame.NoClip(), outdoorRoot: false), - [0x01010001u], - [0x01010001u, 0x01010003u], + [0x0101_0100u], + [0x0101_0100u, 0x0101_0003u], + [0x0101_0003u], default, default, diagnosticPartition: null); @@ -1030,6 +1015,7 @@ public sealed class WorldSceneRendererTests ClipFrame.NoClip(), outdoorRoot: true), [], [], + [], default, default, diagnosticPartition: null); @@ -1062,8 +1048,6 @@ public sealed class WorldSceneRendererTests public bool ThrowOnAbort { get; set; } - public HashSet? TerrainVisibleCellIds { get; } = [0x01010001u]; - public RenderFrameFoundation? SkyFoundation { get; private set; } public RenderFrameFoundation? WeatherFoundation { get; private set; } diff --git a/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs b/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs index 738c506b..3ee8f6e7 100644 --- a/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs +++ b/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs @@ -529,6 +529,62 @@ public sealed class ParticleSystemTests Assert.False(Assert.Single(sys.EnumerateEmitters()).ViewEligible); } + [Fact] + public void ApplyRetailView_UsesLandscapeMembershipButEnvCellConstantVirtual() + { + var sys = MakeSystem(); + var desc = new EmitterDesc + { + DatId = 0x3200007Bu, + Type = ParticleType.Still, + MaxDegradeDistance = 10f, + MaxParticles = 1, + }; + int outdoor = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 21u); + int environment = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 22u); + int cellLess = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 23u); + sys.UpdateEmitterOwnerCell(outdoor, 0x0101_0001u); + sys.UpdateEmitterOwnerCell(environment, 0x0101_0100u); + + sys.ApplyRetailView(Vector3.Zero, new HashSet { 0x0101_0001u }, true); + + ParticleEmitter[] first = sys.EnumerateEmitters().ToArray(); + Assert.True(first.Single(emitter => emitter.Handle == outdoor).ViewEligible); + Assert.True(first.Single(emitter => emitter.Handle == environment).ViewEligible); + Assert.False(first.Single(emitter => emitter.Handle == cellLess).ViewEligible); + + sys.ApplyRetailView(Vector3.Zero, new HashSet(), true); + + ParticleEmitter[] second = sys.EnumerateEmitters().ToArray(); + Assert.False(second.Single(emitter => emitter.Handle == outdoor).ViewEligible); + Assert.True(second.Single(emitter => emitter.Handle == environment).ViewEligible); + Assert.False(second.Single(emitter => emitter.Handle == cellLess).ViewEligible); + } + + [Fact] + public void ApplyRetailView_NoCompletedViewRejectsBothCellFamilies() + { + var sys = MakeSystem(); + var desc = new EmitterDesc + { + DatId = 0x3200007Du, + Type = ParticleType.Still, + MaxDegradeDistance = 10f, + MaxParticles = 1, + }; + int outdoor = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 31u); + int environment = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 32u); + sys.UpdateEmitterOwnerCell(outdoor, 0x0101_0001u); + sys.UpdateEmitterOwnerCell(environment, 0x0101_0100u); + + sys.ApplyRetailView( + Vector3.Zero, + new HashSet { 0x0101_0001u }, + hasCompletedView: false); + + Assert.All(sys.EnumerateEmitters(), emitter => Assert.False(emitter.ViewEligible)); + } + [Fact] public void ApplyRetailView_UnorderedOwnerDistanceMatchesRetailX87Comparison() {