acdream/docs/architecture/retail-divergence-register.md
Erik 7a82317614 fix(#171): gate NPC UP hard-snaps while stuck — the sticky/snap tug-of-war (gate-1 residuals)
Gate 1 (2026-07-04): "better in general" + three residuals — monsters
pushed INTO the player, attacks with stale facing, position
"flashing/flapping instead of gliding". All three are ONE mechanism:
the legacy NPC UpdatePosition handler hard-snaps position, orientation,
and velocity/cycle UNCONDITIONALLY, fighting the armed sticky every UP
(ACE's authoritative rest pose sits ~0.6 m out and its server-side
facing lags the strafing target; the client stick holds 0.3 m + live
facing — oscillation at UP cadence).

Retail is immune by architecture, not by tuning: UP corrections flow
through the InterpolationManager into the SAME per-tick
PositionManager::adjust_offset chain where StickyManager::adjust_offset
OVERWRITES them while armed (0x00555190 chain order; 0x00555430 assigns
m_fOrigin). A server correction can never fight an armed stick
frame-by-frame. The remote-player branch already has exactly that
(queue -> combiner -> sticky overwrite); the legacy NPC path snaps
outside the chain.

Fix: suppress the NPC UP position/orientation/velocity-adoption snaps
while the entity is stuck (PositionManager.GetStickyObjectId() != 0) —
the retail chain semantics translated to the snap architecture.
LastServerPos/Time + cell bookkeeping still record; server truth
reasserts on the first UP after unstick, bounded by the 1 s sticky
lease. Register row TS-44 (same commit); retires with the S6/R6
interp-queue unification of the NPC path.

Apparatus: ACDREAM_PROBE_STICKY=1 — per-guid [sticky] lifecycle lines
(STICK / UNSTICK / LEASE-EXPIRE / TARGET-status teardown), per-armed-
tick steer lines (signed gap dist, applied delta, heading delta, live
resolve), and [sticky-snap-skip] at the suppressed-snap site.
PhysicsDiagnostics.ProbeStickyEnabled owns the flag (rule #5).

Full suite 4038 green. Awaiting gate 2 (pack melee vs retail).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 09:34:25 +02:00

139 KiB
Raw Blame History

Retail Divergence Register — 2026-06-12

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, GL 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) — 16 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 get_state_velocity prefers dat cycle velocity (MotionData.Velocity × speedMod) over the decompiled constant; constant kept only as max-speed clamp src/AcDream.Core/Physics/MotionInterpreter.cs:315 Retail's constant equals the Humanoid RunForward MotionData.Velocity, so both paths agree on retail dats; dat is ground truth for other MotionTables (r03 §1.3) Where dat velocity ≠ constant, body speed differs from the retail binary — DR / observer drift on exotic creatures or modded dats FUN_00528960; _DAT_007c96e0 RunAnimSpeed
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-7 PhysicsScript replay keyed by (scriptId, entityId) replaces the prior instance; retail's ScriptManager linked list could hold duplicates src/AcDream.Core/Vfx/PhysicsScriptRunner.cs:51 Prevents duplicate-stacking on server retriggers; flat keyed list simpler than retail's linked schedule; hedged to retail's common path A server intentionally layering the same script on the same object shows ONE effect where retail shows several (overlapping casts/impacts) ScriptManager::Start FUN_0051be40 / tick FUN_0051bfb0
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) src/AcDream.App/UI/README.md:3 (window mgr: UiRoot.cs RegisterWindow/Show/Hide/Toggle/BringToFront — F12 inventory toggle IS retail-faithful) keystone.dll has no PDB/decomp; semantics reconstructed from the six docs/research/retail-ui/ deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against keystone.dll Device DAT_00837ff4; 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 retail UI is our own UiHost/UiElement retained-mode tree drawing dat-sprite window frames, not a byte-port of keystone.dll's LayoutDesc binary tree. Both the vitals window (LayoutDesc 0x2100006C) and the chat window (LayoutDesc 0x21000006) are rendered by the LayoutDesc importer; UiNineSlicePanel/RetailChromeSprites now back only plugin panels src/AcDream.App/UI/Layout/LayoutImporter.cs (vitals + chat) + src/AcDream.App/UI/Layout/ChatWindowController.cs keystone.dll has no PDB/decomp so a byte-port is impossible by definition; we mirror retail's ElementDesc field model + controls.ini tokens, and the chrome sprites ARE the real dat RenderSurfaces (Step-0 prove-out 2026-06-14 confirmed 0x06004CC2 center + 0x060074BF..C6 bevel). The 8-piece edge/corner→position mapping is DATA-DRIVEN from the dat: the LayoutImporter reads LayoutDesc 0x2100006C/0x21000006 and resolves chrome element positions + sprite ids directly from parsed dat fields; vitals locked by the conformance fixture tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json Remaining residual risk: anchor resolution at non-800×600 and the controls.ini cascade still lack an oracle — layout scaling at non-reference resolution and stylesheet token inheritance differ silently LayoutDesc 0x2100006C/0x21000006 (SHIPPED); docs/research/2026-06-15-layoutdesc-format.md; controls.ini tokens; keystone.dll layout eval (no PDB)
IA-17 Toolbar window FRAME is toolkit-supplied (per-window UiNineSlicePanel 8-piece bevel, drawn over content via UiElement.OnDrawAfterChildren) rather than the window-manager-owned chrome retail paints uniformly around every window. 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/Rendering/GameWindow.cs (toolbar mount) + src/AcDream.App/UI/UiNineSlicePanel.cs + src/AcDream.App/UI/UiCollapsibleFrame.cs; spec: docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md LayoutDesc 0x21000016 has NO baked frame; retail's toolbar frame is window-manager chrome (keystone.dll). We draw the same reusable 8-piece bevel chat/vitals use; border drawn over content so the toolbar's 2px-wide row-2 right cap (W=8) can't poke through. Same pattern as the chat window. Until a central window manager owns chrome uniformly, per-window wraps can drift (size/offset/z-order) from each other and from retail; the border-over-content rule is the toolkit's, not the WM's 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

2. Adaptation (AD) — 33 rows

# Divergence Where (file:line) Why it is safe / justified Risk if assumption breaks Retail oracle
AD-1 Lost-cell machinery replaced by recoverable outdoor demote (#107 safety net) + outdoor-restore max(terrainZ, z) under-terrain lift; retail goes GotoLostCell src/AcDream.Core/Physics/PhysicsEngine.cs:553 (+ :808) acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server GotoLostCell pc:283418; SetPositionInternal 0x00515bd0, pc:283892-283945
AD-2 Async spawn gates replacing retail's synchronous cell load. #135 refinement: an INDOOR spawn/teleport (cell ≥ 0x0100, hydratable) gates ONLY on the EnvCell floor (IsSpawnCellReady), NOT the terrain heightmap; an OUTDOOR spawn (or an unhydratable indoor claim that demotes outdoor) gates on the terrain-ready hold (#106). A dungeon's negative-offset cells can place the spawn's WORLD position in a neighbour terrain landblock the #135 dungeon collapse doesn't load, so a terrain requirement would hang indoor login/teleport forever (cellReady true, terrain null) — the player lands on the cell floor, terrain is irrelevant indoors. Claims beyond NumCells skip the gate (demoted). #145 refinement (2026-06-22): an OUTDOOR teleport additionally requires the destination's OWN landblock terrain to be resident (PhysicsEngine.IsLandblockTerrainResident(destCell), which StreamingController priority-applies so it flips in ~hundreds of ms) before placing — and SampleTerrainZ is short-circuited behind it. On a teleport OUT of a dungeon the collapsed SOURCE dungeon stays resident at the same streaming-local coords and answers SampleTerrainZ (terrain 0), so without the load check the resolve ran against the dungeon's cells and rooted the player at a dungeon-frame cell id (ACE then rejected all movement as "failed transition"). Decision in TeleportWorldReady; the hold→materialize→regain-control transit is driven by TeleportAnimSequencer (the retail fade cover, ticked in OnUpdate), which replaced the retired TeleportArrivalController src/AcDream.App/Rendering/GameWindow.cs (isSpawnGroundReady lambda ~1010 + TeleportWorldReady ~5512 + the TAS transit tick in OnUpdate) (+ src/AcDream.App/Input/PlayerModeAutoEntry.cs:69, src/AcDream.Core/Physics/PhysicsEngine.cs:468) Entering earlier integrates gravity against an empty world (free-fall into void); the gate is the async-streaming equivalent of retail's blocking load; a looser "any struct present" version reproduced the transparent-interior wedge. Indoor-on-cellReady is the faithful equivalent of retail's synchronous cell load + place-on-floor (terrain under a dungeon is meaningless; the pre-#135 terrain hold only passed because the 25×25 window streamed the neighbour terrain) Gate opens early → raw claim commit → outdoor demote mid-building; predicate never satisfied (streamer stall, dat edge case) → login wedges in pre-player mode; an indoor spawn whose cell never hydrates now holds on cellReady alone (no terrain backstop) — but that path is exactly the #107 hold retail synchronous cell load before SetPosition (no gate exists)
AD-3 Outdoor seeds always walk the transit array (retail skips the walk when the seed CLandCell is null/unloaded); per-cell lookups no-op on unhydrated data src/AcDream.Core/Physics/CellTransit.cs:503 Equivalence argument: with nothing hydrated every lookup inside the walk no-ops, so the result matches retail's skipped walk Near partially-streamed landblocks, building-transit promotion silently can't fire until structs hydrate — membership stays outdoor while the player is inside a building CObjCell::find_cell_list 0052b535-0052b56c (null-CLandCell case)
AD-4 point_in_cell against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells src/AcDream.Core/Physics/CellTransit.cs:588 The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root CEnvCell::find_visible_child_cell :311397; cell-BSP vtable[0x84]
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)
AD-6 Per-LANDBLOCK shadow re-flood on hydration vs retail per-CELL recalc_cross_cells src/AcDream.Core/Physics/ShadowObjectRegistry.cs:339 The streaming unit IS the landblock; one hook per hydration event covers both race directions (entity-before-cells, cells-after-spawn) Any cell-hydration path that doesn't raise the landblock hook leaves an entity's shadow set stale — walk-through / missing collisions in just-streamed cells CObjCell::init_objectsrecalc_cross_cells, 0x0052b420 / 0x00515a30
AD-10 Remote slope projection relocated to the queue-empty/head-reached combiner boundary; retail projects inside CTransition::adjust_offset during the sweep src/AcDream.Core/Physics/PositionManager.cs:47 Remote bodies don't run a full local transition sweep; boundary projection removes the ~5 Hz Z staircase on slopes, no-op on flat ground The single-point terrain-normal sample can differ from the sweep's contact plane (cell boundaries, props underfoot) — remote Z drift / stair-stepping CTransition::adjust_offset pc:272296-272346
AD-11 Useability fallback: retail blocks Use entirely on null/zero useability; we allow it (behavioral fallback in the IsUseableTarget caller; justification recorded here) src/AcDream.Core/Physics/PhysicsDiagnostics.cs:163 ACE's seed DB ships many weenies with _useability unset; without the fallback doors/lifestones/creatures are un-Useable on ACE Objects a retail-faithful server intentionally marks non-useable become useable in acdream — wrong interaction gating when the ACE-ships-null assumption stops holding ItemHolder::UseObject pc:402923
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-14 Script anchor world position cached at Play() time; retail fires hooks via vtable dispatch on the live owning PhysicsObj src/AcDream.Core/Vfx/PhysicsScriptRunner.cs:55 Core's runner is decoupled from the entity graph; documented contract pushes per-frame anchor refresh to the owning subsystem (done for AttachLocal) Any caller that forgets the per-frame refresh strands long-running effects at the spawn position while the entity walks away FUN_0051bfb0 per-frame hook dispatch on owner
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/GameWindow.cs:7634 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 GL guarantees only 8 simultaneous clip planes; 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::drawDrawBlock 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/GameWindow.cs:7671 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 point-of-use self-heal (EnsureLoaded re-request in the dispatcher's per-frame meshMissing path, #128); retail loads synchronously — geometry is never absent src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs:211 Documented convergence argument: the self-heal makes absence transient, converging the async pipeline to retail's never-absent guarantee A missing mesh referenced OUTSIDE the dispatcher's walk (a future consumer not touching meshMissing) stays permanently invisible — the #119/#128 broken-stairs class; best case, late pop-in retail synchronous content load (note at WbMeshAdapter.cs:211)
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 hash-deduplicated ((environmentId, structure, surface overrides) → 31-multiplier hash) and instanced; retail draws each CEnvCell's own structure directly src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs:276 Verbatim WB EnvCellRenderManager port (Phase A8); dedup is what makes the single-VAO MDI cell pipeline cheap; intended visuals identical A hash collision between distinct tuples renders the wrong interior shell in some room with NO diagnostic firing — wrong walls/floor in a dungeon room retail PView::DrawCells → per-cell drawing_bsp (cited at :319)
AD-25 Wall-bounce velocity reflection suppressed on landing (fires only airborne-before AND airborne-after); retail bounces unless grounded→grounded-and-not-sledding src/AcDream.App/Input/PlayerMovementController.cs:874 Our per-frame architecture amplifies the artifact (post-reflection +Z defeats the Velocity.Z <= 0 landing-snap gate → micro-bounce death spiral); at elasticity 0.05 retail's landing bounce is imperceptible; sledding reverts to retail rule Landing-reflection-dependent behavior (slope-landing momentum, high-elasticity surfaces) won't reproduce; the suppression masks the landing-snap gate fragility and could outlive its reason handle_all_collisions pc:282699-282715; ACE PhysicsObj.cs:2656-2721
AD-27 Use/PickUp action fired 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); retail sends the action once (server MoveToChain callback completes it) src/AcDream.App/Rendering/GameWindow.cs:12939 (MoveToComplete subscription) + src/AcDream.Core/Physics/Motion/MoveToManager.cs (MoveToComplete seam doc) ACE's server-side chain may have timed out by the time our body arrives; the close-range deferred send hits ACE's WithinUseRadius fast-path. R4-V5 re-anchored from the deleted B.6 AutoWalkArrived event — same fires-on-arrival-only contract (never on cancel) If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers ACE CreateMoveToChain / WithinUseRadius; MoveToManager::CleanUpAndCallWeenie 00529650 §7e (no weenie call)
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-31 Teleport transit covered by a full-screen black FADE (FadeOverlay + TeleportAnimSequencer) instead of retail's 3D portal-tunnel swirl (2026-06-22, spec C). Opaque black holds through the tunnel states; the world ramps back in on WorldFadeIn. src/AcDream.App/Rendering/FadeOverlay.cs + GameWindow.cs (TAS transit tick; _teleportFadeAlpha = ShowTunnel ? 1 : FadeAlpha) The fade is a functional cover that hides the (now-fast) destination load + the post-materialization object flood; the TAS state machine + golden timing constants are retail-verbatim — only the tunnel graphic is approximated. Sibling to AP-49 (fade-curve). Visual-only: the transit shows a black cover, not the animated swirl. Retire by porting the gmSmartBoxUI 3D tunnel render. gmSmartBoxUI::UseTime 0x004d6e30 (tunnel render, unported); TELEPORT_ANIM_* golden constants (spec §2.1)
AD-32 Movement-event staleness gate ADOPTS a newer-incarnation instance stamp and applies the event immediately; retail queues the blob for the not-yet-created object (SmartBox::QueueBlobForObject, dispatch return 4) and replays it once that incarnation exists (L.2g S1, 2026-07-02) src/AcDream.Core/Physics/MotionSequenceGate.cs:105 acdream has no per-object blob queue; a newer instance seq means the new incarnation's CreateObject is in flight, and that CreateObject re-seeds the same gate (advance-only), so old-incarnation stragglers still drop A motion event for a new incarnation applies to the OLD body for up to one CreateObject round-trip — brief wrong-cycle flicker on respawn/re-instance if the new incarnation's motion table differs 0xF74C dispatch pc:357214-357239 (is_newer(update_times[8], seq) + QueueBlobForObject)
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 (doors, statics) keep a diagnostic-recorder-only target — retail gives every CPhysicsObj a MovementManager/CMotionInterp (R2-Q4 seam, narrowed R3-W2, 2026-07-02) src/AcDream.App/Rendering/GameWindow.cs (TickAnimations MotionDoneTarget bind) Door/static motion (On/Off cycles) completes via the manager queue alone; nothing consumes their MotionDone until R5 gives every entity class the full MovementSystem pipeline A door behavior depending on pending_motions bookkeeping (none known — doors have no jump/action semantics) would silently no-op CPhysicsObj::MotionDone 0x0050fdb0; retire in R5 (MovementSystem for all classes)

3. Documented approximation (AP) — 72 rows (AP-79 retired R5-V2 — the P4 TargetTracker adapter replaced by the ported TargetManager voyeur system)

# Divergence Where (file:line) Why it is safe / justified Risk if assumption breaks Retail oracle
AP-1 Snap-path Z settle: validated claims ground on their own walkable polys, but floor-less claims (thresholds, stair lips) fall through to a legacy nearest-in-Z scan over every CellSurface in the landblock; retail settles via CheckPositionInternalfind_valid_position src/AcDream.Core/Physics/PhysicsEngine.cs:614 find_valid_position unported; the #111 fix narrowed the legacy pick's blast radius (validated claims bypass it) rather than replacing it A threshold/stair-lip snap can still pick a neighbouring cell's same-height floor by iteration order — wrong cell or Z at login/teleport arrival (the #111 clobber class) SetPositionInternal :283426 → find_valid_position

| AP-3 | Step-down chain triggered only when contact is invalid OR steeper than walkable; retail's transitional_insert OK-path ALWAYS runs it | src/AcDream.Core/Physics/TransitionTypes.cs:1197 | Conditional preserves the observed-to-matter cases (edge departure, steep cliff-slide) without running the chain every step (per pc:273191 agent reports) | Steps where retail runs step-down despite a valid walkable contact (bump maintenance, edge-slide arming) are skipped — float-off or missed edge slides in untested geometry | transitional_insert OK-path pc:273191 | | AP-4 | CliffSlide check moved BEFORE retail's Branch-1 (!OnWalkable → restore+OK) gate, compensating our L.2.3i FloorZ OnWalkable bookkeeping | src/AcDream.Core/Physics/TransitionTypes.cs:1316 | Retail's order with our incomplete OnWalkable stops the player dead every frame on steep slopes ("stay on the roof"); reorder restores downhill drift | CliffSlide fires in states where retail's Branch 1 would restore-and-OK — body slides where retail holds, e.g. contact-plane-bearing steep geometry near edges | retail EdgeSlide dispatch order (transitional_insert step-down failure) | | AP-5 | Step-down skips Placement validation for the contact-maintenance call (runPlacement=false); ACE/retail run it unconditionally (kept for DoStepUp) | src/AcDream.Core/Physics/TransitionTypes.cs:3393 | Residual wall-slide artifacts made Placement misfire, leaving players stuck near walls; the skip was the targeted L.2.3h fix | Step-down can settle into positions Placement would reject — slight wall embedding, or accepting a step-down through overlap geometry retail catches | CTransition::step_down pc:272952; ACE Transition.cs:731-741 | | AP-6 | Analytic swept-sphere cylinder collision (XY overlap + step-over + wall-slide) instead of retail CylSphere functions via the 6-path dispatcher; A6.P6 step-over branch ports step_sphere_up's clearance check | src/AcDream.Core/Physics/TransitionTypes.cs:2601 | Claimed to match retail for the exercised cases (trunks, NPC bodies, door foot-colliders); step-over and step_up_slide fallback retro-fitted from retail when the door phantom surfaced | Unported branches (push direction, interpenetration resolution) differ from retail against cylinder entities — the phantom-collision / sticky-NPC family | CCylSphere::step_sphere_up pc:324516-324538 | | AP-7 | calc_friction threshold 0.0 with retail's state gate missing; retail uses 0.25 gated by an undecoded state check | src/AcDream.Core/Physics/PhysicsBody.cs:307 | Bumping the threshold without the gate hammered normal walking (3 → 0.16 m/s); as-read 0.0 kept; locomotion probably state-exempted in retail. Filed L.3c-followup | Friction engages under different conditions — post-landing slides, knockback decay, sledding speeds mismatch retail's deceleration | pc:276702-276705 (state gate + 0.25) | | AP-10 | Dry-corner water depth: retail's 0.1 m allowed sink-in collapsed to 0 | src/AcDream.Core/Physics/TerrainSurface.cs:481 | The 0.1 offset destabilizes the feet-exactly-on-plane contact-touch check (dist > EPSILON → SetContactPlane never fires → float/fall); retail's ~10 cm sink-in is visually indistinguishable | Masks a contact-touch epsilon fragility — other water-depth values exercising the same instability could oscillate shoreline walkable validation; retail's wet/dry corner sink-in visual absent | ObjCell.get_water_depth / calc_water_depth (via ACE port) | | AP-11 | Hand-authored 4-keyframe fallback sky set (sunrise/noon/sunset, fog ~80350 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 translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | 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 | | 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-17 | Spell metadata from third-party CSV (3,956 rows, bad rows silently skipped), not the portal.dat SpellTable; Family feeds stacking decisions | src/AcDream.Core/Spells/SpellTable.cs:10 | The dat spell-table port (obfuscated/encrypted aspects) wasn't done; CSV closed #11 fast and unblocked #6 stacking | Any CSV↔dat drift (wrong Family, missing rows) silently produces wrong buff-stacking winners and wrong panel info | portal.dat SpellTable 0x0E00000E | | AP-18 | Radar/indicator RGBA hand-tuned from screenshots; dispatch order ports GetBlipColor exactly but the real RGBAColor_Radar* static data is unrecovered | src/AcDream.Core/Ui/RadarBlipColors.cs:33 | Color constants live in retail static data not yet extracted; comment invites tightening when recovered | Blip/indicator hues differ subtly from retail color cues | gmRadarUI::GetBlipColor 0x004d76f0; RGBAColor_Radar* (unrecovered) | | 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 12 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: two-pass alpha-test (N.5 Decision 2, invented 0.95/0.05 thresholds); AlphaBlend + Additive + InvAlpha all composite under (SrcAlpha, 1SrcAlpha) — retail applies per-surface D3D blend incl. true additive. EnvCellRenderer + ParticleBatcher DO switch to additive; divergence confined to GfxObj/Setup entities via WbDrawDispatcher | src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:1563 (+ Shaders/mesh_modern.frag:10; #52 amendment removed the α≥0.95 discard) | Matches original WB's model; keeps the bindless MDI pipeline at two indirect draws; spec §6 documents the falsifiable fallback — a third indirect call with glBlendFunc(SrcAlpha, One) (~30 min) on a magic-content regression | Additive glow/magic entity surfaces composite darker / occlude instead of brightening — the predicted regression once spell VFX density increases; α<0.05 discard drops faint fringes retail blends | SurfaceType.Additive → D3DBLEND_ONE per-surface routing | | AP-22 | Invented setup.Radius cylinder (height = Height or Radius×2) for shapeless live entities; shape + height formula not from the retail shape walk | src/AcDream.App/Rendering/GameWindow.cs:3250 | ShadowShapeBuilder (faithful walk) only emits CylSphere/Sphere/Part-BSP; the legacy cylinder preserves prior behavior so rare decorative props don't lose collision | Those props collide with an invented footprint (especially the Radius×2 height guess) — slides/blocks at non-retail distances | find_obj_collisionsCPartArray::FindObjCollisions pc:286236 | | AP-23 | Invented per-type use-radius heuristic (3 m creatures / 2 m doors-lifestones-portals-corpses / 0.6 m rest) for close-range gating + the speculative local TurnToObject/MoveToObject install through the player's MoveToManager (R4-V5; retail's client use flow issues the same manager calls, §9a/§9b). R5-V3 narrowed it: the speculative install now threads the target's REAL setup radius/height (GetSetupCylinder, same as the wire mt-6 route) and the player's own radius is real — only the use-radius BUCKETS remain invented (retail reads the object's UseRadius property) | src/AcDream.App/Rendering/GameWindow.cs (InstallSpeculativeTurnToTarget) | ACE broadcasts nothing actionable on the close branch (WithinUseRadius shortcut); the true radius arrives only on the far MoveToObject branch — a local stand-in is required | A target whose real UseRadius differs from the bucket misjudges the gate — Use/PickUp deferred for a completion that never comes, 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 | Jump charge fill rate guessed at 2.0 extent/s (full in 0.5 s); retail's divisor illegible (clobbered x87 in GetPowerBarLevel). Height→velocity formula is byte-faithful | src/AcDream.App/Input/PlayerMovementController.cs:176 | Only time-to-fill diverges; 2.0/s matched retail muscle memory better than 1.0/s; targeted Ghidra decompile of 0x0056ADE0 already flagged (M2 research) | Every held-spacebar jump reaches a different extent than the same hold in retail — fence/gap jumps succeed/fail differently until the constant is recovered | FUN_0056ade0 (GetPowerBarLevel) | | AP-25 | Run/Jump skill pushed to movement = attributeBonus + Init + Ranks — no augmentations, multipliers, or vitae | src/AcDream.Core.Net/GameEventWiring.cs:346 | Closest to ACE's CreatureSkill.Current short of porting the full Aug/Multiplier/Vitae chain (K-fix7/13) | A character with augs or post-death vitae predicts wrong local run speed / jump arc — dying would NOT slow the local player though the server moves them slower: drift + snap-back | ACE CreatureSkill.Current; ACE Skill.cs (Jump=22, Run=24) | | 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-29 | Target-indicator fallback for entities with no baked selection sphere: invented 1.5 m × scale box + 16/12 px screen floors (primary path is a faithful GetObjectBoundingBox port) | src/AcDream.App/UI/TargetIndicatorPanel.cs:86 | Fallback only fires when the Setup didn't bake a selection sphere — rare in practice | Sphere-less entities get a non-retail indicator size/placement; the pixel floors prevent retail's far-distance collapse | SmartBox::GetObjectBoundingBox 0x00452e20; GetSelectionSphere | | AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's Frame::is_equal is an exact float compare | src/AcDream.App/Input/PlayerMovementController.cs:1110 | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | Frame::is_equal pc:700263 | | 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/GameWindow.cs:5604 (const at PortalVisibilityBuilder.ShellDrawLiftZ) | 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 | Landscape-stage alpha deferral is a TWO-PHASE slice split (statics-early / dynamics+particles+weather-late around the #124 look-ins) + outdoor-root attached scene emitters moved to the post-frame pass, not retail's single deferred alpha flush. Residual: building exteriors' / outside-stage dynamics' own translucent MESH batches still draw within their stage draw call (before later stage content) | src/AcDream.App/Rendering/RetailPViewRenderer.cs (DrawLandscapeThroughOutsideView late loop) + GameWindow post-frame Scene pass | The MDI dispatcher draws translucency inside each Draw call; a faithful FlushAlphaList port needs a global deferred alpha list across all landscape draws — the split covers the user-visible cases (#131 portal swirl, #132 candle flame indoors + outdoors) | Translucent landscape content drawn early and screen-overlapped by content drawn later in the stage gets overpainted (no depth self-protection) — the portal-swirl/candle-flame class re-appears in the residual configurations | D3DPolyRender::FlushAlphaList (DrawCells pc:432722) | | 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). #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/DungeonStreamingGate.cs (Compute — per-frame predicate + teleport-hold suppression, called from GameWindow.OnUpdate ~:7401) + GameWindow:IsSealedDungeonCell + :OnLiveEntitySpawnedLocked/:OnLivePositionUpdated (login/teleport pre-collapse hooks) + 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 | 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 | | 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/GameWindow.cs:10421 (UpdateSunFromSky, unchanged) | 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 importer collapses the dat's nested meter structure (Type-7 meter → two Type-3 container children → three Type-3 image-slice grandchildren each) into UiMeter's programmatic 3-slice fields (BackLeft..FrontRight) + reuses UiMeter.DrawHBar's scissor-fill, instead of building those child nodes generically and porting UIElement_Meter::DrawChildren. Vitals number elements are meter children (not recursed); VitalsController attaches a centered UiText child for the cur/max number (Task 8 landed — retail gmVitalsUI uses UIElement_Text), so UiMeter.Label is no longer used for vitals (UiText.Centered reuses the meter's former centering formula → pixel-identical, user-confirmed). The inheritance Merge treats Width/Height==0 as "inherit from base", diverging from format-doc §12 rule 2 (documented inline in ElementReader.cs) | src/AcDream.App/UI/Layout/DatWidgetFactory.cs (BuildMeter/SliceIds) + src/AcDream.App/UI/Layout/LayoutImporter.cs (BuildWidget meter-child skip) | Reuses the tested UiMeter render that already visually matches retail's stacked vitals bars; the full nested-element + DrawChildren scissor port is deferred to Plan 2. Locked by the conformance fixture (tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json) | A LayoutDesc whose meter structure differs from the vitals 2-container/3-slice shape renders an empty/wrong meter — no oracle diff until the Plan-2 port lands | UIElement_Meter::DrawChildren @0x46fbd0; docs/research/2026-06-15-layoutdesc-format.md | | AP-38 | Chat transcript renders pre-split ChatLog lines 1:1; no in-element word-wrap at the panel's current pixel width | src/AcDream.App/UI/UiText.cs | Retail does in-element wrap via UIElement_Text::SizeToFit; our pre-split lines are always shorter than 440 px in practice; a line that overflows clips at the edge rather than wrapping | Very long server system messages (server shutdowns, broadcast announcements) clip rather than wrapping — no information loss, just visual truncation | UIElement_Text::SizeToFit @0x467980; gmMainChatUI layout | | 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 | Single default translucency for the chat window chrome; no focused/unfocused opacity transition; dat font face/size taken from the vitals vitalsDatFont (same dat font, not a chat-specific size lookup) | src/AcDream.App/Rendering/GameWindow.cs (chatController binding line) | Retail fades the chat window to ~80% alpha when unfocused (gmMainChatUI::UpdateAlpha @0x4cdea0); the opacity animation deferred to the Plan-2 window-manager input integration; sharing vitalsDatFont is safe — retail uses the same AC-default font for both | The chat window is always fully opaque/same-font rather than subtly fading when idle; no wrong text, but the focused/unfocused breathing rhythm is absent | gmMainChatUI::UpdateAlpha @0x4cdea0; UCF::SetAceFont @0x4d3940 | | 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; latest-wins matches PrivateUpdateVital/UpdatePosition's existing non-sequence behavior. Sequence tracking added when needed alongside TS-26. | 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-46 | Health-meter gate approximation: retail shows the health meter for IsPlayer() || pet_owner || ClientCombatSystem::ObjectIsAttackable() (full PK/faction logic); acdream's GameWindow.IsHealthBarTarget uses the server PWD bits BF_ATTACKABLE (0x10) OR BF_PLAYER (0x8) | src/AcDream.App/Rendering/GameWindow.cs (IsHealthBarTarget) → SelectedObjectController | The PWD BF_ATTACKABLE/BF_PLAYER bits distinguish monsters + players (bar) from friendly/vendor NPCs (name-only) for the M1.5 dev loop; the pet case and the full ObjectIsAttackable PK/faction refinement (free-PK, PK-vs-PK, PKLite) are not ported | A PK/faction edge (e.g. a hostile-flagged player whose BF_ATTACKABLE is unset, or a pet) could show/hide the bar where retail differs — no impact on the non-PK PvE dev loop | ClientCombatSystem::ObjectIsAttackable acclient_2013_pseudo_c.txt:375385; BF_ATTACKABLE acclient.h:6437 | | AP-47 | Cursor drag ghost reuses the full composited m_pIcon (incl. type-default underlay) instead of retail's dedicated m_pDragIcon (base + custom-overlay, NO type-default underlay). Opacity now matches retail (full). | src/AcDream.App/UI/UiRoot.cs (DrawDragGhost/GhostAlpha) → src/AcDream.App/UI/UiItemSlot.cs (GetDragGhost) | Cosmetic only — the dragged item is still unambiguously identifiable; building the second underlay-less composite is deferred polish; the ghost is item-agnostic in UiRoot via GetDragGhost() | The ghost carries the opaque type-default underlay backing rather than retail's underlay-less copy — a subtle look difference while dragging, no functional effect. | IconData::RenderIcons acclient_2013_pseudo_c.txt:407594-407625 (m_pDragIcon path); deep-dive §3.2/§5.5 | | 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 ElementReaderDatWidgetFactory. | 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 = 560 px constant), contents grid/sub-window/scrollbar/backdrop stretched via overridden Left\|Top\|Bottom anchors. | src/AcDream.App/Rendering/GameWindow.cs (inventory frame setup) | retail's gmInventoryUI resize lives in keystone.dll (no decomp); the anchor-stretch + 372..560 range matches the observed retail behavior for the dev loop. | The expand range / which elements reflow could differ from retail (e.g. retail may allow shrink-below-default); shrink is intentionally disabled for now. | 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 controller state (_openContainer/_selectedItem), reproducing the retail keying (item.itemID == openContainerId / selectedItemId per UpdateOpenContainerIndicator 0x004e3070 / ItemList_SetSelectedItem 0x004e2fe0) 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; ItemList_SetSelectedItem 0x004e2fe0; SetOpenContainerState 0x004e1200; SetSelectedState 0x004e1240 | | AP-58 | Inventory item selection is panel-local + visual-only — clicking a grid item moves the green square but does not wire to the selected-object bar (SelectedObjectController) or to global 3D-world selection. | src/AcDream.App/UI/Layout/InventoryController.cs (SelectItem) | The selected-object bar + 3D-world selection wiring is deferred to the selection follow-up; the panel-local square is the correct first step. | Selecting an inventory item doesn't populate the selected-object strip as retail does — functional gap deferred. | gmInventoryUI/gmBackpackUI item-select → ClientUISystem::SetSelectedObject; SelectedObjectController | | 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 LIKELY0x100001E0 → 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 | Empty equip slots show a generic frame (Slice 1) instead of the retail doll-backed transparent look — retail's slot-view leaves empty armor slots transparent with the live 3-D doll body behind them; Slice 1 has no doll yet, so empty slots render a visible frame (0x06004D20, the inventory grid's empty square) so every position is seen + usable. The "figure" in the paperdoll is the doll, NOT a per-slot silhouette. | src/AcDream.App/UI/Layout/PaperdollController.cs (emptySlotSprite); src/AcDream.App/Rendering/GameWindow.cs (PaperdollController.Bind) | Retired in Slice 2 when the doll UiViewport (0x100001D5) renders behind the slots and the empty slots flip to transparent (EmptySprite=0). | Empty equip slots look like plain framed squares instead of revealing the character body — cosmetic, pending Slice 2's doll viewport. | gmPaperDollUI init SetVisible(0) per slot; user retail screenshots (slot-view) | | AP-67 | Server-spawned weenie fixtures (lanterns, braziers, glowing items carrying Setup.Lights) register their lights at their SPAWN-frame world position via RegisterOwnedLight in OnLiveEntitySpawnedLocked (mirroring the dat-static path in ApplyLoadedTerrainLocked); the light does NOT follow the object if it later moves. Torn down on despawn/respawn by the now-unconditional UnregisterOwner in RemoveLiveEntityByServerGuid. | src/AcDream.App/Rendering/GameWindow.cs (OnLiveEntitySpawnedLocked weenie-light block; RemoveLiveEntityByServerGuid) | Closes the prior gap where ONLY dat-baked statics registered lights, so server-placed lanterns cast nothing; the overwhelming majority of light-bearing weenies (wall lanterns, braziers, candelabra) are stationary, so spawn-frame placement matches retail; retail's add_dynamic_light re-places the light from the object frame each frame | A moving light-bearing weenie (e.g. a torch-carrying NPC) leaves its light at the spawn position instead of following — rare; the faithful fix is to re-place on OnLivePositionUpdated | retail object-borne lights insert_light 0x0054d1b0 / add_dynamic_light 0x0054d420 (per-frame object frame) | | 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 | On a landblock (re)load, acdream re-projects its retained server-object spawns (GameWindow._lastSpawnByGuid — our weenie_object_table for world objects, carrying position + Setup + appearance) into the render world via LandblockEntityRehydrator. The dungeon collapse (#133/#135) and Near→Far demote drop a landblock's RENDER entities for FPS but keep the parsed spawns; ACE will NOT re-broadcast objects whose guid is still in its per-player KnownObjects set (never cleared on a normal teleport — ACE relies on the client retaining its table + doing its own visibility cull). Re-projecting from our own table is the retail "client keeps its weenie_object_table and re-renders from it" model. DIVERGENCE: acdream has NO retail 25 s / 384 m visibility cull — the retained spawn table is pruned ONLY by an explicit server DeleteObject (0xF747) or a spawn de-dup, never by distance/time. (#138) | src/AcDream.App/Streaming/LandblockEntityRehydrator.cs + GameWindow.RehydrateServerEntitiesForLandblock + StreamingController (onLandblockLoaded, Loaded + Promoted) | Re-projection from our own table is retail-faithful AND the only reliable path (ACE won't re-send a known object); fixes the #138 "doors/NPCs/portals gone after a dungeon exit" symptom independent of any server re-broadcast | An object the server silently stopped tracking WITHOUT a DeleteObject (e.g. a creature that wandered out of PVS) is re-hydrated at its last-known position on reload, where retail's 25 s cull would have dropped it — a stale ghost until a real DeleteObject or the next session. Close it by porting holtburger's 25 s/384 m ACE_DESTRUCTION_TIMEOUT self-cull | ACE ObjectMaint.KnownObjects never cleared on teleport (Physics/Common/ObjectMaint.cs, WorldObjects/Player_Tracking.cs); holtburger client 25 s cull liveness.rs ACE_DESTRUCTION_TIMEOUT_SECS; retail client weenie_object_table re-render | | AP-70 | TeleportAnimSequencer.ComputeFadeAlpha uses a smoothstep curve for all fade states; retail's gmSmartBoxUI::UseTime drives fade alpha through a 1024-entry GetAnimLevel lookup table whose contents are unrecovered | src/AcDream.Core/World/TeleportAnimSequencer.cs (ComputeFadeAlpha) | GetAnimLevel table address and contents not yet extracted via cdb; smoothstep is a perceptually-reasonable S-curve that closely approximates a typical gamma-corrected fade; the visual difference is imperceptible under normal teleport conditions | Fade timing differs subtly from retail — ramp-in or ramp-out may be slightly too fast/slow at the black-fade edges; retire by reading the GetAnimLevel table via cdb and replacing smoothstep with a direct 1024-entry lookup (spec §8) | gmSmartBoxUI::UseTime 0x004d6e30; GetAnimLevel 1024-entry table (address unrecovered — spec §8 cdb trace) | | AP-71 | CEnvCell::find_env_collisions omits the CObjCell::check_entry_restrictions gate — retail's CEnvCell::find_env_collisions (pc:309576) calls CObjCell::check_entry_restrictions (pc:309576) FIRST and returns COLLIDED when an access-locked cell's restriction_obj entity rejects the mover (CanMoveInto fails AND CanBypassMoveRestrictions is false). acdream's FindEnvCollisions (src/AcDream.Core/Physics/TransitionTypes.cs:2088) goes straight to BSP collision. | src/AcDream.Core/Physics/TransitionTypes.cs:2088 | No access-restriction cell data exists in acdream. CellPhysics (src/AcDream.Core/Physics/PhysicsDataCache.cs:540) has no restriction_obj field; DatReaderWriter does not model per-cell access locks; no weenie-object-table exists on the client for the gate's CanMoveInto / CanBypassMoveRestrictions dispatch. The gap is inert in all dev content (ACE starter area has no access-locked env cells). | If access-locked dungeon cells are ever modeled (dat + wire), the player will walk through the restriction barrier without being blocked — wrong PK/housing gating. | CObjCell::check_entry_restrictions pc:309576; CEnvCell::find_env_collisions pc:309573309597 | | AP-72 | Per-element dat-font resolver (Fix C) wired into the STUDIO path onlyLayoutImporter.Import + DatWidgetFactory support a Func<uint,UiDatFont?> resolver that gives each text element its own dat font; StudioWindow supplies it. GameWindow's four Import/Build call sites pass null, so the live game still uses a single global font. Retire with #157. | src/AcDream.App/Rendering/RenderBootstrap.cs (RenderStack.ResolveDatFont — pattern to mirror into GameWindow) + src/AcDream.App/Studio/StudioWindow.cs (resolver wired) + src/AcDream.App/Rendering/GameWindow.cs (~4 import sites pass null) | Studio is the proving-ground path; GameWindow threading is Issue #157 (small, safe, orthogonal to M5 gameplay). The live panels still render with the global dat font (same as before Fix C), so behavior is unchanged in the shipped game. | Text elements whose dat FontDid differs from the global font render with the wrong size/face in the live game — cosmetic; the studio shows the correct dat font. | DatWidgetFactory.BuildText; LayoutImporter.Import(fontResolver); commit a0d3395 (Fix C) |


| AP-75 | Adapter-boundary adjust_motion + locomotion velocity/omega synthesis: SetCycle still (a) remaps TurnLeft/SideStepLeft/WalkBackward to their mirrors with negated speed BEFORE dispatch (retail adjusts in CMotionInterp — R3 scope; GameWindow's local-player path passes raw ids), and (b) post-dispatch overwrites sequence velocity (low-bytes 05/06/07/0F/10) and omega (0D/0E, only when dat-silent) with the retail locomotion constants — retail drives BODY velocity from get_state_velocity, not the sequence accumulators (R2-Q4 carry-over of the pre-Q4 adapter tail) | src/AcDream.Core/Physics/AnimationSequencer.cs (SetCycle head remap + tail synthesis) | (a) preserves unadjusted GameWindow callers until R3-W6 unifies the local player onto MotionInterpreter; (b) preserves the remote-DR and local Option-B velocity consumers until R6 root motion drives the body (sibling of IA-3) | (a) none while callers stay in the known set; (b) a dat whose locomotion MotionData carries a REAL velocity different from the constants gets overwritten — exotic-creature speed skew (same class as IA-3's risk) | CMotionInterp::adjust_motion @305343; get_state_velocity 0x00528960; retire (a) R3-W6, (b) R6 | | AP-76 | Remote rotation from the ObservedOmega side-channel: the R2-Q5 sink callbacks (MotionTableDispatchSink.TurnApplied/TurnStopped) seed RemoteMotion.ObservedOmega = (0,0,-(pi/2)*signedTurnSpeed) on wire turns and zero it on turn-stops; GameWindow's per-tick step applies ObservedOmega (preferring it over sequence omega) to the remote body's orientation. Retail rotates the body from the SEQUENCE omega inside the per-tick apply_physics chain (CSequence::apply_physics -> CPhysicsObj omega integration) (carried verbatim from the deleted RemoteMotionSink; H17) | src/AcDream.App/Rendering/GameWindow.cs (sink callbacks in OnLiveMotionUpdated + the omegaToApply step in TickAnimations) | Same angular rate retail derives (pi/2 rad/s x turn speed); starts rotation the same tick as the wire turn without waiting for R6's per-tick order; UpdatePosition orientation snaps bound any drift | If the dat's turn modifier omega differs from the pi/2 constant, remote rotation rate diverges until an orientation snap; double-application risk if R6 lands apply_physics-driven rotation without deleting this seam | retire in R6 (retail per-tick order: apply_physics drives remote rotation) | | AP-77 | apply_current_movement's interpreted-branch tail (ApplyCurrentMovementInterpreted) writes body velocity DIRECTLY via get_state_velocity/set_local_velocity when grounded, instead of dispatching through an IInterpretedMotionSink — retail's real apply_interpreted_movement (0x00528600) drives velocity indirectly through DoInterpretedMotion's animation-table backend (the SAME function the funnel's ApplyInterpretedMovement already uses WITH a sink); this dual-dispatch call site (HitGround/LeaveGround/ReportExhaustion/hold-key toggles/SetWeenieObject/SetPhysicsObject) has no sink threaded through it | src/AcDream.Core/Physics/MotionInterpreter.cs (ApplyCurrentMovementInterpreted) | Direct continuation of the pre-R3-W4 apply_current_movement approximation (R3-W4 plan explicitly keeps this shape rather than relocating it — "the direct grounded-velocity write MOVES to the controller-side call site unchanged"); correct for the grounded, no-animation-table-yet state acdream is in today | A MotionTable whose locomotion cycle bakes a DIFFERENT velocity than get_state_velocity's constants would silently diverge from what the animation actually plays, since this path never touches the animation backend at all | CMotionInterp::apply_interpreted_movement 0x00528600; retire when a sink is threaded through apply_current_movement's callers (R6 root motion) | | 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 (R4-V4 note; pre-existing mechanism, row added per the V4 plan) | src/AcDream.Core/Physics/ServerControlledLocomotion.cs (PlanFromVelocity); 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 | Remote-DR gravity toggled via the Gravity STATE bit: the jump handler sets Body.State \|= Gravity at VectorUpdate and both landing blocks clear it after HitGround(); retail keeps GRAVITY set for the object's whole life and gates gravity ACCELERATION on the Contact transient (calc_acceleration) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so Motion.HitGround()'s verbatim state&0x400 gate runs BEFORE the clear) | src/AcDream.App/Rendering/GameWindow.cs (VectorUpdate jump handler + the two landing blocks) | The DR tick integrates gravity only for airborne remotes; the flag dance delivers exactly that without porting the full contact-gated calc_acceleration chain; the #161 ordering fix keeps the retail HitGround contract satisfied | Any NEW call into Motion.HitGround/LeaveGround placed after the clear silently no-ops on the gravity gate (the #161 leg-2 class); grounded remotes carry a non-retail state word (probes comparing state bits vs retail mislead) | CPhysicsObj::calc_acceleration (contact-gated); set_on_walkable 0x00511310; retire in R6 (contact-gated accel + persistent GRAVITY) |

4. Temporary stopgap (TS) — 39 rows (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)

# Divergence Where (file:line) Why it is safe / justified Risk if assumption breaks Retail oracle
TS-1 PrecipiceSlide context missing — conservative stop-at-edge instead of retail's EdgeSlide → PrecipiceSlide / CliffSlide src/AcDream.Core/Physics/TransitionTypes.cs:1254 Awaiting the next L.2c slice; a diagnostic records which ingredient (precipice context / steep plane / EdgeSlide flag) is missing Player stops dead at precipice edges where retail slides along/over — visible mismatch at cliff and roof edges retail EdgeSlide → PrecipiceSlide chain
TS-2 BspOnlyDispatch reduces retail's (HAS_PHYSICS_BSP_PS && !pvpTargetPlayer && !missileIgnore) to the flag test alone (M1.5 scope: no PK, no missiles) src/AcDream.Core/Physics/TransitionTypes.cs:660 Both omitted terms are genuinely false pre-M2; comment directs wiring them with PK (M2+) and missiles (F.3) If PK or missiles land without the terms, flagged entities get BSP-only where retail tests cyl+sphere — pass-through / wrong blocking in PvP/missile interactions FindObjCollisions pc:276861; HAS_PHYSICS_BSP_PS acclient.h:2833
TS-3 FramesStationaryFall accounting absent (moved = true unconditionally in the accepted-move branch) src/AcDream.Core/Physics/TransitionTypes.cs:3691 Explicitly deferred to the full physics port A body wedged falling-in-place never triggers retail's stuck-fall escalation — indefinite falling-animation wedges CPhysicsObj frames_stationary_fall
TS-4 Path-6 steep-poly slide-tangent shortcut: airborne hits on >FloorZ polys skip retail's SetCollide → Path-4 → ContactPlane landing chain, returning Slid in place src/AcDream.Core/Physics/BSPQuery.cs:2001 Deliberate deviation: our faithful port DID wedge (missing step_up_slide / cliff_slide details on grounded-steep); validated against the 2026-04-30 retail cdb trace (retail body didn't wedge). Filed L.5+ for retail-strict Airborne steep contact never commits Contact / lands as retail — roof-bounce trajectories, landing events, grounded-steep transitions diverge BSPTREE::find_collisions SetCollide pc:323783-323821
TS-5 CanJump always true — burden/stamina gating deferred (stat plumbing incomplete pre-M2). R3-W3 extends this row: IWeenieObject.JumpStaminaCost/PlayerWeenie.JumpStaminaCost are new (feeding jump_is_allowed's verbatim stamina-refusal branch) and are ALSO always-affordable/cost-0 stubs for the same reason src/AcDream.Core/Physics/PlayerWeenie.cs:44 (CanJump), :52 (JumpStaminaCost, R3-W3) Marked deferred; harmless until stats matter Client launches jumps retail refuses (exhausted/overburdened) — server rejection / rubber-band; divergent jump availability vs retail muscle memory CMotionInterp jump path stamina/burden inquiry; jump_is_allowed 0x005282b0 JumpStaminaCost vtable +0x44
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
TS-8 MagicUpdateEnchantment (0x02C2) records carry no StatMod — mid-session buffs don't move vital max until relog (#7/#12) src/AcDream.Core/Spells/Spellbook.cs:150 The wire parser hasn't been extended to the full ~60-64 byte Enchantment payload; PlayerDescription's block IS parsed Vitals HUD percent reads differently from retail for the whole session after any buff cast EnchantAttribute 0x00594570; holtburger 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-10 Setup lights anchored at entity root — per-light Frames not transformed through the animated part chain src/AcDream.Core/Lighting/LightInfoLoader.cs:31 Per-part world transforms aren't exposed to the lighting layer; awaiting animation hook integration A carried torch glows from the character origin, not the hand, and doesn't track swing/idle animations LightInfo.ViewSpaceLocation per-part Frame (r13 §1)
TS-11 CreateBlockingParticleHook consumed as a no-op; no sequencer implements the pause retail performs (consistent with the missing pending_motions chain, 2026-06-04 deep-dive) src/AcDream.Core/Vfx/ParticleHookSink.cs:112 Responsibility assigned to the (future) sequencer layer when the sink was written Animations retail pauses on a particle (cast/effect beats) run straight through — visual beat desynced from the effect retail sequencer blocking-particle handling (r04 §6)
TS-12 Animated entities' emitters use rest-pose part transforms anchored at entity root; retail attaches to the live animated part (per-tick refresh deferred; statics fixed by C.1.5b/#56) src/AcDream.Core/Vfx/ParticleHookSink.cs:80 (+ :20) The renderer doesn't expose per-part world transforms to VFX; root + precomputed matrices reproduce retail placement for everything that doesn't animate Effects hooked to animated parts (swinging hand, nodding head) emit from the rest pose / float at spawn offsets instead of tracking motion ParticleEmitter::UpdateParticles 0x0051d2d4
TS-13 DefaultScriptHook / DefaultScriptPartHook / CallPESHook animation hooks dropped (no OnHook case); blocker comment predates PhysicsScriptRunner (C.1.5a) and may be STALE src/AcDream.Core/Vfx/ParticleHookSink.cs:130 Originally blocked on PhysicsScript dat exposure; spawn-time DefaultScript firing landed via EntityScriptActivator, the animation-frame path never did VFX retail triggers from specific animation frames (mid-animation script calls) never appear CallPES / DefaultScript hook dispatch (r04 §6)
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-16 Click picking is Stage A only: ray-vs-fixed-radius spheres (0.71.0 m) + screen rect matched to the indicator; retail's per-polygon refine deferred (#71); rect-over-circle is a user-approved UX divergence src/AcDream.Core/Selection/WorldPicker.cs:199 Stage B only needed if visual testing surfaces Stage-A over-picks; sphere/rect + cell-BSP occlusion adequate so far Clicks near (not on) an entity still select it; fixed radii can mis-prioritize overlapping candidates vs retail's polygon-accurate test CPolygon::polygon_hits_ray 0x0054c889
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)
TS-20 GfxObj polys drawn by dictionary iteration, not DrawingBSP traversal (#113): physics/no-draw polys referenced by no BSP node render as visible surfaces; the CollectDrawingBspPolygonIds filter exists (:1004) but is NOT applied (naive walk made doors disappear, e46d3d9 un-applied, user-gated 2026-06-11) src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1027 Correct fix is full BSP-traversal-order drawing per the holistic port handoff (docs/research/2026-06-11-building-render-holistic-port-handoff.md); the id filter must first be diagnosed on a door GfxObj (Issue113PhantomStairsDumpTests) Phantom geometry visible NOW (Holtburg meeting-hall "staircase" wall ramp 0x010014C3; 8 orphan polys on hill cottage 0x01000827); draw order also diverges from retail's BSP order D3DPolyRender drawing-BSP traversal; ConstructMesh 0x0059dfa0
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.App/Input/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-23 PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always IsPlayer EdgeSlide src/AcDream.App/Input/PlayerMovementController.cs:1177 Non-PK pair walks through other non-PK players — retail's default for ACE's character-creation defaults too On a PK/PKLite character, local client lets players walk through where retail collides — prediction vs server disagree the moment PvP statuses enter play PWD._bitfield acclient.h:6431-6463; pc:406898-406918
TS-24 RawMotionState action list always empty at runtime — the packer emits num_actions (bits 1115) + per-action u16 pairs (L.2b, RawMotionState::Pack 0x0051ed10), and R3-W1 gives RawMotionState/InterpretedMotionState the retail-faithful action FIFO (AddAction/RemoveAction/ApplyMotion/RemoveMotion, src/AcDream.Core/Physics/RawMotionState.cs + MotionInterpreter.cs), but nothing calls AddAction yet — the outbound caller still builds an empty Actions list, so discrete motion events (emotes, one-shots) are still never broadcast src/AcDream.App/Rendering/GameWindow.cs:8297 (empty Actions); packer src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91; FIFO capability src/AcDream.Core/Physics/RawMotionState.cs Discrete client-initiated motions (D2) not wired yet; packer-ready, state-ready (W1), runtime emission lands with R3-W2's add_to_queue/DoInterpretedMotion population When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates RawMotionState::Pack 0x0051ed10; num_actions PackBitfield acclient.h:46487
TS-25 current_style (stance, flag bit 0x2) never populated at runtime — the packer now emits it when it differs from the retail default 0x8000003D (L.2b), but the outbound caller leaves CurrentStyle at default (stance not tracked here) src/AcDream.App/Rendering/GameWindow.cs:8286 (CurrentStyle left default); packer src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:80 Stance switching is M2 combat scope Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement RawMotionState::Pack current_style 0x0051ed10
TS-26 UpdatePosition's four u16 sequence numbers parsed but never checked for freshness; retail rejects stale/out-of-order packets. (The 0xF74C UpdateMotion side of this gap CLOSED 2026-07-02 — L.2g S1 MotionSequenceGate ports the retail instance/movement/server-control gate; the UP side + teleport/force-position stamps remain open) src/AcDream.Core.Net/Messages/UpdatePosition.cs:30 Loopback ACE rarely reorders, so the gap is invisible in the dev loop On a real network, a reordered/post-teleport straggler applies as-is — remotes snap backward / flicker; a teleport-vs-position race renders an entity in the wrong cell PositionPack trailer (ACE PositionPack.cs::Write); CPhysicsObj::newer_event 0x00451b10
TS-27 Retransmit handling absent: RetransmitRequests/RejectRetransmit parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) src/AcDream.Core.Net/WorldSession.cs:29 Deferred since the one-shot test harness; dev loop is loopback (no loss) On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1
TS-28 LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30 acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client retail post-EnterWorld flow (holtburger 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 Numbered chat tabs (element ids 0x100005220x10000525) render as clickable buttons but do not switch channel filter or affect the transcript — tab state is a no-op src/AcDream.App/UI/Layout/ChatWindowController.cs:210 Retail's tab switching routes transcript lines by chat channel (gmMainChatUI::gmScrollWindow sub-windows per tab); the tab wiring is D.5 scope Tab clicks produce no visible transcript change; retail would filter to the selected channel — all chat always shows in all tabs gmMainChatUI::PostInit tab setup @0x4ce2a0; holtburger chat tab handling
TS-31 Squelch toggle absent (no /squelch slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines src/AcDream.Core/Chat/ChatLog.cs Squelch is a social / moderation feature deferred to post-M1.5; the data structure (ChatLog) has no squelch set today Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent 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 Outbound position-send tracker over-stamped after MoveToState: NotePositionSent writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's SendMovementEvent (0x006b4680) stamps ONLY last_sent_position_time after an MTS — only SendPositionEvent (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (HeartbeatDue) where retail uses ShouldSendPositionEvent (0x006b45e0 — interval OR cell-change OR contact-plane-change), AND acdream's frame-changed diff compares POSITION only (ApproxPositionEqual) where retail's Frame::is_equal compares the full frame incl. ORIENTATION — a stationary heading change (R4-V5: the MoveToManager's HandleTurnToHeading arrival snap, set_heading(send:true)) never triggers an AP, so the server keeps the stale facing until the player next moves. Masked against ACE (ACE rotates server-side on its own mt-8/9 / close-range-use paths and broadcasts the result) src/AcDream.App/Rendering/GameWindow.cs:8331 (MTS NotePositionSent); src/AcDream.App/Input/PlayerMovementController.cs (ApproxPositionEqual + the heartbeat diff); the player MoveToManager setHeading seam in EnterPlayerModeNow (the send flag's would-be consumer) D5 audit-only in the L.2b wire-parity slice; the full cadence port (ShouldSendPositionEvent gate + split MTS/AP stamping + full-frame Frame::is_equal diff) is a dedicated follow-up slice (R7 outbound) AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends, and a stationary server-commanded turn leaves observers with stale facing until the next movement; on a real network this shifts the position-correction rhythm CommandInterpreter::SendMovementEvent 0x006b4680, SendPositionEvent 0x006b4770, ShouldSendPositionEvent 0x006b45e0, Frame::is_equal (pc:700263)
TS-40 Retail's physics_obj->cell ("placed in the world") is proxied by the explicit PhysicsBody.InWorld flag — set by SnapToCell (local player placement) and RemoteMotion construction (remotes exist only for world entities); consumed by CMotionInterp's detached-object link-strip guards (if (cell == 0) RemoveLinkAnimations, raw @305627). Replaces the UNREGISTERED CellPosition.ObjCellId == 0 proxy, which only the local player ever seeded (#145 SnapToCell), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug src/AcDream.Core/Physics/PhysicsBody.cs (InWorld); src/AcDream.Core/Physics/MotionInterpreter.cs (3 guard sites) acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag CMotionInterp::DoInterpretedMotion 0x00528360 tail @305627; CPhysicsObj::RemoveLinkAnimations
TS-35 PhysicsBody.IsFullyConstrained is a stub property (default false, never set by any physics code), read by jump_is_allowed's verbatim IsFullyConstrained gate (raw 305524-305525) src/AcDream.Core/Physics/PhysicsBody.cs (IsFullyConstrained) R3-W3 needed the read site to port jump_is_allowed's full chain. R5-V1 CORRECTED the mechanism (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the ConstraintManager server-position rubber-band leash — armed by SmartBox::HandleReceivedPosition on every inbound server position, IsFullyConstrained = max*0.9 < offset. R5-V1 ported ConstraintManager (src/AcDream.Core/Physics/Motion/ConstraintManager.cs) but does NOT arm it (no acdream SmartBox + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) CPhysicsObj::IsFullyConstrained 0x0050ec60 → ConstraintManager::IsFullyConstrained 0x005560d0; jump_is_allowed 0x005282b0; arming SmartBox::HandleReceivedPosition 0x00453fd0 (issue #167)
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
TS-41 Grounded remote NPCs WITHOUT an armed moveto are body-driven by UP-synthesized server velocity (HasServerVelocity → the SERVERVEL per-tick leg: Body.Velocity = ServerVelocity, MoveToManager.UseTime SKIPPED, stale-decay stop via ApplyServerControlledVelocityCycle(Zero)); retail has no wire-velocity leg-driver anywhere — MovementManager::UseTime runs UNCONDITIONALLY per tick and between-UP translation comes from the motion state (get_state_velocity), UPs only hard-snap. #170 residual fix narrowed this branch: an ARMED moveto (MovementTypeState != Invalid) now always takes the MOVETO leg — the old arbitration starved the verbatim MoveToManager for exactly the duration of a server-side chase (UPs flowing → UseTime never ran → legs stayed Ready while the body glided = the #170 slide; live funnel 16 arms → 1 run install). R5-V3 (#171) narrowed it again: a STUCK entity (PositionManager.GetStickyObjectId() != 0) also takes the MOVETO leg — after the sticky arrival the moveto is cleaned (Invalid) but StickyManager::adjust_offset owns the between-snap translation; SERVERVEL would glide the body against the sticky steer (same starvation class) src/AcDream.App/Rendering/GameWindow.cs (TickAnimations grounded NPC branch, moveToArmed+stickyArmed gate) ACE moves some entities by position updates alone (scripted paths, missiles) with no UM/moveto stream — without a velocity fallback they freeze between UPs; entities WITH a moveto now get the retail drive An entity class that carries BOTH wire velocity and an armed moveto with conflicting truths follows the moveto; UP hard-snaps bound the drift. Non-moveto entities keep the non-retail stale-stop heuristics (AP-80 thresholds) CPhysicsObj::UpdateObjectInternal 0x005156b0 (MovementManager::UseTime call @0x00515998, unconditional); retire in R6 (full per-tick order)
TS-42 Per-tick DRAIN ORDER inverted vs retail: acdream's TickAnimations runs HandleTargettingMoveTo.UseTime FIRST and the animation-completion drain (Sequencer.Advance → AnimDone hooks → MotionTableManager.AnimationDone/UseTimeCMotionInterp.MotionDone pops) LAST, so every motion-completion-gated decision (BeginTurnToHeading's motions_pending wait) sees a queue that is one frame STALE — the unblock after a stop/swing lands one frame later than retail. Retail order (pinned from the named decomp this session): UpdatePositionInternal (CPartArray::Update + process_hooks @0x00512d3d — the drain) runs BEFORE TargetManager::HandleTargetting @0x00515989 → MovementManager::UseTime @0x00515998 → CPartArray::HandleMovement @0x005159a4 (zero-tick sweep) in UpdateObjectInternal src/AcDream.App/Rendering/GameWindow.cs (TickAnimations per-entity phase order; the R2-Q4 comment already marks the placement "provisional until R6") Bounded to exactly ONE frame (~16 ms) of extra latency per completion-gated event; every queue eventually drains identically (RemoteChaseEndToEndHarnessTests conformance) Motion-completion-gated transitions (chase turn start, post-swing re-arm) systematically lag retail by one frame; under compound churn the lag can cost an extra retry cycle CPhysicsObj::UpdateObjectInternal 0x005156b0 + UpdatePositionInternal 0x00512c30 (process_hooks @0x00512d3d); retire in R6 (retail UpdateObjectInternal order)
TS-44 NPC UpdatePosition hard-snaps (position @OnLivePositionUpdated + orientation + velocity/cycle adoption) are SUPPRESSED while the entity is stuck (PositionManager.GetStickyObjectId() != 0) — an adaptation of retail's chain semantics to the legacy snap path: retail routes UP corrections through the InterpolationManager into the SAME per-tick PositionManager::adjust_offset chain where StickyManager::adjust_offset OVERWRITES them while armed (0x00555190 order, 0x00555430 assigns m_fOrigin), so a server correction can never fight an armed stick; the legacy NPC path snaps OUTSIDE the chain, producing snap-out/steer-back position flapping + stale-facing stomps (the 2026-07-04 #171 gate residuals). Bookkeeping (LastServerPos/Time, cell) still records; server truth reasserts on the first UP after unstick, bounded by the 1 s sticky lease src/AcDream.App/Rendering/GameWindow.cs (OnLivePositionUpdated NPC section, snapSuppressedByStick gate) Retail's mechanism (sticky-overwrites-interp) is unreachable until the NPC path gets the interp-queue architecture (the player-remote branch already has it — the R5-V3 combiner→sticky chain); the gate reproduces the retail-observable behavior on the snap architecture A stick that stays armed while ACE moves the monster far (shouldn't happen — sticks follow the target by construction) would drift until unstick+next UP; worst case bounded by the 1 s lease + the next UM re-arm PositionManager::adjust_offset 0x00555190; InterpolationManager UP routing (CPhysicsObj::MoveOrTeleport); retire when the NPC path unifies onto the interp queue (S6/R6)
TS-43 Remote teleport has no teleport_hook equivalent: retail tears down the position managers on every teleport (CPhysicsObj::teleport_hook 0x00514ed0 — CancelMoveTo(0x3c) @0x00514edf, PositionManager::UnStick @0x00514eee, StopInterpolating/UnConstrain); acdream's remote teleport is a bare UP hard-snap, so a stuck/chasing remote that the server teleports keeps its stick/moveto for up to the 1 s sticky lease / next UM. The LOCAL player side IS wired (R5-V3: PlayerMovementController.SetPositionPositionManager.UnStick; the moveto cancel was already there via StopCompletely; the teleport-arrival site also fires the hook's tail — EntityPhysicsHost.NotifyTeleported = TargetManager::ClearTarget + NotifyVoyeurOfEvent(Teleported) @0x00514f1b-0x00514f28, which is what makes mobs stuck to the player drop their sticks on a recall) src/AcDream.App/Rendering/GameWindow.cs (OnLivePositionUpdated — no teleport-flag manager teardown for remotes) Remote teleports are rare (recalls/summons); the sticky 1 s lease + UP hard-snaps self-correct within a second; wiring it properly wants the UP teleport-stamp plumbing (TS-26's stamp work) A teleported-away attacker briefly steers toward its pre-teleport target from the new location (≤1 s) before the lease/next-UM corrects it CPhysicsObj::teleport_hook 0x00514ed0; retire with the TS-26 UP-stamp port

5. Unclear (UN) — 5 rows

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-3 AdminEnvirons fog-override RGB tints hardcoded with no retail constant cited (RedFog 0.60/0.05/0.05 etc.); Snapshot replaces fog COLOR only, keeping keyframe distances on an unverified assumption src/AcDream.Core/World/WeatherState.cs:350 Enum semantics cite ACE EnvironChangeType + r12 §5.2; no source for the RGB values or the color-only override scope A server-forced fog event renders the wrong hue and/or wrong density vs what retail clients showed for the same packet AdminEnvirons 0xEA60; ACE EnvironChangeType.cs
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.

  1. TS-20 — GfxObj DrawingBSP traversal (#113) — phantom geometry is visible in Holtburg RIGHT NOW; the holistic port handoff already specs the fix; first diagnose the id filter against a door GfxObj.
  2. TS-27 — Retransmit handling — sole hard blocker for any non-loopback play; failure mode is silent permanent stalls (entities never spawn). Also fix the stale class-doc gap list while there.
  3. TS-4 — Path-6 steep slide-tangent shortcut — landing/contact state diverges on every airborne-steep hit; the L.5+ retail-strict followup is already filed with the missing-ingredient analysis.
  4. 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).
  5. TS-1 — PrecipiceSlide stop-at-edge — visible movement mismatch at every cliff/roof edge; diagnostic already records which ingredient is missing.
  6. TS-26 — Position sequence freshness — real-network correctness; pairs naturally with TS-27 in one transport-hardening pass.
  7. 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.
  8. 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.
  9. TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12) — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
  10. TS-13 — CallPES/DefaultScript animation hooks — the blocker comment is stale since C.1.5a shipped PhysicsScriptRunner; possibly a cheap wire-up now.
  11. UN-3 — AdminEnvirons tints — invented RGB constants + unverified color-only scope; one decomp lookup against the 0xEA60 handler.
  12. 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-2 (BspOnlyDispatch terms), TS-5 (CanJump gating), TS-23 (PK bits), 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 animation-hook layer lands TS-10/TS-11/TS-12/TS-13/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).