Four live findings, each with the paper-verification failure named: G1 the container-capacity guard counted containers by a local type/capacity heuristic that over-classifies ordinary items; retail buckets from the wire's ContainerProperties at insert. Now reads ClientObjectTable's existing ContainerTypeHint (AP-168 narrowed to the shop-stock half; a pre-check must never false-block). G2 the amount bar never showed live because ACE never sets StackSize on browse listings — DescStackSize is null for every real vendor item and the C4 paper test hand-set the field, bypassing the materializer. The materializer now falls back to the packed supply count (AP-169, ACE adaptation); the new test drives the REAL materializer. G3 an out-of-range Use now dispatches ON ARRIVAL (pickup's shape): ACE's HandleActionUseItem only opens the vendor when the Use finds the player in range — a click-time send is greeted and dropped (AP-170, ACE adaptation; retail's server walks the player, ACE does not). G4 bought items appended because ACE's placement echo (UIQueue) can beat the CreateObject (SmartboxQueue) — cross-queue, no ordering guarantee — and the early echo was silently dropped. ClientObjectTable now stashes unresolved placements and replays them at Ingest: buys land at the retail list head. No register row — this RESTORES parity. Clean-room complete solution: 11,521 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
482 KiB
Retail Divergence Register — current through 2026-07-31
What this is. The single auditable register of every known place acdream's
runtime behavior can deviate from the retail client (Sept 2013 EoR build,
docs/research/named-retail/). It was triggered by a week of "small things"
surfacing one at a time through playtesting — a ±5 m culling-box promise
(#119), an epsilon eye-clip + rescue (knife-edge port), a half-ported cell
walk — each of which was a known deviation that lived only in a code
comment until it produced a visible symptom.
The rule. Every intentional deviation from retail behavior gets a row in this register. A deviation discovered without a row here is a bug twice over: once for the behavior, once for the missing row. When you add a deviation (new adaptation, new stopgap, new approximation), add the row in the same commit. When you retire one (port the retail mechanism), delete the row in the same commit.
The review trigger. Any unexplained visual or physics symptom → scan this register FIRST, before instrumenting. Filter by the subsystem you're staring at; each row's "Risk if assumption breaks" column is written as the symptom you would observe. Most of the historical multi-session sagas (#119 vanishing staircase, #98 cellar ascent, the doorway FLAP) began as a deviation in exactly this register's scope.
Kinds.
- Intentional architecture — deliberate design choices we stand behind; retiring them would be a redesign, not a fix.
- Adaptation — required by a real structural difference (async streaming vs synchronous load, ACE vs retail server semantics, Vulkan vs D3D). Correct given the difference; each carries an equivalence argument.
- Documented approximation — we know retail's mechanism and chose a cheaper/safer stand-in with a recorded justification.
- Temporary stopgap — known-incomplete; explicitly awaiting a port/phase. These are scheduled debt.
- Unclear — the recorded justification is missing, contradictory, or never argued. These are the most dangerous rows and head the retire list.
Dedup convention: one divergence = one row at its primary site; secondary
sites listed in parentheses. Issue numbers in bold are the symptom
history. Sources: 5-area code sweep 2026-06-12 +
docs/architecture/worldbuilder-inventory.md + docs/ISSUES.md
accepted-divergence entries (#96, #49, #50).
1. Intentional architecture (IA) — 18 active rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
| IA-1 | Contact-plane pre-seed on grounded movers (#96 ACCEPTED per ISSUES.md) — retail's CTransition::init clears contact_plane_valid; we seed from the body's previous-frame plane |
src/AcDream.Core/Physics/PhysicsEngine.cs:919 |
Removing it broke last-step stair step_up (892019b, reverted); seed propagates the body's real current plane, behavior matched retail in the A6.P3 gates |
A stale pre-seeded plane lets AdjustOffset project sub-step 1 onto a plane retail wouldn't have yet — wrong slope motion / step-up acceptance right after leaving a surface |
CTransition::init, pc:272547 family |
| IA-2 | Lateral self-heal beyond retail's keep-curr: when no candidate contains the sphere, try FindVisibleChildCell over the claim's stab-list before keeping the claim |
src/AcDream.Core/Physics/CellTransit.cs:912 |
Reuses the recovery retail's own AdjustPosition performs (:280028 stab-list mode), applied at the find_cell_list site to heal near-miss claims without a doorway crossing |
In containment-gap geometry, membership flips to a neighbouring room where retail keeps curr — wrong render root / collision cell at gap positions | find_cell_list keep-curr pc:308788-308825; find_visible_child_cell :311444 |
| IA-3 | NARROWED 2026-07-17 — get_state_velocity may prefer a nonzero dat cycle velocity (MotionData.Velocity × speedMod) over the decompiled constant. Production grounded player/remote translation no longer consumes this value; both use the literal CSequence root Frame. The accessor remains observable for jump launch and headless/test fallbacks |
src/AcDream.Core/Physics/MotionInterpreter.cs (get_state_velocity); grounded owners in PlayerMovementController / RemotePhysicsUpdater |
Installed Humanoid Walk/Run MotionData velocity is zero, so the retail constants remain the jump/fallback result. A nonzero exotic/modded cycle may override them, preserving the earlier adapter contract without affecting ordinary grounded motion | Jump horizontal speed for an exotic MotionTable with nonzero authored velocity can differ from the retail binary's constants | CMotionInterp::get_state_velocity 0x00527D50; CPhysicsObj::UpdatePositionInternal 0x00512C30 |
| IA-5 | Per-ENTITY vertex-derived AABB culling (+5 m animated-drift margin; animated entities bypass cull) vs retail per-PART dat drawing spheres | src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:693 (bounds at src/AcDream.Core/World/WorldEntity.cs:153, src/AcDream.Core/Meshing/GfxObjBounds.cs:14; dead PerEntityCullRadius=5.0f at dispatcher :210) |
Batched MDI rendering can't cheaply cull per part; bounds derive from the SAME dat vertex data that gets drawn (containment by construction — the #119 fix, 6a9b529; memory: feedback_culling_bounds_from_drawn_data) |
Geometry escaping bounds+margin (pose drift >5 m, a hydration path skipping SetLocalBounds) makes the whole entity vanish on-screen — the #119 vanishing-staircase class |
CGfxObj.drawing_sphere / viewconeCheck 0x005a09a4 |
| IA-6 | Chat scrollback 500 lines vs retail ~200 (configurable) | src/AcDream.Core/Chat/ChatLog.cs:19 |
Strictly more useful for a dev client + plugins; deliberate default | Negligible — only if a plugin/UI behavior is ever specified against retail's exact retention cap | retail chat scrollback (~200) |
| IA-8 | Synthetic outdoor cell node as render root (outdoor-as-cell, Option A): one unified DrawInside path; retail roots at a real CLandCell with a separate outdoor pipeline |
src/AcDream.App/Rendering/OutdoorCellNode.cs:23 |
Eliminating the inside/outside render branch kills the indoor FLAP by construction (2026-06-07 cutover); R-A2 restored retail's per-building flood topology | Any consumer assuming the root is a real cell mis-handles the synthetic node — historically the 2↔6 flood-depth oscillation and doorway-flap class | SmartBox::RenderNormalMode → DrawInside, decomp:92635; LScape::draw 0x00506330; ConstructView(CBldPortal) decomp:433827 |
| IA-9 | One unified camera matrix for terrain — retail's separate LScape::update_viewpoint landscape viewpoint does not exist |
src/AcDream.App/Rendering/TerrainModernRenderer.cs:266 |
Phase W T4.2: with one matrix everywhere, viewpoint-desync bugs are unrepresentable — the unification IS the correctness argument | Anything retail derives from the landcell-relative viewpoint (float precision at extreme coords, viewpoint-keyed state) has no analogue; a future port expecting it silently reads the camera | LScape::update_viewpoint; LScape::draw 0x00506330 |
| IA-10 | Transparent groups sorted back-to-front per GROUP by first-instance position (no within-group sort) vs retail per-poly BSP-order draw | src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:1364 (comparer :1662) |
One MDI call per pass requires group-granularity ordering; per-poly sorting is incompatible with instanced multi-draw; works when group instances are spatially coherent | Spatially spread or interleaved transparent groups composite in the wrong order — popping / wrong see-through layering as the camera moves | retail per-poly BSP-order transparent draw (D3DPolyRender / PView::DrawCells) |
| IA-11 | Tier-1 cross-frame batch-classification cache for static entities (retail re-walks part arrays every frame) | src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs:12 |
Issue #53 perf tier; invariants documented (keys = EntityId + OWNING-landblock hint post-#119 fix 2163308; invalidation at despawn/LB-unload; mutation audit 2026-05-10) |
Key collision or missed invalidation serves one entity another's batches — session-sticky wrong meshes (the #119 broken-stairs/water-barrel symptom) | retail per-frame part-array classification (no cache) |
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms). Synthetic wrapper borders/whole-window drag regions use the exact DAT Type-2/Type-9 control cursors. gmPanelUI children are independently imported retained frames rather than one physical parent, but RetailPanelUiController owns their one canonical geometry and exclusive child lifecycle |
src/AcDream.App/UI/README.md:3; src/AcDream.App/UI/CursorFeedbackController.cs; src/AcDream.App/UI/Layout/RetailPanelUiController.cs |
keystone.dll has no PDB/decomp; semantics are reconstructed from retail UI deep-dives, named client methods, and production LayoutDesc media. Separate child wrappers preserve each LayoutDesc's content tree while typed move/resize synchronization gives all registered toolbar/detail children the same persistent parent rectangle | Edge-case low-level input semantics can differ silently even though outer geometry, visibility, and restore-previous ownership match | UIElementManager::CheckCursor 0x0045ABF0; UIElement_Resizebar::StartMouseResizing 0x0046B7E0; UIElement_Dragbar::StartMouseMoving 0x0046C760; gmPanelUI::SetupChildren 0x004BC9E0; docs/research/retail-ui/04-input-events.md |
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | src/AcDream.Core.Net/Messages/GameEventType.cs:11 |
Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | docs/architecture/worldbuilder-inventory.md (code at src/AcDream.{Core,App}/Rendering/Wb/) |
WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail FSplitNESW (#51, pinned by SplitFormulaDivergenceTest), scenery drift accepted (AP-31) |
A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; tests/.../SplitFormulaDivergenceTest.cs |
| IA-15 | D.2b gameplay UI is our own UiHost/UiRoot retained tree, not a byte-port of Keystone. RetailUiRuntime owns the production import/mount graph; RetailWindowManager/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, schema-v2 per-character/per-resolution automatic layouts with per-window authored-geometry revisions, and portable named saveui/loadui profiles; RetailWindowFrame is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals 0x2100006C, chat 0x21000006, toolbar 0x21000016, character 0x2100002E, inventory 0x21000023 plus mounted 0x21000024/22/21, dialog catalog 0x2100003C, radar 0x21000074, and external container 0x21000008 (shared bevel plus a user-directed compact 700-pixel initial content width instead of its authored 800-pixel root). The dialog context/queue/callback lifecycle is now a named-client port; only its retained rendering remains under this Keystone adaptation. |
src/AcDream.App/UI/RetailUiRuntime.cs; src/AcDream.App/UI/RetailWindowManager.cs; src/AcDream.App/UI/RetainedPanelControllerGroup.cs; src/AcDream.App/UI/UiHost.cs; src/AcDream.App/UI/RetailWindowLayoutPersistence.cs; src/AcDream.App/UI/Layout/RetailWindowFrame.cs; src/AcDream.App/UI/Layout/RetailDialogFactory.cs; src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs; src/AcDream.App/UI/Layout/ExternalContainerController.cs; src/AcDream.App/UI/Layout/LayoutImporter.cs; binding supply in GameWindow.cs |
Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle; the external strip's initial width follows the connected visual direction and remains horizontally resizable to its authored extent or the viewport edge. | Persistence and low-level widget rendering are behaviorally reconstructed from retail semantics rather than a Keystone byte-port; lifecycle edge cases remain constrained by conformance tests. The external strip opens 100 pixels narrower than the raw LayoutDesc before user/persistence resizing. | Production LayoutDesc objects; DialogFactory @ 0x004773C0..0x00478470; gmExternalContainerUI @ 0x004CBAD0..0x004CBFE0; docs/research/2026-07-13-retail-dialog-factory-pseudocode.md; Keystone behavior notes in docs/research/retail-ui/ |
| IA-17 | Toolbar chrome is toolkit-supplied through the central RetailWindowFrame mount (UiCollapsibleFrame 8-piece bevel) because LayoutDesc 0x21000016 carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. |
src/AcDream.App/UI/Layout/RetailWindowFrame.cs; src/AcDream.App/UI/UiCollapsibleFrame.cs; toolbar policy in GameWindow.cs; spec: docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md |
The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through | The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) |
| IA-18 | Effect overlay tile (enum 0x10000005) is a ReplaceColor SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload SurfaceWindow::ReplaceColor 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy). |
src/AcDream.App/UI/IconComposer.cs (ReplaceWhiteFromSurface) |
Faithful port of IconData::RenderIcons @407614 → the SURFACE overload ReplaceColor 0x004415b0 (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). |
A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation | IconData::RenderIcons acclient_2013_pseudo_c.txt:407524; ReplaceColor SURFACE overload 0x004415b0:71656; docs/research/2026-06-17-stateful-icon-RESOLVED.md |
| IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail AutoTarget falls back to SelectNext(SELECTION_TYPE_COMPASS_ITEM), whose combat filter can also admit attackable enemy players in compatible PK states. |
src/AcDream.Core/Combat/CombatTargetPolicy.cs; consumers src/AcDream.App/Interaction/WorldSelectionQuery.cs (IsHostileMonster/FindClosestHostileMonster) and SelectionInteractionController.cs (SelectClosestCombatTarget). This row is auto-acquisition-only: as of #298, explicit-target admission and the combat camera route through the separate, retail-exact WorldSelectionQuery.IsAttackableTarget (ObjectIsAttackable-backed) instead, so a compatible-PK player is a valid manual attack/camera target — do not assume one predicate still serves both concerns. |
Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | ClientCombatSystem::AutoTarget @ 0x0056BC80; CPlayerSystem::SelectNext @ 0x0055F9A0; ClientCombatSystem::ObjectIsAttackable @ 0x0056A600 |
| IA-20 | The basic combat bar keeps dark-red media 0x0600715E visible as the centered middle baseline. Retail skill-gates field 0x100005EF to trained Recklessness; the separate bright child remains faithful live SetPowerbarLevel feedback from the absolute left edge. |
src/AcDream.App/UI/UiScrollbar.cs; child-policy extraction in src/AcDream.App/UI/Layout/DatWidgetFactory.cs |
Explicit connected visual direction: the dark middle track remains present behind live attack charge; the exact skill-gated treatment remains tracked by AP-112 | Untrained characters retain the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable | gmCombatUI::RecvNotice_SetPowerbarLevel @ 0x004CC0E0; gmCombatUI::ListenToElementMessage @ 0x004CC430; LayoutDesc 0x21000073 |
| IA-21 | When ACE sends player BoolProperty 68 (SpellComponentsRequired) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. |
src/AcDream.App/Spells/SpellComponentRequirementService.cs |
A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90; ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50; CSpellBase::InqScarabOnlyFormula @ 0x00597050 |
2. Adaptation (AD) — 52 active rows (AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — VendorPricing's double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed SetContactPlane keeps its per-write ContactPlaneCellId, which retail writes only at init_contact_plane; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at RuntimeEntityObjectLifetime.CommitWireCellRebucket, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was PhysicsEngine.Resolve's own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical RuntimeAcceptedPositionDriveController portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate
visible-cell availability, full-catalog containment-root validation, and the
zero-portals point-in-cell guard (rootless payloads are quarantined; only a
missing positive child below a valid root is the inside base case); AD-25 retired 2026-07-30 by the
shared PhysicsObjUpdate.HandleAllCollisions remote path; AD-11 retired
2026-07-23 by the exact low-bit ItemUses predicate; AD-31 retired 2026-07-15
by the DAT-authored portal-space viewport. Recent additions and splits:
AD-47/AD-48 (Vulkan sample/present behavior), AD-50..AD-52 (Campaign N), and
AD-53..AD-55 (Campaign P response-layer findings).
AD-2 clarification (placement Slice 4A, 2026-07-31): Core never creates cells
during placement, so retail DoNotCreateCells has no differential synchronous
loader branch there. Slice 4B must preserve the flag while mapping successful
deferred placement to exact-cell, generation-scoped asynchronous admission;
the presence of the flag in the immutable request is not claimed as exactness.
AD-2 retirement-receipt refinement (2026-08-02): a pending-only live projection bucket survives the atomic origin swap but is not a landblock presentation generation and emits no second full cleanup receipt. A genuine receipt-ledger invariant after spatial detachment is a committed terminal failure, never resumable detach work. This preserves the existing adaptation: one exact asynchronous cleanup owner for each synchronously destroyed retail landblock, while logical live objects survive streaming residence changes.
AP-1/AD-1 checkpoint (placement Slice 4B2 checkpoint 1, 2026-07-31): Runtime now owns the
exact accepted placement/lost-cell transaction, atomic body/contact/cell/
shadow/workset commit, adjusted retained frame, authored mover preparation,
exact-cell/generation wake, append/swap lost buckets, a bounded indexed
deadline heap, independent root/direct-child deadlines, and revisioned ordered
host receipts. A public generation-gated observe/retry/exact-ack channel now
projects that one receipt owner. Shared local-controller body adoption remains
deferred to the atomic all-route ownership cutover. Both rows remain open until
4B2 cuts graphical and no-window
production routes over, quiesces active placement before invoking the dormant
collision-retirement entry, and binds portal
authority to RuntimeWorldTransitState. AD-2 remains the deliberate async
readiness/requeue adaptation. See
docs/research/2026-07-31-canonical-set-position.md.
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
RETIRED 2026-07-31 (Campaign P Slice 1B). Transition.CliffSlide now consumes only collision_info.last_known_contact_plane.N, exactly as retail does. The invented LastWalkablePlane -> LastKnownContactPlane -> UnitZ fallback chain is gone; invalid/default or parallel data takes retail's degenerate OK_TS return. |
src/AcDream.Core/Physics/TransitionTypes.cs (CliffSlide); tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs |
— | — | CTransition::cliff_slide pc:272397 (0050a6d0); last_known_contact_plane maintenance pc:272659-272668 (~0050ad07) |
|
RETIRED 2026-07-31 (Campaign P Slice 1B). Every stored walkable polygon now routes unconditionally to PrecipiceSlide, including a plane steeper than FloorZ; the invented steep-walkable reroute to CliffSlide is gone. |
src/AcDream.Core/Physics/TransitionTypes.cs (EdgeSlideAfterStepDownFailed); tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs |
— | — | CTransition::edge_slide pc:273001-273090 (0050b3d0) |
|
RETIRED 2026-07-30 at 252e8068 — and RE-RETIRED 2026-08-07 after a revert resurrected the row text. The production constant has been the byte-confirmed 0.98480775f (cos 10°) since 252e8068, which also struck this row. Five hours later a8a7d64b — reverting the UNRELATED TS-4 commit 5e2be19b — restored this file's older hunk and resurrected the un-struck row while leaving the code fixed. The zombie row then cost a full duplicate byte-derivation on 2026-08-07 (independent confirmation, identical result: qword [0x007c6b28] = π/18 exactly, live FCOS, threshold cos(10°) = 0.984807753; see docs/research/2026-08-07-ad55-sledding-constant-byte-decode.md). The old 0.99999536f was ACE's error — the radian literal evaluated in degree mode — and acdream inherited then corrected it. Process rule filed to memory: reverting any commit that touched this register must re-verify EVERY row the revert's register hunk touches, because whole-hunk reverts of single-line rows silently undo unrelated row edits. Original text, retained: calc_friction's Sledding slope-flatness test compares GroundNormal.Z > 0.99999536f (≈0.175° from flat); the raw retail decomp literally computes __fcos(0.17453292519943295) (= cos(10°) ≈ 0.984808) and compares that against contact_plane.N.z — physically very different tests (0.175° accepts only essentially-perfectly-flat ground; 10° accepts any modest slope) |
src/AcDream.Core/Physics/PhysicsBody.cs (calc_friction, the Sledding near-flat branch) |
Filed 2026-07-30 splitting AP-7's retirement (Campaign P Slice P2). Two hypotheses, neither confirmed this pass: (a) BN misdecompiled a raw float-constant load as an __fcos() call (a known BN artifact class), or (b) ACE's own port made an independent error and cos(10°) is correct. 0.99999536f is kept provisionally — least churn, since it is what acdream's own prior (structurally unreachable) dead code already had — pending a live Ghidra decompile of 0050ee70 checking whether the FCOS opcode is real or a raw FLD of one of these two constants |
Currently harmless in production: nothing sets PhysicsState.Sledding client-side (see #166 research), so this branch is unreachable either way. The moment a data-authored Sledding toggle exists, the wrong constant changes which slopes get the light 0.2f sled-friction override vs. the heavier default |
CPhysicsObj::calc_friction pc:276694-276822 (0050ee70), the __fcos(0.17453292519943295) slope-flatness comparison; ACE PhysicsObj.calc_friction PhysicsObj.cs:2120-2141 (0.99999536f); docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1, §7 item 3 |
|
| AD-56 | **RESTORED 2026-08-07 — this row was collaterally DELETED by a8a7d64b (the revert of the unrelated-in-hunk TS-4 commit that created it) and stayed missing for eight days while its condition came back to life: TS-4's Path-6 shortcut deletion re-landed for real at Slice 2B (2026-07-31), so the plumb-fall freeze this row guards is reachable at HEAD and its pinning test (Ts4SteepRoofWedgeCaptureTests.FallOntoSteepSlope_PureVertical_..._RetailParity) still runs. Same resurrection mechanism as AD-55's zombie, opposite direction — see feedback_register_revert_resurrection. Original text: A body falling PERFECTLY PLUMB (zero horizontal velocity) onto a steep-but-below-FloorZ polygon (LandingZ-permissive, e.g. a steep roof) freezes at its landing position forever once Path 6's steep-poly shortcut is removed (TS-4). AdjustOffset's crease projection (Cross(ContactPlane.Normal, SlidingNormal)) against a purely-Z gravity offset is mathematically annihilated (Dot(slideOffset, offset) = 0 exactly, since slideOffset.Z = 0 and the offset is purely Z), tripping the abort-small-offset guard before TransitionalInsert can run again |
src/AcDream.Core/Physics/TransitionTypes.cs (AdjustOffset's crease-projection math, shared by every mover); pinned by tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs (FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity) |
Filed 2026-07-30 at the Campaign P final physics slice, split out of the retired TS-4 row. This is not a code bug: the mechanism is present identically in the raw retail decomp, ACE's port, and this port (docs/research/2026-07-30-ts4-116-oracle-plan.md §1.2 Step E, §1.3) — every one of the three references crushes a purely-vertical offset to zero the same way. A live player almost never produces this exact input: WASD, camera-relative movement, and even small numerical noise inject some horizontal component, which the SAME cross product does NOT annihilate (only a component exactly along the downhill/gravity line is removed) — confirmed by the decisive companion fixture FallOntoSteepSlope_WithHorizontalVelocity_..., which converges cleanly with a mere ±0.3 m/s residual horizontal component |
A hypothetical mover that manages a truly zero-horizontal-velocity approach to a steep-but-LandingZ-permissive surface (a vertical-drop elevator platform, a scripted teleport landing) would freeze identically to retail; not reachable by ordinary player/NPC movement | CTransition::adjust_offset pc:272271-272393 (0x0050a370); docs/research/2026-07-30-ts4-116-oracle-plan.md §1.2 Step E, §1.3, §1.5 |
| AD-46 | LIVE. Reframed at Campaign V slice V11 (2026-07-29), when GL was deleted and the comparison that discovered this row ceased to exist. Dense alpha-blended distant scenery (the treeline) may read slightly denser than retail's, because the anisotropic TAP PATTERN is implementation-defined and acdream's Vulkan driver does not tap identically to retail's D3D9 one. Both request the same sampler state — trilinear, clamp-and-repeat, the device's maximum anisotropy. What changed at V11 is only the left-hand side of the comparison: this was measured GL-vs-Vulkan (~15% of the pixels in the band), and it is now a Vulkan-vs-retail question against the D3D oracle in the last column. The measurement below is retained as the evidence that the residual is a tap pattern and not a bug, even though one of its two arms no longer exists. | src/AcDream.App/Rendering/Wb/WorldTextureArray.cs (RhiWorldTextureArray.WorldArrayAnisotropy); measured in plan §5.5.19, reframed §5.5.24 |
Not assumed — narrowed by measurement while both backends still existed, on an offline capture with no session, no entities and both clocks pinned. Anisotropy 1 → 41,509 differing pixels in the tree band; anisotropy 16 (GL's value, and retail's m_D3DCaps.MaxAnisotropy) → 22,266, and the rest of the frame fell to 497 px of 563,200, i.e. 8.8e-04, inside the campaign's 0.001 threshold. The residual was not a sub-pixel shift (an integer shift search found none), not a sharpness change (high-frequency energy matched within 5%), and not depth precision (forcing Vulkan's window-depth range to GL's compressed [0.5, 1] moved it by 3%). Monotone improvement toward GL's own anisotropy with no knob left is what made it a driver property rather than a bug. |
Distant foliage shimmers or reads denser than retail's. The class is confined to alpha-blended dense overlap: opaque terrain, roofs, walls, water, statics, the character and the whole retained UI are inside threshold. Now unfalsifiable by self-differential — with GL gone, the only way to retire this row is a side-by-side against the retail client, not against another acdream backend. | RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800, whose SetSamplerState(stage, 0xA /* D3DSAMP_MAXANISOTROPY */, m_D3DCaps.MaxAnisotropy) at 0x005a4230 is the value acdream requests |
| AD-47 | Filed at Campaign V slice V11 (2026-07-29); the campaign's risk register scheduled this row here. Multisample resolve sample POSITIONS are unspecified by both the Vulkan and D3D9 specifications, so acdream's MSAA-on silhouette edges do not match retail's pixel-for-pixel even at the same sample count. acdream's strict pixel gates therefore run with MSAA forced OFF on every arm, and MSAA-on gets only a relaxed visual smoke. | src/AcDream.App/RuntimeOptions.cs (ACDREAM_MSAA_SAMPLES); forced to 0 in tools/run-offline-pixel-gate.ps1 |
Measured, not assumed: plan §5.5.16 compared two backends at 4x and found 8.83% of the frame differing — 81,359 px of 921,600 — essentially all of it hugging foliage and silhouette edges, which is ninety-fold over the 0.001 gate threshold. That is two implementations' sample patterns, not a renderer divergence, which is why forcing MSAA off is what makes the remaining difference attributable rather than a threshold relaxation. | Edge quality on thin geometry (fence rails, foliage, distant railings) differs from retail at the sub-pixel level whenever MSAA is on, which is the ordinary player configuration. Because the gates run MSAA off, a real regression confined to the multisample path would not be caught by them — that is the actual exposure this row records. | D3D9 D3DRS_MULTISAMPLEANTIALIAS / D3DMULTISAMPLE_TYPE as set by RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800; retail's sample pattern is the driver's, exactly as ours is |
| AD-48 | Filed at Campaign V slice V11 (2026-07-29). Presentation is paced by the Vulkan swapchain present mode (FIFO, i.e. VSync) or by a refresh-rate software pacer when uncapped, rather than by retail's D3D9 Present with its own frame-rate limiter. Frame delivery cadence, and therefore input-to-photon latency, is a property of our present path rather than a port of retail's. |
src/AcDream.App/RuntimeOptions.cs:98-100; src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs |
Retail's limiter and ours both bound the frame rate to the display; the simulation is fixed-step and clock-driven, so gameplay timing does not ride on presentation cadence. The uncapped path exists for measurement and is not the shipping default. | A pacing mismatch shows up as judder or input latency that differs from retail's feel without any visual difference in a captured frame — invisible to every pixel gate by construction. Issue #235 (the capped/RDP jump-presentation cadence alias) is the known live instance of this class. | D3D9 IDirect3DDevice9::Present; retail's frame limiter in RenderDeviceD3D |
| AD-49 | Filed 2026-08-06 at the #334 fix. CellTransit.BuildShadowCellSetFromParts runs the outdoor cell rectangle AT SEED TIME for an outdoor seed, then gates only the growing-array WALK on cell residency. Retail's find_bbox_cell_list @0x00510fc0 gates everything on obj->cell (0x00510fed test eax,eax / je 0x511020), reaching add_all_outside_cells only from the walk. Retail can: a placed CPhysicsObj always holds a resident CObjCell. acdream's CellGraph residency is transiently false during landblock streaming (the #168 / #169 residence-race family), so deferring the rectangle to the walk would drop a landblock static or a live entity to a SINGLE cell for the window before its landblock publishes. This is the same residency policy BuildShadowCellSet already applies to its outdoor seed - retail's CObjCell::find_cell_list calls add_all_outside_cells at 0x0052b53f, ahead of the arg4 walk gate at 0x0052b576 - so the two registration floods differ only in sphere-vs-box, which is the whole of #334. |
src/AcDream.Core/Physics/CellTransit.cs (BuildShadowCellSetFromParts seed block) |
Keeps the sphere and box floods on ONE residency rule, so a future streaming-race fix has one place to change rather than two that disagree. The alternative - retail's literal shape - would introduce a new transient under-inclusive window, which is the #98 / #168 direction. | Over-inclusive only: an object whose landblock is not yet resident registers its full rectangle immediately instead of after the reflood (ShadowObjectRegistry.RefloodOwnerForLandblock, driven by LandblockPhysicsContentBuilder.PublishStaticCollision's tail). The rows are correct the moment the cells exist; nothing is registered that the box does not span. |
CPhysicsObj::find_bbox_cell_list 0x00510fc0 (0x00510fe2 / 0x00510fed); CObjCell::find_cell_list 0x0052b4e0 (0x0052b53f / 0x0052b576) |
| AD-50 | Filed at Campaign N slice N2 (2026-07-29). The inbound sequence tracker's watermark (highestIDReceived_) initializes to 1, not retail's zero-init of ReceiverData. Watermark INIT only — every mechanism (sanity window, duplicate/parked-key path, gap walk, re-park, RejectRetransmit abandonment) is the verbatim retail port. |
src/AcDream.Core.Net/Transport/InboundSequenceTracker.cs (AceInitialWatermark) |
ACE never emits S2C sequence 1: its PacketSequence starts unprimed at uint.MaxValue, the cleartext ConnectRequest takes NextValue 0, and the first ENCRYPTED flush re-primes CurrentValue to 1 so the first encrypted sequenced packet is 2 (ACE NetworkSession.cs:716-717 + Sequence/UIntSequence.cs:9-13,30-41; pinned by the N0 double and the N2 clean-lifecycle conformance test asserting min encrypted S2C sequence == 2 with zero NAKs). A zero-init watermark would gap-walk the permanent id-1 hole: one spurious NAK, the first pre-drawn word mis-assigned to id 1, and the keystream off by one from the very first encrypted packet. holtburger seeds the same value (crates/holtburger-session/src/session/api.rs:30, last_server_seq: 1), mirroring ACE's own C2S-side lastReceivedPacketSequence = 1 (NetworkSession.cs:57). |
Against a hypothetical server that DOES emit sequence 1 as its first encrypted packet (retail's own numbering), init-1 would classify it "not newer" and drop it as a duplicate — the mirror-image wedge. Only ACE-family servers exist for this client today. | ReceiverData zero-init (construction inside SharedNet; highestIDReceived_ starts 0); SharedNet::ProcessNewestSeqNum @ 0x00541930 (the walk that would mis-NAK id 1) |
| AD-51 | Filed at Campaign N slice N4 (2026-07-29). The inbound sequence tracker keeps a reclaimed-word pool (per-parked-word draw ordinals + PriorityQueue consumed lowest-draw-order-first) that retail has no counterpart for: on a VALIDATED cleartext RejectRetransmit, the word the gap walk parked for the reject packet's OWN sequence is removed, every later-drawn parked word is shifted down one position, and the excess word feeds the next fresh draws. |
src/AcDream.Core.Net/Transport/InboundSequenceTracker.cs (OnCleartextRejectSequence, NextWord, ParkedWord); trigger at src/AcDream.Core.Net/WorldSession.cs (RejectRetransmit consumption) |
Retail's inbound invariant is "every missing id was an encrypted packet whose keystream word the server drew" — true against retail servers, whose cleartext packets always borrow live sequences (acks/NAKs reuse highestIDSent_; FlowQueue::TransmitNewPackets @ 0x00547A60 sequences only reliable packets). ACE breaks it in exactly one place: RejectRetransmit takes a FRESH sequence through FlushPackets, cleartext, drawing NO S2C keystream word, and is cached (ACE NetworkSession.cs:299-304, :722-725, :743-748). Without the reclaim, our gap walk pre-draws a word for that id, the inbound stream runs permanently one word ahead, and every later encrypted packet fails checksum — the N2 desync class reintroduced through the reject path. The pool is provably empty against a retail server, so retail behavior is untouched. Reject BODY ids keep the N2 discard (their words were drawn on both sides — consumed-in-place). Known unreachable corner: a reject whose own id later appears inside another reject's body (first reject pruned after 120 s of sustained loss with the session alive) would discard a never-drawn word; probabilistically impossible against ACE's 60 s silence timeout and the 0.6 s NAK cadence. |
Against a hypothetical non-ACE server that assigns fresh cleartext sequences to packets OTHER than RejectRetransmit, those ids would still mis-park with no reclaim trigger — inbound desync. Only ACE-family servers exist for this client today, and ACE has exactly the one path. | SharedNet::ProcessNewestSeqNum @ 0x00541930 (the gap walk whose invariant ACE breaks); SharedNet::HandleEmptyAck @ 0x005448F0 (retail's reject consumption — body ids only, no own-sequence machinery because retail never needs it) |
| AD-52 | Filed at Campaign N slice N6 (2026-07-29). The inbound fragment assembler evicts incomplete partial messages 60 s after their last ACCEPTED fragment (swept on retail's 5 s flush cadence from ReliableTransport.Sweep) and remembers the last 64 completed multi-fragment sequences in a ring so a late duplicate fragment of an already-completed message drops instead of allocating a fresh partial that can never complete. Retail's prune target and horizon differ: its 5 s-TTL FlushTimedOutEphInfo table holds ephemeral-blob ORDERING stamps (the AD-49 deferral), not partial payloads. |
src/AcDream.Core.Net/Packets/FragmentAssembler.cs (SweepExpired, PartialTtlSeconds, CompletedRingSize); cadence in src/AcDream.Core.Net/Transport/ReliableTransport.cs (AssemblerSweepSeconds) |
N4's RejectRetransmit abandonment made an unrecoverable partial a REACHABLE permanent state: ACE pruned a fragment-bearing packet from its 120 s S2C cache and told us to stop asking, so that blob can never complete — without a TTL it leaks for the session's lifetime. 60 s is ≫ every recovery horizon (0.6 s NAK cadence, ACE's 2 s ack, the 120 s cache) and the stamp refreshes on every accepted fragment (retail's own re-stamp rule, ArrivedEphInfo::UpdateNetBlobID @ 0x0054AE00), so only a server-abandoned partial can age out — a merely-slow one cannot. The ring is bounded (64 × 4 B) and its only false negative (a duplicate arriving after 64 later completions) degrades to the pre-N6 behavior, now reclaimed by the TTL. |
If ACE ever legitimately re-served a fragment of a completed message under a REUSED fragment sequence within the ring window, it would be dropped — but fragment sequences are strictly monotonic per session (ACE SessionConnectionData.FragmentSequence), so reuse cannot happen inside one connection. An evicted partial whose fragments later straggle in re-partials and re-evicts — bounded churn, no corruption. | Indicator::FlushTimedOutEphInfo @ 0x0054A3D0 (the 5.0 s flush gate at 0x0054A3DC); ArrivedEphInfo::fTimedOut @ 0x0054AE30 (per-entry 5.0 s TTL); ArrivedEphInfo::UpdateNetBlobID @ 0x0054AE00 (re-stamp on update); retail has no partial-payload TTL — its blob layer trusts its own NAK persistence, which N4's ACE-mandated abandonment (SharedNet::HandleEmptyAck @ 0x005448F0) breaks |
| AD-38 | Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal elapsed >= 1.0 state edge. Incoming fades retain the exact timer. |
src/AcDream.Core/World/TeleportAnimSequencer.cs (OutgoingViewportReachedTerminalProjection) |
An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. | Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. | UIGlobals::GetAnimLevel @ 0x004EE540; gmSmartBoxUI::UseTime @ 0x004D6E30; paired retail/acdream captures documented in docs/research/2026-07-15-retail-portal-space-pseudocode.md |
RETIRED 2026-08-05 (C5a deletion sweep). The legacy recoverable outdoor demote (Resolve's indoor-claim safety net) and the outdoor-restore max(terrainZ, z) lift this row described were PhysicsEngine.Resolve's own body — deleted outright with the exhaustive C5a caller census proving zero production callers (every production placement writer reaches canonical PhysicsEngine.SetPosition only through RuntimeSetPositionState). The divergent mechanism is unreachable from production because it no longer exists. |
src/AcDream.Core/Physics/PhysicsEngine.cs (deletion); docs/research/2026-08-05-c5a-contract.md |
— | — | GotoLostCell pc:283418; SetPositionInternal 0x00515bd0, pc:283892-283945; CPhysicsObj::handle_all_collisions 0x00514780 |
|
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. #229 refinement (2026-07-20): login and F751 portal-space exit now share WorldRevealReadinessBarrier, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (IsSpawnCellReady); an outdoor claim requires those render domains plus terrain/collision residency across the DERIVED reveal window. #280 amendment (2026-08-05): that outdoor window is no longer a hardcoded radius-1 neighbourhood. Retail has exactly ONE landscape square — LScape::mid_radius, assigned directly from the Render.LandscapeDrawDistance preference (SmartBox::SetRegion @0x004531F0; values Render_LandscapeDrawDistance_Values @0x007CA988 = {3,5,8,11,15,25}, default 8, byte-verified) — and that same square is simultaneously the loaded set, the drawn set, and the set LScape::PreFetchCells @0x00505660 blocks on, so retail structurally cannot stream farther than it gates. acdream now DERIVES the outdoor radius from the live streaming window (QualitySettings.FarRadius, read per evaluation from StreamingController so a mid-hold Settings change re-arms the gate the way SmartBox::set_mid_radius @0x00453180 does), and the render-completeness predicate is TIER-AWARE to match acdream's two-tier landscape: inside NearRadius, full Near publication (IsNearTier && IsRenderReady); out to FarRadius, terrain publication only (IsRenderReady, which a Far-tier landblock satisfies through an empty spawn-adapter registration installed after its terrain upload crossed the render-thread barrier). #280 review correction (2026-08-06): as originally written this row asserted that property of a PublicationKind.Far publication only, which was true but not exhaustive — a landblock also reaches Far tier by Near→Far DEMOTE, and the demote's LandblockRetirementStage.MeshReferences left LandblockSpawnAdapter.WantsLoaded == false on a landblock that stays loaded and drawn, with no path that re-publishes it. Both review lenses found the same defect: one demoted member anywhere in the far ring made the gate unsatisfiable for the life of the streaming window (permanent portal-space hang, no recovery short of relog), reachable by two consecutive recalls to the same landblock with walking in between, or by a mid-hold quality-preset drop. The two routes are now genuinely equivalent — GpuWorldState.ReleaseLandblockMeshReferences re-asserts the empty Far registration after retiring the Near layer — rather than the predicate being taught to tolerate two meanings of "ready". Composite-texture warmup stays NearRadius-scoped because it is entity-scoped and Far builds carry no entities, and (same correction) its TRIGGER is scoped the same way: gating warmup on the whole widened gate serialised every composite upload behind the last outer-ring landblock, which is a hold longer than the streaming work requires. The destination reservation opens at exactly the gate's radius, since retail has one square for both. Runtime's readiness invariant is correspondingly a SHAPE check (indoor ⇒ 0, outdoor ⇒ ≥1), never a re-encoded value — Runtime does not own the graphical host's streaming configuration. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud unhydratable-placement path. RuntimeWorldTransitState owns the shared reveal generation, accepted readiness, transit correlation, and exact generation/cell-scoped host-acknowledgement suffix. WorldRevealCoordinator is a graphical adapter holding only App resource receipts; normalized Runtime checkpoints observe ownership without defining another readiness path. Slice E3 refinement (2026-07-24): the same generation now publishes an immediate WorldGenerationQuiescence edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. Slice E4 refinement (2026-07-24): accepted render/physics/static publication may span update frames through retained exact cursors, but reveal still consumes only the completed spatial/render-ready generation; building and EnvCell snapshots remain invisible until complete and the final spatial identity swap stays observer-atomic. Slice E5 refinement (2026-07-24): the reveal generation owns one exact destination reservation across every typed budget dimension. Stale completion cannot consume or clear its replacement, and hydratable incomplete content is never force-revealed; portal transit retains the DAT tunnel and centered retail wait cue until readiness converges. The hold→materialize→regain-control lifecycle remains owned by TeleportAnimSequencer. C4 route 3 refinement (2026-08-04): retail places the local player IMMEDIATELY on the accepted destination Position (SmartBox::TeleportPlayer @0x00453910) and blocks SIMULATION on DAT prefetch (CellManager::blocking_for_cells; SmartBox::UseTime @0x00455410 runs only CheckPrefetchStatus) behind the portal viewport; acdream defers the PLACEMENT itself to this reveal-ready Place edge, executed by the canonical RuntimeAcceptedPositionDriveController portal arm (TryExecuteAcceptedPortalArrival). Two load-bearing notes from that route: (1) every accepted local Apply — including the portal destination Position itself — still writes the raw wire pose onto the local player's WorldEntity via the ordinary generic-remote-render-pose path while portal space covers the viewport (LiveEntityNetworkUpdateController.cs, OwnsSteadyState false for the local player's null route); the committed Place receipt's presentation suffix overwrites it with the resolved pose — tolerated, not suppressed, since suppressing it would be an unowned behaviour change on the ordinary local Apply path (AP-131/#275 territory). (2) The constraint-leash re-arm on a committed portal placement anchors at the RESOLVED post-placement body position (PlayerMovementController.CommitCanonicalTeleportFrame → RearmConstraintLeashAtCurrentPosition), where retail's ConstrainTo @0x0045418A anchors at the received WIRE destination; the two differ by at most the placement adjustment (ring search/floor snap) and the anchor is write-only downstream, so the delta is not user-observable — switching to the wire-destination anchor is a deliberately deferred decision, not adopted here. B4 round-3 review refinement (2026-08-05): the wait cue's trigger predicate (LocalPlayerTeleportController.Tick's placementReady = dataReady && TryAdvancePortalCommit(sequence), gating the cue at haveDestination && !placementReady) now covers a SECOND, distinct cause beyond the original streaming/DAT-readiness gate this row described: TryAdvancePortalCommit returning false while a DeferredCell park is outstanding or a fresh placement attempt has not yet succeeded (B1's TryConsumePortalCommit gate). The cue's five-second trigger and centered-tunnel behavior are unchanged (that trigger is an acdream divergence in its own right — AP-150, filed 2026-08-06); only the SET of conditions that can hold it open grew from "world data not ready" to "world data not ready OR canonical placement not yet committed" — a slow-publishing destination-landblock collision generation now presents identically to a slow asset stream, which is the correct retail-faithful degradation (both are blocking_for_cells causes retail itself does not distinguish), but is worth naming here since a future debugging session seeing the cue must not assume streaming is the only possible cause. |
src/AcDream.Runtime/World/RuntimeWorldTransitState.cs; src/AcDream.App/Streaming/WorldRevealCoordinator.cs; src/AcDream.App/Streaming/WorldGenerationQuiescence.cs; src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs; src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs; src/AcDream.App/Streaming/LandblockPresentationPipeline.cs; src/AcDream.App/Streaming/StreamingController.cs; src/AcDream.App/Rendering/PortalTunnelPresentation.cs; src/AcDream.App/UI/PortalWaitNoticeController.cs; src/AcDream.App/Streaming/GpuWorldState.cs (IsRenderReady); src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs; src/AcDream.Core/Physics/PhysicsEngine.cs (IsSpawnCellReady, IsNeighborhoodTerrainResident) |
This is the asynchronous equivalent of retail leaving SmartBox::position_update_complete false while CellManager::blocking_for_cells is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. |
Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit remains in the authored tunnel and presents the centered wait cue after five seconds — that five-second arming is acdream's own and is NOT retail's trigger; see AP-150. | SmartBox::UseTime 0x00455410; gmSmartBoxUI::UseTime 0x004D6E30; gmSmartBoxUI::EndTeleportAnimation 0x004D65A0; LScape::PreFetchCells 0x00505660; LScape::SetMidRadius 0x00504C00; SmartBox::set_mid_radius 0x00453180; Render_LandscapeDrawDistance_Values 0x007CA988 |
| AD-5 | Outdoor point_in_cell is an identity compare against the global XY-column cell from LandDefs.AdjustToOutside (no per-cell containment test) |
src/AcDream.Core/Physics/CellTransit.cs:865 |
Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | find_cell_list pick pc:308788-308825; CLandCell::point_in_cell (get_block_offset pc:308804) |
RETIRED 2026-07-31 (placement/streaming Slice 3B). Cell/cache/topology/building/static-shadow publication plus every retained non-suspended owner touching or withdrawn from the prefix is one Runtime-owned collision generation. Retained includes dynamics and adjacent-root statics; only target-root statics are superseded by the authored replacement. App and Headless build one shared off-side CollisionWorldState through one-work-unit preparation/capture/seal cursors. Admission captures the active root in O(1); a stable landblock/owner slot suffix materializes non-target leaves incrementally, so resident-world size cannot become a synchronous clone spike. Reusable per-prefix owner slots and one Runtime-scoped versioned journal replace event-time exact-copy fanout: repeated live mutations coalesce by owner, every draft reconciles only that owner's latest exact state one owner per seal call, discovered relevant owners receive scoped exact updates, and visited unrelated owners receive only a cheap coalesced dirty notification before metered replay. Once topology sealing finishes, observed owners temporarily write through exactly until same-call activation; the finite pre-seal queue therefore drains even under continuous multi-owner movement. New drafts start at their captured journal suffix; old slots are superseded rather than reused behind live cursors and compact through the same meter. Unrelated churn therefore never restarts or starves target capture/sealing. Deterministically ordered concurrent preparations receive committed—not merely sealed—peer deltas and rebase one cache, graph, landblock, or owner leaf per seal step; cancellation therefore cannot leak unpublished topology. Demotion/withdrawal cancels a matching queued or active rebase, suppresses the prefix in unfinished source scans, and retires one owner/cache/graph/outdoor leaf per seal call. The complete previous generation remains queryable until one zero-managed-byte volatile root transfer in the same update-thread call as final reconciliation; that preserves PhysicsDataCache, CellGraph, PhysicsEngine, and ShadowObjectRegistry facade identity, revokes staging, and requires no quiet frame. A stale admission or staging failure disposes only that private generation and cannot withdraw the active world or invalidate a newer admission. Authored same-ID target statics, live-current-cell changes, owner departure/reuse, newly relevant seam-crossing statics, and teardown remain coherent across drafts; empty per-prefix owner containers are reclaimed without invalidating captured seal cursors. The commit clears repaired withdrawal markers before its single notification/readiness acknowledgement, so no optional hydration callback can omit reflood and no observer sees mixed old/new cells. |
src/AcDream.Runtime/Physics/RuntimePhysicsState.cs (PrepareCollisionGeneration, AdvanceCollisionGenerationPreparation, AdvanceCollisionGenerationSeal, CommitCollisionGeneration); src/AcDream.Core/Physics/CollisionWorldState.cs; PhysicsDataCache.cs; PhysicsEngine.cs; ShadowObjectRegistry.cs; src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs; src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs; tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs; tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs; tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs |
— | — | CObjCell::init_objects → CPhysicsObj::recalc_cross_cells, 0x0052b420 / 0x00515a30; CPhysicsObj::SetPositionInternal shadow replacement tail 0x00515330 |
|
RETIRED 2026-08-06 by deletion. The row's justification ("remote bodies don't run a full local transition sweep") was false at HEAD: RuntimeRemotePhysicsUpdater.Tick calls PhysicsEngine.ResolveWithTransition with the remote's own body, and that sweep runs acdream's port of CTransition::adjust_offset once per sub-step. (Wording corrected 2026-08-06 at the retail review, F2: this row and four other places called that port "verbatim"/"faithful". It is structurally exact BUT carries exactly two divergences, filed the same day as AD-65 and AD-66 -- so the unqualified word was false from the very next commit. It is a STRUCTURALLY EXACT port with two filed exceptions.) So this was never a relocation of a missing mechanism — it was an EXTRA pre-sweep projection layered on top of the faithful one, against a surface retail never uses (SampleTerrainNormal(x, y), an XY-only landblock lookup blind to the body's Z, its cell, buildings, EnvCells and statics). Measured before deleting: with the projection forced null at both fork sites, the production trajectory of a remote running 30 ticks down a 31-degree ramp is BIT-IDENTICAL, a 8.4-degree ramp differs by at most 2.8e-5 m in Z, and the whole AcDream.Runtime.Tests suite is unchanged. Deleted: both RuntimeRemotePhysicsUpdater sample sites, the terrainNormal parameter and projection block on RemoteMotionCombiner.ComposeOffset AND on the production-dead ComputeOffset, and the now-callerless PhysicsEngine.SampleTerrainNormal. Removing the parameter is what makes an AP-22-shaped one-site-only regression a compile error. Noted 2026-08-06 at the retail review (F3): this redundancy measurement is CONTINGENT on AD-65 -- the two mechanisms agree today partly because both under-travel downhill. That makes this deletion a PREREQUISITE for fixing AD-65 rather than merely compatible with it: had the projection survived, correcting AdjustOffset would have re-introduced a disagreement between two live projections. Two claims in the old row were also stale/backwards and did not survive: it described ComposeOffset's guard as "interpolation-active" when the code is if (!interpolationOverwrote ...), and its second cited site (ComputeOffset ~:163-168) had zero production callers. The roof clause was stale too — since Bug B (204d0ae0) the sample was gated on OnWalkable, and a steep roof is OnWalkable == false, so the path did not run on #32's geometry at all. UNTESTED AXIS, recorded 2026-08-06 at the AD-10 architecture review: the contract's T2 -- its mandatory wrong-plane-versus-right-plane discriminator -- was dropped without record, in breach of the contract's own "record it as an untested axis rather than silently dropping it" clause. Consequence: this change's only claimed BENEFIT (a walkable NON-TERRAIN surface -- bridge, dock, dungeon ramp -- now gets the committed contact plane instead of the terrain plane far below) has ZERO automated coverage and rests on source reasoning alone. The deletion itself is measured; the benefit is not. |
src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs; src/AcDream.Core/Physics/RemoteMotionCombiner.cs; src/AcDream.Core/Physics/PhysicsEngine.cs (deletion); tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs; tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs |
— | — | CTransition::adjust_offset 0x0050a370, pc:272271-272393 (the old anchor pc:272296-272346 truncated both the sliding-normal validity gate at the head and the entire safety push-out block at the tail); per-step call from CTransition::find_transitional_position 0x0050bdf0 |
|
RETIRED 2026-07-23 — the matching binary disproved the old nonzero interpretation: ItemUses::IsUseable executes not bitfield; and eax,1, so absent/reset zero is usable and only USEABLE_NO disables use. Toolbar, item policy, and world interaction now share that exact Core predicate. |
src/AcDream.Core/Items/ClientObject.cs (ItemUseability.IsUseable); src/AcDream.Core/Items/ItemInteractionPolicy.cs; src/AcDream.App/Interaction/WorldSelectionQuery.cs |
— | — | ItemUses::IsUseable @ 0x004FCCC0; matching v11.4186 instructions recorded in docs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.md |
|
| AD-12 | SecondaryAttributeTable coefficients hardcoded (Health=End×0.5, Stam=End×1.0, Mana=Self×1.0) instead of dat-read; unknown attributes contribute 0 | src/AcDream.Core/Player/LocalPlayerState.cs:279 |
Coefficients never vary across retail dat versions; re-confirmed by ACE AttributeFormula.cs + holtburger; dat port can replace later | A customized portal.dat with modified vital formulas silently yields wrong max-vitals; a missing attribute snapshot underestimates max | SecondaryAttributeTable portal.dat 0x0E0..0x0E2; CreatureVital::GetMaxValue 0x0058F2DD |
| AD-13 | 1-second dedup window for identical system chat messages (retail has none) | src/AcDream.Core/Chat/ChatLog.cs:29 |
ACE dual-sends the same system text (0xF7E0 + 0x02EB) for back-compat; without dedup every line doubled (Phase J compromise) | Two genuinely distinct but textually identical system messages within 1 s collapse to one line where retail shows both | ACE dual-send 0xF7E0 + 0x02EB |
| AD-15 | IsEnv masks low-16 of the cell id ((Id & 0xFFFF) >= 0x100) where retail tests the full id |
src/AcDream.Core/World/Cells/ObjCell.cs:25 |
Every real prefixed EnvCell id has low-16 ≥ 0x100 and every outdoor cell ≤ 0x40 — identical answers for all real dat ids, works for both bare and prefixed forms | None for real dat data; a hypothetical convention-violating id would route to the wrong (BSP vs terrain) point-in-cell logic | CObjCell::GetVisible pc:308215 |
| AD-16 | Building-flood gate is a CPU frustum test on each building's PortalBounds AABB; retail floods exactly when the shell draws and an aperture survives (no bounds constant anywhere) |
src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs (RuntimeWorldFrameBuildingSource.Gather) |
Documented as the tight equivalent of the shell viewconeCheck for flood purposes (the FPS fix the Chebyshev≤1 hack approximated); per-portal admission still goes through BuildFromExterior's screen clip; missing-bounds buildings always flood (safe over-include) | A too-small/stale PortalBounds AABB means the interior never floods — doorway shows a hole/black aperture from outside (inverse of the vanishing-staircase class) | DrawBuilding 0x0059f2a0; BSPPORTAL::portal_draw_portals_only 0x53d870 |
| AD-17 | ≤8 GPU gl_ClipDistance half-planes per view region, degrading to a union-AABB scissor (over-include) on multi-polygon / >8-edge views; particles always scissor; scissor slices disable per-object viewcone culling. Retail CPU-clips against the exact portal polygon |
src/AcDream.App/Rendering/ClipPlaneSet.cs:23 |
Vulkan's VkPhysicalDeviceLimits::maxClipDistances floor is 8, the same number GL guaranteed, so the V11 backend change does not move this limit; invariant documented: over-inclusion is safe, under-inclusion is the bug class |
Fallback on complex multi-aperture views draws terrain/sky/particles/objects outside the true aperture but inside its AABB — background/interior bleed strips at doorways (the #130 family) | ACRender::polyClipFinish decomp:702749; PView portal_view slices |
| 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-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 | Live entities with ServerGuid != 0 and null ParentCellId are culled (ClipSlotCull) while indoor clip routing is active; retail objects are always cell-resident (synchronous add-to-cell at creation) |
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:484 |
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) |
| AD-27 | PickUp fires on natural moveto completion via the MoveToComplete client-addition seam (retail's CleanUpAndCallWeenie contains no weenie call in this build and notifies nothing on arrival). The companion MoveToCancelled seam only withdraws the waiting pickup presentation/action. Use retired 2026-07-25: ItemHolder::UseObject sends Event_UseEvent before CPlayerSystem::UsingItem; acdream now does the same and leaves approach to ACE's authoritative MoveToChain. |
src/AcDream.App/Interaction/SelectionInteractionController.cs (OnNaturalMoveToComplete/OnMoveToCancelled); src/AcDream.App/Input/PlayerModeController.cs (player MoveTo seam binding); src/AcDream.Core/Physics/Motion/MoveToManager.cs (MoveToComplete/MoveToCancelled) |
ACE's server-side pickup chain may have timed out by the time our body arrives; the close-range deferred send hits ACE's WithinUseRadius fast-path. | If the server's chain has not timed out, pickup may execute twice or produce protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius; MoveToManager::CleanUpAndCallWeenie 00529650 §7e (no weenie call); ItemHolder::UseObject 0x00588A80 |
| AD-28 | Chat transcript (UiText) and input (UiChatInput) are two separate widget classes placed inside their dat-authored container panels; retail's ChatInterface uses a single mode-flagged UIElement_Text (Type-12) that switches between read and edit mode |
src/AcDream.App/UI/Layout/ChatWindowController.cs:135 (transcript) + :150 (input) |
UIElement_Text is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture |
A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | UIElement_Text (Type-12) @ keystone.dll; gmMainChatUI::PostInit @0x4ce130 |
| AD-29 | ClientObjectTable fires global ObjectAdded/ObjectUpdated/ObjectRemoved events; consumers filter by guid on their end. Retail dispatches per-object via NoticeRegistrar observer dispatch — each UI cell observes only its specific object guid |
src/AcDream.Core/Items/ClientObjectTable.cs:48 (events); src/AcDream.App/UI/Layout/ToolbarController.cs:115 (guid filter) |
NoticeRegistrar is inside keystone.dll with no PDB/decomp; global broadcast + consumer-side filter is functionally equivalent for the current panel count and object volumes seen in practice |
At high object counts (>1 000 objects), every ObjectUpdated wakes every subscribed consumer — O(n·m) notification cost instead of retail's O(1) per-observer dispatch; a consumer that forgets the guid filter processes all objects (a latent correctness bug) |
NoticeRegistrar (keystone.dll, no PDB); retail per-object observer registration in CObjectMaint |
| AD-30 | Cell-march preserves seed landblock id when TryGetTerrainOrigin returns false for an outdoor seed (#145 D, 2026-06-22): BuildCellSetAndPickContaining returns currentCellId verbatim rather than marching via blockOrigin=(0,0,0); retail never encounters this state (cells stored block-local, no streaming-gap concept) |
src/AcDream.Core/Physics/CellTransit.cs:765 |
Equivalence argument: "preserve-verbatim when unregistered" is the same contract as PhysicsEngine.Resolve's NO-LANDBLOCK branch; the player's cell stays the last known-correct cell until the landblock's terrain registers — no march, no lbX=0 wire |
A body whose seed landblock is genuinely absent for >1 physics tick holds its last-known cell rather than discovering the true containing cell; transient only — corrects the instant terrain registers; an indoor seed is explicitly excluded from the guard (outdoor low < 0x100 gate) | CObjCell::find_cell_list + block-local storage (retail has no streaming gap); TryGetTerrainOrigin pc path |
| AD-32 | Movement, Position, State, Vector, Pickup, Delete, and ObjDesc packets for a FUTURE object incarnation are dropped until its CreateObject arrives; retail queues each blob for the not-yet-created object (SmartBox::QueueBlobForObject, dispatch return 4) and replays it after construction. F754/F755 and ParentEvent are no longer part of this divergence: EntityEffectController preserves one mixed effect FIFO per server GUID until the canonical local owner is ready, while runtime-owned ParentAttachmentState retains parent relations by generation. Older-incarnation state packets drop in both clients. |
src/AcDream.Core/Physics/PhysicsTimestampGate.cs (TryAcceptInstance); src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs; src/AcDream.App/World/LiveEntityRuntime.cs; effect exception src/AcDream.App/Rendering/Vfx/EntityEffectController.cs; Parent exception src/AcDream.Runtime/Entities/ParentAttachmentState.cs |
acdream has no general per-object mixed queue for non-effect state packets yet. Dropping those state families is generation-safe: a future packet cannot mutate the old body or advance its stamps, and the newer CreateObject wholesale-seeds all nine channels. The canonical runtime remains the seam for widening the queue later. | The first queued non-effect state can be absent until a later authoritative update; effect and Parent packets retain retail order and ownership | SmartBox::HandleSetState 0x004533E0; HandleVectorUpdate 0x00453480; HandleParentEvent 0x004535D0; HandlePickupEvent 0x00453530; HandleDeleteObject 0x00451EA0; HandleObjDescEvent 0x00453340; UnpackPositionEvent 0x004542C0; F754/F755 dispatch and pending replay pc:357214-357239 |
| AD-33 | CSequence.frame_number at C# double (64-bit); retail is x87 long double (80-bit extended) — every frame-boundary comparison ran at extended precision on retail (Phase R1, 2026-07-02) |
src/AcDream.Core/Physics/Motion/CSequence.cs (FrameNumber) |
double is the widest C# float type; the R1 port removes ACE-style boundary epsilons so comparisons are exact-int against bare boundaries, minimizing ULP sensitivity (ACE's float is far worse) |
A frame landing within 1 double-ULP of an integer boundary could classify differently than retail's 80-bit compare — sub-frame timing skew at pathological framerate×dt combinations | acclient.h:30747 (long double frame_number) |
| AD-34 | Retail's intrusive DLListBase/DLListData lists are managed LinkedList<T>s; node identity via LinkedListNode<> references (Phase R1 anim list, 2026-07-02; extended R2-Q3 to MotionTableManager.pending_animations; extended R4-V2 to MoveToManager.pending_actions — whose node type, retail MoveToManager::MovementNode, is RENAMED MoveToNode to avoid colliding with R2's Motion/MotionNode.cs pending_motions node) |
src/AcDream.Core/Physics/Motion/CSequence.cs (_animList); src/AcDream.Core/Physics/Motion/MotionTableManager.cs (_pendingAnimations); src/AcDream.Core/Physics/Motion/MoveToManager.cs (_pendingActions) + MoveToNode.cs |
Same topology + cursor semantics (curr_anim/first_cyclic/tail-anchored scans are node references); unlink/delete becomes Remove(node); conformance tests pin the surgery state tables |
Any retail behavior depending on the −4 pointer adjustment or node memory reuse (none observed in the decomp) would diverge; a reader grepping for retail's MovementNode name must find it via this row |
acclient.h DLListBase; r1-csequence-decomp.md §0; r2-motiontable-decomp.md §11; r4-moveto-decomp.md node factories §4a |
| AD-35 | MotionTableManager.PerformMovement's unhandled-type default case returns the named sentinel 0xFFFFFFFF (MotionTableManagerError.NotHandled); retail's compiled code leaks the CSequence* pointer reinterpreted as the return code (BN-confirmed artifact, dead/unreachable — callers gate on type first) (R2-Q3, 2026-07-02) |
src/AcDream.Core/Physics/Motion/MotionTableManager.cs (PerformMovement default case) |
No retail caller consults the return value for unhandled types (RawCommand/StopRawCommand/MoveTo*/TurnTo* route elsewhere); returning a stable non-zero sentinel preserves the only observable contract (non-zero = not success) without fabricating a pointer-shaped number | If a future port wires a caller that passes unhandled types AND branches on the exact return value, it would see 0xFFFFFFFF where retail saw an arbitrary pointer — flag at that port |
PerformMovement 0x0051c0b0 (r2-motiontable-decomp.md §11 default-case note) |
| AD-36 | IMotionDoneSink.MotionDone consumed for CREATURE-class entities only: R3-W2 binds the seam to the entity's MotionInterpreter.MotionDone (player via PlayerMovementController.Motion, remotes via RemoteMotion.Motion, resolved at fire time); interp-less entities (statics that never receive a UM/UP and so never get a RemoteMotion) keep a diagnostic-recorder-only target — retail gives every CPhysicsObj a MovementManager/CMotionInterp (R2-Q4 seam, narrowed R3-W2, 2026-07-02; R5-V5 gave every RemoteMotion/player ONE literal MovementManager facade, so the residue is only the no-RemoteMotion class) |
src/AcDream.App/Rendering/GameWindow.cs (TickAnimations MotionDoneTarget bind) |
Motion for entities without a RemoteMotion (never UM/UP-touched) completes via the manager queue alone; nothing consumes their MotionDone until every sequencer-owning entity gets a host/RemoteMotion (doors DO have one since the R4-V5 door fix — first UM creates it) |
An entity behavior depending on pending_motions bookkeeping in that no-RemoteMotion class (none known) would silently no-op | CPhysicsObj::MotionDone 0x0050fdb0; retire when every sequencer-owning entity constructs a RemoteMotion/host (post-M1.5 entity-class unification; R5-V5 closed the facade half) |
| AD-37 | Camera rotation state is a forward VECTOR (nlerp + normalize; roll always 0, up = world Z); retail's sought carries a full Frame and slerps quaternions (Frame::interpolate_rotation shortest-path slerp with 2e-4 nlerp fallback). The dead-band compares forward-vector distance against the same 2e-4 epsilon retail applies per quaternion component |
src/AcDream.App/Rendering/RetailChaseCamera.cs (_dampedForward, ApplyConvergenceSnap) |
The chase camera never rolls (heading frames are Z-up by construction), so a forward vector spans the reachable rotation space; identified (not introduced) during the #180 UpdateCamera tail reading | If a future camera mode needs roll (death cam, cutscene) the vector state can't represent it; large-angle per-frame turns nlerp (chord) vs slerp (arc) — imperceptible at 0.45-stiffness step sizes | Frame::interpolate_rotation 0x00535390, Frame::close_rotation 0x00455d70; pseudocode doc 2026-07-06-camera-sought-position |
| AD-39 | The frames_stationary_fall ladder + fsf≥3 UP-contact-plane manufacture runs AFTER acdream's fused LKCP-restore/contact-marking block, deriving retail's _redo as cleanAdvance || OnWalkable; retail (ACE Transition.cs:1029-1061) interleaves the fsf block BETWEEN the LKCP-restore (sets _redo) and the contact-marking (reads the manufactured plane) (#182 rebuild, 2026-07-07) |
src/AcDream.Core/Physics/TransitionTypes.cs (ValidateTransition fsf tail) |
acdream deliberately fused ACE's separate LKCP-restore + contact-mark blocks (the L.2.3c/L.2.4/A6.P3 contact-retention divergences); running the ladder after them and re-marking grounding inside the manufacture branch is semantically equal (a grounded wall-slide is not a stuck-fall in either arrangement) without disturbing those hard-won fixes | If a future contact-retention change alters when OnWalkable is set relative to the ladder, _redo could misclassify a frame (grounded-jam mistaken for stuck-fall → spurious velocity zero, or vice-versa) — the fsf conformance tests pin the current arrangement |
CTransition::validate_transition 0x0050aa70 pc:272625-656; ACE Transition.cs:1029-1061 |
| AD-40 | The fsf Stationary* transient-bit encode (fsf→0x10/0x20/0x40) lives in the Core resolve writeback (PhysicsEngine.ResolveWithTransition), co-located with the fsf computation; retail encodes it in handle_all_collisions (pc:282737-758). Also: PhysicsBody.CachedVelocity is computed at the player chokepoint but not yet consumed — outbound wire velocity still uses the existing get_state_velocity path, not retail's cached_velocity source (#182 rebuild, 2026-07-07) |
src/AcDream.Core/Physics/PhysicsEngine.cs (writeback); src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (CachedVelocity) |
Encoding in the writeback keeps the seed→ladder→writeback→seed round-trip self-contained in Core (testable without the App loop); the bit values + timing are identical to retail's (set after fsf is final, before the next resolve). CachedVelocity is faithful to carry now; routing the wire through it is a separate, unmeasured change | If a future consumer reads the Stationary* bits expecting retail's handle_all_collisions to have set them (it doesn't run in Core), the Core writeback is the source of truth; a wire-reporting change that assumes CachedVelocity is live would send the wrong velocity until it's wired | handle_all_collisions bit encode pc:282737-758; get_velocity 0x005113c0 (cached_velocity reader) |
| AD-41 | The candidateMoved gate (retail UpdateObjectInternal pc:283657 candidate != m_position) suppresses the WHOLE SetPositionInternal-shaped commit (contact/walkable flags, HitGround/LeaveGround, handle_all_collisions, cached_velocity) on a no-move frame — narrowed 2026-07-30 (#265 bounce rework) from "only handle_all_collisions"; acdream still runs ResolveWithTransition (zero-distance) for cell/contact tracking, where retail skips the whole transition (#182 rebuild, 2026-07-07) |
src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (candidateMoved guard) |
The load-bearing effect is not re-zeroing the gravity velocity that rebuilds after a stuck-fall bleed; the zero-distance resolve is a near-no-op (numSteps 0 → the zero-step early return, no ValidateTransition, contact plane persists via the writeback), so running it is harmless while keeping acdream's per-frame cell/membership refresh | If the zero-distance resolve ever gains a side effect on a no-move frame (a contact-plane clear, an fsf change), it would diverge from retail's skip — a no-move frame must stay a near-no-op | CPhysicsObj::UpdateObjectInternal 0x005156b0 pc:283657 (candidate-moved gate) |
| AD-43 | A malformed/custom PhysicsScript CallPES cycle whose script timeline never advances is rejected with a diagnostic; retail's linked scheduler would continue draining that zero-time tail indefinitely |
src/AcDream.Core/Vfx/PhysicsScriptRunner.cs (timeline-progress ancestry guard) |
Prevents corrupt DAT content from hanging the single update/render thread. Installed-DAT audit plus conformance tests prove the real rolling-weather cycles advance 2.8 seconds per edge and continue unchanged; only a no-progress strongly connected cycle is rejected | A custom DAT that deliberately relies on an infinite zero-time loop observes a rejected play instead of freezing the client | ScriptManager::AddScriptInternal 0x0051B310; ScriptManager::UpdateScripts 0x0051B480; CPhysicsObj::CallPES 0x00511AF0 |
| AD-44 | acdream has no retained character-management screen: startup deterministically selects the first active, non-greyed CharacterList identity, and native-window close performs retail's complete character-logoff handshake plus transport disconnect before exiting instead of returning to character selection. One active ReceiverData equivalent means ClientNet::LogOffServer's per-receiver loop sends one header. |
src/AcDream.Core.Net/Messages/CharacterList.cs (TrySelectFirstAvailable); src/AcDream.App/Rendering/GameWindow.cs (live-session bootstrap, moving to LiveSessionController in Slice 3); src/AcDream.Core.Net/WorldSession.cs (SelectCharacterForEnterWorld, Dispose); src/AcDream.Core.Net/Packets/TransportDisconnect.cs |
This preserves unattended startup and immediate ACE endpoint release while validating that the chosen identity is active/non-greyed and using the server's canonical account. A future retained character-management owner is separate UI/session work. | An account with multiple playable characters enters the first wire-order identity without retail's explicit choice. An eventual in-client "log off character" action cannot reuse the process-exit path; it must retain the authenticated socket after server 0xF653 and return to character management. |
gmCharacterManagementUI::SelectCharacter @ 0x004EC160; gmCharacterManagementUI::EnterGame @ 0x004ED440; gmCharGenMainUI::Update @ 0x004E8460; Proto_UI::LogOffCharacter @ 0x00546A20; CPlayerSystem::RequestLogOff @ 0x00562DD0; CPlayerSystem::ExecuteLogOff @ 0x0055D780; ClientNet::LogOffServer @ 0x00543EF0; SharedNet::SendOptionalHeader @ 0x00543160 |
| AD-45 | App teardown can overlap a newer INSTANCE_TS record after retiring the old active identity. TargetManager therefore retains the exact target host and each TargettedVoyeurInfo retains the exact watcher host; unsubscribe, Sticky live-target reads, inbound sender validation, and ExitWorld delivery compare/use those pointer-like tokens rather than resolving a reused GUID. Retail stores only GUIDs because DeleteObject finishes exit_world/leave_world while the retiring CPhysicsObj remains the sole object-table entry. |
src/AcDream.Core/Physics/Motion/TargetManager.cs; StickyManager.cs; TargettedVoyeurInfo.cs; IPhysicsObjHost exact relationship seams |
This preserves retail's effective object-pointer identity while allowing App resource teardown to fail and retry without blocking an accepted newer server generation. Ordinary GetObjectA remains active-record-only, so tombstones cannot accept new relationships. |
If any target/voyeur path bypasses the exact token, retrying an old teardown can remove or notify a newer same-GUID relationship, or Sticky can steer toward the replacement; retained tokens also keep the small manager graph alive until teardown converges. | CPhysicsObj::exit_world @ 0x00514E60; CObjectMaint::DeleteObject(CPhysicsObj*) @ 0x00508460; ACCObjectMaint::DeleteObject(uint) @ 0x005576F0; TargetManager::SetTarget @ 0x0051AC30; ClearTarget @ 0x0051A7E0; AddVoyeur @ 0x0051A830; RemoveVoyeur @ 0x0051AD90 |
| AD-57 | Re-argued from TS-24 at Campaign P P7 (2026-07-30). Outbound RawMotionState.Actions is always empty at runtime. The packer emits num_actions + per-action pairs (L.2b, RawMotionState::Pack 0x0051ed10) and the R3-W1 action FIFO capability exists (AddAction/RemoveAction/ApplyMotion/RemoveMotion); no production input path ENQUEUES autonomous actions yet because the emote/autonomous-motion feature surface is unimplemented. An empty list is byte-identical to retail's own no-pending-actions state, so this is a feature gap, not a divergence of existing behavior. |
packer src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs; FIFO src/AcDream.Core/Physics/RawMotionState.cs |
Every currently-shipped movement packet matches retail byte-shape; the gap only manifests when emote-class autonomous actions are implemented. | When emotes land, forgetting to route them through the FIFO would silently drop them from the wire. | RawMotionState::Pack 0x0051ed10 |
| AD-58 | Re-argued from TS-40 at Campaign P P7 (2026-07-30). Retail's physics_obj->cell null test ("placed in the world") is proxied by the explicit PhysicsBody.InWorld flag — set by SnapToCell and RemoteMotion construction, consumed by CMotionInterp's detached-object link-strip guards. Equivalence: every acdream body that would have a null retail cell pointer has InWorld == false (bodies exist only for world entities; the flag flips exactly at placement/withdrawal), so the guards fire on the same population. A structural adaptation of retail's pointer-as-state idiom to acdream's explicit-flag idiom, not scheduled debt. |
src/AcDream.Core/Physics/PhysicsBody.cs (InWorld); src/AcDream.Core/Physics/MotionInterpreter.cs (3 guard sites) |
If a future path creates a body before world placement without clearing InWorld, the link-strip guards misfire where retail's null-cell test would not. |
CMotionInterp link-strip guards raw @305xxx |
|
| AD-59 | Filed 2026-08-02 (physics campaign, continuation-executor slice). The SameIncarnationCreate envelope buffers one publish per committed stage and flushes them ALL, in stage order, only after the LAST stage commits (constant-true per-field predicate, IsCurrent-checked at flush - the per-field closure variant was invalidated by WeenieDescription's six-field AdvanceCreateAuthority). A subscriber sees N back-to-back events with no interleaved observation point, each carrying the FINAL merged post-envelope record state, not per-stage state. |
src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs (ApplyEnvelope buffered-publish tail; Publish/PublishNow) |
Retail's own tail is one synchronous critical section, and retail emits ONE notice per Create (ECM_Physics::SendNotice_CreateObject, fired whenever a weenie exists, independent of the physics-registration outcome) - never N per-internal-step notices. The buffered flush is closer to retail's one-signal model than per-step publication would be, though not a literal 1:1 match. |
A subscriber diffing consecutive Updated events from the SAME envelope to isolate one stage's delta gets every stage's cumulative state on each event - silently wrong incremental-diff logic, not a crash. |
SmartBox::HandleCreateObject 0x00454C80 same-incarnation tail (one synchronous critical section); ACCObjectMaint::CreateObject 0x00558870 step 11 (ECM_Physics::SendNotice_CreateObject) |
| AD-60 | Filed 2026-08-02 (physics campaign, continuation-executor slice). LEGACY HALF RETIRED 2026-08-05 (C5b, #275); row REWRITTEN rather than deleted, because wire-cell channels SURVIVE outside the merge and a silent whole-row deletion would hide them. No Position merge commits residency any more: both the executor's ApplyPositionAction and the steady-state RuntimeEntityObjectLifetime.TryApplyPosition refresh canonical.Snapshot.Position with the wire pose while withholding the derived FullCellId (RefreshSnapshot(..., refreshPosition: false); the steady-state site is the RefreshSnapshot call in TryApplyPosition, cite by symbol — the row's former :1338 and the C5 scoping's :1918 were both stale). Only a Runtime SetPosition commit or a simulation full-cell commit may change residency inside the merge. The precise surviving claim: a wire Position never makes a record resident INSIDE THE MERGE OR AHEAD OF CLASSIFICATION. Two steady-state wire-cell writers deliberately remain downstream of it, and are separately filed: (W2) the OnPosition prologue rebucket (LiveEntityNetworkUpdateController → LiveEntityRuntime.RebucketLiveEntity → RuntimeEntityObjectLifetime.CommitRebucket → SetFullCell), which runs for every classification reaching the generic tail and is ALSO the local player's own cell-freshness path — cross-filed at AP-146/#320, and deliberately NOT gated, since gating it would freeze the player's canonical cell between teleports and #319's child-cell equality would inherit the freeze; and (W3) the post-routing wire-cell adopt for non-placing arms (TryAdoptWireCellAfterRouting), filed at AP-135. Packets that return BEFORE W2 — the local force path, the missile arm, and (added at the C5b architecture review's D1 fix) the local ChildUnparentDisposition Superseded/Pending arm — are placement-receipt-authoritative for residency, or unchanged at the last commit on a refused/contended force (AD-62's shapes), which is retail's own body-keeps-its-last-placed-cell behaviour. CORRECTED 2026-08-05 at the D1 fix: that enumeration was presented as exhaustive and was not — the ENTIRE no-window host belonged in it. W2 and W3 both live in AcDream.App, and the two hosts run parallel, non-shared inbound routes (LiveEntitySessionController/LiveEntityNetworkUpdateController.OnPosition versus RuntimeLiveEntitySessionController.OnPositionUpdated), so AcDream.Headless had NO post-merge cell writer at all: every remote's FullCellId was written at create/placement and then frozen for the session, and the local player lost this row's own inbound-Position refresh edge (AP-146/#320). Fixed in that commit by giving the no-window route its own W2 over a NEW shared Runtime owner for the committed value, RuntimeEntityObjectLifetime.CommitWireCellRebucket — which also retires this row's layering inversion, since it no longer has to document itself by naming an App class its own assembly cannot reference. The no-window host has no W3 analogue and needs none: it performs no remote contact routing, so there is no post-routing arm to adopt a wire cell into. The duplicated REACHABILITY decision the fix leaves behind is filed at AD-64. AMENDED 2026-08-05 at the C5b architecture review (finding L1/L2), with a measurement the C5b commit did not have: on the ordinary remote tail W2 and W3 are REDUNDANT, not complementary. Sabotaging W2 alone — either making it adopt the committed cell instead of the wire cell, or skipping the rebucket outright — leaves the entire LiveEntityNetworkOnPositionCollapseMatrixTests file green, because W3's RemoteMotion.CellId write reads through to canonical FullCellId via RuntimePhysicsState.CommitCanonicalCell, whose graphical CellCommitted recovery also re-installs the render bucket. Only removing BOTH channels goes red, and then exactly one test does: LiveEntityNetworkOnPositionCollapseMatrixTests.WithdrawnProjection_AcceptedPositionRestoresBucketAndWireCell, added at that review because C5b shipped its "production installs the bucket at W2 in the same call" claim untested (the commit message's "no fixture covers pickup at that layer" was inaccurate — that file drives the real OnPosition at ~26 call sites). The practical consequence: this row's W2/W3 enumeration is correct as a list of surviving channels, but neither one individually is load-bearing on the remote tail, so a future change that retires one of them will not be caught by anything except that test. |
src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs (ApplyPositionAction, the CANONICAL CELL SEMANTICS comment); src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (TryApplyPosition, the same comment; CommitWireCellRebucket, the shared committed-value owner added at the D1 fix); src/AcDream.App/World/LiveEntityRuntime.cs (RebucketLiveEntity, the graphical W2 caller); src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs (TryCommitAcceptedWireCell, the no-window W2) |
Scoped claim (tightened 2026-08-05 at the C5b closeout — the former bare "Matches retail exactly" opener over-read a row whose own body documents two channels that do NOT match retail exactly). The WITHHOLD matches retail exactly: HandleReceivedPosition @0x00453FD0 reads the wire objcell_id into a LOCAL @0x00453FE3 and never assigns the object's cell — enter_world/MoveOrTeleport's placement commit and SetPosition do; also matches the classifier's documented cellless rule. C5b evidence: RuntimeSteadyStatePositionMergeTests.AcceptedPosition_WithholdsTheWireCellAtTheMergeBoundary (asserted at the merge boundary, never at OnPosition level, where W2 legitimately re-stamps), …ConservesOneRebucketAndOneChildPropagation (both parent classes), and RuntimeAcceptedPositionDriveControllerTests.ContendedForcePosition_WritesNoResidencyAnywhere; all sabotage-verified. D1-fix evidence for the no-window half: RuntimeLiveEntitySessionControllerTests — AcceptedRemotePosition_AdvancesCanonicalResidencyInANoWindowHost, AcceptedLocalPlayerPosition_AdvancesCanonicalResidencyInANoWindowHost, WireCellCommit_HonoursRejection_Residence_AndTheLandblockPreserveRule, BoundProjectilePosition_CommitsNoWireCell_UnboundMissileDoes; HeadlessSessionIsolationTests.RemoteSteadyStatePositionAdvancesTheBotVisibleCell (end to end through a real HeadlessSessionHost); and the two-direction HeadlessSessionHostTests.LocalForcePosition_CommitsTheWireCellOnlyWhenTheDriveDeclined theory, whose handled arm discriminates on a measured resolved cell (0xA9B4001C) that is neither the wire cell nor the spawn cell. Eight sabotages verified, each red on at least one of these; the shared derivation's sabotage additionally reddens the graphical LiveEntityRuntimeTests.CanonicalOnlyRebucket_DoesNotOverwriteAuthoritativeFullCell, which is what establishes that the extracted rule is the same rule both hosts run. |
If a future change passes refreshPosition: true at either site, a wire Position would make a cellless canonical body resident without any placement/collision commit — the classic AP-1-shaped bug this campaign closed. Conversely, gating or deleting W2 for "symmetry" freezes the local player's canonical cell between teleports. And a host without W2 at all freezes EVERY entity's cell after its placement — the D1 defect: a bot's RuntimeEntitySnapshot.CellId never advances, and RuntimeSetPositionState.IsAffectedCollisionResident parks bodies against a landblock they left. |
SmartBox::HandleReceivedPosition 0x00453FD0 (@0x00453FE3 the local read); CPhysicsObj::SetPositionInternal 0x00515BD0 → set_cell; RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition comment |
| AD-61 | Filed 2026-08-02 (C3c review round 1). The #270 settle-timing compression now covers the LOCAL player: RuntimeLocalPlayerPhysicsPublicationState.SettleFirstEntryGroundContact runs the shared SpawnPlacementSettler exactly once after the dormant activation's final commit (suffix-current authority only), compressing retail's first post-enter_world gravity frame — which grants CONTACT/ON_WALKABLE from a real touch — into the placement transaction. The legacy App-era force-seed (Contact|OnWalkable|Active in PlayerMovementController.SetPositionCore) still RUNS during publication-candidate preparation and is then OVERWRITTEN by the faithful activation commit + settle (it was never deleted). Caveat (review minor M2): the settler commits settle.Position but discards settle.CellId — a settle whose few-cm sweep crosses a cell boundary keeps the placement cell until the next resolve corrects it (inherited #270 semantics; ISSUES entry filed) |
src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs (SettleFirstEntryGroundContact); src/AcDream.Core/Physics/SpawnPlacementSettler.cs (TrySettle); overwritten seed src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (SetPositionCore) |
Timing compression only: contact comes exclusively from the sweep's real touch (no caller-bool seeding, no forced transients), an airborne spawn stays genuinely airborne, and the overwritten force-seed leaves no observable residue past the activation commit — the committed state is exactly what retail's first gravity frame produces | A settle crossing a cell boundary reports the stale placement cell for the frames before the next resolve; a future reader trusting SetPositionCore's "treat as grounded" seed comment could reintroduce the Contact-without-plane state the landing family calls unrepresentable |
CPhysicsObj::enter_world 0x00516170; SmartBox::HandleCreateObject 0x00454C80 |
| AD-62 | Filed 2026-08-03 (C4 route 2, round 2); rewritten round 3. General rule: an accepted local-player ForcePosition that this route does not carry through to a committed canonical placement is never re-applied. That half matches retail — SmartBox::BlipPlayer attempts the placement exactly once and never retries. What diverges is that acdream has non-commit outcomes retail cannot reach at all, because retail's world is fully resident and its placement synchronous. Round 3 narrowed the loss to the re-apply alone wherever the packet's placement was actually BEGUN: the retail position event now fires at that packet's terminal outcome whether or not the placement committed (SettlePending's positionEventOwed path), matching BlipPlayer discarding SetPositionSimple's enum SetPositionError and HandleReceivedPosition acking unconditionally @0x00454091. Shapes losing ONLY the re-apply: (i) the destination landblock's collision generation is unpublished so the placement parks (DeferredCell) and is then retired by a non-position cause (collision-generation retirement, the lost-cell deadline, ParkCollisionResidents) with the accepted authority unmoved — the funnel's EQUAL branch; (ii) the same park superseded by a newer ordinary Apply Position which now owns the pose — the ADVANCED+ordinary branch; (iii) any OTHER PositionAuthorityVersion advance moving the record out from under the funnel's re-issue test — TryApplyPickup (RuntimeEntityObjectLifetime.cs:1116), CommitPositionChannelUpdate (:2041), AdvanceCreateAuthority (:2466) — effectively unreachable for a live local player, but they fail silently in the same direction and the funnel cannot tell them from (ii). Shapes still losing BOTH the re-apply and the ack because no placement was ever begun for that packet: (iv) a Contention whose blocking operation is EXTERNAL to this drive (a concurrent portal/teleport placement owns the entity) — nothing is recorded in _pending, so nothing pumps it and the packet is dropped outright; (vi) a re-issue retry marker whose re-issue never manages to begin before the funnel clears it. Losing BOTH for a DIFFERENT reason — the placement WAS begun, but the descriptor was displaced before reaching its own terminal settle: (v) a packet superseded by a newer force whose own placement begins cleanly — SettlePending opens by nulling _pending without reading it, so the older descriptor's owed ack is discarded. Replaying it would be worse than losing it (a stale-sequence report carrying the newer packet's committed pose), and the displacing packet always acks, so ACE always receives a report for the newest force. The DeferredCell park is NOT a precondition of this row: shapes (iv)-(vi) never park. In every shape the body stays where the last successful placement left it and the next accepted Position (ACE broadcasts at 5-10 Hz) carries the corrected pose forward. |
src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs (SettlePending — the single terminal-outcome funnel: its positionEventOwed ack and its two non-reissuing branches; and TryExecuteAcceptedLocalPosition's Contention return) |
Retail has no park and no external placement authority: SmartBox::BlipPlayer runs synchronously against a fully resident world, so "arrived but not yet placeable" and "another placement owns this entity" are both unrepresentable there. Those are our async collision-publication and single-placement-authority adaptations. Re-issuing a retired force instead would be worse than not: shape (ii) would stamp the force route's Teleport|Slide flags and an unconditional ack onto an ordinary echo's pose while skipping the ConstrainTo the ordinary branch runs (RuntimeAuthoritativePositionRouteClassifier.cs:368-388), and shape (i) can re-issue into the same persistent cancellation cause indefinitely. The drive still owns at most one in-flight placement and still re-issues whenever the newest accepted event IS a still-unserved ForcePosition. |
A server correction whose destination collision is slow to publish, or which lands while another placement authority owns the entity, can be silently skipped: the player stays at the pre-correction pose for one broadcast interval (~100-200 ms). Sustained (a slow-publishing destination correcting repeatedly) this reads as rubber-banding that does not take. In shapes (iv)-(vi) ACE additionally receives one fewer AutonomousPosition than retail would have sent, so the server cannot tell its force was not applied. |
SmartBox::HandleReceivedPosition @0x00453FD0 FORCE_POSITION branch (SendPositionEvent @0x00454091, early return @0x0045409D); SmartBox::BlipPlayer @0x00453940 (discards the error, returns void); CPhysicsObj::SetPositionSimple @0x005162B0 (returns enum SetPositionError; other callers test == OK_SPE @0x0055605D/@0x00556021); CommandInterpreter::SendPositionEvent @0x006B4770 |
| AD-63 | Filed 2026-08-04 (cancelled-park presentation rollback). When a cancelled restorable park is rolled back, the entity's presentation is restored EXCEPT the player's selection. ParkDeferred's Withdraw receipt makes the host sink clear the selection if the parked entity was the selected object (_clearSelectionForUnavailableEntity), and the WithdrawalRestored receipt that rolls that withdrawal back deliberately does not re-select it. Every other registration the withdrawal removed — the graphical bucket, projection visibility, plugin world state, the world-event replay set, the effect-pose registry, the local-player shadow, the presentation visibility sinks — IS restored exactly. |
src/AcDream.App/World/RuntimePlacementPresentationSink.cs (TryApplyWithdrawalRestoration vs TryPublishWithdrawal's _clearSelectionForUnavailableEntity call) |
Selection is user intent, not a projection registration. Retail clears the selection when its target becomes unavailable (SelectionChangeReason.SelectedObjectRemoved is acdream's name for the same edge) and never re-selects on the object's behalf; re-selecting here would invent input the player did not give. Retail also cannot reach this state at all — it has no cancel for a lost-cell park (AP-136) — so there is no retail behaviour to match, only two acdream choices, and "do not act for the player" is the conservative one. |
The player loses their target for the ~150 ms park window if the selected object happened to park, and must re-click it. No other state is affected: the object is visible, on the radar, collidable, and assessable again as soon as the restoration receipt drains. Retire together with AP-136 by making the park survive cancellation (issue #309), which removes the withdrawal — and therefore the selection clear — entirely. | AP-136 (the park rollback this rides on); no retail anchor — retail has no cancellable lost-cell park |
| AD-64 | Filed 2026-08-05 at the C5b architecture review's D1 fix. The graphical and no-window hosts run parallel, non-shared inbound entity routes — LiveEntitySessionController → LiveEntityNetworkUpdateController.OnPosition versus RuntimeLiveEntitySessionController.OnPositionUpdated — and AD-60's W2 wire-cell commit is therefore expressed TWICE. The committed VALUE is shared exactly (one owner, RuntimeEntityObjectLifetime.CommitWireCellRebucket, including the landblock-vs-cell preserve branch); what is duplicated is the REACHABILITY decision — which packets may reach it. The graphical host encodes that decision implicitly, as the set of early returns strewn through a 400-line OnPosition (authority gate on Rejected, the local force arm on every drive status except NotApplicable, the missile arm, the ChildUnparentDisposition Superseded/Pending arm, the initial-create residence gate inside RebucketLiveEntity). The no-window host encodes it explicitly, in one method, TryCommitAcceptedWireCell, whose gates were derived from those returns one by one. Two of the graphical gates have no no-window analogue and are deliberately absent rather than reproduced: the ChildUnparentDisposition arm is presentation recovery this host does not perform, and the residence gate's MaterializationResidence is AwaitRuntimePlacement half is App presentation bookkeeping whose no-window equivalent is unconditionally true for a residence-backed record. CORRECTED 2026-08-05 at the C5b closeout (architecture finding L-B): "deliberately absent" was presented as the complete list of differences and it was not — there are three more, and the row's own "derived from those returns one by one" phrasing was the claim that made them invisible. (a) The residence gate is WEAKER than the merge's own. Both hosts' wire-cell commits gate on TryGetInitialCreateResidence (= RuntimeInitialCreateResidenceState.TryGetCurrent), while RuntimeEntityObjectLifetime.TryApplyPosition's FIFO enqueue branch gates on TryGetPendingInitialResidence (= TryGetTransaction = TryGetCurrent OR a completed-but-unretired lease, RuntimeInitialCreateResidenceState.cs:729-748). In that window the merge enqueues the packet as a continuation while the commit reads "no residence" and writes the wire cell AHEAD of the continuation that will replay it. Host-symmetric and pre-existing — the graphical RebucketLiveEntity has the identical pair — but this row previously claimed the AwaitRuntimePlacement half was the only deliberately-absent piece of the residence gate, which is false. (b) The missile gates are two different expressions. The graphical route PREFERS earlyRemoteRoute.OperationKind is RuntimeSetPositionOperationKind.ProjectileAuthoritative and falls back to the Missile-flag / bound-projectile conjunction only when the classification is null; the no-window route ALWAYS uses the conjunction, because it classifies nothing for a remote. They agree today (the conjunction is what the classifier's own projectile test is built from), but they are separately maintained and only the conjunction is reachable on one side — a change to the classifier's projectile predicate moves one host and not the other. (c) The pre-merge PAYLOAD gate was absent entirely, and is now present. The graphical route validates the wire payload before the merge (LiveEntityNetworkUpdateController.OnPosition's payloadIsValid from ProjectileController.CanAcceptPositionPayload — despite the name, not projectile-scoped; it runs for every guid — consumed by LiveEntityInboundAuthorityGate.TryAcceptPosition's !payloadIsValid return). The no-window route had no equivalent, so since D1 an unvalidated update.Position.LandblockId reached CommitWireCellRebucket, whose own doc calls 0 "the withdrawal shape" (cell 0 + landblock 0) — silently de-residencing the entity in the exact field RuntimeEntityObjectViews.Snapshot hands every bot as CellId and RuntimeSetPositionState.IsAffectedCollisionResident reads. Fixed at the closeout by applying the same predicate at the same point: RuntimeAuthoritativePositionRouteClassifier.IsValidCreateWirePosition plus the finite-velocity term, the pair RuntimeEntityObjectLifetime.TryApplyPosition already applies on its initial-residence branch. Rejecting BEFORE the merge (not merely before the commit) is what makes the hosts symmetric — neither lets an invalid payload advance the timestamp gate — and is pinned by RuntimeLiveEntitySessionControllerTests.InvalidPositionPayload_IsRefusedBeforeTheMerge_InANoWindowHost, sabotage-verified in both directions (gate removed -> red at the withdrawal-shape assertion; gate moved to guard only the commit -> red at the pose assertion). The no-window host also has no W3 (TryAdoptWireCellAfterRouting) analogue and needs none — it performs no remote contact routing at all. |
src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (OnPosition, the implicit gate set); src/AcDream.App/World/LiveEntityRuntime.cs (RebucketLiveEntity's residence early return); src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs (TryCommitAcceptedWireCell, IsMissilePacket) |
Retail has one client and therefore one route; there is no retail shape to match, only acdream's own two-host structure. The alternative — unifying the two session controllers so the decision exists once — is the genuinely correct fix and is filed as issue #324, but it is campaign-sized: it has to reconcile presentation recovery, hydration, the equipped-child renderer, and the remote routing arms that only one of the two hosts has. Duplicating a small, individually test-gated decision is the cheaper correct thing meanwhile; duplicating it SILENTLY, which is what the pre-D1 state amounted to (one host simply had none of it), is what this row exists to stop. | The two decisions can drift: a future change to one host's reachability rules will not be caught by the other host's tests. Concretely, if the graphical route later adds an early return, the no-window host keeps committing on that packet shape, and vice versa. Bounded by the eight-sabotage gate the D1 fix left behind, plus the closeout's ninth (the payload gate, red in both directions) — every arm of TryCommitAcceptedWireCell and both directions of the force rule are individually red-verified — so drift shows up as a test that must be deliberately changed, not as a silent divergence. That bound does NOT cover the three differences added at the closeout: the weaker residence predicate (a) and the missile-expression split (b) have no discriminating test on either side, because in both cases the two hosts currently AGREE and the divergence is structural rather than behavioural. They are recorded here precisely because nothing else will catch them. Retire with #324. |
No retail anchor — acdream-only host-structure deviation. Adjacent rows: AD-60 (the W2/W3 channel list), AP-146/#320 (the local player's cell edges) |
RETIRED 2026-08-07 (Campaign S S4), USER-PASSED the same morning ("Slopes feels good" at the downhill/diagonal/jump-landing gate). Transition.AdjustOffset's away-from-plane arm now performs retail's Plane::snap_to_plane @0x00509c50 semantics verbatim: XY preserved, Z re-solved as -(x*Nx + y*Ny)/Nz, no-op under the 0.000199999995f |
N.z | epsilon — replacing the orthogonal projection whose cos²θ downhill XY shortfall this row recorded (25% at 30°, 50% at 45°). Branch polarity ported from the test ah,0x41 idiom at 0x0050a4fa: into-plane subtracts, away-from-plane snaps. Conformance: S4AdjustOffsetConformanceTests exact-value rows, sabotage-verified (the re-instated projection reproduces exactly the recorded cos²30° = 0.75 shrinkage). NOTE: the SIBLING row AD-66 was byte-re-confirmed but its landing was WITHHELD the same night — see issue #341. Original text: Filed 2026-08-06 (found while retiring AD-10; NOT fixed here). Transition.AdjustOffset's collisionAngle > 0 arm — the body moving AWAY from its contact plane — substitutes result -= N * collisionAngle for retail's Plane::snap_to_plane call, making the if and the else arms byte-identical. Retail's two arms are genuinely different: snap_to_plane (0x00509c50) writes ONLY v.z = -(v.x*N.x + v.y*N.y) / N.z and leaves X and Y untouched, while the into-plane arm subtracts the full normal component. So for a horizontal step of length d on a slope of angle theta, retail DESCENDS with XY preserved at d and Z dropping dtan(theta) (speed along the plane d/cos theta), whereas acdream shrinks XY to dcos^2(theta) (speed along the plane d*cos theta). acdream therefore descends slopes SLOWER than retail by cos^2(theta) in XY: 25% slow at 30 degrees, 50% at 45 degrees. MAGNITUDE CORRECTED 2026-08-06 at the AD-10 retail review (F1): this row first said 13%/29%, which is 1-cos(theta) -- the wrong formula for its own stated cos^2(theta) factor, and half the true value. The correction is confirmed by measurement, not just algebra: #331's probe records 0.0735 m travelled for a 0.1 m request at 30.96 degrees, i.e. 26.5% short, which is exactly cos^2(30.96). This matters because the row is a LEAD for #269's slope-slide residual -- at the understated magnitude the lead reads as marginal and could be dismissed. Uphill (collisionAngle <= 0) is correct and identical to retail. |
src/AcDream.Core/Physics/TransitionTypes.cs (AdjustOffset, the else arm commented "Moving away from contact plane: snap to plane surface" — the comment names snap_to_plane but the code does not call it) |
Not justified — this is an unexamined substitution, not a decision. It is filed rather than fixed because it changes LOCAL-PLAYER movement feel and so needs its own visual gate; folding it into a remote-movement change would put a local-player regression behind the wrong acceptance test. | |
RETIRED 2026-08-08 (third reland, ten-run gate PASSED 10/10 bit-identical 0x42667451). AdjustOffset's push-out now uses the BARE radius in trigger and numerator per the byte anchors (0050a5c4/0050a5dc). The mechanism is PLANT-THEN-LIFT: validate_walkable plants at perpendicular rN.z (byte-faithful, untouched), the push lifts once per settle to tangent equilibrium dist=r where it goes quiet — the retail slope hover, arriving via the push. The historical #341 measurement flip that blocked two prior relands is recorded as unexplained-but-unreproducible (37/37 + 10/10 bit-identical across hostile shapes and tiering configs). AD-69's seam-frame correction was deliberately NOT bundled and stays active as its own follow-up. User slope gate PASSED 2026-08-08 ("Yes works fine"). LANDING WITHHELD 2026-08-07 (Campaign S S4) — the row stays ACTIVE and its byte evidence is now DOUBLE-confirmed. The bare-radius port was implemented, conformance-tested, and then PULLED: it collides with the #331 absorb characterization pin through a measurement that contradicted itself (the same clean-room binaries measured both a one-time resting lift and an exact latch, flipping with nothing but the test's post-tick assert shape). Issue #341 carries the observation matrix and the apparatus plan; the two exact-value conformance tests are [Skip]-ed in the tree awaiting the relanding. Do not re-derive the bytes — they were never the question. Filed 2026-08-06 (found while retiring AD-10; NOT fixed here). Transition.AdjustOffset's safety push-out substitutes naturalRestingDist = radius * ContactPlane.Normal.Z for retail's bare radius in BOTH the trigger comparison and the zDist numerator. The substitution is deliberate and carries a written rationale in the code (the LocalSphere origin sits at (0, 0, radius) along WORLD Z, so a sphere resting on a tilted plane is radius * N.z from it, and the bare threshold would fire spuriously on every slope and lift the feet by r(sec theta - 1) — 7 cm at 30 degrees, 48 cm at 60). The rationale may well be correct. What is missing is the register row: an intentional deviation from a byte-confirmed retail constant with no row is precisely what this register exists to catch, and the code comment's claim that "ACE and the published pseudocode have the original threshold" understates it — the retail BINARY has it. |
src/AcDream.Core/Physics/TransitionTypes.cs (AdjustOffset, the ci.ContactPlaneCellId != 0 && !ci.ContactPlaneIsWater block) |
Argued at length in the code comment and empirically motivated (the uncorrected threshold reportedly broke ValidateWalkable's contact check on steep slopes and flickered the Falling animation while running uphill). Filed to make the deviation auditable, not to assert it is wrong. | If the sphere-origin premise is mistaken, the push-out under-fires on slopes and a genuinely penetrating sphere is left below its contact plane. Conversely, if the premise is right, retail itself has the spurious lift and acdream is deliberately smoother than retail on slopes — a feel divergence in the same family as, and possibly interacting with, AD-65 and #269. | CTransition::adjust_offset 0x0050a370; disassembled from the PDB-paired v11.4186 binary: 0050a5c4 fld [ecx+0xc] loads the bare global_sphere->radius and 0050a5c7 fsub [0x7c6878] subtracts 0.00019999999494757503f (bytes 17b75139) for the trigger; 0050a5dc fsubr [ecx+0xc] reloads the bare radius for the numerator before 0050a5df fdiv [esi+8] divides by contact_plane.N.z. Neither site multiplies by N.z. |
|
| AD-67 | Filed 2026-08-07 at the #32 closeout. The narrowed CollisionInfo.SetContactPlane still writes ContactPlaneCellId, which retail's COLLISIONINFO::set_contact_plane @0x00509d80 does not — retail writes the cell id only in CTransition::init_contact_plane (@0x0050e8ca). Kept deliberately at the #32 fix on the research doc's own advice: acdream's consumers (the [support] probe's provenance, water-plane bookkeeping, AdjustOffset's ContactPlaneCellId != 0 gate) rely on the cell id being current per contact write, and retail's equivalent state travels a different route the port has not needed. |
src/AcDream.Core/Physics/TransitionTypes.cs (SetContactPlane, the ContactPlaneCellId = cellId line) |
The #32 fix removed the four LAST-KNOWN writes — the defect — and deliberately did not also change this contact-group field in the same commit; two behaviour changes in one fix would have made the user's cliff gate ambiguous. | A consumer that assumes the cell id changes ONLY at transition seed time (retail's timing) would observe it changing per contact write instead. No such consumer is known; AdjustOffset's gate wants the current value. |
COLLISIONINFO::set_contact_plane 0x00509d80 (22 bytes, no cell-id write); CTransition::init_contact_plane 0x0050e850 (cell id at 0x0050e8ca) |
| AD-68 | Filed 2026-08-07 at the #338 closure. During an entity's ASYNC-RESIDENCY window — its flat Setup collision not yet resident — LiveEntityMotionRuntimeController.GetSetupMoverShape returns a placeholder mover shape: empty sphere list (falling back to the legacy 0.48/1.835 capsule reconstruction) and step heights 0.4/0.4, values that appear nowhere in retail (authored human values are 0.600/1.500; retail's not-on-walkable fallback is 0.04). The local player has the same window between controller construction (0.4f defaults) and the publication candidate's adoption. Retail loads Setups synchronously and has no such window at all. Measured scale: 358 placeholder resolves vs 111,248 authored-pair resolves across one long session — seconds per entity, once. |
src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs (GetSetupMoverShape, the setup is null and <= 0f arms); src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (0.4f field defaults, adopted-over at publication) |
An adaptation to async residency, not a wiring defect — #338's live probe proved prepare/publish/resolve all carry the authored values in steady state. Left as-is deliberately: shrinking the window is streaming work, not physics work. | A remote moving DURING its residency window steps 0.4 instead of its authored heights, and collides as a capsule instead of its sphere list — briefly, once per entity. If a future report says "an NPC stumbled on a stair right as it appeared", this row is the first suspect. | CTransition::step_up 0x0050b610 (0.04 fallback at 0x0050b655); CPartArray::GetStepUpHeight 0x005180d0; issue #338 |
| AD-69 | Filed 2026-08-07 at the S4 pseudocode pass (implementer finding, verified against the decomp). Transition.AdjustOffset's safety push-out computes dist WITHOUT the cell-relative correction retail applies: retail's adjust_offset (and ACE's port, independently) run the sphere centre through LandDefs::get_block_offset against the contact plane's own cell before the plane-distance dot, so a contact plane owned by a DIFFERENT landblock than the mover's current cell measures in the plane's frame. acdream dots the raw world-space centre against the stored plane. Same-landblock contact (the overwhelming case) is identical; a landblock-SEAM contact measures dist offset by the block delta, mis-firing or mis-suppressing the push-out at seams. |
src/AcDream.Core/Physics/TransitionTypes.cs (AdjustOffset, the dist computation ahead of the push-out block) |
Discovered during S4 but deliberately not folded in: S4's own AD-66 half was withheld the same night (#341), and a third change in the same block would have made the anomaly investigation unattributable. Fix alongside the AD-66 relanding. | A mover resting on a contact plane owned by the neighbouring landblock (seam walking) gets a push-out computed against a dist that is wrong by the block offset — either a spurious lift or a missed penetration correction, exactly at landblock seams, the #176/#177 symptom neighbourhood. | CTransition::adjust_offset 0x0050a370 (pc:272271-272393); LandDefs::get_block_offset; ACE Transition.AdjustOffset (cross-check); issue #341 (sequencing) |
RETIRED 2026-08-08 (same day, round-2 cdb capture): the row described retail behavior, not a divergence. Retail's glide alternates exactly as ours does — the capture measured ~1.5 edge_slide entries per find_transitional_position during the glide (the alternation's exact signature: 3 on the arming tick, 0 on the moving tick), lockstep cliff_slide, step_down at 2.5x, and identical stack paths; cliff_slide's bytes match our port and ACE's. The 'retail redirects within the tick' inference misread round-1's set_sliding_normal cadence (per-event, not per-tick). Issue #347 closed without a code change. Filed 2026-08-08 with the #345 fix. Our steep-slope glide alternates: the edge-family arming tick absorbs the request (zero yield) and only the next tick's AdjustOffset pre-projection moves, then the clean move clears the sliding normal — a strict two-tick cycle. Retail redirects WITHIN the tick (edge_slide/cliff_slide 594 each over a ~15 s live glide — every 30 Hz tick, lockstep with set_sliding_normal 538) and yields motion every tick. |
src/AcDream.Core/Physics/TransitionTypes.cs (EdgeSlideAfterStepDownFailed + the insert's post-constraint continuation) |
The #345 landing deliberately touched only validate_walkable's return scoping; the response bodies were freshly user-gated (Campaign S) and AD-66 had just relanded in the same block. |
Gliding along a too-steep face at ~half retail's lateral speed; direction and angle-scaling correct. Visible as "slides but slower than retail" in a side-by-side. | 345-retail-glide.cdb.log counters; Issue345SteepSlopeGlideTests tick trace; issue #347 |
|
| AD-71 | Filed 2026-08-08 (reviewer finding on the #345 fix). ValidateWalkable's walkable test uses the MUTABLE sp.WalkableAllowance where retail's validate_walkable calls CPhysicsObj::is_valid_walkable @0x0050f530 — a FIXED global threshold (N.z >= [0x8ede5c], the walkable constant; the function reads no object state). Several code paths write WalkableAllowance = LandingZ (0.0871557 — TransitionTypes.cs:1688,2264, BSPQuery.cs:2330, FlatBspQuery.cs:2085) and ClearWalkable() does not restore it, so a stale-permissive value entering a grounded !StepDown && OnWalkable validate makes the guard PASS where retail's fails. Every override is permissive, so the #345 fix cannot REGRESS through this path — but for planes with N.z in (0.0872, 0.6642) a stale allowance leaves the old Adjusted-without-push dead loop reachable. The #345 landing GREW this row's blast radius: the operand now gates the return value (OK vs Adjusted), not merely the push (reviewer B, 2026-08-08). Also folds in: our FloorZ = 0.6642f vs ACE's 0.66417414f flips OK/Adjusted in a ~0.002-degree band. |
src/AcDream.Core/Physics/TransitionTypes.cs (ValidateWalkable, the walkable guard operand) |
Deliberately not folded into the #345 landing: the allowance plumbing is shared with the step-down family and needs its own conformance pass over every WalkableAllowance write/restore site. | A too-steep plane between LandingZ and FloorZ validated right after a placement/landing path that left the allowance permissive: the guard pushes+Adjusts where retail returns OK — the #345 stop, in a narrower band. | capstone decode of 0x0050f530 (reviewer A, 2026-08-08); docs/research/2026-08-08-345-d0-branch-pin.md flagged-secondary section |
| AD-72 | Filed 2026-08-07, Slice 5.3 review corrections (fix 6). VendorPricing.BuyPrice/SellPrice compute rate * perUnitValue * quantity at C# double (64-bit); retail's ShopSystem::BuyPrice/SellPrice (0x006B6120/0x006B6180) run the same multiply at x87 long double (80-bit extended) — the same narrowing class AD-33 already recorded for CSequence.FrameNumber. |
src/AcDream.Core/Items/VendorPricing.cs (BuyPrice/SellPrice, the double raw = (double)rate * perUnitValue * quantity; line) |
double is the widest floating-point type available in C# (no 80-bit extended type exists in .NET). The port keeps retail's literal ± 0.1 margin ahead of the floor()/ceil() (see the type's own doc comment) — many orders of magnitude larger than any float/double precision gap at realistic AC item-value magnitudes (rate/value/quantity products in the tens-of-thousands range at most), so the margin absorbs the narrowing before it can move the floor()/ceil() result. |
A price computed at a pathological value/rate/quantity combination landing within a double-ULP of the 0.1 margin boundary could floor/ceil to a different integer than retail's 80-bit compute would. No known installed vendor data approaches this boundary. | ShopSystem::BuyPrice/SellPrice docs/research/named-retail/acclient_2013_pseudo_c.txt:702082-702128, 0x006B6120/0x006B6180; AD-33 (same narrowing class, CSequence.FrameNumber) |
3. Documented approximation (AP) — 120 active rows (AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own PublicWeenieDesc._stackSize is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of 92ea3977 (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with ItemType.Container [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency m_last_sale simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (VendorUiController.cs, VendorState.cs, GameEventWiring.cs, RetailUiRuntime.cs); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global ACCWeenieObject::selectedID, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's m_last_sale-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a GetObjectSplitSize-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the maxReach distance pre-filter is DELETED rather than re-centred, because retail has none: CObjCell::find_obj_collisions @0x0052b750 walks the cell's shadow list and calls CPhysicsObj::FindObjCollisions unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the find_bbox_cell_list port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's maxReach distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, with its direction corrected. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because BuildFloodSpheres carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — ShadowShape.BoundsCenter, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one CPartArray::GetSortingSphere where acdream floods from every Sphere shape, and acdream's cylinder flood ignores CylHeight. AP-152 RETIRED 2026-08-06, one day after it was filed: ShadowShapeBuilder.FromSetup now dispatches BSP-first instead of unioning, and ShadowObjectRegistry.BuildFloodSpheres now applies calc_cross_cells' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: Transition.BspOnlyDispatch had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while BuildFloodSpheres approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's CPhysicsObj::FindObjCollisions dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (CPhysicsObj::FindObjCollisions 0x0050f050 exits at 0x0050f22f je 0x50f31b returning the seeded OK_TS, and CPartArray::GetRadius/GetHeight are absent from its whole call set), so the invented setup.Radius cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit CurrentCellId measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's installPlacementFrame: true, clearParent: true literals no longer exist; InboundPhysicsStateController.TryApplyPosition now computes both flags PRE-MERGE from (disposition, hasAnimations(old)), which is exactly RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows (false/false on the Gate A force row, !HasAnimations/true on every accepted non-force route). Retail decides both writes BEFORE MoveOrTeleport is consulted — Gate A @0x0045400C returns @0x0045409D ahead of unset_parent @0x00454129 and the HasAnims SetPlacementFrame gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: InboundPhysicsStateControllerTests — ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame, ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame, ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment, and the 12-row MergedPrePlacementFieldsMatchTheClassifiedRouteFlags matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — TryPublishPlace now publishes the local player's Place through LocalPlayerShadowSynchronizer.SyncPose, the same publisher ordinary per-tick movement uses, instead of a direct LocalPlayerShadowState.Set that never touched PhysicsEngine.ShadowObjects; AP-1 RETIRED 2026-08-05, C5a deletion sweep — PhysicsEngine.Resolve/ResolvePlacement/HasCellSurface deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses UsePositionFromServer (autonomy_level != 2) where retail's actual gate, SendMovementEvent, is autonomy_level != 0; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; ConstraintManager::adjust_offset only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client Airborne flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at PhysicsBody.InContact, retail's literal transient_state & 1 test at InterpolationManager::adjust_offset @0x00555D52 (bit 0 = CONTACT_TS, acclient.h:3690), while leaving Airborne and all five of its !Body.OnWalkable writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, teleport_hook's call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the WithdrawalRestored receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful step_down now performs retail's final PLACEMENT_INSERT; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — transitional_insert and edge_slide now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — CanMoveInto/RestrictionDB::IsAllowedIn are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live House_UpdateRestrictions 0x0248, resolved through PhysicsEngine.Objects), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by RestrictionObjPrevalenceInspectionTests showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked RestrictionObj, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — check_entry_restrictions ported at the head of the indoor FindEnvCollisions branch, CellPhysics.RestrictionObj wired from the DAT-baked EnvCell field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — calc_friction's threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered AP-94..AP-112 for the confirmed retail-UI completion gaps.
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
| AP-160 | Filed 2026-08-07, Slice 5.3 (vendor browse lifecycle). CORRECTED AND EXTENDED 2026-08-07 at the Slice 5.3 review corrections (fixes 4/5). Correction (fix 4): this row's own Retail-oracle citation originally grouped WorldObject_Use.cs:50,57 under the SAME citation as Vendor.CheckClose/GetCylinderDistance, which read as if the wo.UseRadius ?? 0.6f fallback lived inside the close watcher. It does not: WorldObject_Use.cs:50,57 is WorldObject.IsWithinUseRadiusOf, the APPROACH check ("how close you need to be to open the shop") — a wholly different method from Vendor.CheckClose, which reads UseRadius directly with no fallback of its own (UseRadius is float?; a nullable comparison against a null right operand is always false, so CheckClose never closes at all on an unauthored radius). EnforceRange's own code comment carried the same mis-attribution and, worse, actually APPLIED that mis-borrowed 0.6f as its fallback; it now passes the raw authored UseRadius with no fallback of any kind (0 when absent/unauthored, matching retail's own memset-zero PublicWeenieDesc::_useRadius default — a plain float field, acclient.h:37181, no sentinel). Retail's own behavior for a radius-0 handler is exactly this: close on the very first nonzero-distance check. Extension (fix 5): the watcher reads the SERVER-ECHOED ACCEPTED position snapshot (RuntimeEntityRecord.Snapshot.Position), sampled once per advanced frame at the post-network-command-phase, not retail's continuous live-pose push (retail's own client simulates and renders every entity's pose every frame; CPlayerSystem's range handler reads that live pose, never a periodically-echoed one). Between accepted-position updates the watcher's distance measurement is therefore up to one update-interval stale. The one BLIND WINDOW this staleness could open into a wrong in/out-of-range verdict — an in-session portal/teleport, where the player's and vendor's position snapshots can briefly sit in DIFFERENT landblock coordinate frames mid-transit — is closed unconditionally by this same review's fix 1b (RuntimeWorldTransitState.HasPendingTeleportStart/IsTeleportActive short-circuit the whole distance computation before it runs, closing the session instead of measuring across the transit), so the staleness itself never reaches that particular failure mode; it remains recorded here as a standing precision gap for the window fix 1b does NOT cover (ordinary out-of-transit movement between the same-generation position updates a slow network tick can leave briefly stale). Original text: The client-local vendor-panel distance watcher closes on PLAIN 3D center-to-center distance instead of retail/ACE's CYLINDER-GAP distance (both objects' own collision radius and height subtracted from the center distance before comparing to UseRadius). Retail: gmVendorUI::OpenVendor registers CPlayerSystem::RegisterObjectRangeHandler keyed to the vendor's own PublicWeenieDesc._useRadius; ACE's server-side belt-and-suspenders Vendor.CheckClose closes on GetCylinderDistance(lastPlayer) > UseRadius, i.e. Position::cylinder_distance/Physics.Common.Position.CylinderDistance with each side's real GetRadius()/GetHeight(). |
src/AcDream.Runtime/Gameplay/RuntimeVendorRangeQuery.cs (EnforceRange) |
AcDream.Runtime does not resolve a live per-entity collision radius/height for an arbitrary NPC outside the App-layer's Setup-cylinder resolver (WorldSelectionQuery's _setupCylinder, App-only — out of Runtime's reach per the Core-structure rules, and PhysicsBody/RuntimeEntityRecord carry no radius/height field). Plain center distance is a well-defined, non-degenerate substitute (using ObjectRangeMath.ObjectsInRange's existing useRadii: false branch rather than inventing a new metric) for a CLIENT-LOCAL UI convenience that never touches the wire or any authoritative state — closing the panel is not gated by, nor gates, anything server-visible. Reading the accepted-position snapshot rather than a continuously-integrated live pose is the same "Runtime has no live render-side pose, only the last accepted wire snapshot" constraint every other Runtime-side distance query in this codebase already accepts. |
The panel can close up to (player radius + vendor radius) sooner than exact retail — typically well under a meter for a two-legged NPC — so a player standing exactly at the boundary of a large-radius vendor's UseRadius may see the panel close slightly earlier than retail would. No effect on any transaction, wire message, or authoritative state (Slice 6's buy/sell owns those). Retiring the cylinder-gap half requires a Runtime-owned per-entity collision radius/height source, which does not exist today; retiring the staleness half requires a continuously-updated live-pose source Runtime does not keep either. |
CPlayerSystem::RegisterObjectRangeHandler pc:203677/0x004C4C34; gmVendorUI::OnObjectRangeExit pc:199486/0x004C02F0; ACE Vendor.CheckClose/GetCylinderDistance (references/ACE/Source/ACE.Server/WorldObjects/Vendor.cs:322-367) — a SEPARATE method, WorldObject.IsWithinUseRadiusOf (WorldObject_Use.cs:44-52), owns the unrelated ?? 0.6f approach-check fallback; acclient.h:37181 (float _useRadius, plain memset-zero field, no sentinel); docs/research/2026-08-08-slice5-vendor-browse-research.md §A.3/§B.1/§B.2 |
| AP-141 | Filed 2026-08-04, C4 route 5 (projectile authoritative placement); NARROWED 2026-08-04 at the round-2 delta review (B1/B2) — the far-branch clause was factually wrong for the adopted-body case and is corrected below. Three related projectile-only shapes, all pinned by design (D-P4) rather than ported: (a) the near-Interpolate disposition is a NO-OP for a live missile, where retail would lazily build interpolation machinery (InterpolateTo @0x005163AF) for it; (b) the post-operation ConstrainTo @0x00454272 (MakePositionManager @0x00510523 then PositionManager::ConstrainTo) is never ARMED for a projectile — retail's single arming site has no kind test, so retail WOULD build a PositionManager on demand and arm a missile's leash on any nonzero MoveOrTeleport return; acdream never arms it on any disposition, including the adopted-body case (whose PRE-EXISTING leash the teleport/far branches now un-arm or clear queue state for, but never RE-anchor, per retail's post-operation ConstrainTo); (c) a null-classified or Rejected* accepted Position for a missile is swallowed (write nothing) rather than caught up through any remote-shaped policy. |
src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs (ApplyAcceptedProjectilePosition) |
acdream deliberately does not construct an EntityPhysicsHost/PositionManager/InterpolationManager chain for a ballistic body — the route-5b split the C4 route 5 contract rejected. The context that makes this safe rather than merely convenient: ACE never sends UpdatePosition for a missile (references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:333-334, SendUpdatePosition() commented out inside the PhysicsState.Missile branch at :265) — every half of this row is deterministic-test-gated only, never exercised against a real server. The far branch's StopInterpolating skip is retail-faithful ONLY for a BARE missile (no RemoteMotion — retail's own position_manager != 0 guard @0x005163C9 skips it for a never-interpolated object, so acdream's skip is faithful by consequence there). For the ADOPTED-BODY case (TryBind's shared-body branch: an ordinary remote whose Missile bit was set by a later State packet, still carrying its RemoteMotion), retail's guard IS satisfied and retail WOULD clear the queue — acdream now ports this (route.StopInterpolating && record.RemoteMotion is RemoteMotion adopted → adopted.Interp.Clear()), matching the teleport branch's equivalent StopInterpolating action inside teleport_hook. What remains divergent for the adopted case is the post-operation ConstrainTo re-anchor @0x00454272 — retail re-anchors an existing leash at the just-updated position on every nonzero return; acdream never arms/re-anchors it on any projectile disposition (clause (b)). |
A future change that DOES give projectiles a PositionManager (or a headless/no-window remote-motion consumer that expects one) must re-decide this row rather than silently building the machinery ad hoc; until then, a live missile never shows an ARMED constraint leash and never catches up via the near/UnroutedCatchUp policy — both unreachable in play. An adopted-body missile's INHERITED leash (armed before it became a missile) is un-armed by the teleport hook, has its queue cleared by both teleport and far, but is never re-anchored at the new position by either — its brake accumulator (ConstraintPosOffset) is not reset to zero at each accepted Position the way retail's @0x00454272 re-anchor does. Correction, round 3 (2026-08-04): the round-2 wording here — that a stale leash "would drag the body toward a stale anchor" — was wrong and is retracted. ConstraintManager.ConstraintPos is write-only in both retail and the port (never read by AdjustOffset), and ConstraintManager::adjust_offset @0x00556180 only tapers or zeroes an already-composed per-tick offset while InContact — a leash brakes motion the interp/sticky chain already produced; it has no mechanism to move anything toward the anchor. The real residual is confined to one tick of un-reset brake accumulator, contact-gated, and it cannot move an airborne far-snapped missile at all (the clamp branch does not run while airborne). |
CPhysicsObj::MoveOrTeleport 0x00516330 (InterpolateTo @0x005163AF, IsMovingTo @0x0050EB10 returning 0 without a MovementManager; far branch StopInterpolating @0x005163C9-@0x005163CB); SmartBox::HandleReceivedPosition 0x00453FD0 (ConstrainTo arming site @0x00454272); CPhysicsObj::ConstrainTo 0x00510520 (MakePositionManager @0x00510523); ConstraintManager::adjust_offset 0x00556180 (brake-only taper, write-only anchor); WorldObject_Tick.cs:333-334/:265 (ACE never-sends evidence) |
| AP-142 | Filed 2026-08-04 (C4 route 7, pickup/parent/delete). AMENDED 2026-08-04 at the dual-Opus retail-conformance/architecture review round (R1/A8 MAJOR+LOW; R10 MINOR) — clause (d) added, clause (b) corrected. AMENDED AGAIN 2026-08-04 at the round-3 dual review (N1/N2/N4, B3) — clause (d)'s reasoning corrected and its risk-column scope widened; clause (e) RETIRED — the depth cap it described is deleted outright, replaced by an iterative worklist with no depth concept at all. AMENDED AGAIN 2026-08-05 at the #319 fix — clause (f) added. AMENDED AGAIN 2026-08-05 at the #319 dual-review round (retail PASS, architecture FAIL/6 MAJORs) — clause (f) rewritten: the tripwire moved above the canonical commit and no longer throws (A1), and the deferred late-bind queue A1's fix text originally described was deleted per A6 (both reviews proved it production-unreachable for both producers). acdream collapses retail's CPhysicsObj pair — a cell pointer plus a separately-written objcell_id — into ONE canonical RuntimeEntityRecord.FullCellId, which is also the residency/liveness predicate acdream reads at 45+ sites. Four consequences, all intentional: (a) the removal path propagates ZERO to a subtree's children (withdrawal, delete, EndGeneration), where retail's leave_cell recursion nulls only each child's cell pointer and leaves a STALE non-zero objcell_id (change_cell's removal tail @0x005133C1 never touches a child's id) — reproducing that stale-id residue would leave a child "resident" per every acdream predicate while retail's own gating field (cell == nullptr) says it is not; (b) retail's same-cell depth-1 per-tick objcell_id refresh (SetPositionInternal @0x0051539c-@0x005153d8, gated on the parent NOT crossing a cell) is subsumed by the value-idempotent propagation chokepoint (RuntimeEntityDirectory.SetFullCell's "skip a child whose FullCellId already equals the target" guard) rather than ported as a separate tick loop — a same-value restamp is unobservable with one field playing both retail roles. Correction (R10): this is a clean equivalence only on the REMOVAL side. The skip ALSO prunes the child's whole subtree on a same-value WRITE, which retail's enter_cell does not do — it recurses over children unconditionally (@0x00510f03); only leave_cell prunes (@0x00510f5b, on cell != 0). Currently unreachable-by-construction (after D4 nothing writes a grandchild's cell independently of its own committed parent), but it is an asymmetry, not a proven equivalence; (c) the sustaining propagation itself: retail re-cells children when the parent crosses a cell, recursively, on EVERY SetPositionInternal/change_cell (@0x00515372/@0x00513390), not only at attach — acdream ports this as a single hook every canonical cell-write funnels through, so an attach-only write (the pre-existing shape) is deliberately NOT what shipped. (d) retail's enter_cell gates its ENTIRE body — the write AND the recursion into children — on this->part_array != 0 (@0x00510ed8); a child with a null part array receives nothing and its whole subtree is skipped. acdream's propagation has NO analogue and writes unconditionally. CORRECTED reasoning (round-3 review, N1/N2): the original draft of this clause argued acdream's HasPartArray means something semantically different from retail's part_array (a "renderer built a mesh" flag vs. "this CPhysicsObj has any part array"). That framing is WRONG — retail's part_array has exactly ONE assignment site, CPhysicsObj::makeAnimObject @0x0050e930 → CPartArray::CreateSetup, assigned @0x0050e94d, so retail's flag is ALSO a mesh-construction product; the two are near-synonyms, not different concepts. The REAL reason acdream cannot gate the canonical D1/D2 write on HasPartArray is LAYERING, not semantics: Slice J made the Runtime canonical layer presentation-independent by design (docs/research/2026-07-25-slice-j1-runtime-contract-closeout.md and the Slice J campaign generally), and HasPartArray is populated exclusively by App/graphical code (EquippedChildRenderController.cs:609, DatLiveEntityProjectionMaterializer.cs:203) — the canonical layer structurally cannot depend on a flag only the presentation layer ever writes, headless or not. CORRECTED scope (round-3 review): this is NOT headless-only. PrepareAndTryRealize calls CommitAcceptedParentCellless (hence D1's re-cell) BEFORE TryRealize sets HasPartArray = true at :609 — so at the exact moment D1 runs, child.HasPartArray is FALSE in the GRAPHICAL host too, and gating on it would break attach there as well, not just headless. Retail has no equivalent window at all: part_array is assigned once at construction and enter_cell's guard reads that same, already-settled field. The guard is deliberately NOT reproduced at the canonical layer. (e) RETIRED 2026-08-04 (round-3 review, N4/B3 — both reviews independently found the same defect). Previously: recursion depth capped at 64 levels as hostile/buggy-server hardening. The cap's actual failure mode was worse than what it guarded against: a subtree beyond the cap was left at its PRIOR — on the withdraw path, STALE NONZERO — cell PERMANENTLY, logged only under a probe flag nobody runs by default. On the withdraw path that is the #184 shape verbatim: an entity every acdream residency predicate calls resident that retail (and clause (a) above) says is not. Shipping that inside the slice whose headline is fixing exactly this class was unacceptable. Retired by deleting the cap outright and replacing the recursion with an iterative worklist (RuntimeEntityDirectory._propagationWorklist), which has no stack-frame-bounded depth at all — the only limit is the number of committed relations actually in the system, matching retail's own genuinely unbounded recursion with no acdream-only cap and therefore no register row for one. (f) Filed 2026-08-05 (#319 fix). A CreateObject-carried parent relation (the raw spawn's Physics.Parent field, and the same-generation CreateParentUpdate envelope) names the parent's GUID and location only — neither wire shape carries a parent instance sequence, matching retail's own GUID-only attach (PhysicsDesc::get_parent_id @0x00558a18 → CObjectMaint::GetObjectA @0x00558a2d → CPhysicsObj::set_parent @0x00558a3e; the reverse CObjectMaint::SetChildren @0x00509370 hash-walks by guid with a GetNullObject placeholder @0x005093e6 — no instance-sequence field or comparison exists anywhere in either direction). acdream's committed-relation table is nonetheless keyed by (guid, incarnation) (clause (c)'s D1/D2 requirement), so a CreateObject-carried relation must adopt SOME incarnation to file under; it now LATE-BINDS to the parent's LIVE incarnation at accept time (EquippedChildRenderController.AcceptLateBoundCreateObjectRelation, both the raw-CreateObject and same-generation CreateParentUpdate producers) rather than the previously-hardcoded 0, which silently mis-keyed every player-parented CreateObject relation (a player's ObjectInstance is Character.TotalLogins, never 0) and defeated D1/D2 for the local player's own login equipment and every remote player's observed equipment (#319). A commit-time tripwire (ParentAttachmentState.CanCommitIncarnation, checked BEFORE either half of the commit mutates state — architecture review A1, 2026-08-05, moved it there after the original throw-after-canonical-commit shape was shown to tear the transaction it was built to protect) refuses (logs, returns false, never throws) rather than silently filing a relation under a mismatched incarnation whenever the parent is currently addressable. A1 also settled A6's design question: an initial revision queued a relation whose parent was not yet addressable through a deferred/late-bind retry mechanism; both reviews independently proved that queue was structurally unreachable in production for BOTH producers (RuntimeEntityObjectLifetime.RegisterEntityCore's EnqueueDeferredCreate gate defers the ENTIRE CreateObject, for both wire shapes, before either producer ever runs) while carrying three latent defects of its own (a missing child POSITION_TS gate, a placeholder-incarnation collision with the generation filters, unbounded accumulation) — it was deleted rather than fixed in place; the unaddressable-parent case now logs and refuses outright, matching the invariant the layer above already enforces. |
src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs (SetFullCell, PropagateFullCellToChildren, RefreshSnapshot); src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (CommitAcceptedParentCellless's D1 half, WithdrawCommittedChildrenToCellless); src/AcDream.Runtime/Entities/ParentAttachmentState.cs (TryGetCommittedParent, CanCommitIncarnation, CommitProjection); src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs (HasPartArray); src/AcDream.App/Rendering/EquippedChildRenderController.cs (AcceptLateBoundCreateObjectRelation, OnSpawn, OnCreateParentAccepted, PrepareAndTryRealize); src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs (ResolveAndCommitChildAttachment) |
Reproducing retail's pointer/id split would require a second field acdream's 45+ liveness call sites would then have to be individually audited for which half they mean — the single-field model is a stated, load-bearing simplification, not an oversight; see docs/research/2026-08-04-retail-parent-cell-propagation.md and docs/research/2026-08-04-c4-route-7-contract.md D2/D3/D9. Clause (f) is retail-faithful for the identical reason clauses (a)-(d) are: retail's attach has no incarnation gate on this path at all, so adopting the current holder of the guid IS the retail behavior, not an approximation of it. |
A future consumer that expects retail's exact stale-objcell_id-under-a-null-cell shape (none identified) would see a fully cell-less child instead. (d)'s risk: acdream celling a child retail would leave nowhere — none identified in play against a well-behaved ACE, since a server-authored equip always names a real, DAT-resolvable Setup, and the graphical host's own brief pre-TryRealize window is bridged by D1 running inside the same synchronous transaction as the rest of the attach commit, not by HasPartArray being true. (f)'s risk: none identified against a well-behaved ACE — a CreateObject's parent guid always names the entity that currently holds it by construction. |
CPhysicsObj::change_cell 0x00513390 (@0x005133C1 removal tail); CPhysicsObj::enter_cell 0x00510ed0 (@0x00510ed8 the part_array guard); CPhysicsObj::leave_cell 0x00510f50; CPhysicsObj::SetPositionInternal 0x00515330 (@0x0051536d branch, @0x0051539c-@0x005153d8 same-cell loop, @0x00515372 cell-change branch); CPhysicsObj::makeAnimObject 0x0050e930 (CPartArray::CreateSetup assignment @0x0050e94d); PhysicsDesc::get_parent_id 0x00558a18; CObjectMaint::GetObjectA 0x00558a2d; CPhysicsObj::set_parent 0x00558a3e; CObjectMaint::SetChildren 0x00509370 (GetNullObject placeholder @0x005093e6) |
| AP-143 | Filed 2026-08-04 (C4 route 7 D5, headless parent-realize drive). AMENDED 2026-08-04 at the retail-conformance review round (R7 MINOR) — this row originally described only ONE of the three checks the drive skips. Line citations corrected at the round-3 review (N3). The graphical EquippedChildRenderController.ValidateParentProjection performs three retail-anchored checks before accepting a parent-attach request: (1) self-parenting rejection (relation.ParentGuid == relation.ChildGuid, :915-916); (2) the parent must have a constructed part array (parent.HasPartArray, :920 — the closest acdream analogue to retail's part_array != 0 guard, AP-142 clause d); (3) Setup.HoldingLocations validates the specific holding location (CSetup::GetHoldingLocation @0x0050F896, via PartArray::add_child). AcDream.Headless/AcDream.Runtime's direct-host parent-realize drive (RuntimeLiveEntitySessionController.ResolveAndCommitChildAttachment) performs NONE of the three — it commits on the POSITION_TS gate acceptance and relation resolution alone. (1) is inert by construction: D1's re-cell gate reads parent.FullCellId == 0 (the child was just zeroed by the cell-less edge before D1 runs), and D2's skip-on-equal terminates the resulting one-node cycle — a self-parent headless commits the relation but never observably re-cells through it. (2) has no headless analogue at all (see AP-142 clause d — HasPartArray is populated only by the graphical mesh pipeline, never headless, for ANY entity). (3) has no prepared-content surface (repo-wide grep confirms nothing under src/AcDream.Content/AcDream.Bake carries Setup.HoldingLocations). |
src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs (ResolveAndCommitChildAttachment) |
Precedent: the content-less host already accepts reduced fidelity elsewhere (RuntimeLiveEntitySessionController:108-117's documented content-less registration). A server-sent self-parent, part-array-less parent, or invalid holding location is unreachable against a well-behaved ACE (ACE only emits ParentEvent for a location its own Player_Inventory/wield validation already accepted), so this is a defense-in-depth gap, not a live-play one. |
A malicious or buggy server could attach a child headless where retail and the graphical host would both reject it — inert against ACE today for all three. Retiring (3) means extending the prepared-content bake format with Setup.HoldingLocations, deliberately NOT done in this slice (route 7 contract §4 D5); (2) has no retiring action available until acdream's canonical layer gains its own construction-time part-array concept (a larger architectural question, out of scope here). |
PartArray::add_child (CSetup::GetHoldingLocation 0x0050F896); CPhysicsObj::enter_cell 0x00510ed8 (the part_array guard); EquippedChildRenderController.ValidateParentProjection (graphical port, all three checks) |
| AP-144 | Filed 2026-08-05 (C4 route 3, round-3 review R7). Register discipline finding, not an implementer's disposition — CLAUDE.md's register rule binds regardless of whether the gap has a live symptom yet. RuntimeAcceptedPositionDriveController.ReconcileAndAcknowledgePortal's teleport-arrival movement-event send gates on !RuntimeCharacterState.UsePositionFromServer — retail's CommandInterpreter::UsePositionFromServer @0x006B3B40, which is autonomy_level != 2. But the retail function that ACTUALLY gates this send is a different one: CommandInterpreter::SendMovementEvent @0x006B4680 (the PlayerTeleported tail-jump), which gates on autonomy_level != 0 — the LOOSER test, excluding only level 0, satisfied by BOTH level 1 and level 2. acdream's gate reuses the STRICTER UsePositionFromServer test (excluding two of the three levels, 0 AND 1), built from the wrong retail function, so it sends only at level 2 and wrongly suppresses at level 1. |
src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs (ReconcileAndAcknowledgePortal, the !_usePositionFromServer() guard around TrySendMovement); src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs (UsePositionFromServer, AutonomyLevel) |
The two gates agree at level 0 (both suppress) and level 2 (both send); they diverge only at level 1. RuntimeCharacterState.TrySetAutonomyLevel has zero production callers today, so no live code path can ever reach AutonomyLevel == 1 — the divergence is filed for completeness, not because it is currently reachable. |
The instant a future feature calls TrySetAutonomyLevel(1) (a partial-autonomy mode, if one is ever built), a portal-arrival movement-event ACE expects to receive at level 1 is silently dropped, until this row's fix threads the raw AutonomyLevel through the constructor (touching both host compositions) and gates on != 0 directly instead of reusing UsePositionFromServer. |
CommandInterpreter::UsePositionFromServer @0x006B3B40 (autonomy_level != 2); CommandInterpreter::SendMovementEvent @0x006B4680 (autonomy_level != 0, the PlayerTeleported tail-jump call site) |
| AP-146 | Filed 2026-08-05 (#319 fix, the local player's canonical cell prerequisite; follow-up filed as issue #320). Retail writes the local player's cell on EVERY physics tick (CPhysicsObj::SetPositionInternal @0x00515330, unconditional for any moving body including the player). acdream's canonical FullCellId for the LOCAL player is written only at three edges: login activation (RuntimeSetPositionState.cs:2741-2745), the OnPosition generic tail's prologue rebucket after an accepted inbound Position (LiveEntityNetworkUpdateController → LiveEntityRuntime.RebucketLiveEntity → RuntimeEntityObjectLifetime.CommitRebucket; amended 2026-08-05 by C5b/#275 — this writer was RuntimeEntityDirectory.RefreshSnapshot → RuntimeEntityRecord.cs:234, i.e. the merge itself, until C5b made the merge withhold the wire cell per AD-60; a ForcePosition, which returns before this tail, is now placement-receipt-authoritative instead), and a teleport/portal placement commit (RuntimeSetPositionState.cs:5001-5007; LocalPlayerTeleportController.cs:255). Ordinary WASD movement passes a LANDBLOCK id, not an exact cell (LocalPlayerProjectionController.Project, low 16 bits forced to 0xFFFF in both branches), and LiveEntityRuntime.cs:935-938 explicitly PRESERVES the prior canonical cell for that shape rather than writing the coarser value — so the local player's canonical cell is coarse and mostly-frozen between teleports, never per-crossing-fresh. #319's fix makes a player-parented equipped child inherit exactly this same value (D1/D2 propagate the PARENT's canonical cell to the child verbatim) — the child is stale-but-EQUAL wherever the player's own record already is, not a new staleness class. AMENDED 2026-08-05 at the C5b architecture review's D1 fix: this three-edge enumeration was written from the graphical host and silently assumed both hosts shared it. They do not — the two run parallel, non-shared inbound routes — and the second edge (the OnPosition prologue rebucket) lived in AcDream.App, so the no-window host had only TWO of the three, the login activation and the teleport/portal commit. It now has all three: RuntimeLiveEntitySessionController.TryCommitAcceptedWireCell commits the same value through the same shared owner, RuntimeEntityObjectLifetime.CommitWireCellRebucket. The no-window host reaches that edge on the local ordinary (Apply) Position and on a ForcePosition the accepted-Position drive declined (NotApplicable), mirroring the graphical route exactly — a force the drive HANDLED stays placement-receipt-authoritative. This row's COARSENESS claim is unchanged and applies identically to both hosts: the preserve branch now lives in CommitWireCellRebucket rather than at LiveEntityRuntime.cs:935-938, and the no-window host does not even have the per-frame landblock-shaped caller that motivates it. |
src/AcDream.App/Input/LocalPlayerProjectionController.cs (Project); src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (CommitWireCellRebucket — the landblock-preserve branch, moved here verbatim from LiveEntityRuntime.cs:935-938 at the D1 fix so both hosts share one rule); src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs (TryCommitAcceptedWireCell — the no-window host's inbound-Position edge); src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (activation :2741-2745, teleport commit :5001-5007) |
Making the local player's canonical cell track ordinary movement exactly (an exact-cell rebucket rather than the landblock-only one) is a LARGER slice than #319's key fix alone — it touches the landblock-preserve contract, Rebucketed delta publication cadence (today the player never publishes one during WASD), the route-2/4b-3 PreMergeCommittedCellId classification inputs AP-136/AP-138 spent four review rounds pinning, and the portal-space frozen-source-cell race (LocalPlayerProjectionController.Project:100-103). Deliberately NOT bundled into #319; filed as its own follow-up, issue #320. |
The player's own render/liveness/radar/picking paths already tolerate this staleness today (proven: the player renders correctly everywhere via Source.ParentCellId-driven visibility, not FullCellId) — verified safe for the EXISTING consumer set. UNRESOLVED (this row's own open item, carried into #320): whether RuntimeSetPositionState.IsAffectedCollisionResident's ParkCollisionResidents sweep could retire a spatial-root local player on a stale cell after a long teleport-free WASD run beyond the streaming radius — not established either way; the connected routes exercised so far all teleport between stops, which refreshes the cell and may be masking it. If the player IS a spatial root and this is reachable, the same staleness this row accepts for render/child-inheritance would ALSO apply to collision retirement, which is a materially different risk class. The D1 fix narrows that open item's URGENCY without answering it: before the fix a no-window bot was strictly worse than the graphical client here, because it lacked the inbound-Position edge entirely — a bot running A→B without teleporting kept FullCellId at A for the whole session, so retiring A parked a body physically in B, and retiring B missed it. Both hosts now refresh on every accepted Position; what remains open is the same question this row always asked, at ACE's 5-10 Hz cadence rather than never. |
CPhysicsObj::SetPositionInternal 0x00515330 (unconditional per-tick cell write) |
| AP-147 | Filed 2026-08-05 at the C5b architecture review (finding D3) — an unfiled delta-stream cardinality change C5b introduced, which its own conservation test could not see. A cell-changing accepted steady-state Position now publishes two RuntimeEntityDeltas for the moved entity where it published one, and the intermediate one carries a torn cell/position pair. Pre-C5b the merge itself moved FullCellId, so it published Rebucketed and the OnPosition prologue rebucket's CommitRebucket then early-returned publish-less (previous == fullCellId) — stream [Rebucketed]. Post-C5b the merge moves nothing, so it publishes Updated and CommitRebucket publishes the Rebucketed — stream [Updated, Rebucketed]. The Updated element is assembled from the canonical record BETWEEN the two writes, so its CellId is the OLD (committed) cell while its Position is the NEW wire pose: a pair that did not previously exist on this stream, because pre-C5b both halves moved inside one publish. Total per packet is conserved in KIND and final VALUE — exactly one Rebucketed, at the same cell, from the same publisher — but not in COUNT, and not in intermediate consistency. AMENDED 2026-08-05 at the C5b closeout (bookkeeping only — nothing in this row was false, it was un-updated). This row was written from the graphical host at a moment when it was the only host producing the two-delta stream at all: pre-D1 the no-window host had no post-merge cell writer, so its accepted Position published [Updated] alone and simply LOST the Rebucketed. D1 gave that host its own CommitWireCellRebucket caller, so both hosts now produce [Updated, Rebucketed] with the same torn intermediate. The row's analysis, its "no production consumer identified today" verdict, and its retirement condition are unchanged; what changed is the population — a headless bot's event log is now a REAL instance of the "future consumer that SNAPSHOTS a delta" this row warns about, not a hypothetical one, because the no-window host is the one whose consumers are event streams by construction. |
src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (TryApplyPosition's terminal AcknowledgeProjectionAndPublish, and CommitRebucket); src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs (Snapshot — the record.FullCellId / record.Snapshot.Position pairing that makes the intermediate torn) |
Retail has no delta stream at all, so there is no retail shape to match — this is acdream's own observer contract. The alternative, suppressing the merge's Updated when a rebucket is about to follow, is not available at that layer: the merge cannot know whether its caller will reach W2 (the local force arm, the missile arm, and the ChildUnparentDisposition Superseded/Pending arm all return before it), so suppressing would silently drop the pose delta on exactly the packets where it is the only one. Collapsing the merge's ternary to a constant Updated is likewise wrong — the retained Rebucketed arm has a real producer, the cancelled-park rollback inside the merge. |
Any consumer that treats one accepted Position as one entity delta now sees two, and any consumer that reads CellId and Position from the SAME delta and assumes they agree can transiently pair a new position with the old cell. No production consumer identified today: LiveEntityRuntime and the plugin/world-event surfaces re-read canonical state rather than trusting a delta's paired fields, and the pair reconverges inside the same OnPosition call. A future consumer that SNAPSHOTS a delta — a recorder, a plugin, a headless bot event log — would capture the torn intermediate. Retire together with W2, if the local player's canonical cell ever becomes per-crossing-fresh (AP-146/#320) and the merge and the rebucket can be one write again. |
No retail anchor — acdream-only observer contract. Evidence: RuntimeSteadyStatePositionMergeTests.CellChangingAcceptedPosition_ConservesOneRebucketAndOneChildPropagation asserts the complete ordered stream [Updated, Rebucketed] plus both elements' CellId/Position.ObjCellId, and RuntimeSetPositionStateTests.AcceptedPositionCancellingWakeableParkPublishesRebucketedThroughTheMerge pins the retained arm; both sabotage-verified in both directions at the C5b review. |
| AP-148 | Filed 2026-08-05 at the C5b closeout, from disassembly of the PDB-paired binary — NOT from the pseudo-C, which cannot show it. acdream's local-player Gate A (the FORCE_POSITION self-echo shortcut) requires the wire TELEPORT_TS to be EXACTLY EQUAL to the stored one; retail requires only that it not be OLDER, so equal AND newer both take the shortcut. SmartBox::HandleReceivedPosition @0x0045402B-54 loads player->update_times[4] (TELEPORT_TS; base 0x164, 2 bytes/entry, confirmed by the POSITION_TS store mov word [edx+0x164], ax @0x00454084 and acclient.h:6090), takes abs(stored - wire), picks a wrapped or unwrapped 16-bit compare on > 0x7fff, materialises the carry with sbb eax,eax / neg eax, and SKIPS Gate A on CF — where CF means the wire stamp is strictly older. It is CPhysicsObj::newer_event @0x00451B10's identical idiom with the compare operands swapped. Binary Ninja drops the flag test and renders the whole sequence as if (-((eax_7 - eax_7)) == 0), vacuously true, which is why two C5b review rounds read this function carefully and both recorded the term backwards (docs/research/2026-08-05-c5b-contract.md §1 said first "teleport must NOT be newer", then "TELEPORT_TS equal"; both corrected at §15). Consequence: acdream's ForcePosition disposition is a strict SUBSET of retail's Gate A set. A local ForcePosition carrying a NEWER teleport stamp is misrouted into a full Apply, which is four separate behaviour changes at once — it takes the WIRE heading instead of preserving the body's (InboundPhysicsStateController.ApplyAcceptedPosition:846-856, force-gated), it UNPARENTS and may install a placement frame (clearParent: !force, installPlacementFrame: !force && !hasAnimations — C5b's own truth table), it sets TeleportAdvanced and therefore ZEROES local velocity (:882-885), and it advances TELEPORT_TS and calls OfferTeleportDestination, starting teleport/portal presentation for a packet retail never starts it for. Retail's Gate A deliberately lets a force ride PAST a pending teleport advance without consuming it (it returns @0x0045409D before newer_event(arg2, TELEPORT_TS, arg8) @0x00454158); the ordinary Position channel is what processes that teleport. Not fixed in the filing commit, deliberately: see issue #325 for why it is not a one-line comparison swap. C5b made this marginally BETTER, not worse — clearParent was unconditionally true pre-C5b and is unchanged for the misrouted packet, and installPlacementFrame went unconditional-true to !force && !hasAnimations, i.e. toward retail's "Gate A never reaches SetPlacementFrame". |
src/AcDream.Core/Physics/PhysicsTimestampGate.cs (TryAcceptPositionEvent:199, the teleport == _timestamps[Teleport] term); src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs (ValidAcceptedAuthority, the PreviousTeleportSequence == AcceptedTeleportSequence term — the SAME predicate encoded a second time, and the reason the fix is not one line) |
None argued — this is an unintended narrowing found at a closeout, not a chosen approximation. It is filed as an approximation rather than a defect only because the resulting behaviour is a strictly SMALLER shortcut set, i.e. more packets take the fully-processed path rather than fewer, which fails safe for pose correctness even where it is wrong about heading, parent, velocity, and presentation. The exact retail predicate already exists verbatim in the same file — IsFreshTeleportStart:163 is !IsNewer(teleport, _timestamps[Teleport]) — so the correction itself is trivial; the consumers are not. |
A server correction that arrives while the client's TELEPORT_TS is behind ACE's (a teleport whose Position packet was lost, or arrived after the force) is promoted from "blip me in place" to a full teleporting apply: the player's facing snaps to the wire heading instead of staying where the mouse left it, local velocity is zeroed mid-stride, an equipped child is unparented, and the portal/transit presentation owner is offered a destination for a packet that is not a teleport. Reachability against ACE is UNMEASURED — ACE's two ObjectForcePosition bumps (Player.cs:1148 PKLite re-placement, Player_Tick.cs:488 z-hack correction) do not themselves bump the teleport sequence, but PositionPack serialises the CURRENT teleport sequence, so any client whose TELEPORT_TS lags ACE's is in the divergent window on its next force. |
SmartBox::HandleReceivedPosition 0x00453FD0 (Gate A's teleport test @0x0045402B-0x00454054; the return @0x0045409D; the TELEPORT_TS advance it skips @0x00454158); CPhysicsObj::newer_event 0x00451B10 (the same idiom, operands unswapped); acclient.h:6090 (update_times[4] == TELEPORT_TS) |
| AP-149 | Filed 2026-08-05 at the #280 fix (portal destination prefetch). The reveal gate's OUTER ring accepts terrain-only publication where retail requires the landblock's full static-DAT closure. Retail's LScape::PreFetchCells @0x00505660 walks the whole mid_radius square and, for EVERY in-bounds landblock, requires (1) its terrain record resident, (2) its LandBlockInfo type-2 record resident, and (3) via CLandBlock::PreFetchCells @0x00530240 -> CLandBlockInfo::PreFetchCells @0x0052E7C0 -> CBldPortal::PreFetchCells @0x0053BD00, every EnvCell of every building it contains. acdream's outer ring is Far-tier: heightmap + terrain render mesh + terrain collision, with NO LandBlockInfo, no buildings, no building EnvCells and no procedural scenery, because the Far tier does not load them at all. The gate therefore converges on a strictly weaker condition than retail's out beyond NearRadius. #280 closed the 11.4:1 reveal-window/visible-window ratio; it did NOT close this. Do not let a later closeout claim parity. |
src/AcDream.App/Streaming/StreamingController.cs (IsRenderNeighborhoodResident, the far arm); src/AcDream.App/Streaming/LandblockBuildFactory.cs (the Far build's contents); src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs |
Closing it would mean promoting the entire Far window to Near, i.e. deleting the two-tier streaming design that exists precisely because full hydration of a 25x25 window is unaffordable. Retail affords it because retail's ONE square is 17x17 at its default draw distance and it blocks the whole simulation while loading it (CellManager::blocking_for_cells), which acdream deliberately does not do (see AD-2). The residual is bounded to content that is only ever seen at Far distances. |
A distant BUILDING, its interior EnvCell shells, or distant procedural scenery can still appear after the viewport opens, at Far-ring distances (beyond ~768 m at the shipped High preset), where retail would have kept blocking. Distant TERRAIN — the reported #280 symptom — no longer can. | LScape::PreFetchCells 0x00505660; CLandBlock::PreFetchCells 0x00530240; CLandBlockInfo::PreFetchCells 0x0052E7C0; CBldPortal::PreFetchCells 0x0053BD00 |
| AP-150 | Filed 2026-08-06 at the #280 retail-conformance review (finding F2). acdream arms the "In Portal Space - Please Wait..." cue only after the hold has run five seconds (RuntimeWorldTransitState.RetailWaitCueDelay = TimeSpan.FromSeconds(5), enforced at the readiness tick; PortalTunnelPresentation.TickRotation then re-emits per rotation segment only if (_waitCueVisible)). Retail has no such threshold. The emit site is inside gmSmartBoxUI::UseTime's TAS_TUNNEL* branch, in the else arm of the rotation-segment-expiry test at 0x004D6FCD: when a segment expires retail picks a new random segment and calls ECM_UI::SendNotice_DisplayStringInfo(0x1a, ...) UNCONDITIONALLY, whether or not CellManager::blocking_for_cells is set — the notice is a property of being in the tunnel, not of being blocked. Byte-decoded at 0x004D6FE6-0x004D7049: teleportRotationDuration = RandDouble(0.6, 1.8) s (0x3ffccccc/0xcccccccd = 1.8, 0x3fe33333/0x33333333 = 0.6) and teleportRotationEndAngle = RandDouble(0, 360) (0x40768000). The unrelated 5.0 s constant at 0x007991B0 belongs to CellManager::CheckPrefetchStatus @0x00455BE0, the prefetch RETRY cadence, and was mis-attributed to the cue by #280's commit message. acdream's own segment constants (RotationDurationMin = 0.6f, RotationDurationMax = 1.8f) already match retail exactly, so the cadence is faithful and only the ARMING is not. Pre-dates #280; filed here because #280 reasoned from the wrong model and because the row did not exist. |
src/AcDream.Runtime/World/RuntimeWorldTransitState.cs (RetailWaitCueDelay); src/AcDream.App/Rendering/PortalTunnelPresentation.cs (RotationDurationMin/Max, TickRotation); src/AcDream.App/UI/PortalWaitNoticeController.cs |
Deliberate at the time as a "don't flash a scary notice on a fast portal" softening, but it was never recorded as a divergence and AD-2/AP-115 described it as acdream behaviour without stating that retail has no threshold. Adopting retail's unconditional per-segment emit is a one-line arming change; it is not made here because it is a user-visible presentation change outside the defect this commit fixes, and it wants the user's eyes. Filed as issue #329. | Every acdream portal shorter than 5 s shows a silent tunnel where retail shows the notice; every portal longer than 5 s shows it 3.2-4.4 s late (retail's first segment expires at 0.6-1.8 s). #280 makes holds longer, which MASKS this rather than fixing it. | gmSmartBoxUI::UseTime 0x004D6E30 (emit at the 0x004D6FCD segment-expiry else-arm); ECM_UI::SendNotice_DisplayStringInfo call @0x004D70A1 (-> 0x006925B0); the wait-cue string's PStringBase construction is the neighbouring @0x004D7064, :219516 — corrected 2026-08-06 at the D-1 fix review; this row originally cited the string constructor as the call site; wait-cue string VA 0x007BD6A8; CellManager::CheckPrefetchStatus 0x00455BE0 (the 5.0 s constant, VA 0x007991B0) |
| AP-151 | Filed 2026-08-06 at the #280 retail-conformance review (finding F3). The reveal gate is materially STRICTER than retail's prefetch predicate on the mesh-build/GPU-upload axis, over an equally large square. Retail's LScape::PreFetchCells @0x00505660 requires, per member, only that the DAT records be resident in memory (DBObj::PreFetch -> IN_MEMORY or IN_FILE -> DBObj::Get non-null); no geometry construction, no vertex arrays and no GPU upload are part of the blocking predicate — that work happens lazily at draw. acdream's gate requires, for every member of the derived window (25x25 at the shipped High preset): a worker-thread DAT read, a terrain mesh build, a render-thread TerrainModernRenderer.AddLandblock upload, a spatial commit, a physics collision-generation admission, and a spawn-adapter activation, all metered at MaxCompletionsPerFrame. The hold is therefore systematically longer than retail's for identical content, and nothing currently bounds it. Note this is the OPPOSITE asymmetry from AP-149, which records where the outer ring is WEAKER than retail; both are live simultaneously, on different axes. |
src/AcDream.App/Streaming/StreamingController.cs (IsRenderNeighborhoodResident); src/AcDream.App/Streaming/GpuWorldState.cs (IsRenderReady); src/AcDream.App/Rendering/TerrainModernRenderer.cs; src/AcDream.App/Streaming/StreamingWorkBudget.cs |
It is what makes "no visible assembly after reveal" true at all: acdream draws through a bindless/MDI pipeline whose landblock slots must exist before the viewport opens, where retail can begin drawing a landblock the frame its DAT record lands. Weakening the predicate to DAT residency would restore retail's hold duration and reintroduce the visible-assembly artifact #280 exists to remove. AD-2's blanket "async readiness gates replace retail's synchronous destination cell load" pre-dates the window being 625 members wide and does not name this axis. | Portal/recall holds of several seconds where retail (warm cache) is near-instant, on EVERY transit rather than only on cold DAT. No upper bound is enforced and no progress readout is shown (#327). A slow disk or a saturated upload budget lengthens the hold without limit. | LScape::PreFetchCells 0x00505660; DBObj::PreFetch/DBObj::Get call sites @0x0050575C, @0x0050579C; CellManager::PreFetchCells 0x00455820 |
| AP-153 | Filed 2026-08-06 at the AP-152 retirement — a modelling difference the fix itself introduces. Retail's shape-dispatch flag is CACHED ONCE. CPartArray::CacheHasPhysicsBSP @0x00518110 walks the part array, ORs 0x10000 into CPartArray::pa_state on the first part whose gfxobj->physics_bsp is non-null, and CPhysicsObj::CacheHasPhysicsBSP @0x0050f570 mirrors it onto CPhysicsObj::state+0xa8. A full .text scan for direct call/jmp to 0x0050f570 finds EXACTLY ONE caller, CPhysicsObj::InitPartArrayObject+0x7e @0x0051272e — so after an AnimPartChanged part swap retail's DISPATCH flag is stale while its per-part test (CPhysicsPart::find_obj_collisions @0x0050d8d0) stays live. acdream's step-0 gate is LIVE in both: it re-derives from the effective part identities on every FromSetup call. |
src/AcDream.Core/Physics/ShadowShapeBuilder.cs (FromSetup step 0); src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs (ReconcileAppearance) |
The two disagree only when a swap adds or removes the LAST physics-BSP part. Humanoid part swaps (clothing / armour) involve no physics-BSP GfxObj on either side, so this is unreachable against ACE today. Deliberately NOT modelled with cached state — that would be inventing staleness to reproduce a retail bug. | If a server ever swapped a prop's part array across the physics-BSP boundary, acdream would switch its collision geometry on the swap where retail would keep dispatching on the construction-time flag: a prop that gained a BSP part would lose its primitive immediately in acdream and only on re-init in retail. | CPartArray::CacheHasPhysicsBSP 0x00518110; CPhysicsObj::CacheHasPhysicsBSP 0x0050f570; sole caller CPhysicsObj::InitPartArrayObject+0x7e 0x0051272e |
| AP-154 | Filed 2026-08-06 at the AP-152 retirement (contract §11.6) — an undeclared dependency on a specific server implementation. Retail COMPUTES HAS_PHYSICS_BSP_PS itself from its own part array (AP-153's anchors). acdream's query-time guard Transition.BspOnlyDispatch reads it out of the SERVER's wire PhysicsState: LiveEntityCollisionBuilder.cs:161 copies exactRecord.FinalPhysicsState into ShadowEntry.State, and a repo-wide grep for PhysicsStateFlags.HasPhysicsBsp in src/ returns only that predicate and one unrelated mover-state read. acdream never ORs the bit in client-side. It happens to be correct because ACE derives the same DAT bit (WorldObject_Networking.cs:665-668 from SetupFlags.HasPhysicsBSP), overriding the weenie's authored value — which is why a 2018 weenie dump showing PhysicsState = 0x8 for the cottage door does not contradict our own live capture of 0x10008. |
src/AcDream.Core/Physics/TransitionTypes.cs:1348 (BspOnlyDispatch), call sites :3911 / :3954; src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs:161 |
Narrowed, not closed, by the AP-152 fix: the shape list no longer contains a primitive for a BSP-bearing object, so the guard has nothing left to skip and the OUTCOME is now independent of the wire. The guard itself still keys on the wire. Not bundled — changing registration.State touches every consumer of FinalPhysicsState (Hidden, Missile, ethereal layer 2, the [setstate] log) and needs its own gate. |
Against a server that does not derive the bit from the DAT, a BSP-bearing object built by a producer other than FromSetup would have its primitive tested where retail tests only the BSP. |
CPartArray::CacheHasPhysicsBSP 0x00518110 (derives) vs LiveEntityCollisionBuilder.cs:161 (copies); HAS_PHYSICS_BSP_PS acclient.h:2833 |
| AP-155 | NARROWED AGAIN 2026-08-07 (Campaign S S2) — the Sphere-as-Cylinder emission half is FIXED; what survives is ONLY the has-BSP source split. Both static publication sites now emit an authored Setup Sphere as ShadowShape.Sphere, mirroring the live path's emission exactly (route-independence asserted shape-for-shape incl. CylHeight; dispatch discriminated by a graze/through pair whose cylinder counterfactual verdicts differ numerically; both sites sabotage-reddened independently; population 3,506 of 5,935 installed Setups, structurally equal to AP-157's third-branch count). The flood centre rises by exactly r for this population — outdoor membership unaffected (XY rectangle), indoor EnvCell membership covered by the Session-B dungeon gate. What remains: the static paths derive has-BSP from entity.MeshRefs where the live path derives it from setup.Parts plus post-AnimPartChanged identities; the two sources can disagree, and that half keeps this row ACTIVE. A shared primitive-emitter refactor (compile-time route independence instead of empirical parity tests) is the filed follow-up. Original text: Filed 2026-08-06 at the AP-152 retirement; NARROWED 2026-08-06 to its static-publication half alone. Its flood half was bundled here with a different code path, a different population and a different gate — the exact fault the C4 handoff warns about — and its direction was recorded BACKWARDS; both are now split out as AP-156. Static paths emit a Setup Sphere as a height-capped CYLINDER. LandblockPhysicsPublisher.cs:1030-1037 and LandblockPhysicsContentBuilder.cs:683-690 both convert a Setup Sphere to ShadowCollisionType.Cylinder with CylHeight = radius * 2f and the origin shifted down by one radius; the live path emits a true ShadowCollisionType.Sphere, produced at exactly ONE site in src/ (ShadowShapeBuilder.cs). Retail tests a Setup Sphere with CSphere::intersects_sphere @0x00537a80 / @0x00537fd0 (two overloads) in both cases — 3-D distance, no height clamp. The static paths also derive "has BSP" from entity.MeshRefs (the render mesh list) where the live path derives it from setup.Parts plus the effective post-AnimPartChanged identities; the two sources can disagree. |
src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs:1030-1037; src/AcDream.Content/LandblockPhysicsContentBuilder.cs:683-690 |
Affects static props only and changes their collision geometry over a much larger population than AP-152's 172, so it needs its own count and its own gate. Deliberately not folded into the AP-152 or AP-156 commits. | A static prop whose Setup carries a Sphere blocks over a height-clamped cylinder instead of a true sphere, and rests one radius lower than the authored origin. | CSphere::intersects_sphere 0x00537a80 / 0x00537fd0 |
| AP-156 | SCALE RESIDUAL DECIDED 2026-08-08 by the user: KEEP OURS — permanent, deliberate divergence in the SAFE direction. acdream sizes the flood bubble to the object's actual placed scale; retail ignores the resize and floods at authored size, which under-registers ENLARGED objects (their real geometry pokes into neighbouring cells retail never lists them in — a walk-through edge case at cell boundaries). Copying retail would import that bug for byte-fidelity; the user chose not to. This row's scale question is CLOSED and must not be re-opened as a faithfulness cleanup. Filed 2026-08-06, split out of AP-155(b) at the AP-152 retail-conformance review, WITH ITS DIRECTION CORRECTED — and its worst half FIXED in the same commit. CORRECTION. AP-155(b) recorded the flood approximation as over-inclusive ("a sphere contains the box's inscribed extent but is larger in the diagonal"), and that recorded direction was the stated reason the residual was safe to defer. It was empirically inverted. BuildFloodSpheres took each physics-BSP part's ROOT BOUNDING SPHERE RADIUS (FlatCollisionAssetBuilder.cs:393 -> LiveEntityCollisionBuilder.cs:137) and centred it on the PART ORIGIN (ShadowShapeBuilder.cs:194), discarding the root sphere's own Origin. Measured over the installed client_portal.dat, independently twice: 376 of 973 physics-BSP parts have ` |
origin | > radius/2, worst 20.762 m on a 27.708 m sphere (gfx 0x010036DD, Setup 0x0200129A); **POPULATION CORRECTED 2026-08-06 at the fix review (finding R2).** The row as filed said the flood failed to contain the object's own BSP sphere for '170 of the 172 AP-152 Setups'. That understates it: 172 is AP-152's DISPATCH population (Setups carrying BOTH a primitive and a physics-BSP part). After AP-152 EVERY BSP-bearing Setup floods from its BSP shapes alone, so the discarded origin mis-placed the flood across all 530 of them. Re-measured against PHYSICS-POLYGON VERTICES — a different DAT field from the sphere, so the measurement is not circular — by an independent scratch program outside the repo: **525 of the 530** BSP-bearing Setups have at least one flood sphere move; **428** fail vertex-level containment at a 1 mm tolerance (412 at 1 cm, the figure the fix review quotes); **0** fail after the fix, at any tolerance down to zero. Worst shortfall 35.869 m at entity scale 1.75 on Setup 0x0200129A. The old figures — 170 of 172, worst 9.911 m on 0x02000255 — remain correct for what they measured (root-sphere containment over the 172), and 43 of them had a post-AP-152 flood strictly SMALLER than the pre-AP-152 one. Indoor floods are 3-D (CellTransit.cs:601routes everyid & 0xFFFF >= 0x0100candidate throughFindTransitCellsSphere), so a tall prop or door slab was simply absent from EnvCells it occupies and never a broadphase candidate there — UNDER-inclusive membership, the #98 / #168 class. **FIXED HERE.** ShadowShape.BoundsCentercarries the root sphere's own centre in the shape's local frame;FromSetupandFromLandblockBspPartsfill it from the SAME resolver that supplies the radius, andBuildFloodSpheresplaces the sphere atpartWorldPos + rotate(BoundsCenter, partWorldRot). Retail does exactly this: CGfxObj::physics_sphere ([gfxobj+0x74]) is assigned BSPTREE::GetSphere(physics_bsp) @0x005397e0 (mov eax,[ecx]; add eax,4— the rootBSPNODE's CSphere, past its 4-byte vftable), and CEnvCell::find_transit_cells@0x0052cae0 — the part-array overload reached fromCPhysicsObj::find_bbox_cell_list@0x00510fc0 throughCPartArray::calc_cross_cells_static@0x00518160's[vtbl+0x7c]dispatch — loads it at0x0052cb36 mov esi,[ecx+0x74], transforms its CENTRE through the part's own Positionat[part+0x30] (0x0052cb4c add eax,0x30/0x0052cb5a call Position::localtolocal), and only then reads the radius at 0x0052cb65 fadd [esi+0xc]. The same commit also retired the 10-sphere clamp on this branch: retail's clamp lives inside the CYLSPHERE overload alone (CObjCell::find_cell_list@0x0052b9f0,0x0052ba21 cmp eax,0xa/0x0052ba28 mov ebp,0xa) while the BSP walk has none — 7 installed Setups carry more than 10 physics-BSP parts (max 49, Setup 0x02001A91) and their tail parts were dropped from the flood entirely. **WHAT REMAINS OPEN.** acdream floods from the per-part spheres through its own sphere-vs-portal walk (CellTransit.FindTransitCellsSphere), where retail hands the part array to each cell's own find_transit_cellsand tests every part's sphere against that cell's portal planes in cell-local space. The sphere SET is now exact; the TRAVERSAL is still acdream's.find_bbox_cell_list's name notwithstanding, retail never forms a bounding box — AP-155(b)'s "acdream approximates retail's bounding BOX" was wrong as well. **SECOND RESIDUAL, added 2026-08-06 at the fix review (finding R4): acdream SCALES the flood sphere; retail does not.** ShadowShapeBuildermultiplies both the radius and (new in this commit) the centre by the entity/part scale. Retail'sCEnvCell::find_transit_cells@0x0052cae0 reads onlyCPhysicsPart::pos ([part+0x30]) and never CPhysicsPart::gfxobj_scale ([part+0x24]), while CPhysicsPart::find_obj_collisions@0x0050d8d0 DOES threadgfxobj_scale.zintoSPHEREPATH::cache_localspace_sphere— so retail's cross-cell walk is itself under-inclusive for scaled parts and acdream's is not. Over-inclusive for scale > 1 (safe), under-inclusive for scale < 1 (the #98/#168 direction). **ENFORCEMENT, added 2026-08-06 at the fix review (finding A1).** The invariant now lives at the TYPE, not only at the producer seam:ShadowShape's constructor is private and BSP shapes are built only through ShadowShape.Bsp(..., FlatCollisionSphere localBounds), which takes radius and centre as ONE value and scales them together. The former public 7-argument constructor with BoundsCenter = defaultlet a future BSP producer reintroduce this exact bug silently and green. **CONNECTED-GATE NOTE (finding A2). A null result on tall props is EXPECTED until AP-158 / #333 lands, and is not evidence against this fix.** The geometry now lands in the right cell and is then discarded one layer down by acdream's ownmaxReach` broadphase filter, which measures from the same part origin: 118 of the 477 unique installed physics-BSP GfxObjs have a root-sphere offset above that filter's roughly 2.5 m walking budget, and 46 above 5 m. |
src/AcDream.Core/Physics/ShadowShape.cs (BoundsCenter); src/AcDream.Core/Physics/ShadowShapeBuilder.cs (FromSetup step 3, FromLandblockBspParts); src/AcDream.Core/Physics/ShadowObjectRegistry.cs (BuildFloodSpheres); src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs (single bounds resolver); tests ShadowObjectRegistryMultiPartTests.BuildFloodSpheres_BspShape_CentresOnTheBoundsCentreNotThePartOrigin / _RotatesTheBoundsCentreByThePartRotation / _CapsCylSpheresAtTenButNeverTheBspParts, ShadowRegistrationOverflowTests.FromLandblockBspParts_CarriesTheScaledRootSphereCentre, InstalledSetupBspPrimitiveDispatchTests.InstalledSetups_BspFloodSpheres_ContainTheirOwnPhysicsPolygons (oracle swapped to physics-polygon vertices at the fix review, finding R1: the shipped assertion compared two hand-copies of the same expression and was algebraically identically zero for any DAT input) |
The traversal residual is a genuine approximation with its own gate, not a deferral of this fix. Closing it means porting the per-cell find_transit_cells part-array overload, which is different work from getting the sphere set right. OUTDOOR HALF CLOSED 2026-08-06 by #334 (see AP-159 for what remains). |
| AP-157 | MEASURED AND RE-SCOPED 2026-08-07 (Campaign S S1A) — one half RETIRED as a non-divergence, the other half CONFIRMED against retail's registration set but PROVEN collision-unreachable; fix deferred. CylHeight half: RETIRED. CObjCell::find_cell_list's cylsphere overload @0x0052b9f0 (pseudo-C 309107) copies Position::localtoglobal(low_pt) + radius per cylsphere, capped at 10, and NEVER reads height — retail itself collapses a cylsphere to a base-point sphere of the cylinder radius. acdream's cylinder flood is exactly retail's behaviour; the row's implication that height matters was wrong. Sorting-sphere half: measured over the installed DAT (Ap157SortingSphereFloodMeasurementTests): third-branch population 3,506 of 5,935 Setups (cross-checked: 3,605 sphere-only-no-cylinder minus 99 BSP-dispatched, matching the independently-committed dispatch-test constants); 163 with a zero authored SortingSphere; of the 3,343 evaluated, 1,812 (54%) fail containment at 1 mm (1,722 at 1 cm), worst shortfall 18.135 m (Setups 0x02000D7D / 0x020015B3), while max overshoot is only 1.900 m — overwhelmingly the under-inclusive direction relative to RETAIL'S REGISTRATION SET. BUT: no collision outcome can differ. For this branch the flood spheres and the collision-test geometry are the SAME per-part Sphere list, so every cell acdream omits is a cell the entity's test geometry cannot reach; retail's sorting-sphere flood is wider than ITS OWN per-sphere tests too, so its extra registrations are narrow-phase rejects. The divergence is a registration-set fidelity gap with a perf sign in acdream's favour, not a walk-through. Fix deferred deliberately: flooding from the authored sorting sphere needs SortingSphere plumbed through FlatSetupCollision and the bake schema (Slice I3 version protocol) — real risk for zero behavioural delta. Take it opportunistically at the next bake-schema revision. Original text: Filed 2026-08-06 at the AP-152 retail-conformance review (finding F4) — an unregistered substitution that predates AP-152 and was stepped over when its neighbours were filed. CPhysicsObj::calc_cross_cells' THIRD branch (0x005152dc -> CPartArray::GetSortingSphere @0x00518b00 -> CObjCell::find_cell_list @0x0052b990) floods from ONE authored whole-object sphere: GetSortingSphere returns [partArray+0x54] + 0x70, i.e. CSetup::sorting_sphere (acclient.h: CSetup carries CSphere sorting_sphere immediately after step_up_height), and that overload takes a single sphere with no cap. acdream's only == null branch floods from EVERY non-BSP, non-Cylinder shape instead — the Setup's per-part Spheres array. Different DAT field, different cardinality, different extent. 4,154 of 5,935 installed Setups carry a non-zero SortingSphere and DatReaderWriter.Setup already exposes it, so this is available rather than blocked. Same site, second item: BuildFloodSpheres collapses a Cylinder to one sphere at its BASE point with the cylinder radius and IGNORES CylHeight entirely, where retail's CObjCell::find_cell_list @0x0052b9f0 is handed the CCylSphere array as (low_pt, radius, height). |
src/AcDream.Core/Physics/ShadowObjectRegistry.cs (BuildFloodSpheres, the anyCyl and only == null branches) |
Deliberately NOT folded into the AP-156 fix. It is a different branch of calc_cross_cells, reached only by objects with neither a physics BSP nor a CylSphere, so its population is disjoint from the 172 AP-152 Setups and its live gate is a different set of objects. Bundling it would make the AP-156 connected gate un-attributable — which is exactly how AP-155 came to carry two lifecycles under one id. |
Sorting-sphere half: an object with several authored Spheres floods from all of them rather than from the one authored whole-object sphere — usually wider (max 5 Spheres on any installed Setup, so retail's 10-cap is never the difference), but a sorting_sphere LARGER than every per-part Sphere would make acdream under-inclusive, the #98 / #168 direction. CylHeight half: a tall thin cylinder floods a sphere of its radius at its base and can miss the cells its upper half occupies. |
CPhysicsObj::calc_cross_cells 0x00515230 (0x005152dc / 0x005152e3 / 0x005152fb); CPartArray::GetSortingSphere 0x00518b00 ([+0x54]+0x70); CObjCell::find_cell_list 0x0052b990 (sorting sphere) / 0x0052b9f0 (cylsphere, (low_pt, radius, height)) |
RETIRED 2026-08-06 — the filter is DELETED, not re-centred, and this row's own disassembly is why. The minimal fix this row proposed (carry BoundsCenter on ShadowEntry and measure from the true centre) was deliberately NOT taken: it would have preserved an invention retail does not have, kept a + 2f slack and a movement.Length() term with no retail counterpart, and left a second reach budget to be tuned forever. Transition.FindObjCollisionsInCell now walks the cell's shadow list with no distance pre-check at all, as CObjCell::find_obj_collisions @0x0052b750 does. Cell membership is retail's broad phase, and the BSP walk's own root-node bounding-sphere test — centred correctly, which is precisely what this filter was not — is the early-out that made a second one unnecessary. This retirement closes #333 and #337 (the Neftet plateau: wedged at the top, jumps sinking into the mesh, corpses falling through), whose mechanism it was. The row's predicted symptom was observed live before it was fixed, which is the strongest confirmation a register row gets: it predicted a tall prop AP-156 had just placed correctly would still not block, and the user reported exactly that at Neftet. PERF, MEASURED rather than assumed (Release, synthetic all-BSP cell, per ResolveWithTransition): at 38 candidates — the live maximum — 10.61 µs → 16.68 µs (+6.07, 1.57×); at a deliberately unreachable 200, 17.34 µs → 39.48 µs (2.28×); ≈ 0.16 µs per additional candidate tested. Over 19,701 live [reach-q] samples the in-cell candidate count is p50 = 9, p99 = 32, max 38, so the first row is the bound that matters. Original text, retained for the record: Filed 2026-08-06 at the AP-156 fix review (finding A2) — an UNREGISTERED INVENTION, not a port, that predates AP-156 and is issue #333. The shadow broadphase discards a candidate outright when distToCurr > sphereRadius + obj.Radius + movement.Length() + 2f. Retail has no distance pre-filter at all. CObjCell::find_obj_collisions @0x0052b750, disassembled from the PDB-paired binary for this row rather than inherited: it early-returns OK_TS only when sphere_path.insert_type == INITIAL_PLACEMENT_INSERT (0x0052b759 cmp dword [ebx+0x174],2 / 0x0052b765 je 0x52b7a0), then walks shadow_object_list ([cell+0xc8], count [cell+0xc4]) and calls CPhysicsObj::FindObjCollisions (0x0052b78b call 0x50f050) on every entry whose physobj is unparented ([physobj+0x40] == 0) and is not the mover itself — UNCONDITIONALLY. There is no distance test in the function. Neither the + 2f slack nor the movement.Length() term has a retail counterpart; retail's own cross-cell slack constant is F_EPSILON = 1.9999999e-4 m (0x0052cb5f fld dword [0x7c8c70]), 0.0002 m and not 2 m. Second half of the defect: the filter measures currPos - obj.Position, i.e. from the PART ORIGIN, while obj.Radius is the BSP root bounding-sphere radius measured about a centre that AP-156 established is frequently metres away — ShadowEntry does not carry the BoundsCenter that ShadowShape now does. A mover touching the geometry is up to d + R + r from the part origin and is admitted only when d <= movement + 2, roughly 2.5 m for a walking player. |
src/AcDream.Core/Physics/TransitionTypes.cs:3757-3765; ShadowEntry (src/AcDream.Core/Physics/ShadowObjectRegistry.cs:2735) carries no BoundsCenter; RETIRED: the pre-check is gone from FindObjCollisionsInCell and ShadowEntry needs no BoundsCenter. Tests Issue333BroadphaseReachFilterTests.OffCentreBspFloorStopsAFallingMover (production path end-to-end, DAT-free, sabotage-verified against its CentredBspFloorStopsAFallingMover control — restore the pre-check and the mover falls straight through to the unobstructed 37.800 while the control still blocks) and Issue337NeftetRockGeometryInspectionTests.TheOldBroadphaseMeasuredToTheOriginAndSoRejectedGeometryItStoodOn (installed-DAT evidence, both halves of the diagnosis) |
Deliberately NOT folded into the AP-156 commit: different code path (collision query, not cell membership) and it needed its own retail question answered, which this row answers. The minimal fix is mechanical — carry BoundsCenter on ShadowEntry and measure from obj.Position + rotate(obj.BoundsCenter, obj.Rotation); only whether to keep the + 2f slack at all is genuinely open. |
RETIRED — no residual. The rejectedReach column of the ACDREAM_PROBE_REACH family is kept and is now structurally 0, precisely so a post-fix capture is directly comparable with the pre-fix one that recorded 7,225 rejections on a single owner, every one with wouldAcceptAtCenter=True. Original risk text, retained for the record: This is the gate immediately downstream of AP-156, and it can mask AP-156's entire visible benefit. 118 of the 477 unique installed physics-BSP GfxObjs have a root-sphere offset above the ~2.5 m budget and 46 above 5 m; at a test scale of 1.75 those become 4.4 m and 8.75 m against an unchanged budget. Worked case: Setup 0x02000255, one part, root sphere origin (0.000, -0.007, 9.911), radius 10.522 — a player against its upper half is ~20.4 m from the part origin while maxReach is ~13.5 m. Discarded before BSPQuery ever runs. A tall prop that still does not block after AP-156 is THIS row, not a failure of AP-156. |
CObjCell::find_obj_collisions 0x0052b750 (0x0052b759 / 0x0052b765 / 0x0052b788 / 0x0052b78b), pseudo-C 308916-308940; CEnvCell::find_transit_cells 0x0052cae0 (F_EPSILON at 0x0052cb5f -> 0x7c8c70); issue #333 |
|
| AP-159 | NARROWED 2026-08-07 (Campaign S S1B) — the INDOOR part-array arm is PORTED; what remains is the BUILDING BRIDGE plus a one-ULP boundary tie. CellTransit.FindTransitCellsBox now runs retail's per-portal x per-part walk (CEnvCell::find_transit_cells @0x0052cae0): sphere cheap-reject at F_EPSILON+radius, BOX admit via the 8-corner classification, leads-outside after the admit, destination box_intersects_cell gate through the flat-authoritative dispatcher with a graph referee (20,000 installed comparisons pinned by assertion, zero mismatch). Dual-review PASS; sabotage discriminating; installed direction sweep: rigged population shrinks 978 cells/0 added across 1,520 placements; production-ratio population (box >= sphere, the real relationship — the box is the whole-vertex AABB while the sphere bounds only physics polygons) measured 1 ADD through the loaded-neighbour gate in 950 placements, which is RETAIL-CORRECT direction, so the old 'over-inclusive only, never a missed one' severity line is retired with the port. REMAINDER 1 — the building bridge: CheckBuildingTransit still admits on the sphere test; its retail counterpart is the part-array check_building_transit @0x0052c680 (NOT @0x0052c5d0 — D0 disentangled the function boundaries), whose portal_side convention is INVERTED relative to find_transit_cells (byte table in the retail review §6) and whose admit accepts `eax == 3 |
eax == side; the in-plane early exit of Plane::intersect_boxis byte-confirmed to return CROSSING(3) (jp @0x005aa1bc -> mov eax,3 @0x005aa2e2), so that porter inherits both traps settled. **REMAINDER 2 — one-ULP tie:** ourWhichSidereturns Positive at exactly dist==eps where retail's strict>says IN_PLANE (byte-decoded @0x00444720); measure-zero, float-exact-equality only. Original text: **Filed 2026-08-06 at the #334 fix - the INDOOR half of AP-156's traversal residual, now the whole of it.** #334 ported retail'sCPhysicsObj::find_bbox_cell_list@0x00510fc0 path so a physics-BSP object's OUTDOOR membership is the filled land-cell rectangle its authoredCGfxObj::gfx_bound_box spans (CLandCell::add_all_outside_cells@0x00533360 ->add_cell_block@0x005331d0). The INDOOR arm of that same walk is NOT ported: retail's part-arrayCEnvCell::find_transit_cells@0x0052cae0 admits a neighbour cell on a BOX test -CPhysicsPart::GetBoundingBox@0x0050d600 ->BBox::LocalToLocal @0x005b1e60 (0x0052cbf9) -> Plane::intersect_box @0x005aa170 (0x0052cc05), then BBox::LocalToLocalinto the destination andCCellStruct::box_intersects_cell@0x00533910 ->BSPTREE@0x0053c880 - where acdream keepsCellTransit.FindTransitCellsSphere's sphere-vs-portal-plane test, fed from the SAME per-part CGfxObj::physics_spherevalues retail uses for its cheapeps = F_EPSILON + radiuspre-reject at0x0052cb65. The outdoor building bridge (CEnvCell::check_building_transit @0x0052c5d0) is on the same sphere input for the same reason. Deferred deliberately: closing it needs a new BOX traversal of the containment BSP in BOTH the graph (BSPQuery) and the production flat (FlatBspQuery`) representations plus their exact referee, which is a separately gateable change with no bearing on #334's outdoor defect. Filed as issue #335. |
src/AcDream.Core/Physics/CellTransit.cs (BuildShadowCellSetFromParts, indoor arm); src/AcDream.Core/Physics/ShadowObjectRegistry.cs (BuildBspPartSpheres) |
The sphere set is exact (AP-156) and the sphere is a strictly LOOSER admitter than the box for a convex part, so the indoor set is a superset of retail's. Retail is itself conservative here in four compounding ways (render-mesh AABB over physics hull, axis-aligned re-fit after rotation, filled rectangle over per-cell test, one rectangle unioned across parts), so an over-inclusive indoor set is the same direction retail errs in. | |
RETIRED 2026-08-06 (the commit that filed it is one day old; this retirement corrects four statements in it). ShadowShapeBuilder.FromSetup now DISPATCHES instead of unioning: a step-0 gate derived from the parts suppresses steps 1 and 2 whenever any part's EFFECTIVE GfxObj carries a physics BSP. Retail's priority, re-disassembled from the PDB-paired binary for this commit rather than inherited: CPhysicsObj::FindObjCollisions @0x0050f050 tests HAS_PHYSICS_BSP_PS FIRST (0x0050f165 test dword [esi+0xa8],0x10000 / 0x0050f16f je 0x50f1a2) and leaves the BSP branch through the UNCONDITIONAL 0x0050f19d jmp 0x50f2b0, which is past the CylSphere loop at 0x50f1a2 AND the Sphere loop at 0x50f21d; a CylSphere-bearing object that survives its loop RETURNS (0x0050f1d6 jae 0x50f317); a Setup with zero spheres returns the seeded OK_TS (0x0050f22f je 0x50f31b). BSP wins. CORRECTION 1 — the row's risk statement was FALSE as written. It predicted "catching or stopping on a doorway sill". acdream did not test the extra primitive either: Transition.BspOnlyDispatch (TransitionTypes.cs:1348, landed 2026-05-25 as A6.P7) already skipped BOTH primitive branches (:3911, :3954) whenever the target's wire PhysicsState carries 0x10000, and ACE sets that bit from CSetup.HasPhysicsBSP (WorldObject_Networking.cs:665-668). The row's own anchor column cites the flag it failed to notice acdream was already keying on. So this retirement is NOT a collision-response change; the live half was CELL MEMBERSHIP, which had no such guard (see AP-155). CORRECTION 2 — "the affected primitives are small and centred at the part origin" was FALSE in both halves. The largest is 0x02001741's CylSphere at r = 6.714 m; 0x0200086E's Sphere is r = 5.842 m with origin (0.759, 0.165, 5.842), nowhere near the part origin. CORRECTION 3 — the cottage door's "~14 cm base Sphere" was the wrong field. 0x020019FF's Sphere radius is 0.100 m at origin (0, 0, 0.018); 0.141 is Setup.Radius, which AP-22 had just finished proving is never collision geometry. CORRECTION 4 — the row named ONE pinning test where TWO existed. FromSetup_DoorSetup_SphereAtExpectedLocalOffset also failed under the exclusive rule; both are corrected, neither deleted. Population re-measured independently at 172 of 5,935 (73 CylSphere+BSP, 99 Sphere+BSP; 530 carry a physics-BSP part), agreeing exactly with the filing commit's separate sweep, and now pinned by an installed-DAT test with external bucket controls. |
RETIRED — src/AcDream.Core/Physics/ShadowShapeBuilder.cs (FromSetup step 0 gate + EffectivePartGfxObjId, shared with step 3 so the two can never read different identities); tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderTests.cs (FromSetup_DoorSetup_EmitsBspPartsOnly, FromSetup_DoorSetup_SphereAtExpectedLocalOffset re-hosted on _ => false, FromSetup_DispatchGateReadsTheEffectivePartIdentities); tests/AcDream.App.Tests/Physics/LiveEntityCollisionBuilderTests.cs (CylSphereAndPhysicsBspPart_EmitsOnlyTheScaledBspShape — no App fixture combined a primitive with a BSP part before); tests/AcDream.Content.Tests/InstalledSetupBspPrimitiveDispatchTests.cs (population). Transition.BspOnlyDispatch is deliberately KEPT: retail genuinely dispatches at the query site too, and it guards against a future additive producer. |
— | — | CPhysicsObj::FindObjCollisions 0x0050f050 (0x0050f165 / 0x0050f16f / 0x0050f19d / 0x0050f1d6 / 0x0050f22f); CPhysicsObj::calc_cross_cells 0x00515230 (0x00515285 / 0x0051528f) -> CPhysicsObj::find_bbox_cell_list 0x00510fc0; CPhysicsPart::find_obj_collisions 0x0050d8d0; CPartArray::CacheHasPhysicsBSP 0x00518110; evidence docs/research/2026-08-06-ap152-contract.md |
|
RETIRED 2026-08-05 (C5a commit 1, closing #318; corrected at the architecture-review re-pass, A1/A2). RuntimePlacementPresentationSink.TryPublishPlace now publishes the local player's Place through LocalPlayerShadowSynchronizer.SyncPose(entity, entity.Position, entity.Rotation, record.FullCellId, force: true) — the SAME publisher ordinary per-tick movement uses — instead of writing LocalPlayerShadowState.Set directly. SyncPose calls ShadowPositionSynchronizer.Sync → ShadowObjectRegistry.UpdatePosition (the real PhysicsEngine.ShadowObjects publish) BEFORE it records the dedup cache as its own last step, so the cache can no longer be pre-seeded ahead of the real publish. force: true because this is the authoritative placement commit, not an ordinary refresh — it must never be skipped by SyncPose's own dedup check. TryPublishWithdrawal carried the exact mirror asymmetry (a bare _localPlayerShadow.Clear() with no ShadowObjects.Suspend, leaving a live phantom row at the park's source cell for the whole park window — the #184 shape) and is fixed in the SAME commit, same one-call shape: _localPlayerShadowSync.Suspend(entity). The sink no longer holds a direct LocalPlayerShadowState reference at all — both halves route exclusively through the one synchronizer, which owns the cache internally. One synchronizer instance is constructed in LivePresentationComposition.cs (before the sink) and threaded through LivePresentationResult to SessionPlayerComposition.cs, which no longer builds its own. #318's composition test (RuntimePlacementShadowCompositionTests.cs, 4 facts) proves: the real ShadowObjects registry holds a row at the destination cell (not just the cache) after a bare Place with no subsequent tick; the SOURCE cell's row is gone, not duplicated; a subsequent ordinary per-tick Sync call is a correct no-op; a Withdraw suspends the real registry row (not just the cache) — the source cell carries zero rows and the retained (suspendable) registration survives for a later restore; and a Place for a registered non-local-player entity leaves its row at the source cell and does not pollute the player's cache (route 7 P4 — the fix lives entirely inside the pre-existing player-only gate; the first version of this fact registered nothing for the child and was vacuous under the gate's own removal, corrected at the review). Sabotage-verified all four facts, both directions: reverted, each fails at its own discriminating assertion; applied, all green. |
src/AcDream.App/World/RuntimePlacementPresentationSink.cs (TryPublishPlace, TryPublishWithdrawal); src/AcDream.App/Composition/LivePresentationComposition.cs (LocalPlayerShadowSynchronizer construction + LivePresentationResult field); src/AcDream.App/Composition/SessionPlayerComposition.cs (consumes the shared instance); tests/AcDream.App.Tests/World/RuntimePlacementShadowCompositionTests.cs |
— | — | No retail analogue — retail has no separate shadow-cache/publish split; this was an acdream-only two-object seam (LocalPlayerShadowState cache + LocalPlayerShadowSynchronizer publisher) that a direct .Set()/.Clear() call could desynchronize from |
|
RETIRED 2026-08-05 (C5a deletion sweep). "Production zero-delta routes deliberately remain on the legacy resolver until 4B2" is false at HEAD: the exhaustive receiver census over src/ shows zero PhysicsEngine.Resolve/.ResolvePlacement call sites, and every production placement writer reaches canonical PhysicsEngine.SetPosition only through RuntimeSetPositionState (three call sites total). C5a deleted Resolve, ResolvePlacement, and their HasCellSurface helper outright — the resolver-shaped entry points this row described no longer exist, so the condition is retired structurally, not just narrowed. The narrower survivors (#276 settle-cell discard, AD-61 force-seed, AD-62 non-commit outcomes) are separately filed rows and are unaffected. |
src/AcDream.Core/Physics/PhysicsSetPosition.cs; src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs; src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs; src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs; src/AcDream.Core/Physics/PhysicsEngine.cs (deletion); docs/research/2026-08-05-c5a-contract.md |
— | — | CPhysicsObj::SetPosition 0x005160C0; SetPositionInternal 0x00515BD0; CPhysicsObj::handle_all_collisions 0x00514780; track_object_collision 0x00513F10; report_collision_end 0x00514620; AdjustPosition 0x00511D80; CheckPositionInternal 0x00511E90; CTransition::find_valid_position 0x0050C310; find_placement_position 0x0050C170; validate_placement_transition 0x0050ADC0; validate_placement 0x0050B210 |
| AP-3 | RETIRED 2026-07-31 (Campaign P Slice 1B). TransitionalInsert now returns OK_TS immediately for every valid contact plane. Its ordinary StepDown tail is reachable only from invalid contact and retains the retail Contact / !sphere_path.step_down / check-cell / ObjectInfo.StepDown gates plus the exact one-versus-two-sphere probe split. | src/AcDream.Core/Physics/TransitionTypes.cs (TransitionalInsert, GetStepDownProbePlan); tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs | — | — | CTransition::transitional_insert 0x0050B6F0, named-retail pseudo-C pc:273191–273307 |
| AP-4 | RETIRED 2026-07-31 (Campaign P Slice 1B). EdgeSlideAfterStepDownFailed now evaluates retail Branch 1 (!OnWalkable || !EdgeSlide → restore + OK_TS) before the steep-contact CliffSlide branch. The former compensation is removed. | src/AcDream.Core/Physics/TransitionTypes.cs (EdgeSlideAfterStepDownFailed); tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs | — | — | CTransition::edge_slide 0x0050B3D0, named-retail pseudo-C pc:273001–273090 |
| AP-5 | RETIRED 2026-07-31 (Campaign P Slice 2A). DoStepDown no longer accepts a caller-controlled runPlacement bypass. It resets walk_interp once at entry; after the transitional support probe and retail check_walkables gate succeed, ordinary contact maintenance, edge-slide back-probes, and StepUp all switch to PLACEMENT_INSERT with the exact carried interpolation value, run the final insertion, restore the prior insert type, and accept only OK_TS. Nested DoCheckWalkable uses local current-position saves and preserves the outer SPHEREPATH backup pair needed by edge-slide. The former wall-slide justification is addressed at the actual placement dispatcher boundary: its retail epsilon-shaved overlap test permits exact wall tangency but rejects real penetration; no StepDown path skips validation. | src/AcDream.Core/Physics/TransitionTypes.cs (DoStepDown, DoCheckWalkable); src/AcDream.Core/Physics/BSPQuery.cs / FlatBspQuery.cs (Placement dispatcher); tests/AcDream.Core.Tests/Physics/RetailStepDownPlacementTests.cs; tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs | — | — | CTransition::check_walkable 0x0050AFF0 pc:272811–272856; CTransition::step_down 0x0050B2A0 pc:272946–272998; BSPTREE::find_collisions Placement branch 0x0053A440 pc:323742; CSphere::intersects_sphere 0x00537A80; CCylSphere::intersects_sphere 0x0053B440 |
| AP-7 | RETIRED 2026-07-30 (Campaign P Slice P2) — the "state gate" was a BN decompiler artifact, not a locomotion exemption. calc_friction now ports retail's confirmed 0.25f threshold (if (angle >= 0.25f) return;) unconditionally, no special-cased gate. The "state check at pc:276702" the old row cited is PhysicsState.Sledding (confirmed via ACE's PhysicsObj.calc_friction, references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141, and SLEDDING_PS=0x800000 in acclient.h:2838) — it gates the 1.5625/6.25/near-flat friction-value OVERRIDE, not the threshold return itself; acdream had no live Sledding setter then or now (see #166 research, docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §3), so the branch was simply unreachable dead code, not an exemption for ordinary walking. The reverted 2026-04-30 L.3c attempt (naive 0.0→0.25 bump, forward locomotion 3→0.16 m/s in PlayerMovementControllerTests) does not reproduce on the production graphical local-player path post-R6: PlayerMovementController zeroes Velocity.X/Y to exactly zero every tick before calc_friction runs whenever animation root motion drives the walk, so friction has no horizontal velocity left to hammer (pinned at the PhysicsBody level by GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests). The headless/get_state_velocity movement-controller path and remote/NPC movers still feed real velocity into this function and remain the ones to watch if a similar regression resurfaces there. CORRECTION (2026-07-30, same day, #265/#166 capture bisect): the sentence above undersold the gap — calc_friction wasn't merely "no horizontal velocity to hammer," it was structurally UNREACHABLE with meaningful data on ANY grounded path: (a) the animation-root-motion path zeroed Velocity.X/Y outright every tick (the actual #265/#166 root cause, ten days pre-existing, not a Campaign-P regression), and (b) PhysicsBody.GroundNormal — the vector calc_friction dots velocity against — had ZERO production writers anywhere and silently defaulted to Vector3.UnitZ forever, so even surviving velocity would have been tested against a fake flat-ground normal on any real slope. Both gaps are now closed: PlayerMovementController.cs's grounded block no longer reconstructs Velocity for the animation-root-motion case, and PhysicsEngine.cs syncs body.GroundNormal from the committed ContactPlane.Normal at the same commit point that already publishes ContactPlane. The 0.25f threshold port itself (this row's original subject) was always correct — it just had nothing real to operate on until this fix. See docs/research/2026-07-30-265-capture-bisect.md's as-fixed addendum. | src/AcDream.Core/Physics/PhysicsBody.cs (calc_friction); src/AcDream.Core/Physics/PhysicsEngine.cs (GroundNormal wiring); src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (grounded-velocity fix); tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs (AP-7 test block); tests/AcDream.Core.Tests/Physics/Issue265SteepSlopeCaptureBisectTests.cs; tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs | — | — | CPhysicsObj::calc_friction pc:276694-276822 (0050ee70); ACE PhysicsObj.calc_friction PhysicsObj.cs:2120-2141; docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1; docs/research/2026-07-30-265-capture-bisect.md |
| AP-10 | RETIRED 2026-07-30 (Campaign P Slice P4) — the retail 0.1 m dry-corner water sink-in is restored. TerrainSurface.SampleWaterDepth (src/AcDream.Core/Physics/TerrainSurface.cs) now returns 0.1 for a partially-water cell's dry corner instead of the collapsed 0. The row's own "destabilizes the touch check" justification turned out to be structurally true of retail too (a skipped SetContactPlane reassertion is not a fall in ANY of retail/ACE/acdream, because Contact/OnWalkable are STICKY — PhysicsEngine.ResolveWithTransition's onGround computation ORs the fresh per-call ContactPlaneValid with the seeded, persistent PhysicsBody.TransientState.OnWalkable bit) — traced and confirmed in this slice; see docs/research/2026-07-29-remote-and-world-specials-pseudocode.md §5.2. PhysicsEngine.SampleTerrainWalkable's isWater = waterDepth >= 0.45f threshold means the restore does not flip the dry corner's water classification (0.1 still < 0.45) — only the sink-in depth changes. Full Release suite green (no regression) proves the sticky-bit argument held in practice, not just in theory. | src/AcDream.Core/Physics/TerrainSurface.cs (SampleWaterDepth) | — | — | ObjCell.get_water_depth / calc_water_depth (via ACE port); docs/research/2026-07-29-remote-and-world-specials-pseudocode.md §5.1-5.2 |
| AP-11 | Hand-authored 4-keyframe fallback sky set (sunrise/noon/sunset, fog ~80–350 m) when the Region dat isn't loaded yet | src/AcDream.Core/World/SkyState.cs:167 | A renderable sky is needed during boot before the Region dat parses; safety net on region-load failure | Any window where the fallback is active shows sky/fog lighting only roughly resembling retail's dat-driven values | SkyTimeOfDay keyframes, Region dat 0x13000000 |
| AP-12 | Enchantment family-stacking tiebreak by largest SpellId; retail picks highest Generation, tie-broken by latest cast | src/AcDream.Core/Spells/EnchantmentMath.cs:89 | ActiveEnchantmentRecord doesn't carry Generation; SpellId correlates with generation level in practice | Where spell ids don't track power within a family (or same-generation re-cast), the wrong buff wins — vital-max / stat values diverge from retail | CEnchantmentRegistry::EnchantAttribute 0x00594570 (pc:416110) |
| AP-13 | ComputeDamage is a simplified retail damage formula (no augmentations/ratings) — verified DEAD CODE as of 2026-06-04, M2 scaffolding | src/AcDream.Core/Combat/CombatModel.cs:184 | Not on the critical path; stubbed from r02 §5 + ACE CombatManager for the future M2 predictive display | If wired into the M2 attack-bar estimate as-is, predicted numbers diverge whenever augs/ratings apply | r02 §5; ACE CombatManager |
| AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | src/AcDream.Core/Items/ItemInstance.cs:187 | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) |
| AP-15 | WeenieError sentence table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex. The row's older code-catalog caveat is superseded: WeenieError carried a curated 16-member subset until 2026-07-29 and now holds the full 372-code table, so a code being unnamed is no longer a way for this to bite | src/AcDream.Core/Chat/WeenieErrorMessages.cs:26 | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs; src/AcDream.Core/Physics/WeenieError.cs |
| AP-16 | Point/spot lights selected per-object / per-cell as the 8 nearest reaching lights (sphere-overlap, nearest-first) via LightManager.SelectForObject, capped at MaxLightsPerObject=8; called from WbDrawDispatcher.ComputeEntityLightSet (objects) and EnvCellRenderer.GetCellLightSet (cell shells). Retail's bake (SetStaticLightingVertexColors) sums ALL reaching static lights per vertex with no count cap. Retail's hardware path (minimize_object_lighting 0x0054d480) DOES cap at 8 per object, so the cap is faithful to retail's hardware path — not to its bake path. The LightManager.Tick UBO path survives for DIRECTIONAL (sun) lights only; mesh_modern.vert's UBO loop skips point/spot entries (posAndKind.w != 0 → continue) — point lights reach the shader exclusively via the per-object SSBO (binding 5) | src/AcDream.Core/Lighting/LightManager.cs:234 (SelectForObject); MaxLightsPerObject ~line 174; call sites WbDrawDispatcher.ComputeEntityLightSet + EnvCellRenderer.GetCellLightSet | Matches retail's hardware constraint (8 lights per object/cell); selection is nearest-sphere-overlap which faithfully allocates lights to the surfaces that actually see them | Surfaces reached by >8 point lights are dimmer than retail's uncapped bake — rare (a dungeon room has a handful of torches), but real; see AP-35 for the bake-vs-GPU-evaluate architecture difference | minimize_object_lighting 0x0054d480 (retail's 8-light hardware cap); SetStaticLightingVertexColors 0x0059cfe0 (retail's bake, no count cap) |
| AP-18 | RETIRED 2026-07-10 — faithful retail radar port. Exact RGBAColor_Radar* floats were recovered from named static data and gmRadarUI::GetBlipColor was re-ported with _blipColor overrides plus portal/vendor/attackable-creature/admin/PK/PKLite/free-PK/fellowship precedence. The old implementation was not merely hue-tuned: it also had wrong portal/vendor colors and an incomplete dispatch matrix. | src/AcDream.Core/Ui/RadarBlipColors.cs + radar classification tests | — | — | gmRadarUI::GetBlipColor 0x004d76f0; static RGBA initializers at named decomp pc:1089736-1089804 |
| AP-19 | PortalSideEpsilon 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49 | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; PView::InitCell 0x005a4b70 |
| AP-20 | Sub-pixel view-polygon vertex merge fixed at 1080p-reference NDC units (2/1080); retail merges at ~1 actual screen pixel | src/AcDream.App/Rendering/PortalProjection.cs:179 | Unit approximation whose coarseness only strengthens convergence — the merge is the flood's fixpoint floor (replaced MaxReprocessPerCell=16) | At 4K+ a legitimately visible 1–2 px sliver aperture collapses to degenerate and rejects — a thin/distant doorway stops admitting its flood slightly earlier than retail | Render::copy_view 0x0054dfc0 |
| AP-21 | Entity translucency retains the invented α<0.05 fragment discard. World GfxObj/Setup instances now apply their DAT AlphaBlend/Additive/InvAlpha factors through the retail shared alpha queue, but sealed off-screen WbDrawDispatcher consumers (paperdoll/UI Studio) retain the old immediate normal-alpha pass for all three kinds | src/AcDream.App/Rendering/Shaders/mesh_modern.frag; src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (DrawDeferredAlphaBatch versus immediate Phase 8) | World presentation needed exact per-surface blend for spell/particle density and translucent-object intersections. Off-screen object previews are isolated render targets and have not shown an authored additive entity surface that justifies splitting their compact immediate pass | A faint world fringe below 5% alpha is discarded; a hypothetical additive/inverse-alpha paperdoll or UI Studio entity composites darker than retail inside that private viewport | D3DPolyRender::SetSurface; D3DPolyRender::RenderMeshSubset; SurfaceType.Additive → D3DBLEND_ONE |
| AP-22 | RETIRED 2026-08-06. The invented cylinder is deleted, not re-derived: retail synthesizes NO shape for a shapeless object. CPhysicsObj::FindObjCollisions @0x0050f050 dispatches EXCLUSIVELY — BSP xor CylSphere xor Sphere xor nothing — and with zero cylspheres, zero spheres and no physics BSP it branches straight to the epilogue at 0x0050f22f je 0x50f31b, returning the OK_TS seeded at 0x0050f13b mov edi,1. Byte-verified against the PDB-paired binary (9e847e2f-777c-4bd9-886c-22256bb87f32), disassembled independently rather than read from the Binary Ninja text, whose ebp_1 aliasing in this function is visibly corrupt. CPartArray::GetRadius (0x005180a0) and GetHeight (0x005180b0) are absent from the function's entire call set (which is exactly GetNumCylsphere 0x518080, GetCylsphere 0x518090, GetNumSphere 0x518060, GetSphere 0x518070, CCylSphere::intersects_sphere 0x53b8f0, CSphere::intersects_sphere 0x537fd0, OBJECTINFO::missile_ignore 0x50ceb0, CPartArray::FindObjCollisions 0x518180, COLLISIONINFO::add_object 0x6b4e20) — Setup.Radius/Height serve attack cones, cylinder_distance and MoveTo, never collision geometry; that consumer (LiveEntityMotionRuntimeController.GetSetupCylinder) is retail-faithful and untouched. The row's site list was incomplete and partly wrong: it named ShadowShapeBuilder.cs, which never reads Setup.Radius at all, and omitted TWO real copies — including LandblockPhysicsContentBuilder, the ONLY one the headless host executes. Fixing just the cited site would have left headless statics on the invented footprint. All three are deleted. The row's risk statement was also stale: it described a live approximation over "rare decorative props", but the branch was unreachable dead code. A sweep of all 5,935 Setups in the installed client_portal.dat — validated by byte accounting (5,935/5,935 records consumed with an exact 20 + 48*numLights residual tail and zero unexplained bytes) and reproduced independently by the production FlatCollisionAssetBuilder.FlattenSetup path — finds 0 Setups satisfying the guard: every Setup with Radius > 0.0001 carries at least one CylSphere or Sphere, and all 1,294 genuinely shapeless Setups have Radius exactly 0. Buckets: 678 with ≥1 CylSphere, 3,605 with 0 CylSpheres and ≥1 Sphere, 358 with no primitive but ≥1 physics-BSP part, 1,294 shapeless, 4,282 with Radius > 0.0001. Nothing loses collision, because nothing gained it; no visual gate is required. Pinned by InstalledSetupCollisionReachabilityTests (negative claim plus five external positive controls so a broken enumeration cannot pass it vacuously — sabotage-verified: inverting the claim reddens it, and an emptied enumeration fails on the controls at 0 != 5935 rather than passing) and by ShapelessSetupWithRadius_ProducesNoRegistration, whose sabotage (restoring the deleted block) reddens exactly that fact. The one prior test pinning the fallback built a DAT-impossible Setup; its live state/flag/seed-cell assertions were re-hosted onto a CylSphere fixture rather than deleted, and sabotage-verified in both directions. Evidence: docs/research/2026-08-06-ap22-contract.md. | src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs (Build); src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs (PublishStaticEntity); src/AcDream.Content/LandblockPhysicsContentBuilder.cs (PublishStaticCollision, headless-only); tests/AcDream.Content.Tests/InstalledSetupCollisionReachabilityTests.cs; tests/AcDream.App.Tests/Physics/LiveEntityCollisionBuilderTests.cs | — | — | CPhysicsObj::FindObjCollisions 0x0050f050 (OK_TS seed 0x0050f13b; BSP dispatch test …,0x10000 0x0050f165 / je 0x0050f16f; BSP-branch exit jmp 0x0050f19d; zero-spheres exit je 0x50f31b 0x0050f22f; epilogue 0x0050f31b); CPartArray::GetRadius 0x005180a0 and GetHeight 0x005180b0 (absent from that call set); consumers CPhysicsObj::check_attack 0x0050ec80, get_distance_to_object 0x0050f7a0 | EVIDENCE CORRECTED 2026-08-06 at the AP-22 architecture review. The retirement commit claimed "Headless.Tests 89/89 exercises the site-3 copy"; that is FALSE, proven by sabotage - restoring the invented cylinder in BOTH static sites left the entire suite green. No test references PublishStaticCollision, and the headless suite's dummy DAT proxy makes LandblockLoader.Load fail so the code is never reached. Two of the three deletions, including the headless-only one, are pinned by the installed-DAT reachability proof ALONE. The behaviour is right; the coverage claim was not. Two further precisions: sites 2/3 guarded on the strictly wider Radius > 0f (not site 1's > 0.0001f), which differ over the DAT by exactly one Setup - 0x02001657, denormal radius 1.3e-39 - and the reachability test now evaluates BOTH guards, each measured zero; and the stronger true fact is that all 1,652 no-primitive Setups carry Radius exactly 0, which is what actually makes sites 2/3 safe, rather than the 1,294 figure this row first cited. Retail DOES read GetHeight inside report_object_collision for the quadrant field - that is not a refutation of "never collision geometry", which is a claim about FindObjCollisions' shape dispatch only. Reachability independently reproduced by four decoders (contract sweep, implementer parser, both reviewers' from-scratch parsers) plus tools/SetupInspect.
| AP-23 | Invented per-type pickup-radius heuristic (3 m creatures / 2 m doors-lifestones-portals-corpses / 0.6 m rest) for close-range gating plus the speculative local TurnToObject/MoveToObject install through the player's MoveToManager. R5-V3 narrowed it: the install threads the target's real Setup radius/height (GetSetupCylinder, same as wire mt-6) and the player's real radius; only the radius buckets remain invented. Use retired from this seam 2026-07-25 and now sends immediately. | src/AcDream.App/Interaction/WorldSelectionQuery.cs (TryGetApproach/GetUseRadius); src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs (BeginApproach) | The retained pickup presentation reserves a destination slot before the authoritative transfer; its close branch still needs an arrival boundary | A target whose real UseRadius differs from the bucket misjudges the pickup gate — pickup waits forever or fires early into a server "too far" | ACE Player_Move.cs:66; wire MoveToObject (type 6) carries the true radius; CPhysicsObj::TurnToObject/MoveToObject callers §9a/§9b |
| AP-24 | RETIRED 2026-07-11 — matching v11.4186 x86 disassembly recovered ATTACK_POWERUP_TIME=1.0 seconds and DUAL_WIELD_POWERUP_TIME=0.8 seconds from the operands loaded by GetPowerBarLevel; jump and combat now share those constants. | src/AcDream.Core/Combat/CombatModel.cs; src/AcDream.Runtime/Gameplay/PlayerMovementController.cs; src/AcDream.Runtime/Gameplay/RuntimeCombatAttackState.cs | — | — | ClientCombatSystem::GetPowerBarLevel @ 0x0056ADE0; static data 0x007CEFC8/0x007CEFD0 |
| AP-26 | DDD interrogation answered with an empty dat-version list (count=0); retail reports actual dat iteration state | src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs:18 | ACE is satisfied by the empty ack; pattern from holtburger | A dat-patching-enabled server could push a full patch or reject on version mismatch — the lie is harmless only while the server never acts on it | DDD flow 0xF7E5/0xF7E6 |
| AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69 | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 |
| AP-28 | 3D audio falloff via OpenAL InverseDistanceClamped with picked constants (ref 2 m, max 1000 m, rolloff 1); voice pool/eviction IS cited to retail | src/AcDream.App/Audio/OpenAlAudioEngine.cs:146 | Stands in for retail's DirectSound-era attenuation; r05 §5.3 documents inverse-square behavior but the three AL params were picked, not ported | Sounds attenuate at a different rate — too loud/quiet at range side-by-side; gain-driven eviction comparisons inherit the skew | FUN_00550ad0 (voice pool only); r05 §5.3 |
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (#49/#50, 2026-05-11) | src/AcDream.Core/World/SceneryGenerator.cs (via WbSceneryAdapter) | Piecemeal patching against WB upstream is net-negative (the e279c46 road-check attempt over-suppressed scenery elsewhere, reverted 677a726); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a larger placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | CLandBlock::get_land_scenes; ACME GameScene.cs:1074 per-vertex road filter |
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (ShellDrawLiftZ, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, #119-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via Build(drawLiftZ), seal/punch fans — #130) | src/AcDream.App/Rendering/PortalVisibilityBuilder.cs (ShellDrawLiftZ); src/AcDream.App/Rendering/RetailPViewPassExecutor.cs (DrawPortalDepthWrite) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
| AP-33 | Interior-root look-in cells (#124 sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the #131 portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | src/AcDream.App/Rendering/RetailPViewRenderer.cs (DrawBuildingLookIns) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | viewconeCheck 0x0054c250; nested DrawCells objects pc:432878 |
| AP-34 | The world now shares one delayed alpha queue across Wb GfxObj/Setup entities and scene particles and drains it at retail's landscape/final boundaries. Residual: the modern reconstruction uses one stable scope-global CYpt sort rather than retail's per-CPartCell CShadowPart sort followed by cell traversal; EnvCellRenderer transparent shell batches also remain immediate and outside this queue | src/AcDream.App/Rendering/RetailAlphaQueue.cs; RetailPViewPassExecutor.cs (FlushLandscapeAlpha); Rendering/Wb/WbDrawDispatcher.cs; ParticleRenderer.cs | The mandatory modern renderer no longer owns retail CPartCell shadow lists. The shared queue restores the material consequence that motivated the port—particles and ordinary translucent parts can interleave—without rebuilding a second scene graph; stable sequence retains authored order on equal CYpt | Transparent objects from different cells can exchange order at a narrow overlap compared with retail cell traversal; an alpha-blended EnvCell shell cannot interleave with a particle or Wb entity, so those rare overlaps can still overpaint differently | RenderDeviceD3D::DrawObjCellForDummies 0x005A0760; CShadowPart::insertion_sort 0x006B5130; D3DPolyRender::FlushAlphaList 0x0059D2E0; PView::DrawCells 0x005A4840 |
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (CurrCell.IsEnv && !SeenOutside), an approximation of ACE's full landblock IsDungeon (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. #135 pre-collapse: at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via IsSealedDungeonCell reading the EnvCell dat SeenOutside flag — because the physics CurrCell is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). #215 cell identity: the pre-collapse/recenter decision compares the player's current Position.objcell_id landblock with the received destination objcell_id; it never reconstructs the source from XYZ because dungeon frame origins may be negative. #145/#138 teleport-hold suppression: during a teleport arrival HOLD the player is unplaced, so CurrCell is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (DungeonStreamingGate.Compute(isTeleportHold:true) → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and ExitDungeonExpands, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | src/AcDream.App/Streaming/TeleportLandblockTransition.cs (source/destination cell-ID classification) + src/AcDream.App/Streaming/DungeonStreamingGate.cs (Compute — per-frame predicate + teleport-hold suppression) + src/AcDream.App/World/LiveEntityHydrationPorts.cs (LiveEntityWorldOriginCoordinator.TryInitialize — login pre-collapse) + src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (OnPosition — first accepted canonical Position) + GameWindow:AimTeleportDestination/IsSealedDungeonCell (teleport pre-collapse and DAT predicate) + src/AcDream.App/Streaming/StreamingController.cs (collapse/expand/PreCollapseToDungeon) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same EnvCellFlags.SeenOutside the hydrated ObjCell.SeenOutside is built from (EnvCell.cs:72/PhysicsDataCache.cs:224), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete Position flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE LandblockManager.GetAdjacentIDs (dungeons→empty) Landblock.cs:577-582; IsDungeon Landblock.cs:1264-1277; retail SmartBox::TeleportPlayer 0x00453910 |
| AP-43 | Per-object torch (point/spot) lighting AND sun are both gated on the OBJECT's own cell via the same IndoorObjectReceivesTorches(ParentCellId) predicate ((id & 0xFFFF) >= 0x0100): indoor objects (EnvCell-parented) get torches + NO sun; outdoor objects get the SUN + ambient + NO torches. This is the faithful per-draw port of retail's useSunlight gate — DrawMeshInternal (0x0059f398) calls minimize_object_lighting only if (Render::useSunlight == 0), and PView::DrawCells (0x005a4840) calls useSunlightSet(1) (0x005a485a) for the outdoor stage and useSunlightSet(0) (0x005a49f3) for the interior-cell stage. #142 (2026-06-20): the sun gate is now PER-INSTANCE in the shader (binding=6 instanceIndoor[] flag in mesh_modern.vert, filled by AppendCurrentLightSet) — it was previously a per-FRAME global keyed on the PLAYER cell (UpdateSunFromSky). The per-frame global is retained for sealed dungeons (correctly kills the sun frame-wide when no sky is visible). Residual: the ebp_2 second seen-outside test in CellManager::ChangePosition (0x004559B0) is unaudited — unclear whether it changes the ambient/sun regime for a subset of cells. No observed behavioral impact in tested cells. | src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (IndoorObjectReceivesTorches, ComputeEntityLightSet, AppendCurrentLightSet, _instIndoorSsbo/_indoorData/InstanceGroup.IndoorFlags); src/AcDream.App/Rendering/Shaders/mesh_modern.vert (binding=6 instanceIndoor[] gate on sun loop); per-frame sun src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs (RuntimeWorldFrameEnvironmentPreparation.UpdateSunFromSky) | Torches: outdoor objects never torch-lit (exact retail). Sun: indoor objects (furniture, NPCs, player in a windowed building) never sun-lit (exact retail per-stage). Ambient: per-player-cell regime unchanged (exact retail ChangePosition). | The ebp_2 unaudited test in ChangePosition could affect a narrow class of cells (entrance cells? sub-cells with special flags?) — no symptom observed; audit it if a lighting edge case arises in an unusual cell type | useSunlight gate DrawMeshInternal 0x0059f398; useSunlightSet 0x0054d450; per-stage PView::DrawCells 0x005a4840 (useSunlightSet(1) 0x005a485a / useSunlightSet(0) 0x005a49f3); minimize_object_lighting 0x0054d480; CellManager::ChangePosition 0x004559B0 (ambient + seen_outside) |
| AP-35 | Point/spot lights are now PER-VERTEX Gouraud (pointContribution ~line 153 of mesh_modern.vert) matching retail's SetStaticLightingVertexColors bake path. Half-Lambert wrap ((1/1.5)·(N·D + 0.5·d)) AND norm distance attenuation (distsq>1 ? distsq·d : d) ARE ported (A7 Fix A, aa94ced). Point-light sum clamped to [0,1] on its own accumulator before adding ambient+sun (A7 Fix D D-1, mirrors retail's per-vertex bake clamp). CPU oracle: src/AcDream.Core/Lighting/LightBake.cs, locked by tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs. Residual (two parts): (a) acdream lights in-shader each frame (per-frame GPU evaluate); retail bakes into the vertex buffer ONCE — an architecture/performance difference; the wrap + norm + clamp formula is the same, but bake-once is cheaper for static geometry; (b) acdream's SelectForObject keeps only the 8 NEAREST reaching point/spot lights per object/cell (MaxLightsPerObject=8, see AP-16), whereas retail's bake sums ALL reaching static lights per vertex — a surface reached by >8 point lights is dimmer in acdream than retail's bake result (rare in practice; a room has a handful of torches) | src/AcDream.App/Rendering/Shaders/mesh_modern.vert (pointContribution ~line 153; wrap ~line 163; norm ~line 167; point-sum clamp line 210) | Per-vertex Gouraud + wrap + norm + clamp all match retail. The two residuals are: (a) per-frame GPU vs bake-once — architecture/perf only; (b) 8-light cap dimming when >8 lights reach one surface — rare. LightInfoLoader.cs:81 folds static_light_factor 1.3 into Range | (a) A new frame-time consumer bypassing accumulateLights would need to replicate the wrap + norm formula; per-frame GPU re-evaluate has higher per-frame cost than bake for static geometry. (b) A densely lit scene (>8 torches reaching one wall) renders dimmer than retail — see AP-16 for the 8-cap ownership | calc_point_light 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); SetStaticLightingVertexColors 0x0059cfe0; static_light_factor 0x00820e24 |
| AP-37 | LayoutDesc meters collapse Type-3 slice descendants into UiMeter.BackLeft..FrontRight and reuse UiMeter.DrawHBar rather than building those media descendants and dispatching retail UIElement_Meter::DrawChildren. Non-Type-3 meter children are imported normally. | src/AcDream.App/UI/Layout/DatWidgetFactory.cs (BuildMeter/SliceIds); LayoutImporter.cs meter child predicate | The current vitals/character meter shapes are visually accepted and fixture-pinned; this is a representation adaptation, not a controller overlay. | A meter with a different descendant/media structure can render empty or with incorrect clipping/direction | UIElement_Meter::DrawChildren @ 0x0046FBD0; production meter LayoutDesc fixtures |
| AP-39 | Chat lines carry one color per ChatKind (per-line solid color); retail UIElement_Text supports per-glyph styled runs (bold, different hue per segment) | src/AcDream.App/UI/UiText.cs:13 | Retail glyph-run parsing lives inside keystone.dll with no PDB/decomp; per-line per-kind coloring is the correct tonal palette and covers all existing chat types | Chat lines retail renders with multiple colors or bold names (e.g. "PlayerName says: text") render as one flat color; subtle visual difference but functionally complete | UIElement_Text glyph-run styling (keystone.dll, no decomp) |
| AP-40 | Chat uses one fixed 0.75 outer opacity and has no descendant-focus-driven active/default opacity transition | src/AcDream.App/Rendering/GameWindow.cs chat mount; ChatWindowController.cs | Font resolution is now live and per-element; only the opacity behavior remains deferred to the shared window/focus runtime | Focused chat remains too translucent and idle chat never restores the configured default alpha | ChatInterface::SetOpacity @ 0x004F3120; SetDefaultOpacity @ 0x004F3BC0; SetActiveOpacity @ 0x004F3C40 |
| AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (0x06004C63) used only when ThumbTopSprite/ThumbBotSprite are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | src/AcDream.App/UI/UiScrollbar.cs:35 | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | UIElement_Scrollbar::UpdateLayout @0x4710d0; cap sprites 0x06004C60 (top) + 0x06004C66 (bottom) from base layout 0x2100003E |
| AP-42 | UiMenu item model is flat (label + opaque payload, single-level popup); retail UIElement_Menu::MakePopup @0x46d310 supports hierarchical nested submenus via recursive popup chain | src/AcDream.App/UI/UiMenu.cs | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | UIElement_Menu::MakePopup @0x46d310 |
| AP-45 | PublicUpdatePropertyInt (0x02CE) sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs | Loopback ACE rarely reorders; this property stream has not yet joined the per-object freshness owner introduced for physics messages. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | PublicUpdatePropertyInt sequence byte (ACE GameMessagePublicUpdatePropertyInt) |
| AP-48 | Inventory burden reads the player's wire EncumbranceVal (PropertyInt 5) when present — delivered by B-Wire (login PD-bundle UpsertProperties + live PrivateUpdatePropertyInt 0x02CD); ClientObjectTable.SumCarriedBurden remains only as a defensive fallback for when the server omits it. | src/AcDream.App/UI/Layout/InventoryController.cs (RefreshBurden); src/AcDream.Core.Net/ObjectTableWiring.cs (player-int route) | B-Wire ports the retail read (CACQualities::InqLoad reads the server value); the client sum is now fallback-only, kept defensively. CONFIRM the server actually sends EncumbranceVal at the B-Wire visual gate, then DELETE this row. | If the server omits EncumbranceVal, the bar falls back to the client sum (the original drift) until the first 0x02CD — confirm at the gate. | CACQualities::InqLoad 0x0058f130; ACE PropertyInt.EncumbranceVal=5 |
| AP-49 | Carry-capacity augmentation (PropertyInt 0xE6) read from the player's wire property bundle when present (B-Wire PD UpsertProperties); defaults to 0 (correct for un-augmented characters) when absent. | src/AcDream.App/UI/Layout/InventoryController.cs (RefreshBurden) caller of BurdenMath.EncumbranceCapacity | B-Wire delivers the player's full PropertyInt table (incl. 0xE6 when the server sets it) via UpsertProperties; aug=0 is the correct no-aug default. CONFIRM the server sends 0xE6 for an augmented char at the gate, then DELETE. | An augmented character whose server omits 0xE6 reads slightly low capacity (bar fills higher than retail); un-augmented chars are exact. | EncumbranceSystem::EncumbranceCapacity decomp 256393 (0x004fcc00); retail 0xE6 PropertyInt augmentation |
| AP-50 | Burden meter orientation/direction set programmatically (Vertical=true, FillFromBottom=true) rather than reading retail's m_eDirection property from the LayoutDesc element (property id 0x6f). | src/AcDream.App/UI/Layout/InventoryController.cs; src/AcDream.App/UI/UiMeter.cs (Vertical/FillFromBottom) | The m_eDirection property is not yet read by ElementReader; bottom-up fill is visually confirmed against retail's burden bar art. Retire when 0x6f is wired through ElementReader→DatWidgetFactory. | Wrong fill direction (top-down instead of bottom-up, or horizontal) if a future meter whose art requires a different direction is forced through the same code path. | UIElement_Meter::DrawChildren @0x46fbd0; property 0x6f (m_eDirection) in the LayoutDesc |
| AP-51 | Main-pack m_topContainer cell (element 0x100001C9) draws the constant backpack icon via a hardcoded base-icon literal 0x0600127E + ItemType.Container (composited over the Container type-underlay), rather than resolving it at runtime the way retail does. Sprite VISUALLY CONFIRMED (2026-06-22 live gate). | src/AcDream.App/UI/Layout/InventoryController.cs (Populate, main-pack cell) | Retail's IconData::RenderIcons IsThePlayer branch draws a CONSTANT backpack with m_itemType = TYPE_CONTAINER (the original AP-51 "equipped-pack weenie icon" premise was wrong). The exact runtime resolution is unconfirmed: a research dat-dump misreported GetDIDByEnum(0x10000004,7) as the green tile 0x060011F4 (which rendered green-no-pack at the gate); the real backpack 0x0600127E was identified by dat export + user confirmation. We pin the visually-verified literal (cf. AP-55). Retire when the runtime resolve is reproduced + confirmed. | If the pinned sprite diverges from what retail resolves at runtime, the main-pack icon goes stale. | IconData::RenderIcons IsThePlayer branch 0x0058d1ee (decomp 407546-407549); sprite 0x0600127E dat-exported + visually confirmed |
| AP-52 | Side-bag column slot count defaults to 7 (the dat column height) when the player's ContainersCapacity is absent/0. | src/AcDream.App/UI/Layout/InventoryController.cs (Populate) | ContainersCapacity is not reliably on the player ClientObject yet; 7 is the standard side-pack cap + the dat column (0x100001CA, 36×252) holds exactly 7. | An 8th-pack (Shadow of the Seventh Mule aug) character shows one too few side-bag slots — cosmetic. | retail gmBackpackUI side-pack column; ACE ContainersCapacity |
| AP-53 | Contents grid slot count defaults to 102 (main pack) or 24 (side bag) when the open container's ItemsCapacity is absent/0. Container-switching is now wired (D.2b): clicking a side bag sends Use 0x0036 and the grid repopulates from ViewContents 0x0196. This row covers only the capacity default when the server omits ItemsCapacity. | src/AcDream.App/UI/Layout/InventoryController.cs (Populate) | ItemsCapacity is not reliably on every ClientObject yet; 102/24 are the retail standard capacities. Retire when ItemsCapacity is confirmed delivered for all containers. | A non-102/24 pack shows the wrong empty-slot count — cosmetic only. | retail main-pack capacity 102; side-pack 24; ACE ItemsCapacity; ViewContents 0x0196 |
| AP-54 | Inventory window vertical resize is a toolkit approximation: bottom-edge drag, expand-only (Min = dat default 372 px, Max = available screen height), contents grid/sub-window/scrollbar/backdrop stretched via overridden Left\|Top\|Bottom anchors. | src/AcDream.App/UI/RetailUiRuntime.cs (inventory frame setup) | retail's gmInventoryUI resize lives in keystone.dll (no decomp); using the screen rather than the former arbitrary 560 px cap keeps the full vertical resize usable at every persisted size. | Which elements reflow or whether retail permits shrink-below-default could differ; shrink below the authored default remains disabled. | retail gmInventoryUI resize (keystone.dll, no decomp); UiNineSlicePanel resize |
| AP-55 | The toolbar's item slots keep the hardcoded UiItemSlot.EmptySprite = 0x060074CF rather than resolving their cell template via attribute 0x1000000e. Correct in outcome (the toolbar's 0x1000000e resolves to a generic prototype whose empty media is 0x060074CF) but not dat-resolved. | src/AcDream.App/UI/UiItemSlot.cs (EmptySprite default); src/AcDream.App/UI/Layout/ToolbarController.cs | The inventory lists now port the retail resolver (ItemListCellTemplate); the toolbar was left on its hardcoded default to avoid regressing frozen, working art. Retire when the toolbar is routed through ItemListCellTemplate. | If a future toolbar layout's 0x1000000e points at a non-generic prototype, the toolbar would show stale art. | UIElement_ItemList::InternalCreateItem 0x004e3570; catalog 0x21000037 |
| AP-57 | The open-container triangle (0x06005D9C) + selected-item square (0x06004D21) are drawn as procedural UiItemSlot overlays keyed by _openContainer plus Core SelectionState.SelectedObjectId, reproducing the retail keying (item.itemID == openContainerId / selectedID per UpdateOpenContainerIndicator 0x004e3070 / SetSelectedObject 0x0058c2e0) but not via the dat prototype's m_elem_Icon_OpenContainer/m_elem_Icon_Selected state elements + SetOpenContainerState/SetSelectedState calls. | src/AcDream.App/UI/UiItemSlot.cs; src/AcDream.App/UI/Layout/InventoryController.cs | The procedural overlay produces the correct visual result; the 36×36 container prototype (0x1000033F) lacks the square child, so the procedural overlay is the only way to show the square on a selected bag — the dat-state path retail uses would require the prototype to be richer than it is. | A cell that should show an indicator via a dat-state path retail uses but we don't would be missed; outcome currently matches retail for the open/selected indicator cases. | UIElement_ItemList::UpdateOpenContainerIndicator 0x004e3070; ACCWeenieObject::SetSelectedObject 0x0058c2e0; SetOpenContainerState 0x004e1200; SetSelectedState 0x004e1240 |
| AP-59 | The per-cell container capacity bar (retail UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0, element 0x10000347) is drawn as a procedural UiItemSlot overlay (track 0x06004D22 full + fill 0x06004D23 clipped bottom-up to GetContents(guid).Count / ItemsCapacity), not via a real dat UIElement_Meter child. Right-anchored flush (the dat rect X=26 in a 36px cell sat ~5px off the edge — flush per the visual gate) and bottom-up fill assumed (the dat m_eDirection 0x6f isn't read, cf. AP-50). A CLOSED side bag reads empty until opened (its contents aren't indexed until ViewContents) — faithful to retail's known-children count, divergent if retail pre-loads. | src/AcDream.App/UI/UiItemSlot.cs (CapacityFill draw); src/AcDream.App/UI/Layout/InventoryController.cs (SetCapacityBar) | UiItemSlot is a behavioral leaf that paints overlays procedurally (cf. AP-57); the meter sprites + fill formula are the faithful port. Right-anchor + bottom-up were visual-gate calls. Further visual polish is deferred — ISSUES #146. | Fill direction / exact bar rect could differ from retail's m_eDirection + dat X; closed-bag bars read empty if retail pre-loads container counts. | UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0; element 0x10000347 (back 0x06004D22 / front 0x06004D23); GetNumContainedItems |
| AP-60 | Inventory drag OnDragLift is a no-op + the source cell is not dimmed: retail dims the lifted item's source cell (RecvNotice_ItemListBeginDrag); acdream leaves the item in place during the drag + the floating ghost. | src/AcDream.App/UI/Layout/InventoryController.cs | Cosmetic only — the dragged item is still unambiguously identifiable; dimming the source requires tracking the source cell reference across drag events, deferred to a polish pass. | A drag in progress doesn't visually mark its origin — cosmetic only, no functional effect. | RecvNotice_ItemListBeginDrag acclient_2013_pseudo_c.txt |
| AP-61 | Drop on a CLOSED side bag is advisory-accept: the client can't know a closed bag's item count (contents aren't indexed until opened), so OnDragOver shows green + relies on the server's InventoryServerSaveFailed reject + the rollback; retail knows the count when loaded. | src/AcDream.App/UI/Layout/InventoryController.cs (IsContainerFull) | A drop into a closed-but-full bag shows green then snaps back (a flicker) instead of pre-showing red. Faithful only when the bag has been opened (then GetContents is populated). The server's 0x00A0 + optimistic rollback is the authoritative safety net. | A drop into a closed-but-full bag shows green → brief flicker → snap-back instead of retail's pre-emptive red circle. | UIElement_ItemList::InqDropIconInfo 0x004e26f0 |
| AP-62 | MissileAmmo slot mask LIKELY — 0x100001E0 → MissileAmmo 0x800000 is inferred; the decomp immediate at GetLocationInfoFromElementID 173676 is corrupted to a string pointer, so the mapping was not directly confirmed from the named-retail source. | src/AcDream.App/UI/Layout/PaperdollController.cs (SlotMap) | Dropping ammo onto that slot wields to the wrong location if the mapping is wrong. Gate-verify via dat dump + cdb. | Ammo wields to the wrong equip location — functional gap if wrong. | GetLocationInfoFromElementID decomp 173676; acclient.h:3193 INVENTORY_LOC |
| AP-63 | Dual-wield-into-shield-slot special not implemented — retail OnItemListDragOver (decomp 174302) lets a melee-capable item also drop on the Shield slot; acdream's wieldMask = ValidLocations & slotMask rejects it. | src/AcDream.App/UI/Layout/PaperdollController.cs (HandleDropRelease) | A dual-wielder cannot off-hand a melee weapon via the doll. | Dual-wield via drag-onto-shield-slot is blocked — functional gap for dual-wield characters. | gmPaperDollUI::OnItemListDragOver decomp 174302 |
| AP-64 | Wield-reject rollback assumes InventoryServerSaveFailed 0x00A0 — an optimistic wield rolls back only if ACE emits 0x00A0 for a GetAndWieldItem rejection; otherwise corrected by the next authoritative message. Gate-verify via WireMCP. | src/AcDream.Core.Net/GameEventWiring.cs (0x00A0 handler) | If ACE uses a different reject opcode for wield, the optimistic state is left dangling until the next full update. | Rejected wield briefly shows the item as equipped in the doll — cosmetic flicker or stuck state if 0x00A0 is not the rejection path. | InventoryServerSaveFailed 0x00A0; WireMCP gate-verify |
| AP-65 | PickupEvent (0xF74A) no longer evicts the weenie from ClientObjectTable — only DeleteObject (0xF747) evicts, matching the retail object_table-vs-weenie_object_table split. An item another player picks up near you (only ever gets PickupEvent, never DeleteObject) lingers as a data-only entry (ContainerId 0, not in any view) until teleport/relog Clear(). | src/AcDream.Core.Net/ObjectTableWiring.cs (EntityDeleted handler); src/AcDream.Core.Net/WorldSession.cs (PickupEvent branch) | The weenie entry for nearby pickups is a harmless data ghost — no UI shows it (no container, not wielded). Retail evicts it from weenie_object_table when the object fully leaves interest range via DeleteObject; acdream defers to teleport/relog clear. | Slight memory growth in long sessions if many world items are picked up by other players near you; item data ghosts cannot cause functional issues because no UI queries ContainerId 0. | CACObjectMaint::DeleteObject / SmartBox::HandleDeleteObject; ACE Player_Inventory.cs TryDequipObjectWithNetworking |
| AP-66 | RETIRED 2026-07-13 — authored paperdoll empty-slot presentation. The earlier “no silhouettes” conclusion inspected the ItemList elements' own media but missed UIElement_ItemList::InternalCreateItem, which clones a distinct UIElement_UIItem catalog prototype for each location. All 21 supported jewelry, weapon, ammo, shield, clothing, cloak, trinket, and armor lists now resolve their exact ItemSlot_Empty surface from live DAT; PostInit confirms non-armor lists remain visible while the nine armor lists toggle with Slots. | src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs; ItemListCellTemplate.cs; PaperdollController.cs | — | — | gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0; PostInit @ 0x004A5360; UIElement_ItemList::InternalCreateItem @ 0x004E3570; LayoutDesc 0x21000037 |
| AP-68 | acdream keeps the 128 nearest-to-CAMERA point lights live (MaxGlobalLights=128, BuildPointLightSnapshot) and selects per cell CAMERA-INDEPENDENTLY (by the cell's own bounds), so a building interior stays lit at any distance within a town; retail keeps only the 40 nearest-to-PLAYER static lights (Render::max_static_lights=0x28, distance-sorted replace-farthest insert_light) and re-bakes a cell when its live light set changes, so distant interiors are baked dark and "light up" only as the player approaches and their torches enter the live 40. INTENTIONAL — acdream's always-lit interiors are the preferred behavior (no 1999-era light-budget pop-in); user-confirmed 2026-06-20. | src/AcDream.Core/Lighting/LightManager.cs (MaxGlobalLights=128, BuildPointLightSnapshot, SelectForObject) | The retail pop-in is a fixed-function light-budget artifact, not an intended aesthetic; revert to retail by clamping the global set to 40 + distance-to-player sort if ever desired | Distant town interiors are lit in acdream where retail's are dark until approached — a deliberate, user-preferred divergence | Render::max_static_lights 0x28; insert_light 0x0054d1b0 (distance-sorted, replace-farthest); bake re-trigger SetStaticLightingVertexColors cache burnedInStaticLights != num_static_lights |
| AP-69 | acdream preserves one accepted active record across rebucketing and ports retail's 25-second leave-visibility destruction lifecycle. Spatially resident records cancel expiry; otherwise the ACE compatibility boundary uses holtburger's conservative 384-unit distance envelope and retains attached/container/wielder/parent-owned objects. Expiry uses the exact generation-safe active teardown, then retains only a cold EntitySpawn because ACE can keep the GUID in KnownObjects and omit CreateObject on revisit; explicit F747/new generation/session reset removes it. DIVERGENCE: retail can delete the complete object under its visibility protocol; the fallback does not yet derive visibility from retail/ACE ObjCell PVS (SeenOutside plus VisibleCells), and trade/container preview retention has no separate lifecycle flag. | src/AcDream.App/World/LiveEntityRuntime.cs; src/AcDream.App/World/LiveEntityLivenessController.cs; src/AcDream.App/World/DormantLiveEntityStore.cs; LiveEntityHydrationController.OnPrune | Prevents stale portal destinations from accumulating animation/effect/render owners while still allowing doors, signs, portals, and other ACE-known objects to rematerialize when the server does not resend them | Dormant data-only snapshots can grow with every unique ACE destination until F747 or session reset; a nonresident object outside 384 units that remains visible through an unusual long EnvCell PVS could expire early; a future preview-only object with no parent/container ownership could also expire. Replace the compatibility predicate when exact ObjCell PVS and preview lifetimes are available | CPhysicsObj::prepare_to_leave_visibility 0x00511F40; CPhysicsObj::prepare_to_enter_world 0x00511FA0; CObjectMaint::AddObjectToBeDestroyed 0x00508F70; CObjectMaint::UseTime 0x005089B0; ACE KnownObjects; docs/research/2026-07-18-retail-object-liveness-and-mesh-reclamation-pseudocode.md |
| AP-71 | RETIRED 2026-07-30 (Campaign P Slice P4) — the check_entry_restrictions gate is now ported at the head of the indoor branch of Transition.FindEnvCollisions. ObjectInfo.CheckEntryRestrictions (src/AcDream.Core/Physics/TransitionTypes.cs) reproduces retail's exact order: NPCs/props bypass, a mover with CanBypassMoveRestrictions (new PWD-bitfield decode, BF_ADMIN 0x100000 AND BF_IMMUNE_CELL_RESTRICTIONS 0x400000, acclient.h:6452-6454) bypasses, an ordinary cell (RestrictionObj == 0) is a no-op. CellPhysics.RestrictionObj is now wired from the DAT-baked EnvCell.RestrictionObj field (§4.3's old open question — RESOLVED via references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.cs:32,66-67 and an independent reflection probe of Chorizite.DatReaderWriter 2.1.7's own EnvCell.RestrictionObj field: it is a plain per-cell DAT field gated by EnvCellFlags.HasRestrictionObj (0x8), NOT a live wire override; the BN pseudo-C's "count for an array alloc" reading at the same UnPack offset was the mis-attributed field-name collision feedback_bn_decomp_field_names warned about). Wired in BOTH the dev/graph-fixture path (PhysicsDataCache.CacheCellStruct) and the production/prepared path (CachePreparedCellStruct) — the latter already receives a live parsed envCell for Position/EnvironmentId, so no bake-format change was needed. See AP-129 for the narrower remaining gap this leaves. | src/AcDream.Core/Physics/TransitionTypes.cs (ObjectInfo.CheckEntryRestrictions, Transition.FindEnvCollisions); src/AcDream.Core/Physics/PhysicsDataCache.cs (CellPhysics.RestrictionObj) | — | — | CObjCell::check_entry_restrictions pc:308873-308912 (0x0052b6d0); CEnvCell::find_env_collisions pc:309573-309597; ACCWeenieObject::CanBypassMoveRestrictions 0x0058c500; ACCWeenieObject::CanMoveInto 0x0058da40; references/ACE/Source/ACE.Server/Physics/Common/ObjCell.cs:286-333 |
| AP-129 | NARROWED 2026-07-30 (P4 Opus review fix) — CanMoveInto/IsAllowedIn are now ported and fed; two narrow gaps remain. ObjectInfo.CheckEntryRestrictions resolves the cell's RestrictionObj via PhysicsEngine.Objects (a ClientObjectTable, acdream's GetObjectA equivalent) and evaluates the real owner IID / HouseRestrictionRecord (open flag, allegiance monarch, guest table) fed from CreateObject's HouseOwner/HouseRestrictions/Monarch PWD-tail fields and live House_UpdateRestrictions (0x0248) refreshes — see RestrictionObjPrevalenceInspectionTests (103,766 of 729,888 installed EnvCells, 1,293 landblocks, carry a baked RestrictionObj; this is the whole housing estate, not a rare case, which is why the OLD unconditional-fail-closed row was upgraded to FIX-FIRST rather than shipped). Remaining gaps: (1) House_UpdateRestrictions's Sequence byte is parsed but not used for staleness/reordering rejection — a lost-then-late UDP delivery could transiently apply an older restriction snapshot over a newer one (low-probability; the next full CreateObject or another update self-corrects). (2) Outdoor CLandCell restriction (LandblockInfo.RestrictionTables, a separate per-landblock packed hash table) remains entirely unported — unaffected by this fix, since the gate only reads the indoor/EnvCell CellPhysics.RestrictionObj field. HouseData (0x0225)/HouseStatus (0x0226) and the guest-management opcode family (House_AddPermanentGuest, House_UpdateHAR, etc.) remain unparsed but are NOT consulted by this entry gate (they carry rent/ownership-transfer UI data, not the owner-iid/guest-list pair CanMoveInto needs) — noted for future house-UI work, not a residual of this row. | src/AcDream.Core/Physics/TransitionTypes.cs (ObjectInfo.CheckEntryRestrictions); src/AcDream.Core/Physics/PhysicsEngine.cs (Objects); src/AcDream.Core/Items/{ClientObject,ClientObjectTable,HouseRestrictions}.cs; src/AcDream.Core.Net/{Messages/CreateObject.cs,Messages/GameEvents.cs,GameEventWiring.cs}; src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (production wiring) | A reordered House_UpdateRestrictions pair could transiently apply the older snapshot; self-corrects on the next update or CreateObject. An outdoor restricted cell (if that content ever exists) is not gated at all. | ACCWeenieObject::CanMoveInto 0x0058da40 (pc:407982-408056); RestrictionDB::IsAllowedIn 0x005ae8f0 (pc:444493-444516); references/Chorizite.ACProtocol/Chorizite.ACProtocol/Types/RestrictionDB.generated.cs; references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventHouseUpdateRestrictions.cs |
| AP-72 | Cursor art falls back to OS standard cursors when dat resolution fails — retail always renders MediaDescCursor / EnumIDMap-resolved dat cursor art; acdream's RetailCursorManager.Apply falls back to Silk StandardCursor (IBeam/crosshair/not-allowed/…) when the EnumIDMap chain or RenderSurface decode fails, and RetailCursorResolver/RetailCursorManager permanently negative-cache the failed enum/surface id for the session. | src/AcDream.App/Rendering/RetailCursorManager.cs:47 (ApplyStandard), RetailCursorResolver.cs:47 (negative cache) | Fallback triggers only when the dat lacks the asset — nominal EoR dats always resolve the 0x27/0x28/0x29 chain; an OS cursor keeps the UI usable rather than showing nothing. | A dat-read or decode regression silently shows OS-native cursors instead of surfacing an error — masked failure class; check the [D.2b] cursor log lines before suspecting art. | ClientUISystem::UpdateCursorState 0x00564630 |
| AP-74 | UseDone WeenieError text comes from a hardcoded subset map, not the portal String tables — retail resolves the 0x01C7 UseDone error code through the client String tables into the canonical line ("You are not trained in healing!"); acdream's WeenieErrorText.For hardcodes the handful of codes the current use/heal flows produce (0x001D/0x04EB/0x04FC/0x04FE, texts phrased after the ACE enum names) with a generic code-carrying fallback. | src/AcDream.Core.Net/Messages/WeenieErrorText.cs | Every refusal is now visible; only unmapped wording deviates, and those lines retain the raw code. Retire by porting the String-table lookup (#202). | An unmapped WeenieError shows a generic line instead of retail's exact sentence | retail String-table error lookup; ACE WeenieError.cs values |
| AP-73 | Character raises mutate optimistically, contrary to retail's server-authoritative flow — after sending RaiseAttribute/RaiseVital/RaiseSkill/TrainSkill, CharacterSheetProvider.ApplyLocalRaise immediately bumps ranks and debits XP/credits. Named retail permits one request in flight, ghosts the clicked button, and waits for an authoritative quality-change element message before changing displayed state (#199). | src/AcDream.App/UI/Layout/CharacterSheetProvider.cs | ACE usually accepts client-affordable raises, so its later property echoes conceal the incorrect prediction; Wave 8 removes local mutation and owns one awaiting request | A rejected/reordered raise can display invented state until a later full refresh, and repeated clicks can create multiple speculative spends | gmAttributeUI/gmSkillUI raise and quality-change paths, pinned in docs/research/2026-07-10-retail-panel-behavior-pseudocode.md |
| AP-75 | NARROWED 2026-07-19 — adapter-boundary adjust_motion only. SetCycle remaps TurnLeft/SideStepLeft/WalkBackward to their mirror command with negated speed before dispatch. Retail performs that normalization in CMotionInterp; GameWindow's local-player adapter can still pass raw ids directly | src/AcDream.Core/Physics/AnimationSequencer.cs (SetCycle head remap) | Preserves raw local callers until every caller enters through MotionInterpreter; literal DAT velocity and omega now flow through CSequence's complete Frame | A future caller that already normalizes a raw left/back command but still passes the original id can be adjusted twice | CMotionInterp::adjust_motion @305343; retire with the remaining local caller unification |
| AP-77 | NARROWED 2026-07-19 — animation-less/headless movement fallback only. When MotionInterpreter.DefaultSink or the local PartArray callback is absent, acdream writes grounded command-derived body velocity and applies the DAT-pinned Humanoid TurnRight rate (1.5 radians/second) directly to the body Frame. Production animated players/remotes bind MotionTableDispatchSink plus CSequence and instead consume the complete DAT-authored root Frame; that path preserves airborne orientation while suppressing only origin exactly like retail | src/AcDream.Core/Physics/MotionInterpreter.cs (ApplyCurrentMovementInterpreted); src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (no-PartArray object-quantum fallback) | Keeps isolated/headless physics tests and a deliberately animation-less entity controllable without fabricating a PartArray | A future production entity missing its animation binding uses Humanoid-only yaw/velocity, can foot-slide, and can rotate through an airborne quantum differently from a real creature's DAT Frame | CMotionInterp::apply_interpreted_movement 0x00528600; CPhysicsObj::UpdatePositionInternal 0x00512C30; retire when animation-less production objects have an explicit motion owner |
| 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-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 |
| AP-86 | Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | src/AcDream.App/Physics/RemotePhysicsUpdater.cs; src/AcDream.App/Physics/LiveEntityShadowPublisher.cs; src/AcDream.App/Rendering/GameWindow.cs (local projection); src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (authoritative UP tails); src/AcDream.App/World/LiveEntityPresentationController.cs (ordinary projection residency); src/AcDream.Core/Physics/ShadowObjectRegistry.cs (UpdatePosition) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | CPhysicsObj::SetPositionInternal(CTransition const*) 0x00515330 → change_cell, then remove_shadows_from_cells/add_shadows_to_cells after the resolved frame/contact commit |
| AP-87 | Remote MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07; unified across player-remote and NPC-remote by C4 route 4a, 2026-08-03 — retail's disassembly makes no this==player distinction here either, so the two formerly-duplicated per-kind copies are now the SAME decision): retail CPhysicsObj::MoveOrTeleport (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the ≥96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions — |Body.Position − worldPos| > 4 m (a large correction / an unplaced first-UP body) and !willBeDrTicked (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body → garbage resolved pos → the reverted attempt's INVISIBLE monster. A third condition firstUp (LastServerPosTime <= 0) is RETAINED, not dropped, in the unified seam: it is a belt hint only — the 4 m guard is the load-bearing backstop — and it is structurally false for player remotes because the player-remote caller stamps LastServerPosTime in its diagnostic roll-forward block before it routes, so unifying the two copies on all three conditions leaves the player branch's own behaviour bit-identical | src/AcDream.Runtime/Physics/RuntimeRemoteSteadyStatePosition.cs (ApplyInterpolate, BodySnapThreshold); called from src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs for both the player-remote and NPC-remote near-Interpolate branches. C4 route 4b-2 (2026-08-04) deleted the App's two duplicated MaxPhysicsDistance = 96f / BodySnapThreshold = 4f constant pairs and both _playerController?.Position ?? Vector3.Zero fabrications: the far branch is now a canonical Runtime placement, and the cell-less/rejected/unclassified leftovers call this same seam (AP-137). The 4 m constant exists in exactly one place | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii — inherits TS-46 | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare. 2026-08-04 observed live, then FIXED AT THE SOURCE the same day: the route 4a two-client test caught exactly this risk — a player remote jumping onto a house roof planted there and sat until it had drifted >4 m from the server's slid-down position, at which point this backstop fired (producer=ap87-4m in the capture) and blipped it instead of sliding. The CAUSE was the remote tick forging Contact | OnWalkable and deciding its landing edge from the contact-derived ResolveResult.IsOnGround; that is fixed (Bug B, docs/ISSUES.md #32) and the thresholds and conditions of this row are deliberately UNCHANGED. The snap remains the #184 invisible-but-solid backstop; it should simply fire far less often now that the body genuinely tracks the server, and less often again since AP-140's same-day retirement pointed both routing gates at CONTACT: a steep-face slide now interpolates toward the server pose every packet instead of being classified free-flight and hard-snapped, so bodyToTarget converges rather than being left to drift past 4 m. This row's own thresholds and conditions are unchanged by either fix | CPhysicsObj::MoveOrTeleport 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap ≥96 m); InterpolationManager 100 m AutonomyBlipDistance (the retail large-correction path) |
| AP-89 | TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's CPhysicsPart::SetTranslucency (0x0050e670) → CMaterial::SetTranslucencySimple (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 − translucency) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's mesh_modern.frag has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled color.a (FragColor = vec4(rgb, color.a * vOpacityMultiplier)) | src/AcDream.App/Rendering/Shaders/mesh_modern.frag (final FragColor line); src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (ClassifyBatches opacityMultiplier param, InstanceGroup.Opacities); src/AcDream.Core/Rendering/TranslucencyFadeManager.cs | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since color.a * 1.0 == color.a and the fade multiplier alone then drives the ramp exactly as 1 − translucency would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | CPhysicsPart::SetTranslucency 0x0050e670; CMaterial::SetTranslucencySimple 0x005396f0 (alpha = 1 − translucency, applied to all 4 D3D9 material alpha channels) |
| AP-90 | Radar fellowship/allegiance relationship state is modeled but not yet delivered at runtime. RetailRadar.GetBlipShape and RadarBlipColors.For implement retail's leader/member/allegiance precedence, and RadarSnapshotProvider exposes a relationshipFor(guid) seam, but acdream does not yet maintain live fellowship membership and its AllegianceTree is not wired into GameWindow. PK/PKLite relationship shapes do work from PWD flags. | src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs; src/AcDream.Core/Ui/RetailRadar.cs; src/AcDream.Core/Ui/RadarBlipColors.cs | Preserve the exact model/seam now and avoid inventing membership from names or chat; connect it when the social game-event state is ported | Fellowship members render their ordinary player color/shape instead of bright-green leader/member triangles; allegiance members render an ordinary plus instead of a hollow box | gmRadarUI::GetBlipColor 0x004D76F0; gmRadarUI::GetBlipShape 0x004D7B60 |
| AP-92 | Private creature viewports (paperdoll and examination) render through isolated IGpuRenderTargets and blit into UiViewport; retail renders each CreatureMode directly and advances a cloned CPhysicsObj, while examination currently refreshes its clone from the live target's animated mesh pose. V6l narrowing (2026-07-28), V11 update (2026-07-29): the target is backend-neutral and the blit's V origin is not assumed — IUiViewportRenderer.TextureIsBottomUp derives it from the backend that made the texture. With GL deleted the only answer in the tree is Vulkan's top-left origin, but the seam is kept rather than folded flat, because it costs one property and it is what let the origin question be answered by data instead of by assumption. | src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs; src/AcDream.App/Rendering/PaperdollFramePresenter.cs; src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs; src/AcDream.App/UI/UiViewport.cs | Vulkan render-to-texture is the modern backend equivalent; shared live pose data provides animation without registering a second gameplay entity or duplicating the world sequencer | Alpha, lighting, state isolation, or an assessment-time cloned motion diverging later from the live target can differ from direct retail CreatureMode presentation. The origin half of this risk is closed; a FUTURE backend would have to answer TextureIsBottomUp for itself | CPhysicsObj::makeObject(CPhysicsObj const*) @ 0x005144B0; gmPaperDollUI::PostInit @ 0x004A5360; BasicCreatureExamineUI::Init @ 0x004AB9C0; UIElement_Viewport::SetCamera; retail CreatureMode::Render |
| AP-93 | Paperdoll does not port UpdateForRace; all characters use the cdb-confirmed default held pose 0x030003C0 and default presentation | src/AcDream.App/Rendering/DollEntityBuilder.cs; RetailPaperdollPoseApplicator in src/AcDream.App/Rendering/PaperdollFramePresenter.cs | Exact for the tested Horan character; unresolved for other heritage/gender/body combinations | Non-default races can use the wrong pose, heading, camera framing, or presentation asset | gmPaperDollUI::UpdateForRace @ 0x004A3ED0 |
| AP-94 | Imported Type-12 text defaults to interactive/selectable behavior; display labels can focus/capture/drag, and vitals synthesize duplicate runtime labels instead of binding 0x100000EB/ED/EF | src/AcDream.App/UI/UiText.cs; src/AcDream.App/UI/Layout/VitalsController.cs | Historical widget-generalization default; Wave 1 ports explicit Display/Selectable/Editable roles | Invisible/static text steals input and duplicate labels drift from DAT geometry | UIElement_Text property handlers; gmVitalsUI::PostInit @ 0x004BFCE0 |
| AP-95 | RETIRED 2026-07-11 — UiButton owns retail hover/pressed/released-outside, disabled, selected/toggle, missing-state fallback, hot-click, and distinct press/release controller callbacks. | src/AcDream.App/UI/UiButton.cs; UiButtonStateMachine.cs | — | — | UIElement_Button::UpdateState_ @ 0x00471CF0; mouse handlers 0x00471FF0..0x004721F0 |
| AP-96 | Layout/property import loses raw edge-mode semantics (especially 3/4) and treats default scalar values as absent, so explicit false/0 cannot override inheritance | src/AcDream.App/UI/Layout/ElementReader.cs; LayoutImporter.cs | Existing shipped layouts are hand-gated; Wave 1 adds presence-aware properties and the exact solver | Non-reference resizing recenters/stretches incorrectly; derived DAT properties silently inherit the wrong base value | UIElement::UpdateForParentSizeChange @ 0x00462640; production LayoutDesc inheritance |
| AP-97 | RETIRED 2026-07-10 (Wave 1 retained-widget foundation) — the prior generic Device-timer premise was a decomp misread. Named retail polls the hovered element's tooltip deadline, then broadcasts global UI time message 3; it does not expose the assumed arbitrary timer queue. UiRoot.Tick now preserves that order and subtree removal clears input/time ownership before another pulse. | src/AcDream.App/UI/UiRoot.cs; IUiGlobalTimeListener.cs | — | — | UIElementManager::UseTime and hover/focus paths, pinned in docs/research/2026-07-10-retained-widget-foundations-pseudocode.md |
| AP-98 | RETIRED 2026-07-10 (Wave 2.3) — typed handles/manager own lifecycle; every production/Studio window uses RetailWindowFrame; schema-v2 UiWindowLayout persists outer bounds, visibility, toolbar collapse, and chat maximize by character/resolution with legacy-radar migration, nearest-resolution fallback, clamping, and a pre-login default write guard. Chat maximize uses DAT 100–360 limits and preserves its lower edge when growing upward. | src/AcDream.App/UI/RetailWindowManager.cs; RetailWindowLayoutPersistence.cs; RetailWindowHandle.cs; Layout/RetailWindowFrame.cs; ChatWindowController.cs; SettingsStore.cs | — | — | gmMainChatUI::HandleMaximizeButton @ 0x004CCE50; retail saveui/loadui behavior |
| AP-99 | RETIRED 2026-07-11 (Wave 3.2) — Core ItemInteractionPolicy ports the complete ordered DetermineUseResult/UseObject/AttemptPlaceIn3D matrix; PWD flags and CombatUse survive CreateObject; component-pack membership comes from portal.dat; App owns throttle, UseDone-balanced busy state, target mode, wire/optimistic dispatch, and typed confirmation/auxiliary seams. The two corrupt decompiler operands were pinned from matching x86 as BF_REQUIRES_PACKSLOT and BF_VENDOR. | src/AcDream.Core/Items/ItemInteractionPolicy.cs; src/AcDream.App/UI/ItemInteractionController.cs | — | — | ItemHolder::DetermineUseResult @ 0x00588460; UseObject @ 0x00588A80; AttemptPlaceIn3D @ 0x00588600 |
| AP-101 | RETIRED 2026-07-11 (Wave 4.4e) — toolbar control coverage is complete: DAT panel launchers, combat, Use/Examine, selected-object strip, shortcuts, and exact thrown-weapon/separate-ammo count resolution on authored element 0x10000194. | src/AcDream.Core/Items/ToolbarAmmoPolicy.cs; src/AcDream.App/UI/Layout/ToolbarController.cs | — | — | gmToolbarUI::UpdateAmmoID @ 0x004BF210; UpdateAmmoNumber @ 0x004BE9E0 |
| AP-104 | Vitals detail element 0x100004A9 and root HideDetail/ShowDetail transitions are not wired | src/AcDream.App/UI/Layout/VitalsController.cs | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | gmVitalsUI::ListenToElementMessage @ 0x004BFC00; PostInit @ 0x004BFCE0 |
| AP-105 | PARTIAL 2026-07-13 — inherited scrollbar media/roles now come from DAT (decrement/top 0x06004C69, increment/bottom 0x06004C6C), and both chat backends share typed client-command routing plus one retained ChatVM for reply state. Retained chat still lacks complete tab/filter/unread, social availability, incoming squelch enforcement, and focus-opacity behavior. | src/AcDream.App/UI/Layout/DatWidgetFactory.cs; ChatWindowController.cs; src/AcDream.App/UI/ClientCommandController.cs; chat mount in GameWindow.cs | Shared log/send path, wrapping, scrollbar roles, command ownership, and outer maximize geometry work; later chat work consolidates the remaining presentation/filter state | Tabs are no-ops, squelched lines can still render, contextual social actions are absent, and focus visuals diverge | gmMainChatUI @ 0x004CCCC0..0x004CE2A0; UIElement_Scrollbar::OnSetAttribute @ 0x004714D0; ChatInterface methods |
| AP-107 | RETIRED 2026-07-11 (Wave 3.3 / #197) — typed OfferPrimaryClick returns NotActive, ConsumedSuccess, or ConsumedRejected; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | src/AcDream.App/UI/ItemInteractionController.cs; inventory/paperdoll/toolbar/radar/world call sites | — | — | UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0 |
| AP-108 | Paperdoll/AutoWield still omit the remaining missile/held restrictions and corrupt-mask branch of full AutoWieldIsLegal, dual-wield/off-hand rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous " - cannot unwield the %s" failure suffix (the current send seam reports rejection asynchronously). AutoWear legality retired from this row 2026-07-23: inventory activation and paperdoll drops now apply the retail clothing-priority/location blocker lookup and exact "You must remove your %s to wear that" system notice. Primary replacement retired 2026-07-14; Aetheria retired 2026-07-13. | src/AcDream.App/UI/Layout/PaperdollController.cs; src/AcDream.App/UI/AutoWieldController.cs | Basic equip slots, Aetheria, live doll, AutoWear conflict reporting, and primary weapon/incompatible shield/mismatched ammo blocker sequencing work in peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | CPlayerSystem::AutoWieldIsLegal @ 0x0055ED60; CPlayerSystem::AutoWearIsLegal @ 0x0055EF40; CPlayerSystem::AutoWield @ 0x00560A60; gmPaperDollUI @ 0x004A3590..0x004A5F90 |
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | src/AcDream.App/UI/Layout/CharacterStatController.cs; CharacterSheetProvider.cs | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | gmCharacterTitleUI @ 0x0049A610; gmStatManagementUI::UpdateExperience @ 0x004F0A70 |
| AP-110 | NARROWED 2026-08-09 (Slice 5.4, vendor browse panel) — "vendor" retired from the absent-panels list; see AP-161 for the precise successor (Buy/Sell transaction UI, Slice 6). Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, inscription transaction, retail creature stat/rating/animated-preview presentation, default selection-follow, authored local spell subview with appropriate-formula component state, and the full EoR item-report dispatch: appraisal-only unknowns; exact equipment-set/rating/tinkering/weapon/armor/caster/requirement/XP/healer/rare prose and intentional blank section rows; ordinary/enchantment DAT spell descriptions; live material-decorated appropriate titles plus DAT material and creature names; expiry, decorated material/gem descriptions; and portal/PK restrictions with authored item colors. It still lacks item-object preview, player-dependent effective shield projection, live cooldown-remaining projection, localized augmentation-cost StringInfo, exhaustive character detail regions, and exact creature appraisal FontInfo-list selection. | src/AcDream.App/UI/RetailUiRuntime.cs; src/AcDream.App/UI/Layout/AppraisalUiController.cs; src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs; src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs; src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs; src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs; src/AcDream.Core.Net/LinkStatusSnapshot.cs; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, the Slice 5.4 vendor "Items" browse panel (category-filtered stock list, retail's quantity-correct pricing — ItemHolder::GetObjectSplitSize's split-exempt mask, not a flat per-unit price), and the core examination request/presentation/inscription/creature-preview/item-report loop cover the active loops; the residual examination mechanisms require live player/enchantment/localization state or object-preview ownership rather than fabricated content | Item assessments omit only the listed live/localized/preview projections; enchanted/incomplete creature appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | BasicCreatureExamineUI::Init @ 0x004AB9C0; CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0; gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0; gmExaminationUI::ExamineSpell @ 0x004B6900; SpellExamineUI::ExamineSpell @ 0x004B6210; AttributeInfoRegion::Update @ 0x004F1D90; gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0; ACCWeenieObject::GetObjectName @ 0x0058E6E0; ItemExamineUI::SetAppraiseInfo @ 0x004B72B0; ItemExamineUI::AddItemInfo @ 0x004AC050; ItemExamineUI::Appraisal_ShowCapacity @ 0x004B2680; ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140; ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0; ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10; ItemExamineUI::Appraisal_ShowDescription @ 0x004B6990; MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500; ItemExamineUI::SetInscription @ 0x004AE2F0; CM_Writing::Event_SetInscription @ 0x006A98B0; CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610; LayoutDesc catalog |
| AP-161 | REVIEW CORRECTIONS 2026-08-09 (Opus review of 92ea3977, findings F1-F13): thirteen further fixes, mostly bug-fixes-to-already-claimed-behavior rather than new divergences, so no new AP row is filed for most of them; the exceptions are called out below. F1 ports Buy All's four retail pre-send guards (pyreal/alt-currency affordability, container-slot/item-slot capacity) — see AP-162's narrowing. F2 corrects AddToBuyList from upsert to retail's actual ACCUMULATE-with-5000-cap semantics and ports RemoveFromShop's shop-row hide/restore as staging consumes limited vendor supply. F3 corrects VendorSellAcceptability's too-valuable branch to the byte-verified bitwise-complement form ((~(itemTypeMask >> 16)) & 4), exempting PromissoryNote items. F4 wires BF_RETAINED end to end, RETIRING AP-164 below. F5 ports UpdateDragOver's auto-switch-to-Selling-on-hover. F6 corrects sell staging to always record the FULL stack (never the live split slider) and ports SellSingleItem's partial-stack refusal plus its literal amount-1 send. F7 corrects the X-close confirmation string's missing trailing question mark. F8 disposes a live confirmation dialog on session Close/Reset. F9 repaints the Buying/Selling strips' own selection highlight on every selection change, not just a staging change. F10 unstages a sell entry that leaves ClientObjectTable and a buy entry whose shop row is retired, the latter with retail's exact notice. F11 reorders RequestUse's eligibility check ahead of BeginApproach so an ineligible far target no longer speculatively approaches. F13 makes Sell Item act on the global selection unconditionally, matching retail — a prior version of this port required a staged entry first. New approximations this pass introduced are filed as AP-167 (SellSingleItem's non-empty-container refusal branch not ported) and AP-168 (Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with ItemType.Container). NARROWED 2026-08-09 (Slice 6b/6c, staging+sell arc) — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES. VendorUiController now fully wires both tabs: Buying (Add to List/Buy Item/Buy All/Clear Item/Clear List, backed by VendorStagingList) and Selling (drag-to-sell via IItemListDragHandler, VendorSellAcceptability's port of VendorProfile::InqAcceptability, Sell Item/Sell All/Clear Item/Clear List), plus the X-close staging confirmation dialog (RetailDialogFactory, the exact retail string recovered from the decompiled binary's data segment at 0x007b5bd8). Sell (0x0060) is wired end to end (VendorRequests.BuildSell/WorldSession.SendSell/ItemInteractionController.TrySell). Three narrow residuals from this pass are filed separately rather than folded in here: InqAcceptability's non-sellable bitfield check is unmodeled (AP-164), the Buying tab's stackable-removal-amount test substitutes VendorShopItem.DescStackSize for retail's _maxStackSize (AP-165), and the Buying/Selling tabs' own per-row/purse count text plus the cross-panel "pending sell" inventory highlight are not wired (AP-166). The two PRE-EXISTING residuals below (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) are UNCHANGED by this pass — see the ORIGINAL text below for their citations. REVIEW CORRECTIONS 2026-08-09 (Opus review of 97cf8738, findings F1-F9): none of these are NEW divergences from retail — they are bug fixes that make this row's own claims actually true, so no new AP row is filed for them. F2 fixed the priced/named quantity freezing at a selection-time seed while the Buy button separately read the LIVE slider — both now share one ResolveBuyQuantity computation, so the displayed price always equals what a purchase actually charges (retail: gmVendorUI::RecvNotice_StackSliderChanged re-runs the SAME display update on every slider change, pc:203262-203278). F6 corrected an unauthored "preserve the prior selection if it survives the filter" rule to retail's actual UNCONDITIONAL reselect-to-first-item on every rebuild this controller reaches (VendorItemsUI::UpdateItemsList's notify=1 path, pc:201180-201184, confirmed reached by a fresh open AND a same-vendor refresh via VendorItemsUI::OpenVendor's unconditional SetSelectedItem(...,1), pc:201022). F7 ported BuySingleItem's stack-size-1 quantity clamp (pc:201674-201681) so a stale slider value left over from a previously-selected, DIFFERENT stackable item cannot leak into a non-stack purchase. F8 is recorded inline below, where it corrects this row's own stale claim about the Add-to-List button. NARROWED 2026-08-09 (Slice 6.1-6.3, buy arc) — TWO of the four consciously-deferred residuals below CLOSE. Private per-panel selection is GONE: SelectionState gains a Vendor change source (SelectionChangeSource.Vendor) and is now the AUTHORITY — row clicks, the F4 auto-select-first-item fallback, and right-click examine all call SelectionState.Select/Clear; VendorUiController is a CONSUMER (OnSelectionTransition) exactly like every sibling panel, matching retail's global ACCWeenieObject::selectedID. The examine gap (F7c) is GONE too: VendorShopItemMaterializer (src/AcDream.Runtime/Gameplay/VendorShopItemMaterializer.cs, Slice 6.1) registers every ApproachVendor shop item into ClientObjectTable (guid, ContainerId = vendorGuid, merge-upserted via the ordinary Ingest path, retired on session Close/Reset/vendor-replace via the SAME VendorState.Changed subscription) so AppraisalUiController.Apply's lookup now succeeds; VendorUiController.ExamineItem wires UiItemList.ExamineItemRequested to ItemInteractionController.ExamineSelectedOrEnterMode, mirroring ExternalContainerController. Double-click-to-buy was investigated (research doc docs/research/2026-08-08-slice6-vendor-transactions-research.md §B.2) and confirmed ABSENT from retail — no gmVendorUI::CheckForDoubleClick/VendorItemsUI::CheckForDoubleClick symbol exists anywhere in the 18,366-function named table, unlike sibling panels (gmContractsUI::CheckForDoubleClick, gmPageListUI::CheckForDoubleClick) that DO have one; acdream intentionally does NOT add a double-click shortcut — a user request for it as a deliberate acdream-only UX addition would need its own AP row, per CLAUDE.md's no-invented-mechanisms discipline. The remaining two residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) are UNCHANGED — see below. New approximations this pass introduced are filed separately as AP-162 (no client-side Buy pre-check) and AP-163 (shop-item guid-collision policy). Original REWRITTEN text follows, retained for the two still-open residuals: VendorUiController mounts LayoutDesc 0x21000012/root 0x100000B7 and fully wires only the authored "Items" tab (0x100000B9 — VendorItemsUI: category-filtered browse list with retail's quantity-correct pricing, ItemHolder::GetObjectSplitSize's 0xDC41CB0 split-exempt mask ported locally rather than a flat per-unit price). The other two authored tabs render and switch pages (so the layout looks complete) but are otherwise INERT: "Buying" (0x100000BA, VendorBuyUI — staged-purchase review/confirm, buttons 0x100000C9/CA/CB/CC) and "Selling" (0x100000BB, VendorSellUI — staged-sale review/confirm, buttons 0x100000D2/D3/D4/D5) have no data binding at all. The "Items" page's own Buy button (0x100000C2) correctly enables/disables with selection (UiButton.Enabled, retail SetState(1)/SetState(0xd)) and Slice 6.3 wires it to a real immediate single-item purchase (gmVendorUI::BuySingleItem, pc:201661 — VendorRequests.BuildBuy/WorldSession.SendBuy, opcode 0x005F). Review correction 2026-08-09 (F8): Add to List (0x100000C3, staging) does NOT enable/disable with selection — it is PERMANENTLY disabled (VendorUiController.SetActionButtonsEnabled), because it has no wired OnClick at all; an enabled-but-dead button is a worse affordance than a disabled one, so it stays disabled until the "Buying" tab's staging list is actually implemented. The Buy opcode exists on the wire now; Sell (0x0060) does not. VendorProfile::InqAcceptability (sell-eligibility filtering) is unread — moot without a sell UI. Two divergences remain of the four the F1-F8 fix pass originally recorded — the other two (private per-panel selection, unwired shop-item examine) CLOSED at Slice 6.1/6.2, see the NARROWED note above: (1) the closed-dropdown button face reuses the row template's own two sprites (0x060012B3 normal/0x060012B4 open) through UiMenu's existing single-texture 3-slice DrawButtonFace instead of retail's authored two-piece label+arrow-cap assembly (label 0x1000034D + a separate 17x19 arrow cap 0x1000034E with its own 0x060012B1/0x060012B2 states) — a cosmetic gap only; the popup panel and its rows render with the exact authored geometry and sprites; (2) the alt-currency "you have" holding reads VendorShopProfile.AlternateCurrencyAmount directly instead of tracking retail's gmVendorUI.m_last_sale purchase debit — moot until a sell path exists to actually debit it, since m_last_sale only changes on a completed SALE (retail's own m_last_sale == 0 case, pc:204091/OpenVendor's this->m_last_sale = 0 reset at pc:203790/203800); Slice 6.3's buy path does not touch m_last_sale either (retail's own buy flow never writes it), so this residual is unaffected by the buy arc landing. The "Buying"/"Selling" staging tabs (VendorBuyUI/VendorSellUI) and the full Sell wire remain unwired — unchanged Slice 6b/6c territory per contract decision 6, not a residual of THIS row. | src/AcDream.App/UI/Layout/VendorUiController.cs; src/AcDream.Core/Items/VendorState.cs; src/AcDream.Core.Net/GameEventWiring.cs; src/AcDream.App/UI/RetailUiRuntime.cs | Slice 6 (docs/plans/2026-07-23-world-interaction-completion.md) owns the authoritative buy/sell transaction command, quantity/stack-split selection, drag-to-sell consumption, and InqAcceptability-gated sell UI — Slice 5.4's charter was browse only. Buy (6.3), the global ACCWeenieObject::selectedID coupling (6.2), and shop-item ClientObjectTable registration (6.1) are now DONE, landing exactly the seam this row's original filing fenced off; drag-to-sell consumption and InqAcceptability-gated sell UI remain Slice 6b/6c territory. | A player can browse, select, examine, and BUY (Slice 6.3) — the only remaining unbuilt transaction is Sell. Clicking "Buying"/"Selling" still shows an empty page with no error or explanation, matching "present but does nothing" rather than a disabled/hidden affordance. The dropdown's closed-state button face is missing its separate arrow-cap glyph — a minor visual gap, not a functional one; the open popup itself is pixel-faithful to the authored template. | gmVendorUI::OpenVendor pc:203650 (m_itemsUI/m_buyUI/m_sellUI construction, PostInit pc:199906, m_last_sale reset pc:203790/203800); VendorBuyUI::VendorBuyUI pc:199717; VendorSellUI::VendorSellUI pc:199753; VendorProfile::InqAcceptability pc:484768-484797; UIElement_Menu::MakePopup pc:120705-120764, ::Initialize pc:120789-120828; VendorItemsUI::UpdateItemsUI pc:202539-202820; VendorItemsUI::UpdateItemsList pc:201029-201190; ItemHolder::GetObjectSplitSize pc:401465-401477; gmToolbarUI::HandleSelectionChanged pc:198740-198790 (mask 0xDC41CB0 at pc:198784); ACCWeenieObject::GetObjectName pc:409056-409132; docs/research/2026-08-08-slice5-vendor-browse-research.md §B.4, §D |
| AP-162 | NARROWED 2026-08-09 (Opus review of 92ea3977, finding F1) — the "Buy All" half of this row CLOSES. VendorUiController.BuyAllButtonPressed now ports all four of retail's client-side pre-send guards (pyreal affordability pc:204017, alt-currency affordability pc:204032, container-slot capacity pc:204053, item-slot capacity pc:204067) — see ComputeBuyTransactionValue/ComputeBuySlotsNeeded/CountPlayerContents, each guard returning with staging fully intact and retail's own exact notice string ("You don't have enough money" at 0x007b57b4, "You must empty some slots in your backpack first" at 0x007b5750, both byte-recovered). The container-vs-item slot CLASSIFICATION this port uses (ItemType.Container instead of retail's bitfield/capacity test) is its own new, narrower approximation — filed separately as AP-168 rather than folded in here. Only TryBuy's single-item Buy path (Items tab's own Buy button, and the Buying tab's "Buy Item") remains WITHOUT a client-side pre-check — the risk/oracle columns below now describe that one remaining case, not both. EXTENDED 2026-08-09 (Slice 6b) — the same omission now also covers "Buy All". ItemInteractionController.TryBuyAll (the batched-send path VendorUiController.BuyAllButtonPressed calls) sends unconditionally too, without porting retail's pc:204017/204032/204053/204067 affordability/pack-capacity pre-checks for the MULTI-item case either — the same latency-not-correctness tradeoff this row already documents for the single-item path, extended rather than duplicated into a second row; retiring this row should port both the single- and batched-send pre-checks together. Filed 2026-08-09, Slice 6.3 (buy wire + button). Retail's BuySingleItem (pc:201661) performs TWO client-side pre-checks before ever sending CM_Vendor::Event_Buy: (a) an affordability check against this->m_totalValue (pyreal) or shopVendorProfile->trade_num - m_last_sale (alt-currency), showing a LOCAL string via ECM_UI::SendNotice_DisplayStringInfo and returning without sending anything on failure (pc:201686-201717); (b) a pack/container-capacity pre-check (pc:201730-201746) mirroring the server's own check. acdream's ItemInteractionController.TryBuy sends unconditionally once the shared use/inventory gate is free — no client-side affordability or capacity check runs before dispatch. Every refused purchase pays a full round-trip (send → server rejects → UseDone/GameEventInventoryServerSaveFailed) instead of failing instantly and silently client-side. Swept 2026-08-09 (F4 review fix): TryBuy now also checks whether sendBuy actually reached a live, in-world session before marking the reservation dispatched — an orthogonal reservation-leak bug fix (no session ever produced a stray permanent busy-lock), not an affordability/capacity check; this row's scope and residual are unchanged. | src/AcDream.App/UI/ItemInteractionController.cs (TryBuy) | The research doc's own open question 1 (docs/research/2026-08-08-slice6-vendor-transactions-research.md) recommends deferring this: the server is authoritative either way (ACE re-validates both affordability and capacity server-side — Vendor.BuyItems_ValidateTransaction, Vendor.cs:431-571), so omitting the client pre-check is a LATENCY/UX gap, not a correctness one — a refused purchase still fails cleanly, just one round-trip later than retail. | A player attempting to buy something they cannot afford or have no room for sees the failure arrive after a network round-trip instead of instantly; against a well-behaved ACE server no purchase can succeed that retail's pre-check would have blocked, so no transaction outcome differs — only its latency. Retiring this row means porting BuySingleItem's two pre-check branches (pc:201686-201746) into TryBuy before dispatch. | gmVendorUI::BuySingleItem pc:201661/0x004C2820 (affordability pc:201686-201717, capacity pc:201730-201746); Vendor.BuyItems_ValidateTransaction (references/ACE/Source/ACE.Server/WorldObjects/Vendor.cs:431-571); docs/research/2026-08-08-slice6-vendor-transactions-research.md §D point 4, Open question 1 |
| AP-163 | REVIEW CORRECTION 2026-08-09 (Opus review of 97cf8738, finding F1): this row's ownership discipline is now COMPLETE on both halves, not just the add-time collision guard described below. The retire pass (OnVendorTransition's loop over guids missing from the new ApproachVendor snapshot) previously deleted ANY such guid unconditionally — a plain bug, not a documented divergence, since buying a UNIQUE vendor item re-containers that SAME guid into the buyer's own pack (Player_Commerce.cs:86-108) BEFORE the post-buy refresh that drops it from the shop's own list arrives; the old retire pass would have stripped the just-purchased item straight back out of the buyer's inventory. The exact rule now enforced: each owned guid remembers the vendor id it was registered under (Dictionary<uint,uint>, guid -> vendorId), and the retire pass calls ClientObjectTable.Remove ONLY when the live object's CURRENT ContainerId still equals that recorded vendor id; when it differs (or the object is already gone), the tracking entry is dropped silently and the object itself is left completely untouched — the SAME skip-not-clobber discipline the add-time collision guard below already used, now applied symmetrically on the way out. This is a bug fix, not a new divergence, and does not change this row's still-open scope: retail's actual ClientObjMaintSystem/CObjectMaint collision behavior on a guid collision remains untraced. Filed 2026-08-09, Slice 6.1 (shop-item materialization). VendorShopItemMaterializer registers each ApproachVendor shop item into ClientObjectTable keyed by its own server guid. ACE's UniqueItemsForSale (Vendor.cs:34,638) can list the EXACT guid a player last held (an item sold to this vendor keeps its original guid), so a guid collision against an existing, differently-owned ClientObjectTable entry is a real, if rare, possibility. No retail behavior for this exact case was traced (retail's ClientObjMaintSystem/CObjectMaint guid-keyed registration internals were not decompiled for this pass). acdream's policy is a conscious, conservative default: a guid this materializer did NOT itself add to the table on a previous cycle is treated as owned by something else and is left completely untouched — never overwritten, never later removed by this class. | src/AcDream.Runtime/Gameplay/VendorShopItemMaterializer.cs (OnVendorTransition's collision guard) | Skip-not-clobber is the safe default absent a traced retail mechanism: silently reparenting a live entity's or another container's item into the vendor's ContainerId would corrupt real ownership state (equipment tracking, burden, radar) for a guid this code does not own, which is strictly worse than a single shop row's status-bar/appraisal projection staying blank. The vendor list itself is unaffected either way — VendorUiController reads display fields straight off VendorShopItem, never through ClientObjectTable. | If retail's actual behavior differs (e.g. it always overwrites, or a real UniqueItemsForSale collision is more common than assumed), the one colliding shop row's status-bar/appraisal projection stays stale/blank instead of showing the vendor listing — a narrow, single-row display gap, never a corrupted non-vendor object. Retiring this row requires tracing retail's ClientObjMaintSystem registration behavior on a guid collision, which was out of scope for this pass. | No direct retail citation traced this pass — Vendor.cs:34,638 (UniqueItemsForSale, ACE) establishes the collision is POSSIBLE, not what retail does about it; docs/research/2026-08-08-slice6-vendor-transactions-research.md (task brief: "study how ACE guids vendor stock and state your collision policy with evidence") |
| AP-164 | RETIRED 2026-08-09 (Opus review of 92ea3977, finding F4). VendorProfile::InqAcceptability's non-sellable bitfield check ((*(uint8_t*)((char*)arg2->_bitfield)[3] & 1) != 0, pc:005d1aa7, byte 3 bit 0 — bit 24, 0x01000000, of PublicWeenieDesc's packed flags) is now ported end to end. PublicWeenieFlags.Retained (src/AcDream.Core/Items/ItemInteractionPolicy.cs) names the bit; VendorSellAcceptability.Evaluate takes it as a new publicWeenieBitfield parameter and ORs it into the SAME WrongType outcome the type-mask mismatch produces, matching retail's own OR'd branch exactly. The row's three original claims are each corrected by this fix, not merely superseded: the bit WAS already threaded onto ClientObject (ClientObject.PublicWeenieBitfield, ClientObject.cs:241, populated by ObjectTableWiring.ToWeenieData's PublicWeenieBitfield: s.ObjectDescriptionFlags mapping) — the claim that no PublicWeenieFlags member was named at 0x01000000 was true only because the member had never been added, not because the underlying data was missing; and the "unclear whether ApproachVendor's wire shape carries this flag for a player-owned pack item" question was moot from the start — the drag-to-sell flow always operates on the PLAYER'S OWN pack item (arrived via ordinary CreateObject/EntitySpawn, never ApproachVendor, which only describes the VENDOR'S stock), and that path already carried the field. Nothing is left unmodeled. | src/AcDream.Core/Items/VendorSellAcceptability.cs (Evaluate); src/AcDream.Core/Items/ItemInteractionPolicy.cs (PublicWeenieFlags.Retained) | — | — | VendorProfile::InqAcceptability pc:484768-484797/0x005d1a90, bitfield test at pc:005d1aa7; acclient.h:6456 (BF_RETAINED = 0x1000000) |
| AP-165 | Filed 2026-08-09, Slice 6b (buy staging). Retail's "Buying" tab Buy Item/Clear Item removal-amount rule (gmVendorUI::HandleButtonClicks cases 0x100000c9/0x100000cb, pc:203989-204010/204080-204094) tests the selected item's own pwd._maxStackSize (the item TYPE's stack ceiling) to decide RemoveProfileFromList's amount argument (-1 full removal vs 1 partial decrement). VendorShopItem does not carry a MaxStackSize wire field (only DescStackSize, the item's CURRENT authored per-stack quantity, already threaded through for pricing), so VendorUiController.BuyStagingRemovalAmount substitutes DescStackSize as the stackability test instead. | src/AcDream.App/UI/Layout/VendorUiController.cs (BuyStagingRemovalAmount) | The two fields agree for every case that matters in practice — DescStackSize <= 1 implies MaxStackSize <= 1 — and can only disagree for a vendor stocking a single unit of an otherwise-stackable item TYPE. | In that narrow case, Buy Item/Clear Item would decrement the staged quantity by one instead of clearing the whole entry outright — a minor UI residue (a stale partial-quantity row stays staged) with no wire or money-safety consequence, since a subsequent Buy Item/Clear Item click on the same row converges it to zero anyway. | gmVendorUI::HandleButtonClicks cases 0x100000c9/0x100000cb, pc:203989-204010/204080-204094; docs/research/2026-08-08-slice6b-vendor-completion-research.md §Q3 |
| AP-166 | Filed 2026-08-09, Slice 6b/6c (staging presentation). Two cosmetic gaps in the Buying/Selling tabs: (1) their own per-row count/price text and purse-total text (m_buyListText/m_buyPurseText, D0 ids 0x100000C7/C8; m_sellListText/m_sellPurseText, 0x100000D0/D1) are not wired — a staged row renders as an icon cell only (matching the Items tab's own icon-strip shape), with no adjacent per-row label; (2) a successful Sell Item/Sell All/Clear Item does not port retail's cross-panel gmVendorUI::VendorItemSetSellState — the player's OWN inventory panel highlight that marks an item as "pending sell" is neither shown on stage nor cleared on unstage. | src/AcDream.App/UI/Layout/VendorUiController.cs | The underlying staging/transaction mechanics (add, remove, batched Buy All/Sell All, InqAcceptability gating) are fully functional without these presentation details; the Items tab's shared name/cost text pair already shows price/quantity for whichever row is currently selected (including a staged one), and the shared system-message channel already surfaces rejection feedback. | A player cannot see a NON-selected staged row's price/quantity at a glance (only the currently-selected row's name/cost renders, via the Items tab's shared text pair — there is no per-row label on the Buying/Selling strips themselves), and a pending-sell item shows no visual cue back in the main inventory panel while staged. | VendorBuyUI::VendorBuyUI pc:199717; VendorSellUI::VendorSellUI pc:199753 (purse/list text element construction); gmVendorUI::VendorItemSetSellState (call sites pc:204107/204133); docs/research/2026-08-08-slice5-vendor-browse-research.md §B.4 D0 tree |
| AP-167 | Filed 2026-08-09, Opus review of 92ea3977, finding F6 (Sell Item's SellSingleItem port). Retail's gmVendorUI::SellSingleItem (pc:201808-201881, 0x004c2b40) gates its whole stack-split/send branch behind an OUTER check: if the selected item is container-capable (a bitfield bit this port does not currently decode, ORed with nonzero _itemsCapacity/_containersCapacity) AND it currently holds contents, SellSingleItem refuses with a distinct notice (RecvNotice_SkillAdvancementClassChanged's literal string, not yet recovered) and never reaches the stack-split check or the send at all — matching InqAcceptability's own "a non-empty container always accepted" bypass being the WRONG direction for a DIRECT single-item sell of the CONTAINER itself. VendorUiController.SellItemButtonPressed does not port this outer branch — it goes straight to the stack-split check for every selected item, container or not. | src/AcDream.App/UI/Layout/VendorUiController.cs (SellItemButtonPressed) | The scope of the review finding (F6) was the stack-split refusal and the literal amount-1 send, both fully ported; the container-emptiness branch is a distinct, separately-gated retail mechanism this pass did not trace far enough to port (the exact bitfield bit and the refusal string are both still unrecovered). The server remains authoritative regardless — a client-side accept here is a UX gap, not a wire-safety one. | Selecting a non-empty container (a bag with items still inside it) and pressing "Sell Item" directly would, in retail, refuse locally with a distinct message; this port instead falls through to the ordinary stack-split check (which a non-stackable container passes trivially) and sends the sell — the actual sale's server-side fate for a non-empty container is untraced (ACE may reject, merge, or drop the contents; not investigated here). | gmVendorUI::SellSingleItem pc:201808-201881/0x004c2b40 (outer container-emptiness branch pc:201818-201829); docs/research/2026-08-08-slice6b-vendor-completion-research.md |
| AP-168 | NARROWED 2026-08-08 (grand-gate finding G1) — the player's-OWN-pack half (CountPlayerContents) is FIXED; only the shop-stock half (ComputeBuySlotsNeeded) remains approximated. Live testing surfaced the risk this row already predicted: "Buy All" false-blocked a container purchase while the player visibly had free container slots. Root cause was NOT the theoretical corner case originally described here — it was that the old dual-heuristic (ItemType.Container bit OR nonzero ItemsCapacity/ContainersCapacity) could over-classify an ordinary non-container object as an occupied container slot, undercounting free space. CountPlayerContents now reads ClientObject.ContainerTypeHint first — retail's actual wire ContainerProperties (Item_ServerSaysContainId 0x0022's ContainerType; also carried by ContentProfile/PlayerDescription's per-entry container-kind byte), already threaded onto every owned object by InitializeInventoryManifest/ApplyConfirmedServerMove/ReplaceContents and already used for this identical question by ClientObjectTable.IsContainerListMember — falling back to ItemType.Container alone (the capacity-field legs were dropped) only for the rare object that never received a hint. This matches retail's real _itemsList/_containersList bucketing (ACCWeenieObject::GetNumContainedItems/GetNumContainedContainers @0x0058beb0/0x0058bec0 just report already-bucketed IDList lengths; the bucketing happens once, at insert time, in ServerSaysContainID @0x0058be40, from that same wire field) rather than reconstructing it from the item's own type/capacity fields. Original text: Filed 2026-08-09, Opus review of 92ea3977, finding F1 (Buy All's client pre-send capacity guard). Retail's gmVendorUI::InqListSlotCount (pc:200038-200065, 0x004c0c10) classifies each staged item as needing a CONTAINER slot or an ITEM slot by testing a bitfield bit (a decompiler string-misattribution artifact not yet decoded) ORed with the item's own nonzero _itemsCapacity/_containersCapacity. VendorUiController.ComputeBuySlotsNeeded/CountPlayerContents approximate this with (item.ItemType & ItemType.Container) != 0 instead — correct for the ordinary case (an authored backpack/pouch DOES carry the Container type bit) but not byte-identical for the theoretical case of a non-Container-typed item that still authors nonzero pack/side capacities (or vice versa, a Container-typed item with zero capacity of its own, e.g. a locked/sealed decorative chest never meant to be carried). | src/AcDream.App/UI/Layout/VendorUiController.cs (ComputeBuySlotsNeeded, CountPlayerContents) | VendorShopItem's wire shape (Slice 5's deliberately narrow browse-scope subset) genuinely does not carry PublicWeenieBitfield/ItemsCapacity/ContainersCapacity/ContainerProperties the way ClientObject does for an ordinary CreateObject/membership-sourced item, so ComputeBuySlotsNeeded (the shop-stock side, staged-but-not-yet-owned items) cannot read a wire-truth hint the way the fixed CountPlayerContents (the already-owned side) now does; extending the DTO was out of scope for this fix. The server remains authoritative and re-validates real pack-space regardless (Vendor.BuyItems_ValidateTransaction, Vendor.cs:431-571) — the residual failure mode stays UX/latency, not correctness. | A vendor selling a Container-typed item with zero authored capacity (rare/decorative) would still be misclassified as needing a container slot instead of an item slot, or vice versa for a non-Container-typed item that DOES author capacity (also rare) — the pre-check could still reject a purchase retail's own guard would have allowed, or allow one retail would have blocked, purely on the CLIENT side for the SHOP-STOCK item being bought; the player's-OWN-pack accounting that drives the free-slot count is no longer the source of that risk. | gmVendorUI::InqListSlotCount pc:200038-200065/0x004c0c10; ACCWeenieObject::GetNumContainedItems/GetNumContainedContainers 0x0058beb0/0x0058bec0; ACCWeenieObject::ServerSaysContainID 0x0058be40; docs/research/2026-08-08-slice6b-vendor-completion-research.md |
| AP-169 | Filed 2026-08-08, grand-gate finding G2 (vendor toolbar split-slider absent live). Retail's gmToolbarUI::HandleSelectionChanged reads eax_5->pwd._stackSize (pc:198688/198744/198774/198791) uniformly for both owned-inventory and vendor-owned selections — the same PublicWeenieDesc::_stackSize field an ordinary CreateObject carries. The user's local ACE server never populates that field for a vendor browse-list row: Vendor.LoadInventoryItem (references/ACE/Source/ACE.Server/WorldObjects/Vendor.cs:144-172) builds the listing WorldObject via WorldObjectFactory.CreateNewWorldObject and sets only wo.VendorShopCreateListStackSize (our VendorShopItem.StackSize, the packed ItemProfile "how many for sale" dword) — it never calls wo.SetStackSize(...), so PublicWeenieDesc's own conditional StackSize field (our VendorShopItem.DescStackSize, what GameEventApproachVendor.cs:60's obj.SerializeGameDataOnly walks) comes back null on the real wire for every vendor listing, live-verified with a materializer-level reproduction test. VendorShopItemMaterializer.ToWeenieData now prefers DescStackSize when the wire genuinely carries it (retail-faithful first — a real retail server, or a future ACE fix, populating _stackSize normally is honored unchanged), falling back to the packed StackSize supply-count field (clamped positive) when it doesn't — the field ACE DOES reliably send. | src/AcDream.Runtime/Gameplay/VendorShopItemMaterializer.cs (ToWeenieData, ResolveDisplayStackSize) | This is an ACE-server-constraint adaptation, not a retail redesign: retail's own client-side read (pwd._stackSize) is unchanged and still tried first; only the fallback source differs, and only because the live server this port talks to (the user has no other AC server available) never fills the field retail's own server apparently does. The packed supply-count field is the only OTHER wire-carried "how many" signal VendorShopItem has. | The packed field's -1 "unlimited supply" sentinel has no bounded per-row purchase cap in VendorShopItem's current wire shape (no _maxStackSize carried), so an unlimited-supply vendor stack still falls through to the safe non-splittable default (slider absent, single-unit buy) rather than a genuine retail-faithful per-row cap — a SMALLER residual than the pre-fix "no vendor stack ever shows a slider" defect, but not zero. A finite-quantity vendor stack (the common case, matching the live gate's arrows/trade-notes report) now shows the slider correctly. | gmToolbarUI::HandleSelectionChanged pc:198635-198930/0x004bf380 (stack-size reads at pc:198688/198744/198774/198791); Vendor.LoadInventoryItem Vendor.cs:144-172; GameEventApproachVendor.cs:52-61 |
| AP-170 | Filed 2026-08-08, grand-gate finding G3 (out-of-range vendor Use lost silently). Retail's ItemHolder::UseObject @ 0x00588A80 has no client-side range check and sends Use immediately regardless of distance — this port's ORIGINAL RequestUse faithfully mirrored that shape. Live testing against the user's local ACE server showed it does not hold: walking to a vendor and using it from out of range plays the vendor's cosmetic greeting (a distance-only reaction, independent of Use) but never opens the shop panel — ApproachVendor never arrives. ACE's Player.HandleActionUseItem (references/ACE/Source/ACE.Server/WorldObjects/Player_Use.cs:176-215) explains why: an out-of-range target routes through CreateMoveToChain(item, (success) => TryUseItem(item, success)) (Player_Move.cs:37-96), which polls every 0.1s for the player to reach WithinUseRadius and only then calls ActOnUse — it does not teleport or server-move the player; it waits for the CLIENT's own walk to land, and a Use that arrives before that poll ever starts observing an in-range player is simply never followed by the vendor's ApproachVendor send (Vendor.ActOnUse's own doc comment: "the player will have been commanded to move using DoMoveTo before ActOnUse is called... it should be assumed that the player is within range" — a precondition our immediate send violated). SelectionInteractionController.RequestUse now arms the out-of-range case on the SAME arrival-gated shape SendPickup's close-range (turn-only) branch already used (RuntimeInteractionTransactionState.TryArmPostArrivalUse/TryResolveUseApproachCompletion, mirroring TryArmPostArrivalPickup/TryResolveApproachCompletion field-for-field) — the wire Use dispatches only once the local approach naturally completes. An already-in-range Use (a turn at most, or no approach concept applies) is unaffected and still sends immediately, matching ACE's own "already within use distance" synchronous callback. | src/AcDream.App/Interaction/SelectionInteractionController.cs (RequestUse, HandleApproachCompletion, HandleUseApproachCompletion, CancelPendingApproach, OnEntityHidden, OnEntityRemoved); src/AcDream.Runtime/Gameplay/RuntimeInteractionTransactionState.cs (RuntimePendingUse, TryArmPostArrivalUse, TryResolveUseApproachCompletion, TryCancelPendingUse) | This is an ACE-server-constraint adaptation, not a retail redesign: retail's REAL server walks the player itself before the target's ActOnUse ever sees the request, so the client's immediate send never races anything there. ACE does not do this for a player-initiated Use — it only polls and waits — so arming on arrival is required for correctness against the only server this port can test against, not a stylistic preference. | An interaction path that still calls TryDispatchUse directly without going through RequestUse's approach gate (none identified at this fix) would keep the original race. The armed reservation is a live busy-count reference until arrival/cancellation resolves it; ResetCore releases it unconditionally on any reset/dispose so a teardown that runs without a preceding CancelPendingApproach() (e.g. a headless/no-window host with no SelectionInteractionController) cannot leak it. | ItemHolder::UseObject 0x00588A80; Player.HandleActionUseItem Player_Use.cs:176-215; Player.CreateMoveToChain/MoveToChain Player_Move.cs:37-153; Vendor.ActOnUse Vendor.cs:223-266 |
| AP-111 | RETIRED 2026-07-11 (M2 held-object parenting) — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live 0xF749 ParentEvent is parsed with retail sequence freshness; a focused render controller resolves Setup.HoldingLocations, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs; src/AcDream.Core/Meshing/EquippedChildAttachment.cs; src/AcDream.App/Rendering/EquippedChildRenderController.cs | — | — | ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310; SmartBox::HandleParentEvent @ 0x004535D0; CPhysicsObj::set_parent @ 0x00515A90; CPhysicsObj::UpdateChild @ 0x00512D50 |
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, MaybeStopCompletely, server-response queueing, and auto-repeat, but still omits StartAttackRequest's FinishJump call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | src/AcDream.Runtime/Gameplay/RuntimeCombatAttackState.cs; src/AcDream.App/UI/Layout/CombatUiController.cs | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | ClientCombatSystem::StartAttackRequest @ 0x0056C040; CommandInterpreter::MaybeStopCompletely @ 0x006B3B90; gmCombatUI::ListenToElementMessage @ 0x004CC430 |
| AP-113 | Invalid lifestone-command arguments display the local text Usage: /lifestone; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs; RetailClientCommandCatalog.cs | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | /ls now can show different wording/color from retail while still refusing the invalid request correctly | ClientCommunicationSystem::DoLifestone @ 0x0056FC70 |
| AP-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 | The DAT-authored portal-space viewport, animation SoundTweakedHook, and centered repeating "In Portal Space - Please Wait..." display string are live, but the separate ClientUISystem enter/exit sound enums are not yet presented. 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, and centered wait notice, but lacks retail's short UI enter/exit cue sounds. | 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 | Outdoor particle CLandCell::IsInView state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail LScape::landcell_check uses Render::get_clip_height + Render::block_check on terrain-cell corner intervals | src/AcDream.App/Rendering/TerrainModernRenderer.cs (CollectVisibleCells) | 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-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 |
| AP-121 | The /framerate command persists acdream's live ShowFps value in the modern settings.json Display bag after performing retail's live toggle/notice behavior. The named retail DoFrameRate body itself only flips fShowFramerate and sends SetFramerateDisplay; no equivalent persistence write occurs in that command body. | src/AcDream.App/Settings/RuntimeSettingsController.cs (ToggleFrameRate) | This preserves acdream's shipped L.0 cross-launch display preference while keeping the live retail mechanism and SmartBox visibility result exact; it changes no gameplay or wire state | A relaunch can retain FPS visibility where the named retail command body alone would not; a future port of retail's wider preference owner must avoid double-persisting or applying conflicting startup state | ClientCommunicationSystem::DoFrameRate @ 0x005707D0; CM_UI::SendNotice_SetFramerateDisplay @ 0x0047A050; gmSmartBoxUI::RecvNotice_SetFramerateDisplay @ 0x004D65E0 |
| AP-122 | The toolbar Use hand is Ghosted when selection is empty. Retail sets state 1 (Normal) and an empty-selection click enters generic TARGET_MODE_USE; selected-object enablement and activation remain retail-faithful. | src/AcDream.App/UI/Layout/ToolbarController.cs (RefreshUseButton) | User explicitly requires no selection to present as unavailable. The disabled retained button consumes the click without opening a cursor; every nonempty selection still uses the exact retail CombatUse/type/ItemUses predicate and the shared activation owner. | Generic pick-then-use mode cannot be entered from the empty toolbar hand; an object must first be selected, after which the same Use command is available. | gmToolbarUI::HandleSelectionChanged @ 0x004BF380; gmToolbarUI::ListenToElementMessage @ 0x004BEE90 |
| AP-123 | Item cooldowns use the retained toolkit's existing procedural UiItemSlot leaf rather than materializing retail's ten m_elem_Icon_Cooldown_* child elements. The group lookup, remaining-time formula, exact DAT sprites, step choice, and topmost ReadOrder-8 outcome are faithful. | src/AcDream.App/UI/Layout/ItemCooldownUiController.cs; src/AcDream.App/UI/UiItemSlot.cs | UiItemSlot already consumes/reproduces UIItem children procedurally under the IA-15 retained-toolkit architecture. Selecting one imported sprite at draw time gives every inventory/equipment/shortcut alias the same output without a parallel widget tree or per-cell timers. | A future feature that observes the individual cooldown child visibility/state rather than the rendered UIItem could see no child elements even though the cell looks and advances correctly. | CEnchantmentRegistry::OnCooldown @ 0x005943C0; UIElement_UIItem::UpdateCooldownDisplay @ 0x004E1E20; common UIItem prototype 0x1000033E |
| AP-124 | Local ACE omits the new object's CreateObject to the initiating session after StackableSplitTo3D, sending only F748 Position for the previously unknown GUID. acdream retains retail's one pending split source/count/time identity for ten seconds and, only for that otherwise-impossible unknown Position, hydrates a canonical clone of the source description with the new GUID and authoritative world placement. | src/AcDream.App/World/InventoryWorldDropProjectionController.cs; src/AcDream.App/UI/ItemInteractionController.cs; src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs | Nearby/reconnecting clients receive the ordinary CreateObject, while the initiator otherwise cannot render the authoritative object until relog. A server that sends CreateObject never enters this path; the pending identity is consumed before normal hydration and all other unknown Positions remain rejected. | ACE's F748 does not carry WCID or stack size, so an unrelated unknown Position arriving during the exact pending ten-second window could be associated with the split. Retail confirms WCID/count from CreateObject; removing the approximation requires ACE to send that packet to the initiator. | ACCWeenieObject::UIAttemptSplitTo3D @ 0x0058D850; ACCWeenieObject::DeclareValid @ 0x0058E340; ACE Player.HandleActionStackableSplitTo3D / TryDropItem; docs/research/2026-07-26-retail-inventory-placement-and-world-drop-pseudocode.md |
| AP-125 | Transport control packets (the 2.0 s cumulative AckSequence and the 0.6 s RequestRetransmit) are emitted STANDALONE; retail piggybacks optional headers onto queued outbound packets first-fit (FlowQueue::CoalesceData @ 0x00547740, invoked at TransmitNewPackets @ 0x00547A6E), and EnqueueNaks hands the NAK to PacketController::EnqueueOptionalHeader @ 0x00543C84 rather than emitting directly. | src/AcDream.Core.Net/Transport/AckNakScheduler.cs (EmitCumulativeAck, EmitNakRequest) | ACE honours a RequestRetransmit ONLY when EncryptedChecksum is absent (NetworkSession.cs:283-284) — a retail-style piggyback onto a sequenced packet encrypts the NAK and ACE silently ignores it, making S2C loss unrecoverable; ACE likewise advances its client-sequence watermark on any packet whose flags are not exactly AckSequence (:474-476), so coalesced control content on a borrowed sequence risks skipping a real packet. Standalone exact-flag emission is the only ACE-safe shape; it also keeps reliable packets free of optional headers, making the resend cache strip provably a no-op. | Slightly higher C2S datagram count than retail (one extra small packet per 2.0 s / per NAK window); marginally more loss exposure for the control packets themselves on a metered path. | FlowQueue::CoalesceData @ 0x00547740; SharedNet::EnqueuePak @ 0x00543B10; SharedNet::EnqueueNaks @ 0x00543BD0; ACE NetworkSession.cs:283-284,:342-343,:474-476 |
| AP-126 | One monotonic Stopwatch-backed clock (TransportClock) drives every transport gate (2.0 s ack, 0.6 s NAK, 0.333 s handshake retry, 0.5 s interval, 5 s assembler sweep); retail splits gates between Timer::cur_time (server-adjusted) and Timer::local_time. | src/AcDream.Core.Net/Transport/TransportClock.cs | The cur/local split only matters for gates that must track server clock adjustments; none of the ported gates semantically depend on server time — they are local cadences. A single injectable source also gives the virtual-clock test seam every conformance suite relies on. | A future port of a genuinely server-clock-relative gate could silently use the wrong clock if it reuses TransportClock without checking this row. | SharedNet::EnqueuePak @ 0x00543B10 (cur_time); ClientNet::ProcessConnection @ 0x00545450 (local_time for the 140 s check) |
| AP-127 | RETIRED 2026-07-31 (#268). PlayerSkillMath now owns retail CACQualities::InqSkill ordering for both panel values and Runtime run/jump prediction: intrinsic + positive 0x16D all-skills + the exact +10 category switch, then EnchantSkill, then 0x146 Jack of All Trades +5 and specialized-only 2 × 0x158. Live player PropertyInt changes refresh the immutable Runtime augmentation snapshot. The separately described current-stamina local-copy nuance was re-audited: the query reads current stamina, but ordinary max-vital buffs target the max-secondary key and do not create stamina when current is zero; no independently observable residual remains. | src/AcDream.Core/Player/PlayerSkillMath.cs; src/AcDream.Core/Player/LocalPlayerState.cs; src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs; src/AcDream.Runtime/Session/LiveSessionEventRouter.cs | — | — | CACQualities::InqSkill @ 0x00592660; CACQualities::InqRunRate @ 0x00592800; CEnchantmentRegistry::EnchantSkill @ 0x005947B0 |
| AP-128 | PK-timer jump-cost clock basis unconfirmed (filed at the P3 Opus review, 2026-07-30): PlayerWeenie.JumpStaminaCost evaluates retail's 20-second PK-recency window (LastPkAttackTimestamp PropertyFloat 0x91 + 20.0 >= now) against Environment.TickCount64 process-uptime seconds. The magnitude argument is sound (a 32-bit float cannot hold a Unix epoch with sub-second precision — a conformance test caught the ±128 s swallow), but the wire timestamp's own basis is the SERVER's, so a cross-base compare is latent. INERT today: ACE models neither property, so _lastPkAttackTimestamp is never pushed and the branch never fires. | src/AcDream.Core/Physics/PlayerWeenie.cs (JumpStaminaCost remarks) | Branch unreachable against every ACE-family server; non-PK cost is bit-identical to pre-P3. The basis question is cdb-answerable (Timer::cur_time epoch) if a PK server is ever targeted. | Against a hypothetical server that sends PropertyFloat 0x91, the PK cost bump fires arbitrarily (always/never) instead of on the 20-second window. | CACQualities::JumpStaminaCost 0x00591b90 pc 412934-412968; Timer::cur_time; stat-coupled pseudocode doc §12b |
| AP-130 | Filed 2026-08-02 (physics campaign, continuation-executor slice). The classifier's HasAnimations input is the static proxy (Snapshot.MotionTableId ?? Snapshot.Physics?.MotionTableId) != 0 - "does the Create carry a nonzero motion table" - uniformly for every position source. Retail's HasAnims bit is live animation-QUEUE non-emptiness (CSequence::has_anims = anim_list.head_ != 0), which can differ from mere table assignment. The only confirmed retail HasAnims call site on this path is inside HandleReceivedPosition itself. Amended 2026-08-05 (C5b, #275): the steady-state merge now consumes the SAME static proxy, computed from the pre-merge snapshot with the identical expression, so this row covers both callers. The proxy is deliberately not escalated to a live animation-queue read in that slice. | src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs (ApplyPositionAction, hasAnimations local); src/AcDream.Runtime/Physics/RuntimeAcceptedPositionRouteRequests.cs (Build, hasAnimations local); src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs (TryApplyPosition, hasAnimations local) | Best static proxy available without wiring a live animation-queue read into presentation-independent Position classification; deterministic and testable; gates only ApplyPlacementFrameBeforeRouting (placement-FRAME install), never pose or cell placement. | An entity with an assigned motion table but an empty animation queue (or vice versa) gets the wrong placement-frame decision - a one-frame animation-blend glitch on a Position-driven correction where retail would have done the opposite. | SmartBox::HandleReceivedPosition 0x00453FD0 (the HasAnims gate, pseudo-C ~92992); CPhysicsObj::HasAnims 0x0050F770 -> CSequence::has_anims 0x00524BD0 |
| AP-131 | RETIRED 2026-08-05 (C5b, #275). The unconditional installPlacementFrame: true, clearParent: true literals this row described no longer exist. InboundPhysicsStateController.TryApplyPosition now computes both flags PRE-MERGE from (disposition, hasAnimations(old)) — installPlacementFrame: !force && !hasAnimations, clearParent: !force — which is exactly RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows. Retail decides both pre-placement writes BEFORE MoveOrTeleport is consulted: Gate A 0x0045400C returns at 0x0045409D ahead of unset_parent 0x00454129 and ahead of the HasAnims SetPlacementFrame gate 0x00454137, so neither gate reads the near/far/teleport classification and no route plumbing was required. This row's predicted retirement mechanism did NOT occur: it forecast "the legacy caller is deleted at the production cutover, retiring this row by construction", but the caller was CORRECTED, not deleted — the steady-state merge remains a live production Position caller and is now retail-exact. Pinned by classifier-as-oracle matrix tests over {Apply, ForcePosition} x {animated, not} x {parented, not} x {player 0x5…, creature 0x8…}, sabotage-verified in both directions on each flag. The static hasAnimations proxy itself is unchanged and remains filed at AP-130. | src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs (TryApplyPosition); tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs; docs/research/2026-08-05-c5b-contract.md | — | — | SmartBox::HandleReceivedPosition 0x00453FD0 (Gate A 0x0045400C returning 0x0045409D; unset_parent 0x00454129; the !HasAnims SetPlacementFrame gate 0x00454137 -> 0x00454142) |
| AP-132 | Filed 2026-08-02 (physics campaign, continuation-executor slice). AMENDED 2026-08-05 at the #319 fix — clarifying sentence added distinguishing this row's producer from the CreateObject producer AP-142 clause (f) covers. acdream gates queued parent relations on parent INCARNATION where retail's queue-by-GUID replay is pointer-only. Retail queues a missing-parent relation blob under the PARENT's GUID (QueueBlobForObject ~92326; GUID-keyed CObjectMaint placeholder bucket ~271082-271088) and replays it on GUID (re)creation with only an addressability check (~92312) - no PARENT INSTANCE_TS comparison anywhere on that path (retail's only instance check there is on the CHILD, ~92316-92317). acdream additionally compares the relation's ParentInstanceSequence at admission (pre-existing TryApplyParent/Resolve rules) and at executor replay (ApplyReplayedParentRelation): live-parent-newer discards, relation-newer stays queued for an exact match. The replay's child-missing arm also drops where retail would re-queue under the child's GUID; child-scoped bucket filtering (RemoveObject/RemoveChild) proactively covers the same ledger tradeoff. This row's incarnation gate applies ONLY to the ParentEvent wire producer, which NAMES a specific parent incarnation on the wire (ParentEvent.Parsed.ParentInstanceSequence) - the gate honors data the server explicitly sent. The CreateObject producer (AP-142 clause (f)) is different in kind: neither a raw CreateObject's Physics.Parent nor the same-generation CreateParentUpdate envelope carries a parent instance sequence AT ALL, so there is no wire-named value to gate against; that producer LATE-BINDS to the parent's live incarnation instead of gating on a wire value, which is the same "honor what the server actually sent" principle applied to a message that sent no incarnation. | src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs (ApplyReplayedParentRelation); RuntimeEntityObjectLifetime.cs (TryApplyParent admission gate); ParentAttachmentState.cs (Resolve staleness rules) | The wire event names a SPECIFIC parent incarnation (ParentEvent.Parsed.ParentInstanceSequence) - the gate honors data the server explicitly sent. acdream's own established admission-time rules (ParentAttachmentState.Resolve, predating this slice) already fixed incarnation-gating as the project's parent-staleness posture; the replay path only extends that SAME posture for consistency. | Server GUID reuse between admission and replay: retail would attach the old queued relation to whatever NEW object now holds the GUID (retail's own recycling quirk); acdream discards it (parent newer) or leaves it queued (parent older) - silent loss of a relation retail would have applied, tied to server GUID-recycling cadence, not ordinary play. | Standalone parent handler 0x004535D0 (~92310-92326); CObjectMaint::QueueBlobForObject 0x005092D0 (~271082-271088); child instance check ~92316-92317 |
| AP-133 | Filed 2026-08-03 (#282). A retail CPhysicsObj has exactly ONE cell; ShouldDrawParticles @0x0050fe60 reads that same field and calls IsInView on it, and set_cell_id @0x0050f4f0 / change_cell @0x00513390 are the only things that move it. acdream splits the concept into WorldEntity.ParentCellId (render parent, null for outdoor dat stabs and building shells) and WorldEntity.EffectCellId (authored landcell for those parentless stabs). Every consumer now resolves through the single WorldEntity.VisibilityCellId accessor (ParentCellId ?? EffectCellId); live entities carry ParentCellId only. | src/AcDream.Core/World/WorldEntity.cs (VisibilityCellId); writers LandblockLoader.cs:80,97, LandblockBuildFactory.cs:408 | Outdoor dat stabs deliberately keep a null render parent so portal visibility does not filter them as interior geometry, yet retail still gives their physics object a landcell for particle gating. One accessor keeps the two fields from being read in conflicting orders, which is exactly how #282 arose - EntityEffectPoseRegistry preferred EffectCellId while WbDrawDispatcher and the remote spawn seed preferred ParentCellId. | A future writer that sets EffectCellId on a live entity re-creates #282: it wins VisibilityCellId while the 11 per-tick ParentCellId writers leave it frozen, stranding that entity's particles and lights on a stale cell so they fail IsInView after it crosses a boundary. | CPhysicsObj::ShouldDrawParticles 0x0050fe60; CPhysicsObj::set_cell_id 0x0050f4f0; CPhysicsObj::change_cell 0x00513390 |
| AP-134 | Filed 2026-08-03 (#297). Retail keeps ONE PublicWeenieDesc::_bitfield per object and mutates it in place — SetPlayerKillerStatus @0x005AC7C0 rewrites bits 5/21/25 (PK 0x20 / Free 0x200000 / PKLite 0x2000000, mutually exclusive), driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86, and IsPK/IsImpenetrable/IsPKLite @0x0058C8xx read that same field. acdream replicates the value into FIVE stores: ClientObject.PublicWeenieBitfield (the source, written only by ClientObjectTable.UpdateIntProperty on PropertyInt 134), InboundPhysicsStateController._snapshots[guid].ObjectDescriptionFlags, RuntimeEntityRecord.Snapshot.ObjectDescriptionFlags, the decoded ShadowObjectRegistry registration + per-cell ShadowEntry.Flags, and the local player's RuntimeMovementSkillState own-PWD bitfield. Coherence is maintained by two ObjectUpdated subscribers (RuntimeEntityPvpBitfieldSnapshotSync for the two snapshot stores, LiveEntityPvpBitfieldSync for the decoded shadow flags) plus the appearance-rebuild path re-deriving from the snapshot. The two shadow-flag writers are the SAME invalidation applied at the two edges that can invalidate it, not competing authorities. | src/AcDream.Runtime/Entities/RuntimeEntityPvpBitfieldSnapshotSync.cs; src/AcDream.App/Physics/LiveEntityPvpBitfieldSync.cs; source writer src/AcDream.Core/Items/ClientObjectTable.cs (UpdateIntProperty, PropertyInt 134); decode EntityCollisionFlagsExt.FromPwdBitfield | ACE never re-sends a PublicWeenieDesc after login (EnqueueBroadcastUpdateObject has zero live callers), so PropertyInt 134 over 0x02CE/0x02CD is the ONLY signal a PK status changed — a client cannot learn it from the bitfield itself. The replication exists because acdream separates wire snapshots, canonical records, and the collision shadow registry, which retail does not; each layer needs the decoded value at a different lifetime. Before #297 the snapshot stores were immutable wire captures; this commit is what converts them into write-through caches, and therefore what creates the invariant. | Any future write path that sets ClientObject.PublicWeenieBitfield outside UpdateIntProperty, or any NEW decoded cache of the PK bits, silently re-creates #297: the player walks through PKLite opponents and melee/missile admission refuses them, with no test failing. Note the same class already exists one field over — Properties.Ints[134] is written by UpsertProperties (PlayerDescription 0x0013) and UpdateProperties (IdentifyObjectResponse) WITHOUT mirroring into the bitfield (#300), and retail's OnStatUpdated also rewrites _blipColor (case 0x5f) and _radar_enum (case 0x85) which acdream ignores entirely (#301). | PublicWeenieDesc::SetPlayerKillerStatus 0x005AC7C0; ACCWeenieObject::OnStatUpdated 0x0058DF20 (case 0x86); ACCWeenieObject::IsPK/IsImpenetrable/IsPKLite 0x0058C8xx; retail PKStatusEnum acclient.h:6412-6427 |
| AP-135 | Filed 2026-08-03 (C4 route 4a). Retail CPhysicsObj::MoveOrTeleport 0x00516330 writes NOTHING on the airborne no-op (arg4 == 0 -> return 0 @0x0051636D), and SmartBox::HandleReceivedPosition 0x00453FD0 skips ConstrainTo with it (@0x00454272 sits inside if (MoveOrTeleport(...) != 0) @0x00454254). acdream honours that for every retail-modeled write — body pose, interpolation queue, leash, render entity, collision shadow, and the AP-80 velocity-derived animation cycle — but deliberately KEEPS two acdream-only per-packet bookkeeping writes on that branch: RemoteMotion.CellId = wire landblock and the LastServerPos/LastServerPosTime sample. This was pre-existing player-remote behaviour; route 4a extends it to NPC remotes so both arms are identical | src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (OnPosition, both remote airborne-no-op returns) | The cell id is what acdream's OWN per-tick free-fall ResolveWithTransition sweep gates on (rm.CellId != 0); without it an airborne remote's sphere sweep is skipped and it falls through the floor (#42's neighbourhood). The server sample is what the first grounded packet after the arc synthesizes its velocity from; dropping it would make that velocity span the whole jump. Neither is a retail CPhysicsObj field being written | A remote's cell membership tracks the server's landblock during an arc where retail would keep the cell its own physics last resolved. Visible only if the server's mid-arc landblock disagrees with the client's swept cell — the wire cell is authoritative in every case acdream has observed. Retire together with the free-fall sweep gate, when the remote arc is resolved by the same transition machinery the local player uses | CPhysicsObj::MoveOrTeleport 0x00516330 (@0x0051636D return 0); SmartBox::HandleReceivedPosition 0x00453FD0 (@0x00454254/@0x00454272) |
| AP-136 | Filed 2026-08-04 (C4 route 4b-1 review). AMENDED 2026-08-04 (cancelled-park presentation rollback): this row's central claim — "the entity becomes VISIBLE IMMEDIATELY at the committed destination pose" — was true only of the CANONICAL half until that fix, and the gap was a defect, not a divergence. ParkDeferred publishes a Withdraw receipt whose presentation half the host sink performs (graphical bucket, projection visibility, plugin world state/events, effect-pose registry, local-player shadow, selection), and RestoreParkWithdrawal cannot reach any of it. Its only mirror image was a LATER Place, which a remote that parks on its final Position and then stops moving never receives, because ACE stops broadcasting for a stationary entity — so the entity was left simulated, collidable, and audible while ABSENT from both the world render and the radar for the rest of the session. The rollback now publishes RuntimePlacementProjectionKind.WithdrawalRestored on the same ordered receipt stream, gated on the entity ending the rollback canonically whole (FullCellId != 0 and InWorld) — deliberately NOT on this row's residency arm alone, which the shipped graphical remote path correctly skips because its per-packet prologue rebucket already recommitted a non-zero FullCellId. The AP-136 residual below is unchanged and is now actually observable. Selection alone is not re-established: see AD-63. Retail has NO cancel for a lost-cell park. CPhysicsObj::SetPositionInternal @0x00515BD0 commits the destination pose with store_position @0x00515CE2 and registers the object via CObjectMaint::GotoLostCell @0x00515CF2 (@0x00508210); the registration is removed by exactly one thing, CObjectMaint::InitObjCell @0x00508260, which drains the lost list on cell load and calls CPhysicsObj::reenter_visibility @0x00508296 (@0x00516250) to re-place at the committed pose. An update that performs no SetPosition leaves the registration untouched, so retail keeps the object HIDDEN until its cell loads. acdream's accepted-Position merge cancels the park instead (a shipped, tested invariant), so on cancel we roll the withdrawal back — InWorld, object clock, canonical residency — and the entity becomes VISIBLE IMMEDIATELY at the committed destination pose, uncollidable until its landblock publishes. The pose itself is retail-exact and is deliberately not rolled back. The ShadowObjectRegistry.Suspend applied by WithdrawCanonical is also not lifted, because un-suspending needs a real placement dispatch (ReplacePositionRows); the entity rejoins the broadphase on its next placement | src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (ParkDeferred's restorableOnCancel, Forget, RestoreParkWithdrawal) | The alternative — leaving the cancelled park's withdrawal in place — strands the entity invisible AND intangible for the rest of the session, because CancelCoreDeferred restores none of it and the operation that was the only thing able to wake it is gone. Restoring at the pre-park pose was tried and is wrong: retail commits the destination pose, and route 2's tests pin that the pose survives the cancel. Restore covers the plain unplaceable-destination park and — narrowed 2026-08-04 at the C4 route 4b-2 delta review, then relocated at that slice's round 3 — SubmitPreparedPlacementCore's two collision-prefix-QUIESCENCE parks. Corrected round 4 (D1): NOT "unconditionally" for any of the three. Since the relocation, the same post-snap quiescence test gates EVERY park including the plain one, which the row's own next sentences already described; the word contradicted them. The original blanket "no quiescence park is restorable" was over-broad: its stated reason — re-admitting a spatial root into a retiring prefix blocks the retirement — is exact for ParkCollisionResidents, where the entity's OWN cell is retiring, but TryGetBlockingQuiescence also fires on prefixes the placement merely TOUCHES (any QueriedCellIds entry, i.e. a NEIGHBOUR landblock the sweep crossed a seam into; and the request's CurrentCellId, which on a FIRST submit names the destination rather than the departed source because both accepted-Position callers commit the accepted wire cell to record.FullCellId before submitting — scoped at round 4 (D5): a retained retry re-submits with no fresh merge, and a non-Position rebucket writer (the projection materializer DatLiveEntityProjectionMaterializer — C4 route 4b-3 deleted the second shipped writer, RemoteTeleportController's rollback, and C4 route 7 D4 demoted the third, the equipped-child renderer EquippedChildRenderController.TickChild, to a presentation-only bucket move that no longer touches record.FullCellId) can rebucket that field to a third landblock, so the arm is live). The decision is taken inside ParkDeferred, AFTER SnapToCell, as !IsCollisionPrefixQuiescing(body.CellPosition.ObjCellId) — the cell RestoreParkWithdrawal will actually restore residency into, tested against EVERY live quiescence rather than against the single minimum-OperationId token TryGetBlockingQuiescence happened to return, and read after LandDefs.AdjustToOutside may have moved it (the reachable half of that, and the one a test now pins, is the re-derived cell: a wire (cell, position) pair whose position lies past its own named block's seam is exactly the pair #107's re-derivation distrusts, and it lands residency in a NEIGHBOUR landblock — see QuiescingOwnPrefix_SeamCrossingParkIsRestoredAtTheReDerivedNeighbourCell). A cell id of 0 is AdjustToOutside's map-edge failure sentinel, never landblock (0,0), so the map is not consulted with it (C3c-F3). Round 4 (D6) added the same test at RESTORE time, in RestoreParkWithdrawal's residency arm: the park-time answer is a snapshot, and route 2's park is RETAINED until the next packet's merge-time Forget ~150 ms later, so a prefix clean when the park was taken can be quiescing when the rollback runs. The InWorld/transient/clock half is still restored unconditionally — it is per-entity simulation state, not a claim on any landblock's collision generation. So the rollback re-admits nothing into ANY quiescing prefix, at the moment it actually writes residency rather than only as of when the park was taken, while leaving these parks non-restorable stranded the entity InWorld = false / clock suspended / not a spatial root with the only operation able to wake it destroyed by its own next accepted Position. A retirement park (ParkCollisionResidents) is still never restored, and now says so explicitly rather than relying on a parameter default | A remote — or the LOCAL PLAYER, which traverses the same shared core through route 2 — that teleports into a non-resident landblock and then STOPS MOVING stays visible at the destination without collision, where retail would hide it and re-show it on cell load — ACE stops broadcasting for a stationary entity, so no later packet corrects it. At 5-10 Hz the ordinary case is superseded within ~150 ms. Retire by making the park SURVIVE cancellation (issue #309), which is blocked on re-deciding the newer-Position-cancels-the-park invariant pinned by NewerPositionPickupAndParentEachCancelExactLostOperation and on teardown convergence. ACCEPTED AS A STANDING DIVERGENCE 2026-08-06 (user decision): #309 is DEFERRED, not planned. The retail-faithful survive-cancellation end state was implemented and reverted this round; landing it costs reversing that deliberate shipped invariant plus GameRuntime teardown convergence (stage 10, where surviving parks never converge on shutdown), and the observable requires a remote to teleport into a non-resident landblock AND then stop moving. This row is therefore the permanent record rather than a staging note — do not re-open the retirement without new evidence that the observable occurs in ordinary play, or unless the teardown-convergence work lands for another reason. The deferral does not cancel this row's connected check, which validates the SHIPPED rollback path, not the deferred fix. This row carries a user-observable change to shipped paths — restorableOnCancel: true sits in SubmitPreparedPlacementCore, the shared core behind every production placement — so it needs the two-client connected check written up in #309 — rewritten by this slice, not merely extended (round-4 D2 correction: this summary used to describe only the original three remote steps plus "route 2's corrections are unchanged", which no longer matches the issue's own body). #309 is now six steps run with ACDREAM_PROBE_PARK=1: the three original remote-park steps, plus a quiescing swept-NEIGHBOUR step and a quiescing-DESTINATION step that both exercise the LOCAL PLAYER through route 2 and both carry a stated [park]/[park-restore] confirmation signal (a quiescence window cannot be synchronised by hand, so without one the step passes while broken), plus the unchanged-ordinary-correction step. Step 5 also asks the tester to confirm the destination landblock's retirement still COMPLETES, and states correctly that the deliberately non-restored park does NOT recover on the next ordinary Position | CPhysicsObj::SetPositionInternal @0x00515BD0 (@0x00515C1D/@0x00515CDA/@0x00515CE2/@0x00515CF2/@0x00515CF7/@0x00515D07); CObjectMaint::GotoLostCell @0x00508210; CObjectMaint::InitObjCell @0x00508260 (@0x00508296); CPhysicsObj::reenter_visibility @0x00516250 |
| AP-137 | Filed 2026-08-04 (C4 route 4b-2); rewritten 2026-08-04 at the dual Opus review; REWRITTEN AGAIN 2026-08-04 (C4 route 4b-3) — the cell-less enqueue-vs-place delta this row existed to record is RETIRED, not merely re-scoped: the teleport arm now ports retail's teleport_hook verbatim and places unconditionally through the canonical Runtime placement owner, exactly like retail's this_1->cell == 0 @0x00516386 branch. What survives is the two acdream-only divergences retail has no state for at all. acdream can classify a remote's accepted Position into two states retail cannot reach, sharing ONE stated handler (RuntimeRemoteFarSnapPosition.ResolveArm's UnroutedCatchUp, AP-87's shared ApplyInterpolate catch-up) instead of a duplicated near/far block. The states: (a) no classification at all — RuntimeAcceptedPositionRouteRequests.TryBuild refuses to fabricate a local-player position, so ClassifyRemoteAcceptedPosition returns null for EVERY remote packet until the local movement controller exists (the login window) and whenever the canonical record has not claimed a local id; route 4b-3 adds two more null-producing reasons — the merge observed no PRIOR canonical record for this entity, so the classifier has no honest pre-merge cell to feed the teleport predicate and declines rather than fabricate one (D1); and the dormant initial-residence enqueue path (RuntimeEntityObjectLifetime.TryApplyPosition's EnqueueDormant return, reached BEFORE the method's own PreMergeCommittedCellId write), whose timestamps therefore always carry PreMergeCommittedCellId: null too — fix round 2026-08-04 (R8), unverified from static reading whether OnPosition reaches ClassifyRemoteAcceptedPosition for an enqueued packet at all, stated honestly rather than guessed; (b) RejectedAuthority/RejectedData — acdream validates wire authority and payload finiteness, retail validates neither. D1 — the visibility arm is deleted, not merely narrowed. Before 4b-3, LiveEntityRuntime.TryApplyPosition computed projectionRequiresTeleportHook as pre-merge FullCellId == 0 OR !IsSpatiallyProjected OR !IsSpatiallyVisible — a presentation predicate with NO retail analogue, since retail's MoveOrTeleport never reads visibility. That whole computation, the lifetime parameter, and the headless false argument are deleted; a not-visible remote's Position now classifies purely by distance/contact like any other, and visibility is presentation-only. D2 — the wire-airborne leftover shape. After the teleport/cell-less classification moves onto its own arm, a packet whose classification is null/RejectedAuthority/RejectedData AND whose wire contact bit is clear takes retail's return-0 shape: AP-135's two bookkeeping writes only (server-cell adopt, LastServerPos/LastServerPosTime), no body/queue/render write, no leash arm. This deletes the legacy player-arm fallback's entity-revert quirk (entity.SetPosition(rmState.Body.Position)) and unifies player and NPC remotes on one behaviour. R3 (retained from the prior rewrite) — RejectedData is APPLIED anyway when grounded. It is the one classification meaning "this payload failed validation" (ClassifyAcceptedPosition emits it for a ValidPosition failure and for a non-finite/negative derived player_distance), and UnroutedCatchUp hands the same payload to ApplyInterpolate. Not a regression — the legacy block did the same. Headless (contract item 6) is satisfied vacuously and that is stated, not implied: nothing in AcDream.Headless constructs RuntimeRemotePlacementDriveController (SessionPlayerComposition is the only construction site) and RuntimeLiveEntitySessionController.OnPositionUpdated returns early for every non-local GUID, so both the far snap and the teleport arm are graphical-host-only paths | src/AcDream.Runtime/Physics/RuntimeRemoteFarSnapPosition.cs (ResolveArm, RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp); src/AcDream.Runtime/Physics/RuntimeRemoteTeleportPosition.cs (OwnsTeleportPlacement, the retired predicate's replacement); applied at src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (ApplyRemoteContactRouting's teleport check and default arm, the D2 wire-airborne shape); headless statement on IRuntimeRemotePlacementServiceWindow | AP-87's own snap conditions (firstUp \|\| !willBeDrTicked \|\| bodyToTarget > 4 m) still PLACE an unplaced or badly-lagging body for the two survivors, so a remote keeps tracking the server through the login window and through a rejected packet | The two survivors are unaffected by 4b-3: a leftover-classified remote beyond 96 m that is already tracking catches up over a packet interval instead of snapping — invisible in practice at that range. If AP-87's 4 m backstop were ever weakened, this arm would become a silent-freeze path | CPhysicsObj::MoveOrTeleport 0x00516330 (@0x00516386 cell-0/teleport — now ported, @0x005163AF near, @0x005163C1-E8 far); CPhysicsObj::teleport_hook @0x00514ED0; RuntimeAcceptedPositionRouteRequests.TryBuild; GameRuntime.cs:288-290 (the no-fabricated-Vector3.Zero rule) |
| AP-138 | Filed 2026-08-04 (C4 route 4b-2, dual Opus review). Retail's remote far snap is unconditional and unrefusable: CPhysicsObj::MoveOrTeleport @0x005163D9 calls SetPositionSimple, discards its SetPositionError, and returns 1 @0x005163E8, so SmartBox::HandleReceivedPosition arms ConstrainTo @0x00454272 every time. acdream's far snap is a canonical Runtime placement that can decline for reasons retail has no analogue for, and this row records the complete residual. (1) An outcome that never reached the engine is a store_position; one that did is not. Retail's SetPositionInternal @0x00515BD0 has exactly two shapes and acdream now represents both (corrected 2026-08-04 at the delta review, which found the first version of this row asserting — wrongly — that no acdream non-commit outcome could represent the second). STORES, because the resolve never ran: Refused (the pre-flight declined the destination), Contention (another authority owns the operation, or the Setup/world-frame preparation is retryable), RejectedPreparation (RejectedAuthority/InvalidData — preparation refused before anything was submitted), and NotApplicable. For those ApplyAcceptedRemoteFarSnap writes the accepted destination pose to the canonical body, exactly as retail commits it on the no-transition branch — prepare_to_leave_visibility @0x00515CDA, store_position @0x00515CE2, GotoLostCell @0x00515CF2, return 0 @0x00515D07 — so the remote keeps tracking the server at 5-10 Hz, at the destination, with no resolved cell; retail would additionally have hidden it until cell load, which is AP-136's scope, not this one. DOES NOT STORE, because the resolve DID run and refused: RejectedByPlacement (PhysicsEngine.SetPosition returned a non-Ok error, acdream's port of retail's CheckPositionInternal == 0 @0x00515C85/@0x00515CD5 and curr_cell == 0 @0x00515C8F/@0x00515CB2, neither of which stores; or authority displaced after the engine ran, which includes the CommitCanonical-already-settled shape) and Deferred (Core parked, and ParkDeferred has ALREADY snapped the body to the parked result — the accepted destination for the pre-sweep park, the collision-settled spherePath.CurPos for the post-sweep one — which RestoreParkWithdrawal deliberately leaves alone). (2) A quiescence park a far snap can provoke is now restorable at the source, not refused by a pre-flight. Rewritten 2026-08-04 at the delta review. CanAttemptDestination (service window + Core's own IsCollisionPrefixQuiescing) reads ONE prefix, the destination's, and stays as an optimisation. It cannot be the correctness mechanism: Core's PlacementTouchesPrefix also matches the request's CurrentCellId (see the round-3 measurement below for what that arm actually names), and ResultTouchesPrefix scans every QueriedCellIds entry, a sweep footprint that spans NEIGHBOUR landblocks (CellTransit.AddOutsideCell re-derives the block id from the global lcoord and has no same-block filter) and does not EXIST until the sweep has run. Worse, the post-sweep check is result.IsSuccessful && TryGetBlockingQuiescence(result, …) and sits ahead of the restorable result.IsDeferred park, so a healthy about-to-COMMIT far snap near a seam was rewritten to DeferredCell and parked non-restorably. The fix is in SubmitPreparedPlacementCore: both quiescence parks are restorable, and ParkDeferred decides safety on the cell it will actually restore into — see AP-136 for the exact predicate and for why it does not re-open the retirement stall AP-136's blanket scoping was protecting against. On a FIRST submit the CurrentCellId half of PlacementTouchesPrefix is NOT the "source landblock a far snap is leaving": both accepted-Position callers committed the accepted wire cell to record.FullCellId before submitting (the graphical remote path through LiveEntityRuntime.RebucketLiveEntity in its shared prologue, route 2 through the merge), so that arm named the destination — measured 2026-08-04 at round 3. AMENDED 2026-08-05 at the C5b architecture review: the route-2 half of that measurement is now STALE and the two callers no longer agree. C5b made the merge withhold the wire cell (AD-60), and route 2 submits from TryExecuteAcceptedLocalPosition BEFORE the OnPosition prologue rebucket (W2) it returns ahead of — so on a route-2 FIRST submit PlacementTouchesPrefix's CurrentCellId arm now names the SOURCE landblock the local player is leaving, not the destination. The graphical REMOTE half is unchanged: its prologue rebucket still runs ahead of the far-snap submit. The consequence is confined to which prefix the quiescence pre-flight matches, which this row's own part (2) already established cannot be the correctness mechanism (SubmitPreparedPlacementCore's restorable parks are); it widens rather than narrows the set of prefixes a local force can be parked against. Scoped at round 4 (D5): that is a first-submit property only, and the arm is live rather than dead code. A RETAINED operation re-submits from its own cadence pump with no fresh merge (both drives re-read record.FullCellId at submit), and the surviving non-Position rebucket writer (the projection materializer — C4 route 4b-3 deleted the second shipped writer, RemoteTeleportController's rollback, and C4 route 7 D4 demoted the third, the equipped-child renderer, to a presentation-only move that no longer touches record.FullCellId) can rebucket it to a third landblock, so a retry can genuinely name a third landblock — which CanAttemptDestination's own doc already said and the two summaries elsewhere contradicted. (3) The leash is not armed through a superseded incarnation. Retail arms unconditionally on the nonzero return; acdream re-validates position ownership after the placement (the receipt is published synchronously and the projection sink can replace or delete the incarnation from inside it) and returns without arming if the owner moved. Both remote arms now run that check BEFORE their arming call — the player arm used to arm first, the NPC arm second, and one of the two mirror images had to be wrong | src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs (RuntimeRemotePlacementExecutionStatus + StoresAcceptedDestination, ApplyAcceptedRemoteFarSnap, StoreAcceptedDestinationPose, Advance's window-drop path, CanAttemptDestination, SubmitAndResolve); src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (ParkDeferred's post-snap restorable decision and the two SubmitPreparedPlacementCore quiescence parks); src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (both arms' re-validate-then-arm order) | The alternative to (1) is the shipped pre-review state: an emptied interpolation queue plus a stale body pose, i.e. a frozen remote that the next packet reproduces identically, since nothing about a refusal reason changes at packet cadence. That is strictly further from retail than either the deleted legacy block (which always tracked) or retail itself. The alternative tried and rejected in between — storing on EVERY non-commit outcome — is worse still in the other direction: it teleports the canonical body into a destination the engine's own sweep just refused, and overwrites a freshly settled pose (contact plane, step-down) whenever CommitCanonical landed and only the projection ownership was displaced. The alternative to (2) — keeping the pre-flight as the correctness mechanism and widening it — is structurally impossible, because the swept footprint half of Core's predicate does not exist until the sweep has run; the alternative of leaving the parks non-restorable strands the remote outright. The alternative to (3) — arming a leash on a host that is no longer the entity's canonical position owner — is a write through superseded state, the exact class the re-validation exists to prevent, and retail has no superseded-incarnation state for its unconditional arm to arbitrate | A remote whose destination this host cannot place into keeps moving and rendering but does not become collidable or cell-resident until a later packet commits — it can be walked through at range. Bounded by the 5-10 Hz packet stream and by how long the destination stays unpublished/quiescing. A remote whose destination the ENGINE refuses, or whose commit was displaced, keeps its last resolved pose for that packet instead of tracking — retail-exact, but it means a remote can look one packet stale near geometry it cannot be placed into. A quiescence park whose blocking prefix is a swept neighbour re-shows the entity immediately at the destination rather than hiding it until cell load (AP-136's own residual, now reachable through this path and through route 2's local-player corrections). C4 route 4b-3 adds a second producer of the visible-without-collision shape in item (1)'s storing list: the teleport arm inherits the identical store-and-stay-visible residual for the same reasons — a remote that teleports into a non-published landblock and stands still is visible but not collidable until a later packet commits. No new machinery; the retirement path is the same #309. A superseded incarnation's leash is left unarmed for one packet; the replacement incarnation arms its own on its next accepted Position. Retire (1) by making the far arm's failure path open retail's lost-cell registration instead of a bare pose write, which is issue #309's territory (the park must survive cancellation first) | CPhysicsObj::MoveOrTeleport 0x00516330 (@0x005163D9, @0x005163E8); CPhysicsObj::SetPositionSimple @0x005162B0 (flags 0x1012 @0x005162C4); CPhysicsObj::SetPositionInternal @0x00515BD0 (@0x00515C1D, @0x00515CDA, @0x00515CE2, @0x00515CF2, @0x00515CB2, @0x00515CD5, @0x00515D07); SmartBox::HandleReceivedPosition @0x00453FD0 (@0x00454254, @0x00454272) |
| AP-139 | Filed 2026-08-04 (Bug B). The remote tick clears its InterpolationManager queue on the LANDING edge — retail’s own set_on_walkable(1) transition, the same edge HitGround fires from. Retail has no such clear on a ground or contact edge: its only queue teardown outside a completed walk is PositionManager::StopInterpolating from CPhysicsObj::teleport_hook @0x00514EFD and the InterpolationManager::UseTime @0x00555f20 stall/autonomy blips. The clear is carried over unchanged in intent from the deleted hand-rolled landing block (#184, 2026-07-07), which hung it on a hand-rolled Airborne && IsOnGround && Velocity.Z <= 0 test that also fired on a steep (non-walkable) contact; Bug B re-derived the edge without changing the behaviour it was written for | src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs (the SetPositionInternal commit block); the packet-side twin lives in src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (OnPosition, the player-remote landing snap) | A contact-free arc never enqueues — route 4a's airborne no-op writes nothing at all — so anything still queued when the body lands is a pre-arc waypoint, and the first catch-up after touchdown would otherwise walk the body backward toward it | A remote that regains contact while a legitimately fresh waypoint is queued loses one correction and re-acquires it on the next accepted Position (~5-10 Hz). A body that repeatedly loses and regains contact (a bounce chain down a rough face) clears the queue once per bounce. Retire when the arc itself feeds the queue, at which point the pre-arc waypoints are no longer stale | CPhysicsObj::teleport_hook @ 0x00514ED0 (StopInterpolating @0x00514EFD); InterpolationManager::UseTime @ 0x00555f20; CPhysicsObj::SetPositionInternal @ 0x00515330 |
4. Temporary stopgap (TS) — 36 active rows (TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable ObjectInfoState bits via the new ClientObjectTable-backed EntityCollisionFlagsExt.ResolveMoverPvpState — narrative corrected 2026-08-03 (#297): "real" only became true at #297. Until then the bits existed but the source PublicWeenieBitfield was frozen at CreateObject, so every one of those sites read a stale value for the whole session. The site enumeration is also incomplete: RuntimeSetPositionMoverPreparation.cs:183-188 is a SEVENTH mover-flags site that decodes record.Snapshot.ObjectDescriptionFlags directly rather than calling ResolveMoverPvpState, and it also derives ObjectInfoState.IsPlayer from the PWD bit, contradicting EntityCollisionFlags.cs:119-123's claim that every site uses a GUID-prefix heuristic. See AP-134. — and PlayerWeenie.JumpStaminaCost's pk parameter reads the real PlayerKillerStatus/LastPkAttackTimestamp pair against a 20-second window instead of a hardcoded false; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since ResolveMoverPvpState and the PK-timer predicate both resolve to a no-op for PublicWeenieBitfield absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (CPhysicsObj::transition 0x00512dc0 → SPHEREPATH::init_sphere 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (CPartArray::GetStepUpHeight/GetStepDownHeight, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled SphereCollision replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — frames_stationary_fall accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
RETIRED 2026-07-30 (Campaign P Slice P2) — the row was stale, not the code. The cited :1254 line is unrelated stepping-loop code; the file moved substantially since the row was written. Retail's EdgeSlide → PrecipiceSlide / CliffSlide chain is already a real, tested port: SpherePath.PrecipiceSlide (TransitionTypes.cs:943-970, retail SPHEREPATH::precipice_slide pc:274316), Transition.CliffSlide (:2080-2164, retail CTransition::cliff_slide pc:272397, return-value mapping verified against acclient.h:6100-6108), and Transition.EdgeSlideAfterStepDownFailed (:1907-2078, mirrors CTransition::edge_slide pc:273001-273090). The one real gap (back-probe fallback skipping retail's walkable_check_pos/localspace_sphere recache, pc:274318-274326) needed no code change: acdream's WalkableVertices/GlobalSphere are populated in unified world space at assignment time (SetWalkable/SetWalkableTransformed, SetCheckPos/RestoreCheckPos), so both operands BSPQuery.FindCrossedEdge compares are already commensurable — retail's per-cell local-frame reprojection is a no-op correction here. Documented in-code at the back-probe site and pinned by EdgeSlideBackProbePrecipiceSlideTests. The chain's two acdream-only compensating branches (CliffSlide's three-source reference-normal fallback; the walkable-steepness reroute to CliffSlide before PrecipiceSlide) are real, non-retail additions — filed as AD-53 / AD-54 rather than folded into this row. |
src/AcDream.Core/Physics/TransitionTypes.cs (SpherePath.PrecipiceSlide, Transition.CliffSlide, Transition.EdgeSlideAfterStepDownFailed); tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs |
— | — | SPHEREPATH::precipice_slide pc:274316 (0050cc80); CTransition::cliff_slide pc:272397 (0050a6d0); CTransition::edge_slide pc:273001-273090 (0050b3d0); SPHEREPATH::get_walkable_pos/cache_localspace_sphere/set_walkable_check_pos pc:274318-274326 (0050a8f0/0050c9d0/00509ce0); docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §2, §6 Step 1 |
|
RETIRED 2026-07-31 (Campaign P Slice 2B; corrective acceptance complete). The graph and prepared-flat Path-6 implementations now match retail's exact two-sphere split: every primary/foot polygon hit calls SetCollide, sets WalkableAllowance=LandingZ, and returns Adjusted; only a secondary/head hit writes CollisionNormal and returns Collided. The steep tangent shortcut and every BSP-layer SetSlidingNormal write are deleted. Exact site tests pin all changed and preserved fields plus raw-bit graph/flat parity. A corrective 90-tick already-airborne, zero-root-motion Core suite executes acceleration, body integration, transition resolution, exact commit, and handle_all_collisions while retaining every behavior-bearing collision/body field used by that specialized quantum. Vertical, inward, tangential, downhill, and positive-Z uphill-jump traces match graph/flat by raw bits, reject penetration/fixed points/second launches, and pin exact terminal velocity, contact, sliding, and contact-plane state. The older resolver-only capture is explicitly historical and restored to its three-second bound. |
src/AcDream.Core/Physics/BSPQuery.cs; src/AcDream.Core/Physics/FlatBspQuery.cs; tests/AcDream.Core.Tests/Physics/Ts4Path6ConformanceTests.cs; tests/AcDream.Core.Tests/Physics/Ts4ProductionQuantumConformanceTests.cs; tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs |
— | — | BSPTREE::find_collisions 0x0053A440: head 0x0053A793..0x0053A7A4, foot 0x0053A7B3..0x0053A7DC; research §10 |
|
| TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | src/AcDream.Core/World/WeatherState.cs:200 |
Decomp research verified the sky loop never reads DefaultPesObjectId; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found |
Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) |
| TS-7 | SkyObject weather_enabled gate not honored — weather-flagged sky objects (bit 0x04) always instantiate |
src/AcDream.Core/World/SkyDescLoader.cs:50 |
No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire | Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them | GameSky::MakeObject 0x00506ee0, guard at decomp:268630 |
RETIRED 2026-07-31 (#268 stat-chain closeout). EnchantmentWireReader parses the complete 0x02C2 payload and GameEventWiring publishes its StatMod type/key/value and bucket through the same ActiveEnchantmentRecord used at login. An end-to-end dispatch test proves a mid-session skill modifier changes LocalPlayerState.GetEffectiveSkill immediately. |
src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs; src/AcDream.Core.Net/GameEventWiring.cs; tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs |
— | — | CEnchantmentRegistry::EnchantAttribute @ 0x00594570; CEnchantmentRegistry::EnchantSkill @ 0x005947B0; holtburger messages/magic/types.rs |
|
| TS-9 | MP3 (0x55) and MS-ADPCM (0x02) waves undecoded — affected sounds skipped; retail decoded both via winmm ACM | src/AcDream.Core/Audio/WaveDecoder.cs:33 |
Managed decoder (NAudio or similar) deferred; PCM covers the vast majority of ~3500 waves | Any MP3 (common for music-ish clips) or ADPCM cue plays as silence where retail plays it | winmm ACM path (r05 §2.1) |
| TS-14 | Setup Flatten ignores ParentIndex part hierarchy (treats every placement as root-local); still in production use (GameWindow hydration, SkyRenderer) |
src/AcDream.Core/Meshing/SetupMesh.cs:15 |
Most Setups are flat single-level rigs where root-local equals composed; hierarchical composition deferred ("Phase 3") | Any Setup with genuinely nested parts renders them at wrong offsets — mis-assembled multi-part objects in the Flatten paths | retail Setup ParentIndex chain composition |
| TS-15 | No distance-driven degrade (LOD): always close-detail slot 0; plus the #47 static Degrades[0] swap for 34-part humanoids only (structural sentinel detector) |
src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs:57 (+ src/AcDream.App/Rendering/GameWindow.cs:2608) |
LOD plumbing doesn't exist; slot 0 is correct for player + nearby NPCs; #47 closed the visible low-detail-arms bug without porting UpdateViewerDistance | Distant objects render max-detail (perf + wrong visuals where far meshes intentionally differ/hide parts); a future 34-part non-humanoid matching the sentinel gets the wrong mesh swap | CPhysicsPart::UpdateViewerDistance 0x0050E030; ::Draw 0x0050D7A0; ::LoadGfxObjArray 0x0050DCF0 |
| TS-17 | AttackConditions suffix always empty in combat chat — formatting ported, wire bitflag not plumbed (Phase I.7 follow-up) | src/AcDream.Core/Chat/CombatChatTranslator.cs:233 |
Only the wire plumbing is missing; the holtburger-ported formatter is ready | Combat log omits "[Sneak Attack]"-style suffixes retail displays — hidden combat-mechanic feedback | holtburger chat.rs:588-595 |
| TS-18 | LandCell.BuildingCellId (CSortCell building bridge) declared but never populated — always null in Stage 1 |
src/AcDream.Core/World/Cells/LandCell.cs:19 |
Cell graph shipped in stages; population is explicitly membership Stage 2 (the outdoor→indoor entry path the physics digest flags as unvalidated) | Cell-graph paths that should discover a building's EnvCells from the outdoor cell silently find nothing — the doorway-entry bug class | CSortCell (acclient.h:31880) |
| TS-19 | Legacy non-retail ChaseCamera (invented pitch/distance, K-fix12 airborne Z-pin) retained behind ACDREAM_RETAIL_CHASE=0 / DebugPanel toggle; both update every frame |
src/AcDream.App/Rendering/ChaseCamera.cs:49 |
Diagnostic before/after comparison path, "pending the follow-up deletion commit" | When toggled on, the eye diverges from retail's spring-arm — and the render roots at the VIEWER cell, so a non-retail eye changes the render root near doorways, masking or manufacturing flap symptoms during debugging | CameraManager::UpdateCamera (retail path in RetailChaseCamera.cs) |
RETIRED AS A FALSE ATTRIBUTION 2026-07-16 — CGfxObj::InitLoad passes the complete polygon array to D3DPolyRender::ConstructMesh; ordinary GfxObj rendering does not filter it through DrawingBSP. Building DrawingBSP traversal discovers and orders portal apertures after RemoveNonPortalNodes; it is not a global visible-polygon selector. The alleged building-shell "orphans" are DrawingBSPNode.Portals, omitted by the old diagnostic collector; the corrected node-polygons ∪ portal-polygons audit finds no true orphans. Applying the proposed filter would repeat the door disappearance regression from e46d3d9. |
docs/research/2026-06-11-holistic-map/wf1-gfxobj-draw.md; docs/research/2026-06-11-holistic-map/wf1-building-shells.md; tests/AcDream.Core.Tests/Rendering/Wb/Issue113DoorVanishDiagnosticTests.cs |
— | — | CGfxObj::InitLoad @ 0x005346B0; D3DPolyRender::ConstructMesh @ 0x0059DFA0; BSPTREE::build_draw_portals_only @ 0x00539860 |
|
| TS-21 | Default run/jump skills 200/300 tuned to feel until the first PlayerDescription lands (the stale "we don't parse yet" comment was FIXED in R4-V5; K-fix7 parses PD → SetCharacterSkills) | src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:311 |
Defaults rule only pre-PD or on PD parse failure; jump bumped 200→300 on user complaint (3.01 m max felt too low) | Any window with defaults live predicts run/jump speeds the server disagrees with — observer rubber-banding, local snap-backs | retail height = (skill/(skill+1300))×22.2 + 0.05 |
| TS-27 | NARROWED 2026-07-29 (Campaign N Slice N1) — OUTBOUND is ported: sent-packet cache + header-rebuilt resend on server RequestRetransmit, ids[0] implicit ack, wrap-safe watermark prune (src/AcDream.Core.Net/Transport/). Residual: INBOUND loss is still fatal — no sequence-aligned inbound ISAAC discipline, no client NAK emission, no RejectRetransmit consumption (Campaign N slices N2/N4) |
src/AcDream.Core.Net/WorldSession.cs (ProcessDatagram inbound path); docs/plans/2026-07-29-network-transport-campaign.md §2.2/§2.3 |
Campaign N executes the port one direction per slice; the N0 ACE double grades each slice before the next lands | One lost S2C packet still shifts the inbound keystream permanently — every later encrypted packet fails checksum and the session goes silently deaf until timeout | SharedNet::ProcessPacket @ 0x00544790; ReceiverData::AddNakked @ 0x00549240; SharedNet::EnqueueNaks @ 0x00543BD0 |
| TS-28 | NARROWED 2026-08-03 — F751 teleports resend LoginComplete only after the DAT-authored portal-space viewport and final world fade finish. Initial login no longer acknowledges raw PlayerCreate receipt: graphical and prepared headless hosts send exactly once after canonical local-player first placement; content-less headless sends after its accepted direct Create because it has no placement conductor. Residual: initial login still does not enter the full portal-space presentation. | src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs; src/AcDream.App/Net/GraphicalSessionEventRoute.cs; src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs; src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs; src/AcDream.Core.Net/WorldSession.cs |
Initial placement is now the shared readiness contract that releases ACE's intentional Hidden/pink-bubble state without racing presentation. The content-less direct host uses its only truthful admission edge. | The persistent login materialization haze is fixed and server updates no longer unlock before canonical placement. The remaining difference is presentation-only: initial login skips retail's wormhole sequence. | gmSmartBoxUI::UseTime @ 0x004D6E30; retail post-EnterWorld flow; holtburger client/messages.rs:391-422 |
| TS-29 | Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays | src/AcDream.App/Audio/OpenAlAudioEngine.cs:331 |
Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately | Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) | retail MIDI + ambient system (r05) |
| TS-30 | Chat DAT elements 0x10000522–0x10000525 render but have no controller semantics; the older claim that they are numbered in-window filter tabs is unproven |
src/AcDream.App/UI/Layout/ChatWindowController.cs |
Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior | The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive | gmMainChatUI @ 0x004CCCC0..0x004CE2A0; correction in docs/research/2026-07-10-retail-panel-behavior-pseudocode.md |
| TS-31 | NARROWED 2026-07-13 — /squelch, /unsquelch, /filter, /unfilter, and /messagetypes send the exact modification events and consume the authoritative retail SquelchDB; incoming ChatLog lines are not yet filtered through that database, and clickable name-tag social actions remain absent |
src/AcDream.Core/Social/SquelchState.cs; src/AcDream.Core.Net/Messages/SocialStateMessages.cs; src/AcDream.App/UI/ClientCommandController.cs; src/AcDream.Core/Chat/ChatLog.cs |
Command/state transport is complete; enforcement belongs at the shared inbound-chat boundary so both backends remain identical | A squelch appears in the list and persists server-side but matching incoming lines can still render; contextual name actions remain unavailable | SquelchDB::UnPack @ 0x006B1900; ChatFilter::IsSquelched; retail right-click player name → Squelch menu |
| TS-32 | ClientObjectTable has no pre-queue for a child CreateObject that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their ContainerId links a not-yet-known container. Retail's null_object_table + null_weenie_object_table hold unresolvable objects until the parent arrives |
src/AcDream.Core/Items/ClientObjectTable.cs (Ingest) |
PD↔CreateObject ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ |
A container's child object arriving before the container is ingested as a root item — it won't appear in GetContents until the next RecordMembership or a move event corrects the parent link |
CObjectMaint::null_object_table / null_weenie_object_table (acclient.h / named-retail pc) |
| TS-33 | NARROWED 2026-07-15 — full AP tracker semantics are ported: MTS stamps time only; AP stamps complete cell-local Position + contact plane + time; ShouldSendPositionEvent compares cell/contact inside the interval and the complete Frame including orientation afterward. Residual: acdream's single update path snapshots the AP predicate, emits a same-update MTS first when input changed, then AP. Retail proves UseTime performs Should→AP, but MTS originates in separate input callbacks; their relative same-tick callback/wire order is not yet traced |
src/AcDream.Runtime/Gameplay/LocalPlayerOutboundController.cs (pre/post network slots); src/AcDream.Runtime/Gameplay/PlayerMovementController.cs (ported tracker) |
Preserve the pre-existing acdream wire order until a focused retail packet/breakpoint trace establishes input callback versus UseTime; do not infer it from UseTime alone |
In the rare update where both packets are due, ACE may observe their position timestamps/action sequences in the opposite order from retail, shifting only that correction tick; stationary target-facing is live-gated because full-frame orientation now publishes | CommandInterpreter::UseTime 0x006B3BF0; SendMovementEvent 0x006B4680; SendPositionEvent 0x006B4770; ShouldSendPositionEvent 0x006B45E0; Frame::is_equal 0x00424C30 |
| TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had contact_allows_move (0x00528240) arm StandingLongJump as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; ChargeJump (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule |
src/AcDream.Core/Physics/MotionInterpreter.cs (contact_allows_move, ChargeJump) |
N/A — retired | N/A — retired | CMotionInterp::charge_jump 0x005281c0 @305448 |
| TS-38 | MotionInterpreter.Initted defaults to true in both constructors, not retail's false — retail's CMotionInterp is never observed pre-enter_default_state (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step |
src/AcDream.Core/Physics/MotionInterpreter.cs (Initted property + both constructors) |
Defaulting true is the C# equivalent of "the constructor already did what enter_default_state would have done to this flag" — EnterDefaultState() remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, LeaveGround tail) when a caller wants them |
None observed: no code path needs apply_current_movement/ReportExhaustion to no-op before an explicit EnterDefaultState() call, since nothing constructs a MotionInterpreter and defers initialization today. If a future caller DOES need staged construction (build now, EnterDefaultState() later), it must explicitly set Initted = false first |
CMotionInterp::enter_default_state 0x00528c80 @306124 sets initted = 1; retire if/when construction is staged through EnterDefaultState() uniformly |
RETIRED 2026-07-07 (remote-creature de-overlap #184) — the SERVERVEL synth-velocity body-drive (Body.Velocity = ServerVelocity / get_state_velocity() leg) is DELETED. Grounded NPC remotes now translate by the retail interp CATCH-UP (RemoteMotionCombiner.ComputeOffset → InterpolationManager::adjust_offset toward the MoveOrTeleport-queued server waypoint) and MovementManager::UseTime (TickRemoteMoveTo) runs UNCONDITIONALLY per tick — the retail UpdateObjectInternal shape (no wire-velocity leg-driver). The de-overlap sweep resolves the catch-up movement; the resolved position is written back into the SHADOW (AP-86) so it persists. Residual: the non-retail anim-cycle stale-stop heuristic (ApplyServerControlledVelocityCycle(Zero) on a >0.6 s velocity-staleness timer) is kept as ANIM-only and stays covered by AP-80; it no longer drives the body. |
src/AcDream.App/Physics/RemotePhysicsUpdater.cs (grounded NPC branch) |
— | — | CPhysicsObj::UpdateObjectInternal 0x005156b0 (MovementManager::UseTime @0x00515998, unconditional); MoveOrTeleport 0x00516330; InterpolationManager::adjust_offset 0x00555d30 |
|
| TS-44 | NPC UpdatePosition enqueue is suppressed while StickyManager is armed (PositionManager.GetStickyObjectId() != 0). Position and complete orientation otherwise share the ported InterpolateTo → Position::subtract2 → PositionManager::adjust_offset Frame, so the former orientation hard-snap residual is retired. Retail would still enqueue the server Position and let Sticky overwrite that Frame each tick; acdream retains the gate so no queued waypoint survives the stick. Scope re-affirmed by C4 route 4a (2026-08-03): the gate stays NPC-only and stays in the App caller. The shared Runtime seam route 4a introduced is deliberately indifferent to the sticky lease, because folding the check into it would have silently extended TS-44 to player remotes — which are stickable but have never had this suppression, and whose far branch would still not have it |
NPC-only caller gate, src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs (snapSuppressedByStick) |
Avoids replaying an old ACE waypoint immediately after a stick lease ends; all live during-stick pose ownership is now otherwise retail-shaped | After unstick the body waits for the next UP instead of consuming the latest waypoint already in the queue; at low packet cadence this can pause correction for one update interval | PositionManager::adjust_offset 0x00555190; CPhysicsObj::MoveOrTeleport 0x00516330; retire by allowing enqueue while Sticky overwrites the shared complete Frame |
RETIRED 2026-07-07 — the hand-rolled SphereCollision (forced combinedR+1 cm radial de-penetration + leaked SetSlidingNormal + always-Slid, head-sphere ignored) is REPLACED by the faithful CSphere::intersects_sphere family port (branch dispatcher 0x00537A80 + step_sphere_up/slide_sphere/land_on_sphere/collide_with_point/step_sphere_down), routing the grounded slide through the shared crease SlideSphere (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. SphereCollisionFamilyTests (slide-around, block, ethereal) + docs/research/2026-07-07-csphere-collision-family-pseudocode.md. Residual AP-91 (PerfectClip TOI dead in M1.5). |
— | — | — | CSphere::intersects_sphere 0x00537A80 (pc:321678) |
|
| TS-47 | NARROWED 2026-07-13 — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs; src/AcDream.App/UI/ClientCommandController.cs; src/AcDream.Core.Net/Messages/ClientCommandRequests.cs |
The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | ClientCommunicationSystem command-table construction around 0x00581A40..0x005850A0; docs/research/2026-07-13-retail-client-command-routing-pseudocode.md; docs/research/2026-07-13-retail-client-command-families-pseudocode.md |
| TS-48 | Dragging an item onto another player honors the authoritative DragItemOnPlayerOpensSecureTrade option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through GiveObjectRequest 0x00CD works when the option is disabled; NPC giving is complete. |
src/AcDream.App/UI/ItemInteractionController.cs (PlaceIn3D, PolicyActionMessage); src/AcDream.Core/Items/ItemInteractionPolicy.cs |
The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | ItemHolder::AttemptPlaceIn3D @ 0x00588600; PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0; ClientTradeSystem; docs/research/2026-07-13-retail-give-item-pseudocode.md |
| TS-49 | Hidden-object availability is bridged through TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld) because acdream has not ported retail's DetectionManager. Retail CObjCell::hide_object sends LeftDetection to detection voyeurs; acdream instead withholds Hidden hosts from ordinary GetObjectA relationship creation and uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. |
src/AcDream.App/Physics/EntityPhysicsHost.cs (NotifyHidden); src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs (ResolvePhysicsHost); src/AcDream.Core/Physics/Motion/TargetManager.cs (NotifyVoyeurOfEventAndClear) |
The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. | Plugins or future systems listening specifically for retail detection enter/leave events receive no LeftDetection; only movement/sticky target consumers observe the equivalent availability loss. |
CObjCell::hide_object @ 0x0052BE30; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through LeftDetection |
| TS-50 | AnimationDone executes semantically at each owner's retail CPhysicsObj::process_hooks boundary, but all other animation hooks are retained in AnimationHookFrameQueue until final root/part/equipped-child pose publication. Retail executes the complete hook stream before transition and the Target/Movement/PartArray/Position manager tail because its current CPartArray pose already exists in-place. Static owners correctly reach process_hooks only after their root, parts, and children are current. |
src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs; src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs; shared frame drain in src/AcDream.App/Update/LiveObjectFrameController.cs (LiveEffectFrameController) |
The modern renderer publishes immutable effect-pose snapshots after all root/child composition; deferred visual sinks avoid attaching particles/lights/audio to the previous pose. Semantic AnimationDone is split out and exact, so motion completion and manager behavior are not delayed. Pose-owner lifetime tokens prevent deferred hooks from crossing delete/local-ID reuse. |
A non-AnimationDone hook with same-quantum semantic consequences (notably CallPES, default-script chaining, audio/particle creation relative to a transition) runs later than retail and can observe post-tail state or start one render frame late. |
CPhysicsObj::process_hooks @ 0x00511550; CPhysicsObj::UpdatePositionInternal @ 0x00512C30; CPhysicsObj::animate_static_object @ 0x00513DF0; retire by publishing the current per-object/child pose before hook routing or splitting semantic and presentation sinks without changing authored hook order |
| TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each ordinary object's ParticleManager then ScriptManager inside every admitted UpdateObjectInternal quantum; animate_static_object instead advances that static owner's ScriptManager then ParticleManager and only then process_hooks, using its whole admitted elapsed interval. acdream's shared tail is Particle → Script after static hook capture. |
src/AcDream.App/Update/LiveObjectFrameController.cs (LiveObjectFrameController + LiveEffectFrameController shared _particles.Tick / _scripts.Tick tail); src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs |
The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and ordinary manager order faithful without pretending the shared tails have per-owner timing or static-tail order. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static hooks can route before their script/particle managers and static default scripts/particles use render elapsed in Particle → Script order rather than animate_static_object elapsed/discard and Script → Particle → hooks timing. |
CPhysicsObj::UpdateObjectInternal @ 0x005156B0; CPhysicsObj::animate_static_object @ 0x00513DF0; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum/order |
| TS-52 | The terrain shader applies retail-authored base/overlay/road TerrainTex.TexTiling but omits the separate Environment Detail Textures pass and its viewer-distance fade (#226). |
src/AcDream.App/Rendering/TerrainAtlas.cs; src/AcDream.App/Rendering/TerrainModernRenderer.cs; src/AcDream.App/Rendering/Shaders/terrain_modern.frag |
bb5acab9 fixed the user-visible stretched/blurry regression by porting the distinct base-tiling contract. An earlier experimental detail array darkened the whole ground because its source/neutral blend contract was wrong, so it was correctly reverted rather than guessed into production. |
With retail's Environment Detail Textures preference enabled, close terrain lacks the extra high-frequency modulation/fade even though authored base texture scale is correct. | LScape::GenerateDetailSurfaces / SetDetailTexturing @ 0x00506B40; ACRender::landPolyDraw @ 0x006B6450..0x006B6525; issue #226 |
| TS-53 | acdream advances retained UI time on the draw seam and local teleport/UI-camera presentation after its SmartBox-shaped object → inbound network → CommandInterpreter barrier. Retail Client::UseTime calls UIElementManager::UseTime first, whose global time message reaches gmSmartBoxUI::UseTime, and publishes player-camera work from the physics/player callback rather than one post-network camera tail. Slices 6–7 preserve the accepted host order as ownership-only extractions. |
src/AcDream.App/Update/UpdateFrameOrchestrator.cs (post-live-frame teleport/camera phases); src/AcDream.App/Rendering/PrivatePresentationRenderer.cs (RetainedGameplayUiFrame.Render); docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md; docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md |
Current retained UI, portal transit, reveal, camera, and connected movement traces are accepted; changing cross-subsystem host order while extracting ownership would combine a behavior change with the structural cutover. | Retained UI, teleport, and camera presentation can observe same-frame object/inbound/player state one host update earlier or later than retail at transition boundaries; a future exact host-order port must prove UI, input, reveal, and camera consequences together. | Client::UseTime @ 0x00411C40; UIElementManager::UseTime; gmSmartBoxUI::UseTime @ 0x004D6E30; CPhysics::UseTime @ 0x00509950; retire only with a focused host-order port and connected portal/camera comparison |
| TS-54 | AdminEnvirons sound values 0x65..0x7B are diagnosed by retail enum name but do not play audio. Retail checks that the local player physics object and UI sound table exist, then calls SoundManager::PlaySoundFromCenter(Sound_UI_*, table) for Roar through Thunder6. |
src/AcDream.App/World/WorldEnvironmentController.cs (ApplyAdminEnvirons) |
The current audio owner has no typed retail UI-sound-table binding; logging preserves the inbound evidence without inventing wave DIDs or routing the sounds through positional world audio. | Server-authored ambience/thunder packets are silent in acdream while retail plays the centered UI sound. | CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20 (0x0055E07F..0x0055E2C7); SoundManager::PlaySoundFromCenter @ 0x00550950 |
| TS-55 | AdminEnvirons fog values remain a color-only WeatherSystem.Override approximation. Retail values 1..5 install authored ambient color/level plus fog color/max; value 6 also forces transition/min/max and blanks radar; Clear restores all override fields and radar; 0x270F installs a separate authored override. |
src/AcDream.App/World/WorldEnvironmentController.cs (ApplyAdminEnvirons); src/AcDream.Core/World/WeatherState.cs (EnvironOverrideColor) |
Preserves the already accepted enum bridge while Slice 8 moves ownership; porting the complete environment/radar presentation is a separate behavior change requiring focused visual gates. | Forced-fog hue, density, scene ambient, and radar blanking differ from retail; 0x270F is ignored. |
CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20 (0x0055DE2B..0x0055E344) |
| TS-57 | No outbound RejectRetransmit: a server NAK for an id no longer in the sent-packet cache is dropped silently (counted in TransportStats.UncachedNakIds); retail answers RejectRetransmit @ FlowQueue so the server abandons the id immediately |
src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs (OnRetransmitRequest) |
ACE parses RejectRetransmit and no-ops it (NetworkSession.cs — no handler), and the standalone unsequenced form would trip ACE's watermark hole (campaign doc §3 row 3: any cleartext non-ack packet with a live sequence advances the watermark and skips a real packet forever) |
Against a server that DOES honor RejectRetransmit, an uncached NAKed id keeps being re-requested until that server's own NAK give-up logic fires — never against ACE, which forgets the id when its next cumulative ack passes it | RecipientData::ProcessNaks @ 0x00547010; ACE NetworkSession.cs:299-304 (server-side emit), no client-consume handler |
| TS-58 | No outbound TimeSync/EchoRequest keepalive (retail sends both every 6 half-second intervals, ~3 s). The 2.0 s cumulative AckSequence is the sole idle keepalive; it refreshes ACE's 60 s timeout, which is the only server-side consumer. | src/AcDream.Core.Net/Transport/TransportClock.cs; src/AcDream.Core.Net/Transport/AckNakScheduler.cs |
Standalone unsequenced TimeSync/Echo packets trip ACE's exactly-AckSequence watermark rule (NetworkSession.cs:474-476) and are only ACE-safe piggybacked, which needs retail's CoalesceData (AP-125). The ack keepalive covers the timeout; no transport RTT sample is lost that LinkStatus' app-level ping does not already provide. | No transport-level RTT/latency sample; a future server gating on TimeSync cadence would see silence; ACE's speedhack echo checks never engage. | ClientFlowQueue::IncrementLocalInterval @ 0x00547F10; ACE NetworkSession.cs:474-476, Session.cs:101-102 |
| TS-59 | No outbound Flow report (retail emits a 6-byte bytes-received+interval header whenever the inbound remote interval advances). | src/AcDream.Core.Net/Transport/ReliableTransport.cs (interval clock present; no Flow emission) |
ACE parses the Flow header and has no handler (PacketHeaderOptional.cs:117-124); the standalone unsequenced form would trip the watermark hole. Retail itself never consumes inbound Flow and has no throttle (WireRoomLeft is a folded return-1). |
A future server that rate-adapts on client Flow reports sees nothing. | SharedNet::ProcessNewRemoteInterval @ 0x00543A80; ClientFlowQueue::WireRoomLeft @ 0x0052C1C0 (folded) |
| TS-60 | No 140 s dead-link declaration or referral auto-reconnect in the transport; a silent server is only visible through LinkStatusSnapshot.SecondsSinceLastPacket (presentational). |
src/AcDream.Core.Net/Transport/ReliableTransport.cs; src/AcDream.Core.Net/WorldSession.cs (BuildLinkStatus) |
The input (seconds since last inbound) is already exposed; session lifecycle/reconnect is Runtime's ownership domain and deserves its own campaign rather than a transport-embedded side effect. Every ACE transport death is silence, so nothing server-side depends on the client reacting at 140 s. | A dead link idles until the user acts; no automatic recall/referral reconnect where retail would attempt one. | ClientNet::ProcessConnection @ 0x00545450 tail (the two 140.0 literals) |
| TS-61 | A UDP send failure burns the reliable sequence and its ISAAC word (the encode commits before _net.Send); retail keeps the sealed packet at the queue head and retries with the same key. |
src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs (SendGameMessage) |
A connectionless-socket SendTo failure is effectively unreachable in practice (no route/ICMP errors surface on later receives, not sends, on Windows UDP); recovering it faithfully needs a full outbound packet queue. The N1 review accepted the exposure explicitly. |
One SocketException on send would desync the outbound cipher permanently (session death; observable as [net-out-EX] followed by silence). |
FlowQueue::TransmitNewPackets @ 0x00547C2C (retry-from-head) |
| TS-56 | Chase-camera mouse input retains acdream's invented post-filter yaw/pitch scalars (0.004/0.003 radians per count), and held-key pitch/zoom retain their non-retail integration shapes. Retail mouse look passes FilterMouseInput(delta) × configured sensitivity × 1/15 as the replacement scale to CameraSet::Rotate, which then applies the shared 8° angle; retail held pitch uses the same angle and zoom scales the viewer offset multiplicatively. |
src/AcDream.App/Input/CameraPointerInputController.cs; src/AcDream.App/Input/MouseLookController.cs; src/AcDream.App/Rendering/CameraFrameController.cs |
Slice 8 is behavior-preserving ownership work. The named-retail audit proves the mismatch but has not yet extracted the configured mouse-sensitivity default or the exact caller flags needed for a complete feel port; changing only one scalar here would create a mixed input model. | RMB/MMB orbit, held pitch, and zoom can feel slower, faster, or differently accelerated than retail even though callback ordering and filtering are correct. | CameraSet::Rotate @ 0x00458310; CameraSet::MouseLookHandler call at 0x00458EF9; CameraSet::Raise @ 0x00457B00; CameraSet::Closer @ 0x004586D0; docs/research/2026-06-11-holistic-map/wf2-camera-viewer.md |
| TS-62 | Filed 2026-08-02 (physics campaign, continuation-executor slice). NO Position route in the dormant executor runs a live ConstrainTo binding - including the SetPosition/SetPositionSimple routes. RuntimeAuthoritativePositionRoute.ConstrainPhase (None/Before/After) is classified for EVERY accepted route and recorded into the execution trace, but the constrain-before-vs-after distinction exists purely as classified metadata pending a live binding at the production cutover. |
src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs (ApplyPositionAction/BuildPositionTrace); RuntimeAuthoritativePositionRouteClassifier.cs (ConstrainPhase) |
Host-cutover work with no Runtime-side owner to bind to yet; the canonical snapshot's Position IS refreshed on every accepted route, so the fact is retained - only the live constrain/smoothing behavior is deferred. The trace carries the exact phase a host must bind. | Until a host wires it, ANY Position continuation applies its raw pose with no constrain-distance clamp or smoothing - a visible pop instead of retail's constrained correction, on exactly the entities created while an authored placement was in flight. | SmartBox::HandleReceivedPosition 0x00453FD0, the three ConstrainTo sites (~93007 remote-after, ~93024 teleport-after, ~93041 local-ordinary-before) |
| TS-63 | Filed 2026-08-02 (physics campaign, continuation-executor slice). ApplyResidentCellCleanup's three branches: (1) claimed-cell + celless + NOT under lost-cell/deferred ownership - retail's genuine AddObjectToBeDestroyed case - has no safe Runtime destruction owner yet, so the executor performs a typed ABANDONMENT (RejectedAuthority) instead of destroying; (2) claimed + celless + deferred returns DeferredUnderLostCellOwnership - retail's destruction bookkeeping for this exact entity is already owned by the lost-cell/deferred SetPosition lifetime (a statement, not a parallel mechanism); (3) claimedCell==0 returns CelllessNoWeenieMarkUnreachable and is NOT a divergence - every admitted envelope structurally carries a WeenieDescription (HasValidShape), so retail's no-weenie destruction alternative is unreachable through this construction. |
src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs (ApplyResidentCellCleanup; the Abandon conversion in ApplyEnvelope) |
No production caller yet; every branch is typed and test-observable; building a parallel destruction mechanism ahead of the object-table/lost-cell cutover wiring would be the exact workaround class CLAUDE.md forbids - failing closed is the honest interim. | Branch (1): a genuinely claimed-but-celless-undeferred entity aborts the drain and SURVIVES where retail destroys it, until the cutover wiring lands. Branch (3): a future envelope construction without a WeenieDescription would break the premise and needs re-examination. | SmartBox::HandleCreateObject 0x00454C80 tail (~93933 destruction mark; ~93942-93943 un-mark/no-weenie) |
5. Unclear (UN) — 4 rows (UN-8 retired 2026-07-30 by the P1 Opus review: CanJump polarity byte-proven load < 2.0 from the PDB-paired binary — fld/fcomp [0x007c5e24=2.0f]/test ah,5/jp; unordered refuses. Evidence: stat-coupled pseudocode doc §12)
These rows have a missing, contradictory, or never-argued justification. They are the highest-priority audits: each needs either a recorded equivalence argument (promote to AD/AP) or a fix.
| # | Divergence | Where (file:line) | Recorded justification (deficient) | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
| UN-1 | CheckOtherCells iterates the overlap set SORTED by cell id; retail walks the CELLARRAY in build order — and the loop halts on the first non-OK result, so order is behavior-bearing |
src/AcDream.Core/Physics/CellTransit.cs:1718 |
Justified only as "deterministic order for greppable probe logs" — no equivalence argument vs retail's array order recorded | A sphere straddling two cells that would each return a different non-OK result halts on a different cell than retail — different collision normal / slide direction at multi-cell straddles | CTransition::check_other_cells pc:272717-272798 |
| UN-4 | GfxObj double-sided/negative-surface handling keeps WB's legacy logic (cull-mode double-siding, no reversed-winding duplicate, different neg-surface predicate) while the CellStruct path follows the retail-cited ConstructMesh reading |
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1059 (CellStruct contrast :1396-1410) |
No recorded justification on the GfxObj side — it is the unmodified WB extraction; the retail citation was added only to the CellStruct path | GfxObj models retail draws via duplicated-reversed-winding get wrong back-face lighting (normals not inverted) or missing/extra negative faces — dark or absent faces from behind | D3DPolyRender::ConstructMesh 0x0059dfa0 |
| UN-6 | Fixed 200 ms sleep between ConnectRequest and ConnectResponse; retail inserts no delay. Annotated only as "with 200ms race delay"; the 2026-06-04 audit flagged it, the follow-up refuted "forbidden workaround" but wrote no fuller rationale back | src/AcDream.Core.Net/WorldSession.cs:484 |
Presumed ACE port+1 listener race guard — four words, no citation | Every login eats a flat 200 ms; if the race needs longer on a loaded server, the handshake fails intermittently (ConnectResponse ignored → CharacterList never arrives, exit-29 shape) with no retry — a timing constant masking an unconfirmed root cause | (none recorded) |
| UN-7 | Outdoor OBJECT point lighting uses calc_point_light (wrap/norm + per-channel cap, ~1/d²) for ALL meshes including static buildings, but retail's object path is unconfirmed — config_hardware_light (0x0059ad30) sets D3D-FF point lights (Diffuse=color×intensity, Attenuation=(0,1,0)⇒1/d, Range=falloff×1.5, material.diffuse=white) yet that math would blow walls WHITE while retail stays DIM, so static buildings may instead use the SetStaticLightingVertexColors bake. Model + the brightness-scaling factor both UNRESOLVED (issue #140 / Fix D) |
src/AcDream.App/Rendering/Shaders/mesh_modern.vert (pointContribution); src/AcDream.Core/Lighting/LightManager.cs (SelectForObject) |
Fix A/B ported calc_point_light + per-object selection for objects without confirming retail uses that model for static buildings; cdb captured the D3D-FF path but it contradicts the observed dim result | Outdoor buildings blow out warm near torches (the #140 meeting-hall symptom); whichever model is wrong, the object torch contribution is too strong | config_hardware_light 0x0059ad30; SetStaticLightingVertexColors 0x0059cfe0; rangeAdjust=1.5 0x00820cc4 — see docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md |
6. Retire-next shortlist
Temporary-stopgap + unclear rows, ordered by risk (symptom severity × likelihood the guarding assumption breaks). Items below the line are phase-gated — they carry their trigger in their row and should land WITH that phase, not before.
- TS-27 — INBOUND retransmit handling — the outbound sent-packet cache + resend landed with Campaign N Slice N1 (2026-07-29, class-doc gap list fixed same commit); the inbound sequence-aligned ISAAC + client NAK emission (N2/N4) remain the hard blocker for non-loopback play — one lost S2C packet still deafens the session permanently.
- UN-1 — CheckOtherCells iteration order — behavior-bearing halt order with a log-cosmetics justification; trivial to fix (iterate CELLARRAY build order, sort only in probe output).
- UN-6 — 200 ms ConnectResponse sleep — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check.
- UN-4 — GfxObj sides/negative-surface logic — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it.
- TS-55 — AdminEnvirons fog/radar presentation — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and
0x270Ftogether. - TS-19 — Legacy ChaseCamera deletion — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging.
Phase-gated (do WITH the phase, flagged here so they aren't forgotten): M2 combat must land TS-25 (stance in MoveToState), TS-17 (AttackConditions), and revisit AP-13 (ComputeDamage) + AP-24 (jump-charge constant via the 0x0056ADE0 decompile). Emote work must land TS-24 (command-list packing). Membership Stage 2 must land TS-18 (BuildingCellId). The audio phase lands TS-9/TS-29; the remaining live-pose animation work lands TS-14.
Maintenance: this register is part of the definition of done for any
phase that adds or removes a divergence. Sources merged 2026-06-12:
5-area code sweep, docs/architecture/worldbuilder-inventory.md,
docs/ISSUES.md accepted-divergence entries (#96, #49, #50).