The register's UN-2 row recorded a contradiction: the GetMaxSpeed XML doc claimed the bare run rate was retail-correct (~5.9 m/s catch-up, calling the xRunAnimSpeed multiply a misread), while the implementation multiplied by RunAnimSpeed citing ACE. Settled against the binary, not the pseudo-C: - BN pseudo-C (acclient_2013_pseudo_c.txt:305127) renders get_max_speed as void with a bare `this->my_run_rate;` because it DROPS x87 instructions. - Disassembling the PDB-matched v11.4186 binary at VA 0x00527cb0: all THREE return paths end `fld <rate>; fmul dword ptr [0x007C8918]; ret`, and the .rdata dword at 0x007C8918 is 4.0f. Sibling get_adjusted_max_speed (0x00527d00) carries the same trailing fmul. Verifier committed at tools/verify_un2_fmul.py (PE parse + byte decode, rerunnable). - Retail paths: weenie null -> 1.0 x4; InqRunRate ok -> queried x4; InqRunRate failed -> my_run_rate x4. ACE MotionInterp.cs:665-676 matches. Changes: - Doc-comment rewritten: the implementation is retail-correct; the catch-up speed 2 x get_max_speed ~= 23.5 m/s at run 200 IS retail. The 1-Hz remote-blip symptom the old comment attributed to this multiply is therefore UNEXPLAINED by it (if it recurs: #41 family, not this). - Weenie-null path aligned to retail's LITERAL 1.0 default (was MyRunRate). - Tests re-pinned to the three retail paths (the old NoWeenie test pinned the non-retail fallback). - Register: UN-2 row deleted per the retire rule (6 -> 5 UN rows); shortlist renumbered. This is the 2nd confirmed instance of the BN x87-dropout artifact class (memory: feedback_bn_decomp_field_names) deciding a register row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
67 KiB
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) — 14 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 |
2. Adaptation (AD) — 27 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: terrain-ready hold (#106) + indoor cell-hydration hold (#107, IsSpawnCellReady); claims beyond NumCells skip the gate (demoted) |
src/AcDream.App/Rendering/GameWindow.cs:1008 (+ 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 | Gate opens early → raw claim commit → outdoor demote mid-building; predicate never satisfied (streamer stall, dat edge case) → login wedges in pre-player mode | 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_objects → recalc_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 invented PunchMarkDepthBias = 0.0005 NDC; 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 (hills, closer buildings), painting interiors through them; the two-pass form is the z-buffered equivalent of retail's ordering safety. DO-NOT-RETRY: punch must stay depth-gated (ISSUES #108) | Bias is depth-dependent: an occluder within ~bias in front of a distant aperture gets punched through; door-plane-hugging geometry just beyond it re-occludes the aperture (a #108-class regression) | D3DPolyRender::DrawPortalPolyInternal 0x0059bc90 (maxZ1=7 / maxZ2=6) |
| AD-19 | Under outdoor roots, ALL dynamics draw in one z-buffered final pass; retail draws objects painter-ordered per landcell inside the landscape pass (interior roots route per #118) | src/AcDream.App/Rendering/RetailPViewRenderer.cs:126 |
The dynamics-drawn-LAST invariant is what makes the aperture depth punch safe (first BR-2 attempt punched after dynamics and erased the player, reverted 88be519); z-buffer substitutes for painter's order on opaque geometry |
Punch/seal correctness hinges on an ordering invariant — any pass added after DrawDynamicsLast, or alpha content needing painter order, gets erased inside apertures or composites wrong | LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124; PView::DrawCells 0x005a4840 |
| AD-20 | Camera sweep fallback seeds the eye's AdjustPosition from the PLAYER's cell; retail re-seats at the sought eye's own tracked cell (rest of function is a verbatim update_viewer port) |
src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:97 |
acdream's camera doesn't track the sought-eye's cell separately; the eye is near the player so the player-cell stab list is assumed to cover it | An eye outside the player cell's stab-list coverage (boundary corners, cross-landblock pull-back) seats in the wrong cell — and the viewer cell roots the whole render: one-frame wrong root (flap-class flash) | SmartBox::update_viewer 0x00453ce0, pc:92878-92883 |
| AD-21 | Null-clipRoot legacy outdoor safety path (no portal visibility, no punches/seals, no-clip terrain) for pre-spawn / login / legacy cameras; in-world retail always has a viewer_cell root | src/AcDream.App/Rendering/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 |
3. Documented approximation (AP) — 31 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 CheckPositionInternal → find_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 ~80–350 m) when the Region dat isn't loaded yet | src/AcDream.Core/World/SkyState.cs:167 |
A renderable sky is needed during boot before the Region dat parses; safety net on region-load failure | Any window where the fallback is active shows sky/fog lighting only roughly resembling retail's dat-driven values | SkyTimeOfDay keyframes, Region dat 0x13000000 |
| AP-12 | Enchantment family-stacking tiebreak by largest SpellId; retail picks highest Generation, tie-broken by latest cast | src/AcDream.Core/Spells/EnchantmentMath.cs:89 |
ActiveEnchantmentRecord doesn't carry Generation; SpellId correlates with generation level in practice |
Where spell ids don't track power within a family (or same-generation re-cast), the wrong buff wins — vital-max / stat values diverge from retail | CEnchantmentRegistry::EnchantAttribute 0x00594570 (pc:416110) |
| AP-13 | ComputeDamage is a simplified retail damage formula (no augmentations/ratings) — verified DEAD CODE as of 2026-06-04, M2 scaffolding |
src/AcDream.Core/Combat/CombatModel.cs:184 |
Not on the critical path; stubbed from r02 §5 + ACE CombatManager for the future M2 predictive display | If wired into the M2 attack-bar estimate as-is, predicted numbers diverge whenever augs/ratings apply | r02 §5; ACE CombatManager |
| AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | src/AcDream.Core/Items/ItemInstance.cs:187 |
Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) |
| AP-15 | WeenieError 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 with 10% range slack (own r13 design); retail bound D3D lights per object/cell | src/AcDream.Core/Lighting/LightManager.cs:10 |
Honors retail's 8-hardware-light constraint while fitting a global-uniform shader; 1.1 slack is anti-pop hysteresis | 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); pop thresholds differ | 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 1–2 px sliver aperture collapses to degenerate and rejects — a thin/distant doorway stops admitting its flood slightly earlier than retail | Render::copy_view 0x0054dfc0 |
| AP-21 | Entity translucency: two-pass alpha-test (N.5 Decision 2, invented 0.95/0.05 thresholds); AlphaBlend + Additive + InvAlpha all composite under (SrcAlpha, 1−SrcAlpha) — 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_collisions → CPartArray::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 |
4. Temporary stopgap (TS) — 30 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.7–1.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 | UI panels drawn as flat translucent rectangles + 1 px border; retail composes 9-slice dat sprite backgrounds via LayoutDesc trees | src/AcDream.App/UI/UiPanel.cs:10 |
Development visibility until the D.2b retail-look toolkit consumes the dat assets | Purely visual until D.2b — but pixel-position assumptions built against the placeholder (hit regions, layout constants) may not survive the swap to retail sprite metrics | RenderSurface 0x06xxxxxx 9-slice; LayoutDesc 0x21xxxxxx |
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) |
6. Retire-next shortlist
Temporary-stopgap + unclear rows, ordered by risk (symptom severity × likelihood the guarding assumption breaks). Items below the line are phase-gated — they carry their trigger in their row and should land WITH that phase, not before.
- TS-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.
- 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.
- 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.
- 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.
- 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).
- TS-1 — PrecipiceSlide stop-at-edge — visible movement mismatch at every cliff/roof edge; diagnostic already records which ingredient is missing.
- TS-22 — adjust_motion port — active bug-class generator: any new
get_state_velocityconsumer during backward/strafe silently gets zero velocity. - TS-26 — Position sequence freshness — real-network correctness; pairs naturally with TS-27 in one transport-hardening pass.
- UN-6 — 200 ms ConnectResponse sleep — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check.
- UN-4 — GfxObj sides/negative-surface logic — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it.
- TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12) — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
- TS-13 — CallPES/DefaultScript animation hooks — the blocker comment is stale since C.1.5a shipped PhysicsScriptRunner; possibly a cheap wire-up now.
- UN-3 — AdminEnvirons tints — invented RGB constants + unverified color-only scope; one decomp lookup against the 0xEA60 handler.
- 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). D.2b lands TS-30; 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).