acdream/docs/architecture/retail-divergence-register.md
Erik 6b562ad077 docs: file #140 (Fix D — outdoor objects too bright near torches) + register UN-7
A7 lighting Fix A/B/C shipped this session; Fix D (object torch over-brightness)
grounded but blocked on the render-path capture. Filed as #140 + divergence
register UN-7 (object point-light model unconfirmed). Detail in the 2026-06-18
handoff doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:37:02 +02:00

83 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) — 15 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-4 MultiplyFramerate omits retail's negative-factor StartFrame↔EndFrame swap (direction encoded in Framerate sign instead) src/AcDream.Core/Physics/AnimationSequencer.cs:129 Our callers (ForwardSpeed updates) only pass positive factors; Advance loop handles negative framerates against StartFrame as lower bound A future negative-factor caller (reverse playback) scales without swapping bounds — wrong frame range traversal instead of clean reversal FUN_005267E0; ACE Sequence.cs L277-287
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 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)

2. Adaptation (AD) — 28 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) src/AcDream.App/Rendering/GameWindow.cs (isSpawnGroundReady lambda ~1010 + TeleportArrivalReadiness ~5012) (+ 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-7 Full collision exemption on ETHEREAL alone; retail requires ETHEREAL_PS and IGNORE_COLLISIONS_PS (ETHEREAL-alone takes the unported obstruction_ethereal path) src/AcDream.Core/Physics/CollisionExemption.cs:78 ACE's Door.Open() broadcasts ETHEREAL only (0x0001000C); without the shortcut, opened doors stay solid on ACE ETHEREAL-only targets generate NO contact where retail records contact-but-allows-passage; against a retail-semantics server the bit means something different than we implement pc:276782 (combined gate), :276795 (obstruction_ethereal)
AD-8 MoveTo arrival gate max(minDistance, distanceToObject); retail tests dist <= min_distance only src/AcDream.Core/Physics/RemoteMoveToDriver.cs:161 ACE ships the threshold in distance_to_object with min_distance == 0; without the max, monsters never "arrive" and oscillate at melee range (user-reported 2026-04-28) A server using both wire fields with retail semantics + large distance_to_object makes remotes stop short of the retail arrival point MoveToManager::HandleMoveToPosition chase-arrival
AD-9 1.5 s stale-destination give-up timer on remote MoveTo (retail's MoveToManager runs until cancelled) src/AcDream.Core/Physics/RemoteMoveToDriver.cs:136 Liveness guard sized to ACE's ~1 Hz re-emit cadence; prevents steering toward a stale destination after a missed cancel (the run-in-place symptom) A server emitting MoveTo slower than ~1.5 s makes remotes freeze mid-chase and snap later instead of steering continuously MoveToManager (no equivalent timeout)
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:1212 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-26 Auto-walk arrival requires facing alignment (invented 5° arrive / 30° walk-while-turning bands); retail's check is dist <= radius exact src/AcDream.App/Input/PlayerMovementController.cs:575 ACE does the final Rotate(target) server-side before the Use callback; without a local gate the body used items while facing away (user feedback 2026-05-15). Thresholds are NOT retail constants Arrival delayed by the rotation phase; if heading convergence fights another yaw writer, AutoWalkArrived never fires and the queued Use/PickUp never completes MoveToManager::HandleMoveToPosition; apply_interpreted_movement
AD-27 Use/PickUp action re-sent on natural auto-walk arrival; retail sends the action once (server MoveToChain callback completes it) src/AcDream.App/Input/PlayerMovementController.cs:322 ACE's server-side chain may have timed out by the time our body arrives; the close-range re-send hits ACE's WithinUseRadius fast-path 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
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

3. Documented approximation (AP) — 41 rows

# 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-2 Visual-AABB fallback collision shape for Setups with no retail physics data; retail emits NO shapes (phantom). #101 fixed the GfxObj-only class; the Setup-without-shapes fallback remains src/AcDream.Core/Physics/PhysicsDataCache.cs:96 Lets the player collide with decorative meshes shipping no CylSphere/part-BSP instead of walking through furniture-like props Retail-phantom entities block movement (the #100/#101 family), and the synthetic box gives non-retail push-out normals when it collides CPartArray::InitParts (cited at PhysicsDataCache.cs:386-389)
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-8 Remote MoveTo driver is a minimum viable subset: no target re-tracking, no sticky/StickTo, no fail-distance detector, no sphere-cylinder distance variant src/AcDream.Core/Physics/RemoteMoveToDriver.cs:44 All server-side concerns the local body needn't model; ACE re-emits MoveTo ~1 Hz with refreshed origins, substituting for re-tracking If the re-emit cadence assumption breaks (or sticky-follow packets appear), chase/flee motion visibly diverges — orbiting, overshoot, giving up where retail tracks MoveToManager::HandleMoveToPosition 0x00529d80
AP-9 Fixed π/2 rad/s in-motion turn rate; per-creature TurnSpeed unwired src/AcDream.Core/Physics/RemoteMoveToDriver.cs:77 Matches ACE's monster TurnSpeed default; field hook documented for the future port Creatures with non-default turn speeds rotate at the wrong rate — facing-correction mismatch vs retail observers run_turn_factor 0x007c8914; apply_run_to_command 0x00527be0
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 Global nearest-8 viewer-distance light selection (own r13 design); retail bound D3D lights per object/cell. NO viewer-range candidacy filter — each light's range cutoff is applied per-surface in the shader (the earlier Range²×1.1 slack filter was removed; it dropped torches the viewer stood outside, the #133 "lighting off" report) src/AcDream.Core/Lighting/LightManager.cs:10 Honors retail's 8-hardware-light constraint while fitting a global-uniform shader; nearest-8 is an allocation-free partial-select (no per-frame list/sort) With >7 nearby lights, different objects are lit than retail would light (retail's per-object pick can light a far object by ITS nearest lights) r13 §12.2 (acdream design); retail D3D 8-light constraint
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 + speculative turn-to-target src/AcDream.App/Rendering/GameWindow.cs:11120 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 (B.6) A target whose real UseRadius differs from the bucket misjudges the gate — Use/PickUp deferred for an auto-walk that never comes, or fires early into a server "too far" ACE Player_Move.cs:66; wire MoveToObject (type 6) carries the true radius
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:170 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:1541 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) src/AcDream.App/Rendering/GameWindow.cs:6895 (per-frame predicate) + :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-35 Point/spot lights use a single PER-PIXEL accumulation that ports calc_point_light's (1 dist/falloff_eff) LINEAR ramp (falloff_eff = Falloff × static_light_factor 1.3) + standard Lambert N·L; retail's path is PER-VERTEX Gouraud and additionally applies a half-Lambert wrap (0.5·dist + N·L_vec, lights surfaces down to N·L ≥ 0.5) and an x87-obscured normalization factor, neither ported src/AcDream.App/Rendering/Shaders/mesh_modern.frag:52 (+ mesh.frag; LightInfoLoader.cs:81 folds 1.3 into Range) The linear ramp is the user-visible fix (kills the hard-disc "spotlight" edge, #133 A7); the dropped wrap/normalization only re-shade the gradient slightly, and per-pixel vs per-vertex Gouraud chiefly differs on coarse geometry. Half-Lambert wrap + factor are an x87-decompile refinement (same artifact class as GetPowerBarLevel AP-24) Surfaces facing slightly away from a torch (0.5 ≤ N·L < 0) stay dark where retail's wrap lights them faintly; near-light gradient shading differs subtly from retail's per-vertex bake calc_point_light 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); 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

4. Temporary stopgap (TS) — 31 rows

# 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) src/AcDream.Core/Physics/PlayerWeenie.cs:44 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
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; "we don't parse yet" comment is STALE (K-fix7 parses PD → SetCharacterSkills) src/AcDream.App/Input/PlayerMovementController.cs:341 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-22 adjust_motion not ported — backward (×0.65) / strafe (×1) translation hand-mirrored at controller call sites; get_state_velocity returns (0,0,0) for backward/strafe-left src/AcDream.App/Input/PlayerMovementController.cs:1021 Duplication exists because LeaveGround through the unported path wiped strafe/backward jump velocity (straight-up backward jumps) Any NEW get_state_velocity consumer during backward/strafe motion silently gets zero velocity (the exact prior bug class); hand-mirrored formulas can drift from the grounded block they copy FUN_00528010 (adjust_motion); FUN_00528960
TS-23 PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always IsPlayer EdgeSlide src/AcDream.App/Input/PlayerMovementController.cs:1128 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 command list always empty (bits 11-31 = 0) — discrete motion events (emotes, one-shots) never packed outbound src/AcDream.Core.Net/Messages/MoveToState.cs:34 Discrete client-initiated motions aren't implemented yet; documented builder scope When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates RawMotionState pack (holtburger types.rs)
TS-25 FlagCurrentStyle (stance, bit 0x2) never written to outbound MoveToState src/AcDream.Core.Net/Messages/MoveToState.cs:130 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 RawMotionFlags CurrentStyle 0x2 (holtburger)
TS-26 UpdatePosition's four u16 sequence numbers parsed but never checked for freshness; retail rejects stale/out-of-order packets 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)
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

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-5 Run multiplier applied to backward (and strafe) speed while the wire reports speed 1.0; the 0.65 backward factor IS retail's, the runMul on top is justified only by feel ("~2.4× ratio felt wrong"); strafe cites holtburger, backward cites nothing src/AcDream.App/Input/PlayerMovementController.cs:909 Feel fix (K-fix3); no retail citation for run-scaling backward movement If retail does NOT run-scale backward, the local body moves up to ~2.4× faster backward than the wire declares — observers dead-reckon slower and see lag/teleport when backing up at run adjust_motion FUN_00528010 (0.65 only); holtburger common.rs (sidestep)
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-5 — Backward/strafe run multiplier — potential ~2.4× local-vs-wire speed mismatch on a common input (S at run); one cdb session against retail answers it.
  5. 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).
  6. TS-1 — PrecipiceSlide stop-at-edge — visible movement mismatch at every cliff/roof edge; diagnostic already records which ingredient is missing.
  7. TS-22 — adjust_motion port — active bug-class generator: any new get_state_velocity consumer during backward/strafe silently gets zero velocity.
  8. TS-26 — Position sequence freshness — real-network correctness; pairs naturally with TS-27 in one transport-hardening pass.
  9. 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.
  10. 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.
  11. TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12) — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
  12. TS-13 — CallPES/DefaultScript animation hooks — the blocker comment is stale since C.1.5a shipped PhysicsScriptRunner; possibly a cheap wire-up now.
  13. UN-3 — AdminEnvirons tints — invented RGB constants + unverified color-only scope; one decomp lookup against the 0xEA60 handler.
  14. 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).