acdream/docs/architecture/retail-divergence-register.md
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

176 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) — 17 rows

# Divergence Where (file:line) Why it is safe / justified Risk if assumption breaks Retail oracle
IA-1 Contact-plane pre-seed on grounded movers (#96 ACCEPTED per ISSUES.md) — retail's CTransition::init clears contact_plane_valid; we seed from the body's previous-frame plane src/AcDream.Core/Physics/PhysicsEngine.cs:919 Removing it broke last-step stair step_up (892019b, reverted); seed propagates the body's real current plane, behavior matched retail in the A6.P3 gates A stale pre-seeded plane lets AdjustOffset project sub-step 1 onto a plane retail wouldn't have yet — wrong slope motion / step-up acceptance right after leaving a surface CTransition::init, pc:272547 family
IA-2 Lateral self-heal beyond retail's keep-curr: when no candidate contains the sphere, try FindVisibleChildCell over the claim's stab-list before keeping the claim src/AcDream.Core/Physics/CellTransit.cs:912 Reuses the recovery retail's own AdjustPosition performs (:280028 stab-list mode), applied at the find_cell_list site to heal near-miss claims without a doorway crossing In containment-gap geometry, membership flips to a neighbouring room where retail keeps curr — wrong render root / collision cell at gap positions find_cell_list keep-curr pc:308788-308825; find_visible_child_cell :311444
IA-3 NARROWED 2026-07-17 — get_state_velocity may prefer a nonzero dat cycle velocity (MotionData.Velocity × speedMod) over the decompiled constant. Production grounded player/remote translation no longer consumes this value; both use the literal CSequence root Frame. The accessor remains observable for jump launch and headless/test fallbacks src/AcDream.Core/Physics/MotionInterpreter.cs (get_state_velocity); grounded owners in PlayerMovementController / RemotePhysicsUpdater Installed Humanoid Walk/Run MotionData velocity is zero, so the retail constants remain the jump/fallback result. A nonzero exotic/modded cycle may override them, preserving the earlier adapter contract without affecting ordinary grounded motion Jump horizontal speed for an exotic MotionTable with nonzero authored velocity can differ from the retail binary's constants CMotionInterp::get_state_velocity 0x00527D50; CPhysicsObj::UpdatePositionInternal 0x00512C30
IA-5 Per-ENTITY vertex-derived AABB culling (+5 m animated-drift margin; animated entities bypass cull) vs retail per-PART dat drawing spheres src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:693 (bounds at src/AcDream.Core/World/WorldEntity.cs:153, src/AcDream.Core/Meshing/GfxObjBounds.cs:14; dead PerEntityCullRadius=5.0f at dispatcher :210) Batched MDI rendering can't cheaply cull per part; bounds derive from the SAME dat vertex data that gets drawn (containment by construction — the #119 fix, 6a9b529; memory: feedback_culling_bounds_from_drawn_data) Geometry escaping bounds+margin (pose drift >5 m, a hydration path skipping SetLocalBounds) makes the whole entity vanish on-screen — the #119 vanishing-staircase class CGfxObj.drawing_sphere / viewconeCheck 0x005a09a4
IA-6 Chat scrollback 500 lines vs retail ~200 (configurable) src/AcDream.Core/Chat/ChatLog.cs:19 Strictly more useful for a dev client + plugins; deliberate default Negligible — only if a plugin/UI behavior is ever specified against retail's exact retention cap retail chat scrollback (~200)
IA-8 Synthetic outdoor cell node as render root (outdoor-as-cell, Option A): one unified DrawInside path; retail roots at a real CLandCell with a separate outdoor pipeline src/AcDream.App/Rendering/OutdoorCellNode.cs:23 Eliminating the inside/outside render branch kills the indoor FLAP by construction (2026-06-07 cutover); R-A2 restored retail's per-building flood topology Any consumer assuming the root is a real cell mis-handles the synthetic node — historically the 2↔6 flood-depth oscillation and doorway-flap class SmartBox::RenderNormalMode → DrawInside, decomp:92635; LScape::draw 0x00506330; ConstructView(CBldPortal) decomp:433827
IA-9 One unified camera matrix for terrain — retail's separate LScape::update_viewpoint landscape viewpoint does not exist src/AcDream.App/Rendering/TerrainModernRenderer.cs:266 Phase W T4.2: with one matrix everywhere, viewpoint-desync bugs are unrepresentable — the unification IS the correctness argument Anything retail derives from the landcell-relative viewpoint (float precision at extreme coords, viewpoint-keyed state) has no analogue; a future port expecting it silently reads the camera LScape::update_viewpoint; LScape::draw 0x00506330
IA-10 Transparent groups sorted back-to-front per GROUP by first-instance position (no within-group sort) vs retail per-poly BSP-order draw src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:1364 (comparer :1662) One MDI call per pass requires group-granularity ordering; per-poly sorting is incompatible with instanced multi-draw; works when group instances are spatially coherent Spatially spread or interleaved transparent groups composite in the wrong order — popping / wrong see-through layering as the camera moves retail per-poly BSP-order transparent draw (D3DPolyRender / PView::DrawCells)
IA-11 Tier-1 cross-frame batch-classification cache for static entities (retail re-walks part arrays every frame) src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs:12 Issue #53 perf tier; invariants documented (keys = EntityId + OWNING-landblock hint post-#119 fix 2163308; invalidation at despawn/LB-unload; mutation audit 2026-05-10) Key collision or missed invalidation serves one entity another's batches — session-sticky wrong meshes (the #119 broken-stairs/water-barrel symptom) retail per-frame part-array classification (no cache)
IA-12 UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms). Synthetic wrapper borders/whole-window drag regions use the exact DAT Type-2/Type-9 control cursors. gmPanelUI children are independently imported retained frames rather than one physical parent, but RetailPanelUiController owns their one canonical geometry and exclusive child lifecycle src/AcDream.App/UI/README.md:3; src/AcDream.App/UI/CursorFeedbackController.cs; src/AcDream.App/UI/Layout/RetailPanelUiController.cs keystone.dll has no PDB/decomp; semantics are reconstructed from retail UI deep-dives, named client methods, and production LayoutDesc media. Separate child wrappers preserve each LayoutDesc's content tree while typed move/resize synchronization gives all registered toolbar/detail children the same persistent parent rectangle Edge-case low-level input semantics can differ silently even though outer geometry, visibility, and restore-previous ownership match UIElementManager::CheckCursor 0x0045ABF0; UIElement_Resizebar::StartMouseResizing 0x0046B7E0; UIElement_Dragbar::StartMouseMoving 0x0046C760; gmPanelUI::SetupChildren 0x004BC9E0; docs/research/retail-ui/04-input-events.md
IA-13 GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled src/AcDream.Core.Net/Messages/GameEventType.cs:11 Retail also ignores them — dropping matches retail by construction If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission retail GameEvent dispatch (ignored-event set)
IA-14 Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) docs/architecture/worldbuilder-inventory.md (code at src/AcDream.{Core,App}/Rendering/Wb/) WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail FSplitNESW (#51, pinned by SplitFormulaDivergenceTest), scenery drift accepted (AP-31) A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests retail decomp per algorithm; tests/.../SplitFormulaDivergenceTest.cs
IA-15 D.2b gameplay UI is our own UiHost/UiRoot retained tree, not a byte-port of Keystone. RetailUiRuntime owns the production import/mount graph; RetailWindowManager/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, schema-v2 per-character/per-resolution automatic layouts, and portable named saveui/loadui profiles; RetailWindowFrame is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals 0x2100006C, chat 0x21000006, toolbar 0x21000016, character 0x2100002E, inventory 0x21000023 plus mounted 0x21000024/22/21, dialog catalog 0x2100003C, radar 0x21000074, and external container 0x21000008 (shared bevel plus a user-directed compact 700-pixel initial content width instead of its authored 800-pixel root). The dialog context/queue/callback lifecycle is now a named-client port; only its retained rendering remains under this Keystone adaptation. src/AcDream.App/UI/RetailUiRuntime.cs; src/AcDream.App/UI/RetailWindowManager.cs; src/AcDream.App/UI/RetainedPanelControllerGroup.cs; src/AcDream.App/UI/UiHost.cs; src/AcDream.App/UI/RetailWindowLayoutPersistence.cs; src/AcDream.App/UI/Layout/RetailWindowFrame.cs; src/AcDream.App/UI/Layout/RetailDialogFactory.cs; src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs; src/AcDream.App/UI/Layout/ExternalContainerController.cs; src/AcDream.App/UI/Layout/LayoutImporter.cs; binding supply in GameWindow.cs Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle; the external strip's initial width follows the connected visual direction and remains horizontally resizable to its authored extent or the viewport edge. Persistence and low-level widget rendering are behaviorally reconstructed from retail semantics rather than a Keystone byte-port; lifecycle edge cases remain constrained by conformance tests. The external strip opens 100 pixels narrower than the raw LayoutDesc before user/persistence resizing. Production LayoutDesc objects; DialogFactory @ 0x004773C0..0x00478470; gmExternalContainerUI @ 0x004CBAD0..0x004CBFE0; docs/research/2026-07-13-retail-dialog-factory-pseudocode.md; Keystone behavior notes in docs/research/retail-ui/
IA-17 Toolbar chrome is toolkit-supplied through the central RetailWindowFrame mount (UiCollapsibleFrame 8-piece bevel) because LayoutDesc 0x21000016 carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. src/AcDream.App/UI/Layout/RetailWindowFrame.cs; src/AcDream.App/UI/UiCollapsibleFrame.cs; toolbar policy in GameWindow.cs; spec: docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump)
IA-18 Effect overlay tile (enum 0x10000005) is a ReplaceColor SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload SurfaceWindow::ReplaceColor 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy). src/AcDream.App/UI/IconComposer.cs (ReplaceWhiteFromSurface) Faithful port of IconData::RenderIcons @407614 → the SURFACE overload ReplaceColor 0x004415b0 (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation IconData::RenderIcons acclient_2013_pseudo_c.txt:407524; ReplaceColor SURFACE overload 0x004415b0:71656; docs/research/2026-06-17-stateful-icon-RESOLVED.md
IA-19 Automatic combat acquisition is narrowed to attackable non-player monsters. Retail AutoTarget falls back to SelectNext(SELECTION_TYPE_COMPASS_ITEM), whose combat filter can also admit attackable enemy players in compatible PK states. src/AcDream.Core/Combat/CombatTargetPolicy.cs; consumer src/AcDream.App/Rendering/GameWindow.cs Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually ClientCombatSystem::AutoTarget @ 0x0056BC80; CPlayerSystem::SelectNext @ 0x0055F9A0; ClientCombatSystem::ObjectIsAttackable @ 0x0056A600
IA-20 The basic combat bar keeps dark-red media 0x0600715E visible as the centered middle baseline. Retail skill-gates field 0x100005EF to trained Recklessness; the separate bright child remains faithful live SetPowerbarLevel feedback from the absolute left edge. src/AcDream.App/UI/UiScrollbar.cs; child-policy extraction in src/AcDream.App/UI/Layout/DatWidgetFactory.cs Explicit connected visual direction: the dark middle track remains present behind live attack charge; the exact skill-gated treatment remains tracked by AP-112 Untrained characters retain the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable gmCombatUI::RecvNotice_SetPowerbarLevel @ 0x004CC0E0; gmCombatUI::ListenToElementMessage @ 0x004CC430; LayoutDesc 0x21000073

2. Adaptation (AD) — 37 rows (AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover)

# Divergence Where (file:line) Why it is safe / justified Risk if assumption breaks Retail oracle
AD-38 Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal elapsed >= 1.0 state edge. Incoming fades retain the exact timer. src/AcDream.Core/World/TeleportAnimSequencer.cs (OutgoingViewportReachedTerminalProjection) An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. UIGlobals::GetAnimLevel @ 0x004EE540; gmSmartBoxUI::UseTime @ 0x004D6E30; paired retail/acdream captures documented in docs/research/2026-07-15-retail-portal-space-pseudocode.md
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 readiness gates replace retail's synchronous destination cell load. Login placement keeps #135's split: a hydratable indoor claim gates on its EnvCell floor (IsSpawnCellReady) rather than meaningless dungeon terrain; outdoor placement gates on terrain. #218 refinement (2026-07-16): F751 portal-space exit joins BOTH publication domains in TeleportWorldReady: indoor requires the center landblock's Near-tier static/EnvCell mesh set uploaded plus the EnvCell in physics, while outdoor requires that Near-tier render readiness plus terrain/collision residency for the priority near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud forced-placement path. The hold→materialize→regain-control lifecycle remains owned by TeleportAnimSequencer. src/AcDream.App/Rendering/GameWindow.cs (TeleportWorldReady + TAS transit tick); src/AcDream.App/Streaming/StreamingController.cs (IsRenderNeighborhoodResident); src/AcDream.App/Streaming/GpuWorldState.cs (IsRenderReady); src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs; src/AcDream.Core/Physics/PhysicsEngine.cs (IsSpawnCellReady, IsNeighborhoodTerrainResident) This is the asynchronous equivalent of retail completing its blocking cell load before EndTeleportAnimation: neither an empty collision world, a terrain-only Far shell, nor a published-but-not-drawable GPU landblock may be revealed. Indoor does not require a terrain heightmap, only the owning render landblock and the exact EnvCell. Gate opens early → grey/untextured reveal, free-fall, wrong-cell rooting, or missing scenery; predicate never satisfies (streamer/DAT/upload failure) → portal transit reaches its existing loud timeout/forced-placement diagnostic instead of silently hanging forever. retail synchronous cell load before SetPosition / gmSmartBoxUI::EndTeleportAnimation 0x004D65A0
AD-3 Outdoor seeds always walk the transit array (retail skips the walk when the seed CLandCell is null/unloaded); per-cell lookups no-op on unhydrated data src/AcDream.Core/Physics/CellTransit.cs:503 Equivalence argument: with nothing hydrated every lookup inside the walk no-ops, so the result matches retail's skipped walk Near partially-streamed landblocks, building-transit promotion silently can't fire until structs hydrate — membership stays outdoor while the player is inside a building CObjCell::find_cell_list 0052b535-0052b56c (null-CLandCell case)
AD-4 point_in_cell against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells src/AcDream.Core/Physics/CellTransit.cs:588 The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root CEnvCell::find_visible_child_cell :311397; cell-BSP vtable[0x84]
AD-5 Outdoor point_in_cell is an identity compare against the global XY-column cell from LandDefs.AdjustToOutside (no per-cell containment test) src/AcDream.Core/Physics/CellTransit.cs:865 Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) find_cell_list pick pc:308788-308825; CLandCell::point_in_cell (get_block_offset pc:308804)
AD-6 Per-LANDBLOCK shadow re-flood on hydration vs retail per-CELL recalc_cross_cells src/AcDream.Core/Physics/ShadowObjectRegistry.cs:339 The streaming unit IS the landblock; one hook per hydration event covers both race directions (entity-before-cells, cells-after-spawn) Any cell-hydration path that doesn't raise the landblock hook leaves an entity's shadow set stale — walk-through / missing collisions in just-streamed cells CObjCell::init_objectsrecalc_cross_cells, 0x0052b420 / 0x00515a30
AD-10 Remote slope projection relocated to the queue-empty/head-reached combiner boundary; retail projects inside CTransition::adjust_offset during the sweep src/AcDream.Core/Physics/PositionManager.cs:47 Remote bodies don't run a full local transition sweep; boundary projection removes the ~5 Hz Z staircase on slopes, no-op on flat ground The single-point terrain-normal sample can differ from the sweep's contact plane (cell boundaries, props underfoot) — remote Z drift / stair-stepping CTransition::adjust_offset pc:272296-272346
AD-11 Useability fallback: retail blocks Use entirely on null/zero useability; we allow it (behavioral fallback in the IsUseableTarget caller; justification recorded here) src/AcDream.Core/Physics/PhysicsDiagnostics.cs:163 ACE's seed DB ships many weenies with _useability unset; without the fallback doors/lifestones/creatures are un-Useable on ACE Objects a retail-faithful server intentionally marks non-useable become useable in acdream — wrong interaction gating when the ACE-ships-null assumption stops holding ItemHolder::UseObject pc:402923
AD-12 SecondaryAttributeTable coefficients hardcoded (Health=End×0.5, Stam=End×1.0, Mana=Self×1.0) instead of dat-read; unknown attributes contribute 0 src/AcDream.Core/Player/LocalPlayerState.cs:279 Coefficients never vary across retail dat versions; re-confirmed by ACE AttributeFormula.cs + holtburger; dat port can replace later A customized portal.dat with modified vital formulas silently yields wrong max-vitals; a missing attribute snapshot underestimates max SecondaryAttributeTable portal.dat 0x0E0..0x0E2; CreatureVital::GetMaxValue 0x0058F2DD
AD-13 1-second dedup window for identical system chat messages (retail has none) src/AcDream.Core/Chat/ChatLog.cs:29 ACE dual-sends the same system text (0xF7E0 + 0x02EB) for back-compat; without dedup every line doubled (Phase J compromise) Two genuinely distinct but textually identical system messages within 1 s collapse to one line where retail shows both ACE dual-send 0xF7E0 + 0x02EB
AD-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 bounded CPU replay residency, per-frame upload budgets, and point-of-use self-heal (EnsureLoaded re-request in the dispatcher's mesh-missing path, #128); retail loads synchronously — geometry is never absent src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs; src/AcDream.App/Rendering/Wb/MeshUploadCaches.cs; src/AcDream.App/Rendering/Wb/MeshUploadFrameBudget.cs Immutable preparation descriptors and the bounded CPU cache can re-stage an evicted mesh; dispatcher self-heal makes absence transient while upload budgets prevent a portal arrival from monopolizing a frame A future consumer that neither retains an owner nor reaches the self-heal/replay path can remain invisible; under heavy admission pressure a valid mesh can pop in later than retail's synchronous path retail synchronous content load; docs/architecture/worldbuilder-inventory.md portal-readiness and bounded-residency seams
AD-23 Live entities with ServerGuid != 0 and null ParentCellId are culled (ClipSlotCull) while indoor clip routing is active; retail objects are always cell-resident (synchronous add-to-cell at creation) src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:484 Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, given membership resolves promptly An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight retail per-cell object lists in PView traversal
AD-24 EnvCell shell geometry 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 REMOTE-DR sweep only (the player half retired 2026-07-07 by the #182 verbatim rebuild): the remote dead-reckoning post-resolve still reflects velocity with the airborne-before-AND-after suppression; retail bounces unless grounded→grounded-and-not-sledding. The PLAYER path now runs the ported handle_all_collisions (PhysicsObjUpdate) with retail's should_reflect rule — the micro-bounce spiral it guarded is gone (contact is committed BEFORE the reflect and the small-velocity-zero is ungated) src/AcDream.App/Rendering/GameWindow.cs (remote sweep post-resolve, #173 block) The remote DR sweep hasn't been rebuilt yet (it has no fsf/SetPositionInternal chain); the old airborne-only suppression keeps remote landings from micro-bouncing on the remote landing-snap gate Remote landing-reflection behavior (slope-landing momentum) won't reproduce; retire when the remote-DR sweep gets the same UpdateObjectInternal rebuild as the player handle_all_collisions pc:282699-282715; ACE PhysicsObj.cs:2656-2721
AD-27 Use/PickUp action fired on natural moveto completion via the MoveToComplete client-addition seam (retail's CleanUpAndCallWeenie contains no weenie call in this build and notifies nothing on arrival); retail sends the action once (server MoveToChain callback completes it) src/AcDream.App/Rendering/GameWindow.cs:12939 (MoveToComplete subscription) + src/AcDream.Core/Physics/Motion/MoveToManager.cs (MoveToComplete seam doc) ACE's server-side chain may have timed out by the time our body arrives; the close-range deferred send hits ACE's WithinUseRadius fast-path. R4-V5 re-anchored from the deleted B.6 AutoWalkArrived event — same fires-on-arrival-only contract (never on cancel) If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers ACE CreateMoveToChain / WithinUseRadius; MoveToManager::CleanUpAndCallWeenie 00529650 §7e (no weenie call)
AD-28 Chat transcript (UiText) and input (UiChatInput) are two separate widget classes placed inside their dat-authored container panels; retail's ChatInterface uses a single mode-flagged UIElement_Text (Type-12) that switches between read and edit mode src/AcDream.App/UI/Layout/ChatWindowController.cs:135 (transcript) + :150 (input) UIElement_Text is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract UIElement_Text (Type-12) @ keystone.dll; gmMainChatUI::PostInit @0x4ce130
AD-29 ClientObjectTable fires global ObjectAdded/ObjectUpdated/ObjectRemoved events; consumers filter by guid on their end. Retail dispatches per-object via NoticeRegistrar observer dispatch — each UI cell observes only its specific object guid src/AcDream.Core/Items/ClientObjectTable.cs:48 (events); src/AcDream.App/UI/Layout/ToolbarController.cs:115 (guid filter) NoticeRegistrar is inside keystone.dll with no PDB/decomp; global broadcast + consumer-side filter is functionally equivalent for the current panel count and object volumes seen in practice At high object counts (>1 000 objects), every ObjectUpdated wakes every subscribed consumer — O(n·m) notification cost instead of retail's O(1) per-observer dispatch; a consumer that forgets the guid filter processes all objects (a latent correctness bug) NoticeRegistrar (keystone.dll, no PDB); retail per-object observer registration in CObjectMaint
AD-30 Cell-march preserves seed landblock id when TryGetTerrainOrigin returns false for an outdoor seed (#145 D, 2026-06-22): BuildCellSetAndPickContaining returns currentCellId verbatim rather than marching via blockOrigin=(0,0,0); retail never encounters this state (cells stored block-local, no streaming-gap concept) src/AcDream.Core/Physics/CellTransit.cs:765 Equivalence argument: "preserve-verbatim when unregistered" is the same contract as PhysicsEngine.Resolve's NO-LANDBLOCK branch; the player's cell stays the last known-correct cell until the landblock's terrain registers — no march, no lbX=0 wire A body whose seed landblock is genuinely absent for >1 physics tick holds its last-known cell rather than discovering the true containing cell; transient only — corrects the instant terrain registers; an indoor seed is explicitly excluded from the guard (outdoor low < 0x100 gate) CObjCell::find_cell_list + block-local storage (retail has no streaming gap); TryGetTerrainOrigin pc path
AD-32 Movement, Position, State, Vector, Pickup, Delete, and ObjDesc packets for a FUTURE object incarnation are dropped until its CreateObject arrives; retail queues each blob for the not-yet-created object (SmartBox::QueueBlobForObject, dispatch return 4) and replays it after construction. F754/F755 and ParentEvent are no longer part of this divergence: EntityEffectController preserves one mixed effect FIFO per server GUID until the canonical local owner is ready, while runtime-owned ParentAttachmentState retains parent relations by generation. Older-incarnation state packets drop in both clients. src/AcDream.Core/Physics/PhysicsTimestampGate.cs (TryAcceptInstance); src/AcDream.App/World/InboundPhysicsStateController.cs; src/AcDream.App/World/LiveEntityRuntime.cs; effect exception src/AcDream.App/Rendering/Vfx/EntityEffectController.cs; Parent exception src/AcDream.App/World/ParentAttachmentState.cs acdream has no general per-object mixed queue for non-effect state packets yet. Dropping those state families is generation-safe: a future packet cannot mutate the old body or advance its stamps, and the newer CreateObject wholesale-seeds all nine channels. The canonical runtime remains the seam for widening the queue later. The first queued non-effect state can be absent until a later authoritative update; effect and Parent packets retain retail order and ownership SmartBox::HandleSetState 0x004533E0; HandleVectorUpdate 0x00453480; HandleParentEvent 0x004535D0; HandlePickupEvent 0x00453530; HandleDeleteObject 0x00451EA0; HandleObjDescEvent 0x00453340; UnpackPositionEvent 0x004542C0; F754/F755 dispatch and pending replay pc:357214-357239
AD-33 CSequence.frame_number at C# double (64-bit); retail is x87 long double (80-bit extended) — every frame-boundary comparison ran at extended precision on retail (Phase R1, 2026-07-02) src/AcDream.Core/Physics/Motion/CSequence.cs (FrameNumber) double is the widest C# float type; the R1 port removes ACE-style boundary epsilons so comparisons are exact-int against bare boundaries, minimizing ULP sensitivity (ACE's float is far worse) A frame landing within 1 double-ULP of an integer boundary could classify differently than retail's 80-bit compare — sub-frame timing skew at pathological framerate×dt combinations acclient.h:30747 (long double frame_number)
AD-34 Retail's intrusive DLListBase/DLListData lists are managed LinkedList<T>s; node identity via LinkedListNode<> references (Phase R1 anim list, 2026-07-02; extended R2-Q3 to MotionTableManager.pending_animations; extended R4-V2 to MoveToManager.pending_actions — whose node type, retail MoveToManager::MovementNode, is RENAMED MoveToNode to avoid colliding with R2's Motion/MotionNode.cs pending_motions node) src/AcDream.Core/Physics/Motion/CSequence.cs (_animList); src/AcDream.Core/Physics/Motion/MotionTableManager.cs (_pendingAnimations); src/AcDream.Core/Physics/Motion/MoveToManager.cs (_pendingActions) + MoveToNode.cs Same topology + cursor semantics (curr_anim/first_cyclic/tail-anchored scans are node references); unlink/delete becomes Remove(node); conformance tests pin the surgery state tables Any retail behavior depending on the 4 pointer adjustment or node memory reuse (none observed in the decomp) would diverge; a reader grepping for retail's MovementNode name must find it via this row acclient.h DLListBase; r1-csequence-decomp.md §0; r2-motiontable-decomp.md §11; r4-moveto-decomp.md node factories §4a
AD-35 MotionTableManager.PerformMovement's unhandled-type default case returns the named sentinel 0xFFFFFFFF (MotionTableManagerError.NotHandled); retail's compiled code leaks the CSequence* pointer reinterpreted as the return code (BN-confirmed artifact, dead/unreachable — callers gate on type first) (R2-Q3, 2026-07-02) src/AcDream.Core/Physics/Motion/MotionTableManager.cs (PerformMovement default case) No retail caller consults the return value for unhandled types (RawCommand/StopRawCommand/MoveTo*/TurnTo* route elsewhere); returning a stable non-zero sentinel preserves the only observable contract (non-zero = not success) without fabricating a pointer-shaped number If a future port wires a caller that passes unhandled types AND branches on the exact return value, it would see 0xFFFFFFFF where retail saw an arbitrary pointer — flag at that port PerformMovement 0x0051c0b0 (r2-motiontable-decomp.md §11 default-case note)
AD-36 IMotionDoneSink.MotionDone consumed for CREATURE-class entities only: R3-W2 binds the seam to the entity's MotionInterpreter.MotionDone (player via PlayerMovementController.Motion, remotes via RemoteMotion.Motion, resolved at fire time); interp-less entities (statics that never receive a UM/UP and so never get a RemoteMotion) keep a diagnostic-recorder-only target — retail gives every CPhysicsObj a MovementManager/CMotionInterp (R2-Q4 seam, narrowed R3-W2, 2026-07-02; R5-V5 gave every RemoteMotion/player ONE literal MovementManager facade, so the residue is only the no-RemoteMotion class) src/AcDream.App/Rendering/GameWindow.cs (TickAnimations MotionDoneTarget bind) Motion for entities without a RemoteMotion (never UM/UP-touched) completes via the manager queue alone; nothing consumes their MotionDone until every sequencer-owning entity gets a host/RemoteMotion (doors DO have one since the R4-V5 door fix — first UM creates it) An entity behavior depending on pending_motions bookkeeping in that no-RemoteMotion class (none known) would silently no-op CPhysicsObj::MotionDone 0x0050fdb0; retire when every sequencer-owning entity constructs a RemoteMotion/host (post-M1.5 entity-class unification; R5-V5 closed the facade half)
AD-37 Camera rotation state is a forward VECTOR (nlerp + normalize; roll always 0, up = world Z); retail's sought carries a full Frame and slerps quaternions (Frame::interpolate_rotation shortest-path slerp with 2e-4 nlerp fallback). The dead-band compares forward-vector distance against the same 2e-4 epsilon retail applies per quaternion component src/AcDream.App/Rendering/RetailChaseCamera.cs (_dampedForward, ApplyConvergenceSnap) The chase camera never rolls (heading frames are Z-up by construction), so a forward vector spans the reachable rotation space; identified (not introduced) during the #180 UpdateCamera tail reading If a future camera mode needs roll (death cam, cutscene) the vector state can't represent it; large-angle per-frame turns nlerp (chord) vs slerp (arc) — imperceptible at 0.45-stiffness step sizes Frame::interpolate_rotation 0x00535390, Frame::close_rotation 0x00455d70; pseudocode doc 2026-07-06-camera-sought-position
AD-39 The frames_stationary_fall ladder + fsf≥3 UP-contact-plane manufacture runs AFTER acdream's fused LKCP-restore/contact-marking block, deriving retail's _redo as cleanAdvance || OnWalkable; retail (ACE Transition.cs:1029-1061) interleaves the fsf block BETWEEN the LKCP-restore (sets _redo) and the contact-marking (reads the manufactured plane) (#182 rebuild, 2026-07-07) src/AcDream.Core/Physics/TransitionTypes.cs (ValidateTransition fsf tail) acdream deliberately fused ACE's separate LKCP-restore + contact-mark blocks (the L.2.3c/L.2.4/A6.P3 contact-retention divergences); running the ladder after them and re-marking grounding inside the manufacture branch is semantically equal (a grounded wall-slide is not a stuck-fall in either arrangement) without disturbing those hard-won fixes If a future contact-retention change alters when OnWalkable is set relative to the ladder, _redo could misclassify a frame (grounded-jam mistaken for stuck-fall → spurious velocity zero, or vice-versa) — the fsf conformance tests pin the current arrangement CTransition::validate_transition 0x0050aa70 pc:272625-656; ACE Transition.cs:1029-1061
AD-40 The fsf Stationary* transient-bit encode (fsf→0x10/0x20/0x40) lives in the Core resolve writeback (PhysicsEngine.ResolveWithTransition), co-located with the fsf computation; retail encodes it in handle_all_collisions (pc:282737-758). Also: PhysicsBody.CachedVelocity is computed at the player chokepoint but not yet consumed — outbound wire velocity still uses the existing get_state_velocity path, not retail's cached_velocity source (#182 rebuild, 2026-07-07) src/AcDream.Core/Physics/PhysicsEngine.cs (writeback); src/AcDream.App/Input/PlayerMovementController.cs (CachedVelocity) Encoding in the writeback keeps the seed→ladder→writeback→seed round-trip self-contained in Core (testable without the App loop); the bit values + timing are identical to retail's (set after fsf is final, before the next resolve). CachedVelocity is faithful to carry now; routing the wire through it is a separate, unmeasured change If a future consumer reads the Stationary* bits expecting retail's handle_all_collisions to have set them (it doesn't run in Core), the Core writeback is the source of truth; a wire-reporting change that assumes CachedVelocity is live would send the wrong velocity until it's wired handle_all_collisions bit encode pc:282737-758; get_velocity 0x005113c0 (cached_velocity reader)
AD-41 The candidateMoved gate (retail UpdateObjectInternal pc:283657 candidate != m_position) suppresses ONLY handle_all_collisions + cached_velocity on a no-move frame; acdream still runs ResolveWithTransition (zero-distance) for cell/contact tracking, where retail skips the whole transition (#182 rebuild, 2026-07-07) src/AcDream.App/Input/PlayerMovementController.cs (candidateMoved guard) The load-bearing effect is not re-zeroing the gravity velocity that rebuilds after a stuck-fall bleed; the zero-distance resolve is a near-no-op (numSteps 0 → the zero-step early return, no ValidateTransition, contact plane persists via the writeback), so running it is harmless while keeping acdream's per-frame cell/membership refresh If the zero-distance resolve ever gains a side effect on a no-move frame (a contact-plane clear, an fsf change), it would diverge from retail's skip — a no-move frame must stay a near-no-op CPhysicsObj::UpdateObjectInternal 0x005156b0 pc:283657 (candidate-moved gate)
AD-42 Enter-world placement is split across two Core calls: legacy Resolve performs retail AdjustPosition + the host's established floor snap, then ResolvePlacement runs the verbatim object-aware find_placement_pos ring search. Retail runs initial environment placement, ring search, and final step-down inside one find_placement_position transition src/AcDream.App/Rendering/GameWindow.cs (EnterPlayerModeNow); src/AcDream.Core/Physics/PhysicsEngine.cs (ResolvePlacement) The first call has already committed the same validated cell/floor point that feeds the ring search; the second call uses the same sphere dimensions, collision registry, and cell id. Keeping the split preserves the proven indoor-login snap while adding the missing occupied-position behavior A spawn that requires retail's final placement step-down after a ring candidate (rather than the existing floor snap before it) could settle at a slightly different Z on a ledge/water boundary; the overlap is still cleared CPhysicsObj::enter_world 0x00516170; CTransition::find_placement_position 0x0050C170; CTransition::find_placement_pos 0x0050BA50
AD-43 A malformed/custom PhysicsScript CallPES cycle whose script timeline never advances is rejected with a diagnostic; retail's linked scheduler would continue draining that zero-time tail indefinitely src/AcDream.Core/Vfx/PhysicsScriptRunner.cs (timeline-progress ancestry guard) Prevents corrupt DAT content from hanging the single update/render thread. Installed-DAT audit plus conformance tests prove the real rolling-weather cycles advance 2.8 seconds per edge and continue unchanged; only a no-progress strongly connected cycle is rejected A custom DAT that deliberately relies on an infinite zero-time loop observes a rejected play instead of freezing the client ScriptManager::AddScriptInternal 0x0051B310; ScriptManager::UpdateScripts 0x0051B480; CPhysicsObj::CallPES 0x00511AF0

3. Documented approximation (AP) — 85 active rows

Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered AP-94..AP-112 for the confirmed retail-UI completion gaps.

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

| AP-3 | Step-down chain triggered only when contact is invalid OR steeper than walkable; retail's transitional_insert OK-path ALWAYS runs it | src/AcDream.Core/Physics/TransitionTypes.cs:1197 | Conditional preserves the observed-to-matter cases (edge departure, steep cliff-slide) without running the chain every step (per pc:273191 agent reports) | Steps where retail runs step-down despite a valid walkable contact (bump maintenance, edge-slide arming) are skipped — float-off or missed edge slides in untested geometry | transitional_insert OK-path pc:273191 | | AP-4 | CliffSlide check moved BEFORE retail's Branch-1 (!OnWalkable → restore+OK) gate, compensating our L.2.3i FloorZ OnWalkable bookkeeping | src/AcDream.Core/Physics/TransitionTypes.cs:1316 | Retail's order with our incomplete OnWalkable stops the player dead every frame on steep slopes ("stay on the roof"); reorder restores downhill drift | CliffSlide fires in states where retail's Branch 1 would restore-and-OK — body slides where retail holds, e.g. contact-plane-bearing steep geometry near edges | retail EdgeSlide dispatch order (transitional_insert step-down failure) | | AP-5 | Step-down skips Placement validation for the contact-maintenance call (runPlacement=false); ACE/retail run it unconditionally (kept for DoStepUp) | src/AcDream.Core/Physics/TransitionTypes.cs:3393 | Residual wall-slide artifacts made Placement misfire, leaving players stuck near walls; the skip was the targeted L.2.3h fix | Step-down can settle into positions Placement would reject — slight wall embedding, or accepting a step-down through overlap geometry retail catches | CTransition::step_down pc:272952; ACE Transition.cs:731-741 | | AP-7 | calc_friction threshold 0.0 with retail's state gate missing; retail uses 0.25 gated by an undecoded state check | src/AcDream.Core/Physics/PhysicsBody.cs:307 | Bumping the threshold without the gate hammered normal walking (3 → 0.16 m/s); as-read 0.0 kept; locomotion probably state-exempted in retail. Filed L.3c-followup | Friction engages under different conditions — post-landing slides, knockback decay, sledding speeds mismatch retail's deceleration | pc:276702-276705 (state gate + 0.25) | | AP-10 | Dry-corner water depth: retail's 0.1 m allowed sink-in collapsed to 0 | src/AcDream.Core/Physics/TerrainSurface.cs:481 | The 0.1 offset destabilizes the feet-exactly-on-plane contact-touch check (dist > EPSILON → SetContactPlane never fires → float/fall); retail's ~10 cm sink-in is visually indistinguishable | Masks a contact-touch epsilon fragility — other water-depth values exercising the same instability could oscillate shoreline walkable validation; retail's wet/dry corner sink-in visual absent | ObjCell.get_water_depth / calc_water_depth (via ACE port) | | AP-11 | Hand-authored 4-keyframe fallback sky set (sunrise/noon/sunset, fog ~80350 m) when the Region dat isn't loaded yet | src/AcDream.Core/World/SkyState.cs:167 | A renderable sky is needed during boot before the Region dat parses; safety net on region-load failure | Any window where the fallback is active shows sky/fog lighting only roughly resembling retail's dat-driven values | SkyTimeOfDay keyframes, Region dat 0x13000000 | | AP-12 | Enchantment family-stacking tiebreak by largest SpellId; retail picks highest Generation, tie-broken by latest cast | src/AcDream.Core/Spells/EnchantmentMath.cs:89 | ActiveEnchantmentRecord doesn't carry Generation; SpellId correlates with generation level in practice | Where spell ids don't track power within a family (or same-generation re-cast), the wrong buff wins — vital-max / stat values diverge from retail | CEnchantmentRegistry::EnchantAttribute 0x00594570 (pc:416110) | | AP-13 | ComputeDamage is a simplified retail damage formula (no augmentations/ratings) — verified DEAD CODE as of 2026-06-04, M2 scaffolding | src/AcDream.Core/Combat/CombatModel.cs:184 | Not on the critical path; stubbed from r02 §5 + ACE CombatManager for the future M2 predictive display | If wired into the M2 attack-bar estimate as-is, predicted numbers diverge whenever augs/ratings apply | r02 §5; ACE CombatManager | | AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | src/AcDream.Core/Items/ItemInstance.cs:187 | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) | | AP-15 | WeenieError translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | src/AcDream.Core/Chat/WeenieErrorMessages.cs:26 | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs | | AP-16 | Point/spot lights selected per-object / per-cell as the 8 nearest reaching lights (sphere-overlap, nearest-first) via LightManager.SelectForObject, capped at MaxLightsPerObject=8; called from WbDrawDispatcher.ComputeEntityLightSet (objects) and EnvCellRenderer.GetCellLightSet (cell shells). Retail's bake (SetStaticLightingVertexColors) sums ALL reaching static lights per vertex with no count cap. Retail's hardware path (minimize_object_lighting 0x0054d480) DOES cap at 8 per object, so the cap is faithful to retail's hardware path — not to its bake path. The LightManager.Tick UBO path survives for DIRECTIONAL (sun) lights only; mesh_modern.vert's UBO loop skips point/spot entries (posAndKind.w != 0 → continue) — point lights reach the shader exclusively via the per-object SSBO (binding 5) | src/AcDream.Core/Lighting/LightManager.cs:234 (SelectForObject); MaxLightsPerObject ~line 174; call sites WbDrawDispatcher.ComputeEntityLightSet + EnvCellRenderer.GetCellLightSet | Matches retail's hardware constraint (8 lights per object/cell); selection is nearest-sphere-overlap which faithfully allocates lights to the surfaces that actually see them | Surfaces reached by >8 point lights are dimmer than retail's uncapped bake — rare (a dungeon room has a handful of torches), but real; see AP-35 for the bake-vs-GPU-evaluate architecture difference | minimize_object_lighting 0x0054d480 (retail's 8-light hardware cap); SetStaticLightingVertexColors 0x0059cfe0 (retail's bake, no count cap) | | AP-18 | RETIRED 2026-07-10 — faithful retail radar port. Exact RGBAColor_Radar* floats were recovered from named static data and gmRadarUI::GetBlipColor was re-ported with _blipColor overrides plus portal/vendor/attackable-creature/admin/PK/PKLite/free-PK/fellowship precedence. The old implementation was not merely hue-tuned: it also had wrong portal/vendor colors and an incomplete dispatch matrix. | src/AcDream.Core/Ui/RadarBlipColors.cs + radar classification tests | — | — | gmRadarUI::GetBlipColor 0x004d76f0; static RGBA initializers at named decomp pc:1089736-1089804 | | AP-19 | PortalSideEpsilon 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49 | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; PView::InitCell 0x005a4b70 | | AP-20 | Sub-pixel view-polygon vertex merge fixed at 1080p-reference NDC units (2/1080); retail merges at ~1 actual screen pixel | src/AcDream.App/Rendering/PortalProjection.cs:179 | Unit approximation whose coarseness only strengthens convergence — the merge is the flood's fixpoint floor (replaced MaxReprocessPerCell=16) | At 4K+ a legitimately visible 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 retains the invented α<0.05 fragment discard. World GfxObj/Setup instances now apply their DAT AlphaBlend/Additive/InvAlpha factors through the retail shared alpha queue, but sealed off-screen WbDrawDispatcher consumers (paperdoll/UI Studio) retain the old immediate normal-alpha pass for all three kinds | src/AcDream.App/Rendering/Shaders/mesh_modern.frag; src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (DrawDeferredAlphaBatch versus immediate Phase 8) | World presentation needed exact per-surface blend for spell/particle density and translucent-object intersections. Off-screen object previews are isolated render targets and have not shown an authored additive entity surface that justifies splitting their compact immediate pass | A faint world fringe below 5% alpha is discarded; a hypothetical additive/inverse-alpha paperdoll or UI Studio entity composites darker than retail inside that private viewport | D3DPolyRender::SetSurface; D3DPolyRender::RenderMeshSubset; SurfaceType.Additive → D3DBLEND_ONE | | AP-22 | Invented setup.Radius cylinder (height = Height or Radius×2) for shapeless live entities; shape + height formula not from the retail shape walk | src/AcDream.App/Rendering/GameWindow.cs:3250 | ShadowShapeBuilder (faithful walk) only emits CylSphere/Sphere/Part-BSP; the legacy cylinder preserves prior behavior so rare decorative props don't lose collision | Those props collide with an invented footprint (especially the Radius×2 height guess) — slides/blocks at non-retail distances | find_obj_collisionsCPartArray::FindObjCollisions pc:286236 | | AP-23 | Invented per-type use-radius heuristic (3 m creatures / 2 m doors-lifestones-portals-corpses / 0.6 m rest) for close-range gating + the speculative local TurnToObject/MoveToObject install through the player's MoveToManager (R4-V5; retail's client use flow issues the same manager calls, §9a/§9b). R5-V3 narrowed it: the speculative install now threads the target's REAL setup radius/height (GetSetupCylinder, same as the wire mt-6 route) and the player's own radius is real — only the use-radius BUCKETS remain invented (retail reads the object's UseRadius property) | src/AcDream.App/Rendering/GameWindow.cs (InstallSpeculativeTurnToTarget) | ACE broadcasts nothing actionable on the close branch (WithinUseRadius shortcut); the true radius arrives only on the far MoveToObject branch — a local stand-in is required | A target whose real UseRadius differs from the bucket misjudges the gate — Use/PickUp deferred for a completion that never comes, or fires early into a server "too far" | ACE Player_Move.cs:66; wire MoveToObject (type 6) carries the true radius; CPhysicsObj::TurnToObject/MoveToObject callers §9a/§9b | | AP-24 | RETIRED 2026-07-11 — matching v11.4186 x86 disassembly recovered ATTACK_POWERUP_TIME=1.0 seconds and DUAL_WIELD_POWERUP_TIME=0.8 seconds from the operands loaded by GetPowerBarLevel; jump and combat now share those constants. | src/AcDream.Core/Combat/CombatModel.cs; src/AcDream.App/Input/PlayerMovementController.cs; src/AcDream.App/Combat/CombatAttackController.cs | — | — | ClientCombatSystem::GetPowerBarLevel @ 0x0056ADE0; static data 0x007CEFC8/0x007CEFD0 | | 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-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's Frame::is_equal is an exact float compare | src/AcDream.App/Input/PlayerMovementController.cs:1110 | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | Frame::is_equal pc:700263 | | AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (#49/#50, 2026-05-11) | src/AcDream.Core/World/SceneryGenerator.cs (via WbSceneryAdapter) | Piecemeal patching against WB upstream is net-negative (the e279c46 road-check attempt over-suppressed scenery elsewhere, reverted 677a726); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a larger placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | CLandBlock::get_land_scenes; ACME GameScene.cs:1074 per-vertex road filter | | AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (ShellDrawLiftZ, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, #119-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via Build(drawLiftZ), seal/punch fans — #130) | src/AcDream.App/Rendering/GameWindow.cs:5604 (const at PortalVisibilityBuilder.ShellDrawLiftZ) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) | | AP-33 | Interior-root look-in cells (#124 sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the #131 portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | src/AcDream.App/Rendering/RetailPViewRenderer.cs (DrawBuildingLookIns) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | viewconeCheck 0x0054c250; nested DrawCells objects pc:432878 | | AP-34 | The world now shares one delayed alpha queue across Wb GfxObj/Setup entities and scene particles and drains it at retail's landscape/final boundaries. Residual: the modern reconstruction uses one stable scope-global CYpt sort rather than retail's per-CPartCell CShadowPart sort followed by cell traversal; EnvCellRenderer transparent shell batches also remain immediate and outside this queue | src/AcDream.App/Rendering/RetailAlphaQueue.cs; RetailPViewRenderer.cs (FlushLandscapeAlpha); Rendering/Wb/WbDrawDispatcher.cs; ParticleRenderer.cs | The mandatory modern renderer no longer owns retail CPartCell shadow lists. The shared queue restores the material consequence that motivated the port—particles and ordinary translucent parts can interleave—without rebuilding a second scene graph; stable sequence retains authored order on equal CYpt | Transparent objects from different cells can exchange order at a narrow overlap compared with retail cell traversal; an alpha-blended EnvCell shell cannot interleave with a particle or Wb entity, so those rare overlaps can still overpaint differently | RenderDeviceD3D::DrawObjCellForDummies 0x005A0760; CShadowPart::insertion_sort 0x006B5130; D3DPolyRender::FlushAlphaList 0x0059D2E0; PView::DrawCells 0x005A4840 | | AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (CurrCell.IsEnv && !SeenOutside), an approximation of ACE's full landblock IsDungeon (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. #135 pre-collapse: at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via IsSealedDungeonCell reading the EnvCell dat SeenOutside flag — because the physics CurrCell is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). #215 cell identity: the pre-collapse/recenter decision compares the player's current Position.objcell_id landblock with the received destination objcell_id; it never reconstructs the source from XYZ because dungeon frame origins may be negative. #145/#138 teleport-hold suppression: during a teleport arrival HOLD the player is unplaced, so CurrCell is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (DungeonStreamingGate.Compute(isTeleportHold:true) → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and ExitDungeonExpands, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | src/AcDream.App/Streaming/TeleportLandblockTransition.cs (source/destination cell-ID classification) + src/AcDream.App/Streaming/DungeonStreamingGate.cs (Compute — per-frame predicate + teleport-hold suppression, called from GameWindow.OnUpdate ~:7401) + GameWindow:IsSealedDungeonCell + :OnLiveEntitySpawnedLocked/:OnLivePositionUpdated (login/teleport pre-collapse hooks) + src/AcDream.App/Streaming/StreamingController.cs (collapse/expand/PreCollapseToDungeon) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same EnvCellFlags.SeenOutside the hydrated ObjCell.SeenOutside is built from (EnvCell.cs:72/PhysicsDataCache.cs:224), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete Position flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE LandblockManager.GetAdjacentIDs (dungeons→empty) Landblock.cs:577-582; IsDungeon Landblock.cs:1264-1277; retail SmartBox::TeleportPlayer 0x00453910 | | AP-43 | Per-object torch (point/spot) lighting AND sun are both gated on the OBJECT's own cell via the same IndoorObjectReceivesTorches(ParentCellId) predicate ((id & 0xFFFF) >= 0x0100): indoor objects (EnvCell-parented) get torches + NO sun; outdoor objects get the SUN + ambient + NO torches. This is the faithful per-draw port of retail's useSunlight gate — DrawMeshInternal (0x0059f398) calls minimize_object_lighting only if (Render::useSunlight == 0), and PView::DrawCells (0x005a4840) calls useSunlightSet(1) (0x005a485a) for the outdoor stage and useSunlightSet(0) (0x005a49f3) for the interior-cell stage. #142 (2026-06-20): the sun gate is now PER-INSTANCE in the shader (binding=6 instanceIndoor[] flag in mesh_modern.vert, filled by AppendCurrentLightSet) — it was previously a per-FRAME global keyed on the PLAYER cell (UpdateSunFromSky). The per-frame global is retained for sealed dungeons (correctly kills the sun frame-wide when no sky is visible). Residual: the ebp_2 second seen-outside test in CellManager::ChangePosition (0x004559B0) is unaudited — unclear whether it changes the ambient/sun regime for a subset of cells. No observed behavioral impact in tested cells. | src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (IndoorObjectReceivesTorches, ComputeEntityLightSet, AppendCurrentLightSet, _instIndoorSsbo/_indoorData/InstanceGroup.IndoorFlags); src/AcDream.App/Rendering/Shaders/mesh_modern.vert (binding=6 instanceIndoor[] gate on sun loop); per-frame sun src/AcDream.App/Rendering/GameWindow.cs:10421 (UpdateSunFromSky, unchanged) | Torches: outdoor objects never torch-lit (exact retail). Sun: indoor objects (furniture, NPCs, player in a windowed building) never sun-lit (exact retail per-stage). Ambient: per-player-cell regime unchanged (exact retail ChangePosition). | The ebp_2 unaudited test in ChangePosition could affect a narrow class of cells (entrance cells? sub-cells with special flags?) — no symptom observed; audit it if a lighting edge case arises in an unusual cell type | useSunlight gate DrawMeshInternal 0x0059f398; useSunlightSet 0x0054d450; per-stage PView::DrawCells 0x005a4840 (useSunlightSet(1) 0x005a485a / useSunlightSet(0) 0x005a49f3); minimize_object_lighting 0x0054d480; CellManager::ChangePosition 0x004559B0 (ambient + seen_outside) | | AP-35 | Point/spot lights are now PER-VERTEX Gouraud (pointContribution ~line 153 of mesh_modern.vert) matching retail's SetStaticLightingVertexColors bake path. Half-Lambert wrap ((1/1.5)·(N·D + 0.5·d)) AND norm distance attenuation (distsq>1 ? distsq·d : d) ARE ported (A7 Fix A, aa94ced). Point-light sum clamped to [0,1] on its own accumulator before adding ambient+sun (A7 Fix D D-1, mirrors retail's per-vertex bake clamp). CPU oracle: src/AcDream.Core/Lighting/LightBake.cs, locked by tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs. Residual (two parts): (a) acdream lights in-shader each frame (per-frame GPU evaluate); retail bakes into the vertex buffer ONCE — an architecture/performance difference; the wrap + norm + clamp formula is the same, but bake-once is cheaper for static geometry; (b) acdream's SelectForObject keeps only the 8 NEAREST reaching point/spot lights per object/cell (MaxLightsPerObject=8, see AP-16), whereas retail's bake sums ALL reaching static lights per vertex — a surface reached by >8 point lights is dimmer in acdream than retail's bake result (rare in practice; a room has a handful of torches) | src/AcDream.App/Rendering/Shaders/mesh_modern.vert (pointContribution ~line 153; wrap ~line 163; norm ~line 167; point-sum clamp line 210) | Per-vertex Gouraud + wrap + norm + clamp all match retail. The two residuals are: (a) per-frame GPU vs bake-once — architecture/perf only; (b) 8-light cap dimming when >8 lights reach one surface — rare. LightInfoLoader.cs:81 folds static_light_factor 1.3 into Range | (a) A new frame-time consumer bypassing accumulateLights would need to replicate the wrap + norm formula; per-frame GPU re-evaluate has higher per-frame cost than bake for static geometry. (b) A densely lit scene (>8 torches reaching one wall) renders dimmer than retail — see AP-16 for the 8-cap ownership | calc_point_light 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); SetStaticLightingVertexColors 0x0059cfe0; static_light_factor 0x00820e24 | | AP-37 | LayoutDesc meters collapse Type-3 slice descendants into UiMeter.BackLeft..FrontRight and reuse UiMeter.DrawHBar rather than building those media descendants and dispatching retail UIElement_Meter::DrawChildren. Non-Type-3 meter children are imported normally. | src/AcDream.App/UI/Layout/DatWidgetFactory.cs (BuildMeter/SliceIds); LayoutImporter.cs meter child predicate | The current vitals/character meter shapes are visually accepted and fixture-pinned; this is a representation adaptation, not a controller overlay. | A meter with a different descendant/media structure can render empty or with incorrect clipping/direction | UIElement_Meter::DrawChildren @ 0x0046FBD0; production meter LayoutDesc fixtures | | AP-39 | Chat lines carry one color per ChatKind (per-line solid color); retail UIElement_Text supports per-glyph styled runs (bold, different hue per segment) | src/AcDream.App/UI/UiText.cs:13 | Retail glyph-run parsing lives inside keystone.dll with no PDB/decomp; per-line per-kind coloring is the correct tonal palette and covers all existing chat types | Chat lines retail renders with multiple colors or bold names (e.g. "PlayerName says: text") render as one flat color; subtle visual difference but functionally complete | UIElement_Text glyph-run styling (keystone.dll, no decomp) | | AP-40 | Chat uses one fixed 0.75 outer opacity and has no descendant-focus-driven active/default opacity transition | src/AcDream.App/Rendering/GameWindow.cs chat mount; ChatWindowController.cs | Font resolution is now live and per-element; only the opacity behavior remains deferred to the shared window/focus runtime | Focused chat remains too translucent and idle chat never restores the configured default alpha | ChatInterface::SetOpacity @ 0x004F3120; SetDefaultOpacity @ 0x004F3BC0; SetActiveOpacity @ 0x004F3C40 | | AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (0x06004C63) used only when ThumbTopSprite/ThumbBotSprite are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | src/AcDream.App/UI/UiScrollbar.cs:35 | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | UIElement_Scrollbar::UpdateLayout @0x4710d0; cap sprites 0x06004C60 (top) + 0x06004C66 (bottom) from base layout 0x2100003E | | AP-42 | UiMenu item model is flat (label + opaque payload, single-level popup); retail UIElement_Menu::MakePopup @0x46d310 supports hierarchical nested submenus via recursive popup chain | src/AcDream.App/UI/UiMenu.cs | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | UIElement_Menu::MakePopup @0x46d310 | | AP-45 | PublicUpdatePropertyInt (0x02CE) sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs | Loopback ACE rarely reorders; this property stream has not yet joined the per-object freshness owner introduced for physics messages. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | PublicUpdatePropertyInt sequence byte (ACE GameMessagePublicUpdatePropertyInt) | | AP-48 | Inventory burden reads the player's wire EncumbranceVal (PropertyInt 5) when present — delivered by B-Wire (login PD-bundle UpsertProperties + live PrivateUpdatePropertyInt 0x02CD); ClientObjectTable.SumCarriedBurden remains only as a defensive fallback for when the server omits it. | src/AcDream.App/UI/Layout/InventoryController.cs (RefreshBurden); src/AcDream.Core.Net/ObjectTableWiring.cs (player-int route) | B-Wire ports the retail read (CACQualities::InqLoad reads the server value); the client sum is now fallback-only, kept defensively. CONFIRM the server actually sends EncumbranceVal at the B-Wire visual gate, then DELETE this row. | If the server omits EncumbranceVal, the bar falls back to the client sum (the original drift) until the first 0x02CD — confirm at the gate. | CACQualities::InqLoad 0x0058f130; ACE PropertyInt.EncumbranceVal=5 | | AP-49 | Carry-capacity augmentation (PropertyInt 0xE6) read from the player's wire property bundle when present (B-Wire PD UpsertProperties); defaults to 0 (correct for un-augmented characters) when absent. | src/AcDream.App/UI/Layout/InventoryController.cs (RefreshBurden) caller of BurdenMath.EncumbranceCapacity | B-Wire delivers the player's full PropertyInt table (incl. 0xE6 when the server sets it) via UpsertProperties; aug=0 is the correct no-aug default. CONFIRM the server sends 0xE6 for an augmented char at the gate, then DELETE. | An augmented character whose server omits 0xE6 reads slightly low capacity (bar fills higher than retail); un-augmented chars are exact. | EncumbranceSystem::EncumbranceCapacity decomp 256393 (0x004fcc00); retail 0xE6 PropertyInt augmentation | | AP-50 | Burden meter orientation/direction set programmatically (Vertical=true, FillFromBottom=true) rather than reading retail's m_eDirection property from the LayoutDesc element (property id 0x6f). | src/AcDream.App/UI/Layout/InventoryController.cs; src/AcDream.App/UI/UiMeter.cs (Vertical/FillFromBottom) | The m_eDirection property is not yet read by ElementReader; bottom-up fill is visually confirmed against retail's burden bar art. Retire when 0x6f is wired through ElementReaderDatWidgetFactory. | Wrong fill direction (top-down instead of bottom-up, or horizontal) if a future meter whose art requires a different direction is forced through the same code path. | UIElement_Meter::DrawChildren @0x46fbd0; property 0x6f (m_eDirection) in the LayoutDesc | | AP-51 | Main-pack m_topContainer cell (element 0x100001C9) draws the constant backpack icon via a hardcoded base-icon literal 0x0600127E + ItemType.Container (composited over the Container type-underlay), rather than resolving it at runtime the way retail does. Sprite VISUALLY CONFIRMED (2026-06-22 live gate). | src/AcDream.App/UI/Layout/InventoryController.cs (Populate, main-pack cell) | Retail's IconData::RenderIcons IsThePlayer branch draws a CONSTANT backpack with m_itemType = TYPE_CONTAINER (the original AP-51 "equipped-pack weenie icon" premise was wrong). The exact runtime resolution is unconfirmed: a research dat-dump misreported GetDIDByEnum(0x10000004,7) as the green tile 0x060011F4 (which rendered green-no-pack at the gate); the real backpack 0x0600127E was identified by dat export + user confirmation. We pin the visually-verified literal (cf. AP-55). Retire when the runtime resolve is reproduced + confirmed. | If the pinned sprite diverges from what retail resolves at runtime, the main-pack icon goes stale. | IconData::RenderIcons IsThePlayer branch 0x0058d1ee (decomp 407546-407549); sprite 0x0600127E dat-exported + visually confirmed | | AP-52 | Side-bag column slot count defaults to 7 (the dat column height) when the player's ContainersCapacity is absent/0. | src/AcDream.App/UI/Layout/InventoryController.cs (Populate) | ContainersCapacity is not reliably on the player ClientObject yet; 7 is the standard side-pack cap + the dat column (0x100001CA, 36×252) holds exactly 7. | An 8th-pack (Shadow of the Seventh Mule aug) character shows one too few side-bag slots — cosmetic. | retail gmBackpackUI side-pack column; ACE ContainersCapacity | | AP-53 | Contents grid slot count defaults to 102 (main pack) or 24 (side bag) when the open container's ItemsCapacity is absent/0. Container-switching is now wired (D.2b): clicking a side bag sends Use 0x0036 and the grid repopulates from ViewContents 0x0196. This row covers only the capacity default when the server omits ItemsCapacity. | src/AcDream.App/UI/Layout/InventoryController.cs (Populate) | ItemsCapacity is not reliably on every ClientObject yet; 102/24 are the retail standard capacities. Retire when ItemsCapacity is confirmed delivered for all containers. | A non-102/24 pack shows the wrong empty-slot count — cosmetic only. | retail main-pack capacity 102; side-pack 24; ACE ItemsCapacity; ViewContents 0x0196 | | AP-54 | Inventory window vertical resize is a toolkit approximation: bottom-edge drag, expand-only (Min = dat default 372 px, Max = available screen height), contents grid/sub-window/scrollbar/backdrop stretched via overridden Left\|Top\|Bottom anchors. | src/AcDream.App/UI/RetailUiRuntime.cs (inventory frame setup) | retail's gmInventoryUI resize lives in keystone.dll (no decomp); using the screen rather than the former arbitrary 560 px cap keeps the full vertical resize usable at every persisted size. | Which elements reflow or whether retail permits shrink-below-default could differ; shrink below the authored default remains disabled. | retail gmInventoryUI resize (keystone.dll, no decomp); UiNineSlicePanel resize | | AP-55 | The toolbar's item slots keep the hardcoded UiItemSlot.EmptySprite = 0x060074CF rather than resolving their cell template via attribute 0x1000000e. Correct in outcome (the toolbar's 0x1000000e resolves to a generic prototype whose empty media is 0x060074CF) but not dat-resolved. | src/AcDream.App/UI/UiItemSlot.cs (EmptySprite default); src/AcDream.App/UI/Layout/ToolbarController.cs | The inventory lists now port the retail resolver (ItemListCellTemplate); the toolbar was left on its hardcoded default to avoid regressing frozen, working art. Retire when the toolbar is routed through ItemListCellTemplate. | If a future toolbar layout's 0x1000000e points at a non-generic prototype, the toolbar would show stale art. | UIElement_ItemList::InternalCreateItem 0x004e3570; catalog 0x21000037 | | AP-57 | The open-container triangle (0x06005D9C) + selected-item square (0x06004D21) are drawn as procedural UiItemSlot overlays keyed by _openContainer plus Core SelectionState.SelectedObjectId, reproducing the retail keying (item.itemID == openContainerId / selectedID per UpdateOpenContainerIndicator 0x004e3070 / SetSelectedObject 0x0058c2e0) but not via the dat prototype's m_elem_Icon_OpenContainer/m_elem_Icon_Selected state elements + SetOpenContainerState/SetSelectedState calls. | src/AcDream.App/UI/UiItemSlot.cs; src/AcDream.App/UI/Layout/InventoryController.cs | The procedural overlay produces the correct visual result; the 36×36 container prototype (0x1000033F) lacks the square child, so the procedural overlay is the only way to show the square on a selected bag — the dat-state path retail uses would require the prototype to be richer than it is. | A cell that should show an indicator via a dat-state path retail uses but we don't would be missed; outcome currently matches retail for the open/selected indicator cases. | UIElement_ItemList::UpdateOpenContainerIndicator 0x004e3070; ACCWeenieObject::SetSelectedObject 0x0058c2e0; SetOpenContainerState 0x004e1200; SetSelectedState 0x004e1240 | | AP-59 | The per-cell container capacity bar (retail UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0, element 0x10000347) is drawn as a procedural UiItemSlot overlay (track 0x06004D22 full + fill 0x06004D23 clipped bottom-up to GetContents(guid).Count / ItemsCapacity), not via a real dat UIElement_Meter child. Right-anchored flush (the dat rect X=26 in a 36px cell sat ~5px off the edge — flush per the visual gate) and bottom-up fill assumed (the dat m_eDirection 0x6f isn't read, cf. AP-50). A CLOSED side bag reads empty until opened (its contents aren't indexed until ViewContents) — faithful to retail's known-children count, divergent if retail pre-loads. | src/AcDream.App/UI/UiItemSlot.cs (CapacityFill draw); src/AcDream.App/UI/Layout/InventoryController.cs (SetCapacityBar) | UiItemSlot is a behavioral leaf that paints overlays procedurally (cf. AP-57); the meter sprites + fill formula are the faithful port. Right-anchor + bottom-up were visual-gate calls. Further visual polish is deferred — ISSUES #146. | Fill direction / exact bar rect could differ from retail's m_eDirection + dat X; closed-bag bars read empty if retail pre-loads container counts. | UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0; element 0x10000347 (back 0x06004D22 / front 0x06004D23); GetNumContainedItems | | AP-60 | Inventory drag OnDragLift is a no-op + the source cell is not dimmed: retail dims the lifted item's source cell (RecvNotice_ItemListBeginDrag); acdream leaves the item in place during the drag + the floating ghost. | src/AcDream.App/UI/Layout/InventoryController.cs | Cosmetic only — the dragged item is still unambiguously identifiable; dimming the source requires tracking the source cell reference across drag events, deferred to a polish pass. | A drag in progress doesn't visually mark its origin — cosmetic only, no functional effect. | RecvNotice_ItemListBeginDrag acclient_2013_pseudo_c.txt | | AP-61 | Drop on a CLOSED side bag is advisory-accept: the client can't know a closed bag's item count (contents aren't indexed until opened), so OnDragOver shows green + relies on the server's InventoryServerSaveFailed reject + the rollback; retail knows the count when loaded. | src/AcDream.App/UI/Layout/InventoryController.cs (IsContainerFull) | A drop into a closed-but-full bag shows green then snaps back (a flicker) instead of pre-showing red. Faithful only when the bag has been opened (then GetContents is populated). The server's 0x00A0 + optimistic rollback is the authoritative safety net. | A drop into a closed-but-full bag shows green → brief flicker → snap-back instead of retail's pre-emptive red circle. | UIElement_ItemList::InqDropIconInfo 0x004e26f0 | | AP-62 | MissileAmmo slot mask LIKELY0x100001E0 → MissileAmmo 0x800000 is inferred; the decomp immediate at GetLocationInfoFromElementID 173676 is corrupted to a string pointer, so the mapping was not directly confirmed from the named-retail source. | src/AcDream.App/UI/Layout/PaperdollController.cs (SlotMap) | Dropping ammo onto that slot wields to the wrong location if the mapping is wrong. Gate-verify via dat dump + cdb. | Ammo wields to the wrong equip location — functional gap if wrong. | GetLocationInfoFromElementID decomp 173676; acclient.h:3193 INVENTORY_LOC | | AP-63 | Dual-wield-into-shield-slot special not implemented — retail OnItemListDragOver (decomp 174302) lets a melee-capable item also drop on the Shield slot; acdream's wieldMask = ValidLocations & slotMask rejects it. | src/AcDream.App/UI/Layout/PaperdollController.cs (HandleDropRelease) | A dual-wielder cannot off-hand a melee weapon via the doll. | Dual-wield via drag-onto-shield-slot is blocked — functional gap for dual-wield characters. | gmPaperDollUI::OnItemListDragOver decomp 174302 | | AP-64 | Wield-reject rollback assumes InventoryServerSaveFailed 0x00A0 — an optimistic wield rolls back only if ACE emits 0x00A0 for a GetAndWieldItem rejection; otherwise corrected by the next authoritative message. Gate-verify via WireMCP. | src/AcDream.Core.Net/GameEventWiring.cs (0x00A0 handler) | If ACE uses a different reject opcode for wield, the optimistic state is left dangling until the next full update. | Rejected wield briefly shows the item as equipped in the doll — cosmetic flicker or stuck state if 0x00A0 is not the rejection path. | InventoryServerSaveFailed 0x00A0; WireMCP gate-verify | | AP-65 | PickupEvent (0xF74A) no longer evicts the weenie from ClientObjectTable — only DeleteObject (0xF747) evicts, matching the retail object_table-vs-weenie_object_table split. An item another player picks up near you (only ever gets PickupEvent, never DeleteObject) lingers as a data-only entry (ContainerId 0, not in any view) until teleport/relog Clear(). | src/AcDream.Core.Net/ObjectTableWiring.cs (EntityDeleted handler); src/AcDream.Core.Net/WorldSession.cs (PickupEvent branch) | The weenie entry for nearby pickups is a harmless data ghost — no UI shows it (no container, not wielded). Retail evicts it from weenie_object_table when the object fully leaves interest range via DeleteObject; acdream defers to teleport/relog clear. | Slight memory growth in long sessions if many world items are picked up by other players near you; item data ghosts cannot cause functional issues because no UI queries ContainerId 0. | CACObjectMaint::DeleteObject / SmartBox::HandleDeleteObject; ACE Player_Inventory.cs TryDequipObjectWithNetworking | | AP-66 | RETIRED 2026-07-13 — authored paperdoll empty-slot presentation. The earlier “no silhouettes” conclusion inspected the ItemList elements' own media but missed UIElement_ItemList::InternalCreateItem, which clones a distinct UIElement_UIItem catalog prototype for each location. All 21 supported jewelry, weapon, ammo, shield, clothing, cloak, trinket, and armor lists now resolve their exact ItemSlot_Empty surface from live DAT; PostInit confirms non-armor lists remain visible while the nine armor lists toggle with Slots. | src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs; ItemListCellTemplate.cs; PaperdollController.cs | — | — | gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0; PostInit @ 0x004A5360; UIElement_ItemList::InternalCreateItem @ 0x004E3570; LayoutDesc 0x21000037 | | AP-68 | acdream keeps the 128 nearest-to-CAMERA point lights live (MaxGlobalLights=128, BuildPointLightSnapshot) and selects per cell CAMERA-INDEPENDENTLY (by the cell's own bounds), so a building interior stays lit at any distance within a town; retail keeps only the 40 nearest-to-PLAYER static lights (Render::max_static_lights=0x28, distance-sorted replace-farthest insert_light) and re-bakes a cell when its live light set changes, so distant interiors are baked dark and "light up" only as the player approaches and their torches enter the live 40. INTENTIONAL — acdream's always-lit interiors are the preferred behavior (no 1999-era light-budget pop-in); user-confirmed 2026-06-20. | src/AcDream.Core/Lighting/LightManager.cs (MaxGlobalLights=128, BuildPointLightSnapshot, SelectForObject) | The retail pop-in is a fixed-function light-budget artifact, not an intended aesthetic; revert to retail by clamping the global set to 40 + distance-to-player sort if ever desired | Distant town interiors are lit in acdream where retail's are dark until approached — a deliberate, user-preferred divergence | Render::max_static_lights 0x28; insert_light 0x0054d1b0 (distance-sorted, replace-farthest); bake re-trigger SetStaticLightingVertexColors cache burnedInStaticLights != num_static_lights | | AP-69 | acdream now preserves one accepted live record across rebucketing and ports retail's 25-second leave-visibility destruction lifecycle. Spatially resident records cancel expiry; otherwise the ACE compatibility boundary uses holtburger's conservative 384-unit distance envelope and retains attached/container/wielder/parent-owned objects. Expiry routes through the exact generation-safe F747 teardown. DIVERGENCE: the fallback does not yet derive visibility from retail/ACE ObjCell PVS (SeenOutside plus VisibleCells), and trade/container preview retention has no separate lifecycle flag. | src/AcDream.App/World/LiveEntityRuntime.cs; src/AcDream.App/World/LiveEntityLivenessController.cs; src/AcDream.App/Streaming/GpuWorldState.cs; GameWindow.OnLiveEntityPruned | Prevents stale portal destinations from accumulating animation/effect/render owners while retaining loaded long-view objects and canonical identity | A nonresident object outside 384 units that remains visible through an unusual long EnvCell PVS could expire early; a future preview-only object with no parent/container ownership could also expire. Replace the compatibility predicate when exact ObjCell PVS and preview lifetimes are available | CPhysicsObj::prepare_to_leave_visibility 0x00511F40; CPhysicsObj::prepare_to_enter_world 0x00511FA0; CObjectMaint::AddObjectToBeDestroyed 0x00508F70; CObjectMaint::UseTime 0x005089B0; docs/research/2026-07-18-retail-object-liveness-and-mesh-reclamation-pseudocode.md | | AP-71 | CEnvCell::find_env_collisions omits the CObjCell::check_entry_restrictions gate — retail's CEnvCell::find_env_collisions (pc:309576) calls CObjCell::check_entry_restrictions (pc:309576) FIRST and returns COLLIDED when an access-locked cell's restriction_obj entity rejects the mover (CanMoveInto fails AND CanBypassMoveRestrictions is false). acdream's FindEnvCollisions (src/AcDream.Core/Physics/TransitionTypes.cs:2088) goes straight to BSP collision. | src/AcDream.Core/Physics/TransitionTypes.cs:2088 | No access-restriction cell data exists in acdream. CellPhysics (src/AcDream.Core/Physics/PhysicsDataCache.cs:540) has no restriction_obj field; DatReaderWriter does not model per-cell access locks; no weenie-object-table exists on the client for the gate's CanMoveInto / CanBypassMoveRestrictions dispatch. The gap is inert in all dev content (ACE starter area has no access-locked env cells). | If access-locked dungeon cells are ever modeled (dat + wire), the player will walk through the restriction barrier without being blocked — wrong PK/housing gating. | CObjCell::check_entry_restrictions pc:309576; CEnvCell::find_env_collisions pc:309573309597 | | AP-72 | Cursor art falls back to OS standard cursors when dat resolution fails — retail always renders MediaDescCursor / EnumIDMap-resolved dat cursor art; acdream's RetailCursorManager.Apply falls back to Silk StandardCursor (IBeam/crosshair/not-allowed/…) when the EnumIDMap chain or RenderSurface decode fails, and RetailCursorResolver/RetailCursorManager permanently negative-cache the failed enum/surface id for the session. | src/AcDream.App/Rendering/RetailCursorManager.cs:47 (ApplyStandard), RetailCursorResolver.cs:47 (negative cache) | Fallback triggers only when the dat lacks the asset — nominal EoR dats always resolve the 0x27/0x28/0x29 chain; an OS cursor keeps the UI usable rather than showing nothing. | A dat-read or decode regression silently shows OS-native cursors instead of surfacing an error — masked failure class; check the [D.2b] cursor log lines before suspecting art. | ClientUISystem::UpdateCursorState 0x00564630 | | AP-74 | UseDone WeenieError text comes from a hardcoded subset map, not the portal String tables — retail resolves the 0x01C7 UseDone error code through the client String tables into the canonical line ("You are not trained in healing!"); acdream's WeenieErrorText.For hardcodes the handful of codes the current use/heal flows produce (0x001D/0x04EB/0x04FC/0x04FE, texts phrased after the ACE enum names) with a generic code-carrying fallback. | src/AcDream.Core.Net/Messages/WeenieErrorText.cs | Every refusal is now visible; only unmapped wording deviates, and those lines retain the raw code. Retire by porting the String-table lookup (#202). | An unmapped WeenieError shows a generic line instead of retail's exact sentence | retail String-table error lookup; ACE WeenieError.cs values | | AP-73 | Character raises mutate optimistically, contrary to retail's server-authoritative flow — after sending RaiseAttribute/RaiseVital/RaiseSkill/TrainSkill, CharacterSheetProvider.ApplyLocalRaise immediately bumps ranks and debits XP/credits. Named retail permits one request in flight, ghosts the clicked button, and waits for an authoritative quality-change element message before changing displayed state (#199). | src/AcDream.App/UI/Layout/CharacterSheetProvider.cs | ACE usually accepts client-affordable raises, so its later property echoes conceal the incorrect prediction; Wave 8 removes local mutation and owns one awaiting request | A rejected/reordered raise can display invented state until a later full refresh, and repeated clicks can create multiple speculative spends | gmAttributeUI/gmSkillUI raise and quality-change paths, pinned in docs/research/2026-07-10-retail-panel-behavior-pseudocode.md |

| AP-75 | NARROWED 2026-07-19 — adapter-boundary adjust_motion only. SetCycle remaps TurnLeft/SideStepLeft/WalkBackward to their mirror command with negated speed before dispatch. Retail performs that normalization in CMotionInterp; GameWindow's local-player adapter can still pass raw ids directly | src/AcDream.Core/Physics/AnimationSequencer.cs (SetCycle head remap) | Preserves raw local callers until every caller enters through MotionInterpreter; literal DAT velocity and omega now flow through CSequence's complete Frame | A future caller that already normalizes a raw left/back command but still passes the original id can be adjusted twice | CMotionInterp::adjust_motion @305343; retire with the remaining local caller unification | | AP-77 | NARROWED 2026-07-19 — animation-less/headless movement fallback only. When MotionInterpreter.DefaultSink or the local PartArray callback is absent, acdream writes grounded command-derived body velocity and applies the DAT-pinned Humanoid TurnRight rate (1.5 radians/second) directly to the body Frame. Production animated players/remotes bind MotionTableDispatchSink plus CSequence and instead consume the complete DAT-authored root Frame; that path preserves airborne orientation while suppressing only origin exactly like retail | src/AcDream.Core/Physics/MotionInterpreter.cs (ApplyCurrentMovementInterpreted); src/AcDream.App/Input/PlayerMovementController.cs (no-PartArray object-quantum fallback) | Keeps isolated/headless physics tests and a deliberately animation-less entity controllable without fabricating a PartArray | A future production entity missing its animation binding uses Humanoid-only yaw/velocity, can foot-slide, and can rotate through an airborne quantum differently from a real creature's DAT Frame | CMotionInterp::apply_interpreted_movement 0x00528600; CPhysicsObj::UpdatePositionInternal 0x00512C30; retire when animation-less production objects have an explicit motion owner | | AP-80 | PlanFromVelocity survives for velocity-only NPC cycles (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables. The adaptation is now structurally limited to replacing Ready/Walk/Run-family states, so authoritative actions/substates (especially Dead) always win. | src/AcDream.Core/Physics/ServerControlledLocomotion.cs (PlanFromVelocity, CanApplyVelocityCycle); consumer GameWindow.ApplyServerControlledVelocityCycle | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) | | AP-81 | Remote-DR gravity toggled via the Gravity STATE bit: the jump handler sets Body.State \|= Gravity at VectorUpdate and both landing blocks clear it after HitGround(); retail keeps GRAVITY set for the object's whole life and gates gravity ACCELERATION on the Contact transient (calc_acceleration) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so Motion.HitGround()'s verbatim state&0x400 gate runs BEFORE the clear) | src/AcDream.App/Rendering/GameWindow.cs (VectorUpdate jump handler + the two landing blocks) | The DR tick integrates gravity only for airborne remotes; the flag dance delivers exactly that without porting the full contact-gated calc_acceleration chain; the #161 ordering fix keeps the retail HitGround contract satisfied | Any NEW call into Motion.HitGround/LeaveGround placed after the clear silently no-ops on the gravity gate (the #161 leg-2 class); grounded remotes carry a non-retail state word (probes comparing state bits vs retail mislead) | CPhysicsObj::calc_acceleration (contact-gated); set_on_walkable 0x00511310; retire in R6 (contact-gated accel + persistent GRAVITY) | | AP-82 | StickyManager deep-overlap back-off sign pin: when the stick-gap overlap exceeds one tick's step (speed×quantum < \|dist\|, dist < 0), acdream applies delta = (speed×quantum) (rate-limited back-off); ACE's literal port keeps +delta there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | src/AcDream.Core/Physics/Motion/StickyManager.cs (AdjustOffset delta clamp; conformance StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb StickyManager::adjust_offset trace with a forced overlap when convenient | StickyManager::adjust_offset 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) | | AP-85 | Point-light pool = single 128-cap player-nearest list, optionally FILTERED by LAST FRAME's rendered visible-cell set, vs retail's dual pools (7 dynamic + 40 static, degrade-scaled) collected from a DBObj-load/flush-bounded resident registry (A7.L1, 2026-07-09 — third revision, Town Network starvation fix #79/#93/#176/#177): retail's CEnvCell::visible_cell_table (add_visible_cell 0x0052de40) is populated ON DEMAND as cells are approached/seen (DBObj::Get-loads) and pruned by flush_cells — so a real dungeon's per-frame candidate set stays small (naturally proximity-bounded) even though the collection walk itself (add_dynamic_lights 0x0052d410) is "the whole resident table, not a re-flood." acdream's _all list instead registers at LANDBLOCK-granularity load/unload (a whole single-landblock dungeon streams as ONE unit), so for the Town Network (463 registered fixtures, one landblock) _all is effectively "everything ever loaded in this dungeon," not a proximity-bounded set — wide enough that the player-nearest-128 cap alone let a straight-line-closer-but-wall-disconnected corridor's fixtures out-rank the player's own room, starving it. Fix: BuildPointLightSnapshot(playerWorldPos, visibleCells) takes an optional candidacy FILTER — a light joins the pool iff CellId==0 (cell-less, always in) or visibleCells.Contains(CellId) — narrowing candidates to the frame's actual visible cells BEFORE the existing dynamics-first player-nearest cap runs; GameWindow feeds LAST FRAME's already-rendered RetailPViewFrameResult.DrawableCells (one frame / ~16 ms latency, chosen specifically to avoid re-threading a mid-DrawInside callback — the exact mechanism, c500912b, that caused the #176 seam-floor flicker regression when it re-flooded an independent CAMERA-seeded set mid-frame). The distance-sort anchor stays the PLAYER (unchanged from the prior revision) — only candidacy narrows. Remaining deviation: this is a RENDER-visibility approximation of retail's true on-demand-load/flush RESIDENCY bound, with one frame of latency, not a port of the DBObj-load/flush mechanism itself; and the pool is still ONE 128-cap list vs retail's separate 7-dynamic/40-static degrade-scaled pools | src/AcDream.Core/Lighting/LightManager.cs (BuildPointLightSnapshot, MaxGlobalLights); src/AcDream.App/Rendering/GameWindow.cs (_lightPoolVisibleCells/_lightPoolVisibleCellsValid); pins PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant, PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics, PointSnapshot_ResidentCollection_CellTagDoesNotFilter, BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell, BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded | The render already computes a visible-cell set every frame for drawing (single source of truth, no duplicate flood) — reusing it as a candidacy filter approximates retail's proximity-bounded residency without porting DBObj on-demand load/flush; one-frame latency is imperceptible at normal camera speeds and structurally differs from the reverted mechanism (no independent re-flood mid-frame) | On a portal crossing, the FIRST indoor frame after re-entry (or after any outdoor-only frame) is unscoped (fail-open) — one frame may show slightly wider pool composition than steady-state; a room with >7 resident dynamics still shows them all (retail trims to 7 player-nearest) — slightly purpler wedge than retail; adopt the dual pools + degrade caps + true DBObj-bounded residency in later A7-arc work | insert_light 0x0054d1b0 (player-sorted, capped); add_visible_cell 0x0052de40 (on-demand-load resident registry + flush); add_dynamic_lights 0x0052d410 (whole-table walk); caller 0x00452d30; calc_point_light 0x0059c8b0 (static 1/d³ curve — A7 fix #2) | | AP-84 | BSP shadow-shape part poses = motion-table default-state frame snapshot at registration, not retail's live CPhysicsPart pose (#175): server entities with a wire MotionTableId register their BSP part shapes at the default style's first-cycle LowFrame pose (the closed pose for doors — GameWindow.MotionTableDefaultPose); retail collision reads each part's CURRENT pose every test. Equivalent for the door lifecycle (closed = default pose; open = ETHEREAL bypasses collision entirely, #150) and for idle statics | src/AcDream.App/Rendering/GameWindow.cs (MotionTableDefaultPose + the RegisterServerEntityCollision override); src/AcDream.Core/Physics/ShadowShapeBuilder.cs (partPoseOverride) | Registration is one-shot in acdream (retail re-poses parts per frame); the default-state pose is the correct idle pose and the only non-ethereal pose doors ever collide in | An entity whose server-driven motion state materially MOVES a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case — doors are the dominant BSP-part weenies); revisit if animated non-ethereal BSP movers appear | CPhysicsPart live pose (see #150 notes); motion-table default state = CPartArray init; ShadowShapeBuilder placement-frame fallback for table-less entities | | AP-83 | CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary: the CCylSphere family port (2026-07-05, retires AP-6) reads collide_with_point's PerfectClip time-of-impact math (0x0053adb6+) from ACE CylSphere.CollideWithPoint because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (movement.Z + radius in the not-definite ascending case; GlobalCurrCenter[0] used even for head-sphere hits — the latter matches the raw decomp read). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. The non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified. Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | src/AcDream.Core/Physics/TransitionTypes.cs (CylCollideWithPoint; pseudocode doc docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | CCylSphere::collide_with_point 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs CollideWithPoint | | AP-91 | CSphere collide_with_point PerfectClip TOI decoded via ACE, not the binary: the CSphere family port reads the unreadable x87 tail from ACE Sphere.CollideWithPoint/FindTimeOfCollision; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | src/AcDream.Core/Physics/TransitionTypes.cs (SphereCollideWithPoint; FindSphereTimeOfCollision) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | CSphere::collide_with_point @ 0x00537230; ACE Sphere.CollideWithPoint | | AP-86 | Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | src/AcDream.App/Physics/RemotePhysicsUpdater.cs; src/AcDream.App/Physics/LiveEntityShadowPublisher.cs; src/AcDream.App/Rendering/GameWindow.cs (local projection + authoritative UP tails); src/AcDream.App/World/LiveEntityPresentationController.cs (ordinary projection residency); src/AcDream.Core/Physics/ShadowObjectRegistry.cs (UpdatePosition) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | CPhysicsObj::SetPositionInternal(CTransition const*) 0x00515330 → change_cell, then remove_shadows_from_cells/add_shadows_to_cells after the resolved frame/contact commit | | AP-87 | NPC MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07): retail CPhysicsObj::MoveOrTeleport (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the ≥96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions — |Body.Position worldPos| > 4 m (a large correction / an unplaced first-UP body) and !willBeDrTicked (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body → garbage resolved pos → the reverted attempt's INVISIBLE monster. firstUp (LastServerPosTime<=0) is a belt hint only — the 4 m guard is the load-bearing backstop | src/AcDream.App/Rendering/GameWindow.cs (OnLivePositionUpdated NPC MoveOrTeleport routing, BodySnapThresholdNpc/willBeDrTickedNpc) | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii — inherits TS-46 | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare | CPhysicsObj::MoveOrTeleport 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap ≥96 m); InterpolationManager 100 m AutonomyBlipDistance (the retail large-correction path) | | AP-89 | TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's CPhysicsPart::SetTranslucency (0x0050e670) → CMaterial::SetTranslucencySimple (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 translucency) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's mesh_modern.frag has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled color.a (FragColor = vec4(rgb, color.a * vOpacityMultiplier)) | src/AcDream.App/Rendering/Shaders/mesh_modern.frag (final FragColor line); src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (ClassifyBatches opacityMultiplier param, InstanceGroup.Opacities); src/AcDream.Core/Rendering/TranslucencyFadeManager.cs | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since color.a * 1.0 == color.a and the fade multiplier alone then drives the ramp exactly as 1 translucency would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | CPhysicsPart::SetTranslucency 0x0050e670; CMaterial::SetTranslucencySimple 0x005396f0 (alpha = 1 translucency, applied to all 4 D3D9 material alpha channels) | | AP-90 | Radar fellowship/allegiance relationship state is modeled but not yet delivered at runtime. RetailRadar.GetBlipShape and RadarBlipColors.For implement retail's leader/member/allegiance precedence, and RadarSnapshotProvider exposes a relationshipFor(guid) seam, but acdream does not yet maintain live fellowship membership and its AllegianceTree is not wired into GameWindow. PK/PKLite relationship shapes do work from PWD flags. | src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs; src/AcDream.Core/Ui/RetailRadar.cs; src/AcDream.Core/Ui/RadarBlipColors.cs | Preserve the exact model/seam now and avoid inventing membership from names or chat; connect it when the social game-event state is ported | Fellowship members render their ordinary player color/shape instead of bright-green leader/member triangles; allegiance members render an ordinary plus instead of a hollow box | gmRadarUI::GetBlipColor 0x004D76F0; gmRadarUI::GetBlipShape 0x004D7B60 | | AP-92 | Paperdoll renders its creature through a private FBO/RTT and blits it into UiViewport; retail renders CreatureMode directly in the UI viewport/in-cell presentation path | src/AcDream.App/Rendering/PaperdollViewportRenderer.cs; src/AcDream.App/UI/UiViewport.cs | GL RTT is the modern backend equivalent and the accepted pixels/camera/pose match retail; state is sealed with GLStateScope | FBO origin, alpha, lighting, or state isolation can diverge from direct viewport rendering | gmPaperDollUI::PostInit @ 0x004A5360; UIElement_Viewport::SetCamera; retail CreatureMode::Render | | AP-93 | Paperdoll does not port UpdateForRace; all characters use the cdb-confirmed default held pose 0x030003C0 and default presentation | src/AcDream.App/Rendering/DollEntityBuilder.cs; paperdoll mount wiring | Exact for the tested Horan character; unresolved for other heritage/gender/body combinations | Non-default races can use the wrong pose, heading, camera framing, or presentation asset | gmPaperDollUI::UpdateForRace @ 0x004A3ED0 | | AP-94 | Imported Type-12 text defaults to interactive/selectable behavior; display labels can focus/capture/drag, and vitals synthesize duplicate runtime labels instead of binding 0x100000EB/ED/EF | src/AcDream.App/UI/UiText.cs; src/AcDream.App/UI/Layout/VitalsController.cs | Historical widget-generalization default; Wave 1 ports explicit Display/Selectable/Editable roles | Invisible/static text steals input and duplicate labels drift from DAT geometry | UIElement_Text property handlers; gmVitalsUI::PostInit @ 0x004BFCE0 | | AP-95 | RETIRED 2026-07-11UiButton owns retail hover/pressed/released-outside, disabled, selected/toggle, missing-state fallback, hot-click, and distinct press/release controller callbacks. | src/AcDream.App/UI/UiButton.cs; UiButtonStateMachine.cs | — | — | UIElement_Button::UpdateState_ @ 0x00471CF0; mouse handlers 0x00471FF0..0x004721F0 | | AP-96 | Layout/property import loses raw edge-mode semantics (especially 3/4) and treats default scalar values as absent, so explicit false/0 cannot override inheritance | src/AcDream.App/UI/Layout/ElementReader.cs; LayoutImporter.cs | Existing shipped layouts are hand-gated; Wave 1 adds presence-aware properties and the exact solver | Non-reference resizing recenters/stretches incorrectly; derived DAT properties silently inherit the wrong base value | UIElement::UpdateForParentSizeChange @ 0x00462640; production LayoutDesc inheritance | | AP-97 | RETIRED 2026-07-10 (Wave 1 retained-widget foundation) — the prior generic Device-timer premise was a decomp misread. Named retail polls the hovered element's tooltip deadline, then broadcasts global UI time message 3; it does not expose the assumed arbitrary timer queue. UiRoot.Tick now preserves that order and subtree removal clears input/time ownership before another pulse. | src/AcDream.App/UI/UiRoot.cs; IUiGlobalTimeListener.cs | — | — | UIElementManager::UseTime and hover/focus paths, pinned in docs/research/2026-07-10-retained-widget-foundations-pseudocode.md | | AP-98 | RETIRED 2026-07-10 (Wave 2.3) — typed handles/manager own lifecycle; every production/Studio window uses RetailWindowFrame; schema-v2 UiWindowLayout persists outer bounds, visibility, toolbar collapse, and chat maximize by character/resolution with legacy-radar migration, nearest-resolution fallback, clamping, and a pre-login default write guard. Chat maximize uses DAT 100360 limits and preserves its lower edge when growing upward. | src/AcDream.App/UI/RetailWindowManager.cs; RetailWindowLayoutPersistence.cs; RetailWindowHandle.cs; Layout/RetailWindowFrame.cs; ChatWindowController.cs; SettingsStore.cs | — | — | gmMainChatUI::HandleMaximizeButton @ 0x004CCE50; retail saveui/loadui behavior | | AP-99 | RETIRED 2026-07-11 (Wave 3.2) — Core ItemInteractionPolicy ports the complete ordered DetermineUseResult/UseObject/AttemptPlaceIn3D matrix; PWD flags and CombatUse survive CreateObject; component-pack membership comes from portal.dat; App owns throttle, UseDone-balanced busy state, target mode, wire/optimistic dispatch, and typed confirmation/auxiliary seams. The two corrupt decompiler operands were pinned from matching x86 as BF_REQUIRES_PACKSLOT and BF_VENDOR. | src/AcDream.Core/Items/ItemInteractionPolicy.cs; src/AcDream.App/UI/ItemInteractionController.cs | — | — | ItemHolder::DetermineUseResult @ 0x00588460; UseObject @ 0x00588A80; AttemptPlaceIn3D @ 0x00588600 | | AP-101 | RETIRED 2026-07-11 (Wave 4.4e) — toolbar control coverage is complete: DAT panel launchers, combat, Use/Examine, selected-object strip, shortcuts, and exact thrown-weapon/separate-ammo count resolution on authored element 0x10000194. | src/AcDream.Core/Items/ToolbarAmmoPolicy.cs; src/AcDream.App/UI/Layout/ToolbarController.cs | — | — | gmToolbarUI::UpdateAmmoID @ 0x004BF210; UpdateAmmoNumber @ 0x004BE9E0 | | AP-104 | Vitals detail element 0x100004A9 and root HideDetail/ShowDetail transitions are not wired | src/AcDream.App/UI/Layout/VitalsController.cs | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | gmVitalsUI::ListenToElementMessage @ 0x004BFC00; PostInit @ 0x004BFCE0 | | AP-105 | PARTIAL 2026-07-13 — inherited scrollbar media/roles now come from DAT (decrement/top 0x06004C69, increment/bottom 0x06004C6C), and both chat backends share typed client-command routing plus one retained ChatVM for reply state. Retained chat still lacks complete tab/filter/unread, social availability, incoming squelch enforcement, and focus-opacity behavior. | src/AcDream.App/UI/Layout/DatWidgetFactory.cs; ChatWindowController.cs; src/AcDream.App/UI/ClientCommandController.cs; chat mount in GameWindow.cs | Shared log/send path, wrapping, scrollbar roles, command ownership, and outer maximize geometry work; later chat work consolidates the remaining presentation/filter state | Tabs are no-ops, squelched lines can still render, contextual social actions are absent, and focus visuals diverge | gmMainChatUI @ 0x004CCCC0..0x004CE2A0; UIElement_Scrollbar::OnSetAttribute @ 0x004714D0; ChatInterface methods | | AP-107 | RETIRED 2026-07-11 (Wave 3.3 / #197) — typed OfferPrimaryClick returns NotActive, ConsumedSuccess, or ConsumedRejected; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | src/AcDream.App/UI/ItemInteractionController.cs; inventory/paperdoll/toolbar/radar/world call sites | — | — | UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0 | | AP-108 | Paperdoll/AutoWield still omit full AutoWieldIsLegal/dual-wield rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous " - cannot unwield the %s" failure suffix (the current send seam reports rejection asynchronously). Primary replacement retired from this row 2026-07-14: inventory activation and paperdoll drops share the confirmed blocker transaction, preserve explicit slot intent, relocate an already-worn item to an explicitly selected compatible slot, and emit retail's successful move-to-backpack status. Aetheria retired 2026-07-13. | src/AcDream.App/UI/Layout/PaperdollController.cs; src/AcDream.App/UI/AutoWieldController.cs | Basic equip slots, Aetheria, and live doll work; primary weapon, incompatible shield, and mismatched ammo blockers sequence through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | CPlayerSystem::AutoWield @ 0x00560A60; ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0; gmPaperDollUI @ 0x004A3590..0x004A5F90 | | AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | src/AcDream.App/UI/Layout/CharacterStatController.cs; CharacterSheetProvider.cs | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | gmCharacterTitleUI @ 0x0049A610; gmStatManagementUI::UpdateExperience @ 0x004F0A70 | | AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/examine/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters | src/AcDream.App/UI/RetailUiRuntime.cs; src/AcDream.Core.Net/LinkStatusSnapshot.cs; D.5/D.6 roadmap | Basic combat plus M3 spellbook/component/effects/favorite-spell/Link/Vitae surfaces now cover the active melee/missile/magic loops; the Link page has exact ping RTT but reports the current transport's zero default until its missing reliability statistics land | Large portions of retail gameplay still have no production UI; real packet loss is displayed as 0.00% instead of retail's moving average | Named gm*UI::PostInit methods; CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610; LayoutDesc catalog | | AP-111 | RETIRED 2026-07-11 (M2 held-object parenting) — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live 0xF749 ParentEvent is parsed with retail sequence freshness; a focused render controller resolves Setup.HoldingLocations, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs; src/AcDream.Core/Meshing/EquippedChildAttachment.cs; src/AcDream.App/Rendering/EquippedChildRenderController.cs | — | — | ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310; SmartBox::HandleParentEvent @ 0x004535D0; CPhysicsObj::set_parent @ 0x00515A90; CPhysicsObj::UpdateChild @ 0x00512D50 | | AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, MaybeStopCompletely, server-response queueing, and auto-repeat, but still omits StartAttackRequest's FinishJump call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | src/AcDream.App/Combat/CombatAttackController.cs; src/AcDream.App/UI/Layout/CombatUiController.cs | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | ClientCombatSystem::StartAttackRequest @ 0x0056C040; CommandInterpreter::MaybeStopCompletely @ 0x006B3B90; gmCombatUI::ListenToElementMessage @ 0x004CC430 | | AP-113 | Invalid lifestone-command arguments display the local text Usage: /lifestone; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs; RetailClientCommandCatalog.cs | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | /ls now can show different wording/color from retail while still refusing the invalid request correctly | ClientCommunicationSystem::DoLifestone @ 0x0056FC70 |

| AP-114 | RETIRED 2026-07-14 (protection-effect corrective gate) — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail Always2D classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | src/AcDream.App/Rendering/ParticleRenderer.cs; RetailParticleGeometryClassifier.cs; particle_mesh.vert/.frag | — | — | CPhysicsPart::Draw @ 0x0050D7A0; CPhysicsPart::Always2D @ 0x0050D8A0; ParticleEmitter::SetInfo @ 0x0051CE90; docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md | | AP-115 | The DAT-authored portal-space viewport and its animation SoundTweakedHook are live, but the separate ClientUISystem enter/exit sound enums and centered repeating "In Portal Space - Please Wait..." display string are not yet presented. | src/AcDream.App/Rendering/PortalTunnelPresentation.cs; teleport event switch in GameWindow.cs | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300; gmSmartBoxUI::UseTime @ 0x004D6E30 | | AP-116 | Default Particle Range = Extended multiplies DAT-authored particle degradation distances by 2; the Retail option restores exact values | src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs; src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs; src/AcDream.Core/Vfx/ParticleSystem.cs | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510; GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0; CPhysicsObj::ShouldDrawParticles @ 0x0050FE60 | | AP-117 | Outdoor particle CLandCell::IsInView state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail LScape::landcell_check uses Render::get_clip_height + Render::block_check on terrain-cell corner intervals | src/AcDream.App/Rendering/TerrainModernRenderer.cs (CollectVisibleCells) | The mandatory modern renderer batches terrain by landblock and has no retail ViewIntervalType product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | LScape::landcell_check @ 0x005050A0; CLandCell::IsInView @ 0x00532CB0; CPhysicsObj::ShouldDrawParticles @ 0x0050FE60 | | AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative WieldObject, a mode that settled without a blocker transition clears immediately; local ACE's observed pre-wield transition plus ready -> NonCombat, or post-wield NonCombat -> ready -> NonCombat, causes one normal ChangeCombatMode request from the trailing notice. Explicit user combat input cancels settlement. Retail's client does not need this extra request against the retail server. | src/AcDream.App/UI/AutoWieldController.cs; production binding in GameWindow.cs | Local ACE queues a trailing NonCombat callback during primary-weapon replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later explicit combat request, replacement, or logout clears it; peace-mode equips send none | CPlayerSystem::AutoWield @ 0x00560A60; ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0; ACE Player_Inventory.TryShuffleStance / TryDequipObjectWithNetworking |

4. Temporary stopgap (TS) — 34 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled SphereCollision replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — frames_stationary_fall accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)

# Divergence Where (file:line) Why it is safe / justified Risk if assumption breaks Retail oracle
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-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. Includes a SetSlidingNormal write at both sites — retail's BSP layer never writes collision_info.sliding_normal (only validate_transition 0x0050ac21 does; the #137 mechanism-2 class), so on transition success the steep-face normal persists to the body and seeds the next frame src/AcDream.Core/Physics/BSPQuery.cs (Path-6 steep branches, worldNormal.Z < FloorZ) 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; a persisted steep-face normal can absorb an exactly-anti-parallel next-frame push (#137 wedge class) until an oblique input clears it BSPTREE::find_collisions SetCollide pc:323783-323821
TS-5 CanJump always true — burden/stamina gating deferred (stat plumbing incomplete pre-M2). R3-W3 extends this row: IWeenieObject.JumpStaminaCost/PlayerWeenie.JumpStaminaCost are new (feeding jump_is_allowed's verbatim stamina-refusal branch) and are ALSO always-affordable/cost-0 stubs for the same reason src/AcDream.Core/Physics/PlayerWeenie.cs:44 (CanJump), :52 (JumpStaminaCost, R3-W3) Marked deferred; harmless until stats matter Client launches jumps retail refuses (exhausted/overburdened) — server rejection / rubber-band; divergent jump availability vs retail muscle memory CMotionInterp jump path stamina/burden inquiry; jump_is_allowed 0x005282b0 JumpStaminaCost vtable +0x44
TS-6 Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp src/AcDream.Core/World/WeatherState.cs:200 Decomp research verified the sky loop never reads DefaultPesObjectId; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings)
TS-7 SkyObject weather_enabled gate not honored — weather-flagged sky objects (bit 0x04) always instantiate src/AcDream.Core/World/SkyDescLoader.cs:50 No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them GameSky::MakeObject 0x00506ee0, guard at decomp:268630
TS-8 MagicUpdateEnchantment (0x02C2) records carry no StatMod — mid-session buffs don't move vital max until relog (#7/#12) src/AcDream.Core/Spells/Spellbook.cs:150 The wire parser hasn't been extended to the full ~60-64 byte Enchantment payload; PlayerDescription's block IS parsed Vitals HUD percent reads differently from retail for the whole session after any buff cast EnchantAttribute 0x00594570; holtburger magic/types.rs
TS-9 MP3 (0x55) and MS-ADPCM (0x02) waves undecoded — affected sounds skipped; retail decoded both via winmm ACM src/AcDream.Core/Audio/WaveDecoder.cs:33 Managed decoder (NAudio or similar) deferred; PCM covers the vast majority of ~3500 waves Any MP3 (common for music-ish clips) or ADPCM cue plays as silence where retail plays it winmm ACM path (r05 §2.1)
TS-14 Setup Flatten ignores ParentIndex part hierarchy (treats every placement as root-local); still in production use (GameWindow hydration, SkyRenderer) src/AcDream.Core/Meshing/SetupMesh.cs:15 Most Setups are flat single-level rigs where root-local equals composed; hierarchical composition deferred ("Phase 3") Any Setup with genuinely nested parts renders them at wrong offsets — mis-assembled multi-part objects in the Flatten paths retail Setup ParentIndex chain composition
TS-15 No distance-driven degrade (LOD): always close-detail slot 0; plus the #47 static Degrades[0] swap for 34-part humanoids only (structural sentinel detector) src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs:57 (+ src/AcDream.App/Rendering/GameWindow.cs:2608) LOD plumbing doesn't exist; slot 0 is correct for player + nearby NPCs; #47 closed the visible low-detail-arms bug without porting UpdateViewerDistance Distant objects render max-detail (perf + wrong visuals where far meshes intentionally differ/hide parts); a future 34-part non-humanoid matching the sentinel gets the wrong mesh swap CPhysicsPart::UpdateViewerDistance 0x0050E030; ::Draw 0x0050D7A0; ::LoadGfxObjArray 0x0050DCF0
TS-17 AttackConditions suffix always empty in combat chat — formatting ported, wire bitflag not plumbed (Phase I.7 follow-up) src/AcDream.Core/Chat/CombatChatTranslator.cs:233 Only the wire plumbing is missing; the holtburger-ported formatter is ready Combat log omits "[Sneak Attack]"-style suffixes retail displays — hidden combat-mechanic feedback holtburger chat.rs:588-595
TS-18 LandCell.BuildingCellId (CSortCell building bridge) declared but never populated — always null in Stage 1 src/AcDream.Core/World/Cells/LandCell.cs:19 Cell graph shipped in stages; population is explicitly membership Stage 2 (the outdoor→indoor entry path the physics digest flags as unvalidated) Cell-graph paths that should discover a building's EnvCells from the outdoor cell silently find nothing — the doorway-entry bug class CSortCell (acclient.h:31880)
TS-19 Legacy non-retail ChaseCamera (invented pitch/distance, K-fix12 airborne Z-pin) retained behind ACDREAM_RETAIL_CHASE=0 / DebugPanel toggle; both update every frame src/AcDream.App/Rendering/ChaseCamera.cs:49 Diagnostic before/after comparison path, "pending the follow-up deletion commit" When toggled on, the eye diverges from retail's spring-arm — and the render roots at the VIEWER cell, so a non-retail eye changes the render root near doorways, masking or manufacturing flap symptoms during debugging CameraManager::UpdateCamera (retail path in RetailChaseCamera.cs)
TS-20 RETIRED AS A FALSE ATTRIBUTION 2026-07-16CGfxObj::InitLoad passes the complete polygon array to D3DPolyRender::ConstructMesh; ordinary GfxObj rendering does not filter it through DrawingBSP. Building DrawingBSP traversal discovers and orders portal apertures after RemoveNonPortalNodes; it is not a global visible-polygon selector. The alleged building-shell "orphans" are DrawingBSPNode.Portals, omitted by the old diagnostic collector; the corrected node-polygons portal-polygons audit finds no true orphans. Applying the proposed filter would repeat the door disappearance regression from e46d3d9. docs/research/2026-06-11-holistic-map/wf1-gfxobj-draw.md; docs/research/2026-06-11-holistic-map/wf1-building-shells.md; tests/AcDream.Core.Tests/Rendering/Wb/Issue113DoorVanishDiagnosticTests.cs CGfxObj::InitLoad @ 0x005346B0; D3DPolyRender::ConstructMesh @ 0x0059DFA0; BSPTREE::build_draw_portals_only @ 0x00539860
TS-21 Default run/jump skills 200/300 tuned to feel until the first PlayerDescription lands (the stale "we don't parse yet" comment was FIXED in R4-V5; K-fix7 parses PD → SetCharacterSkills) src/AcDream.App/Input/PlayerMovementController.cs:311 Defaults rule only pre-PD or on PD parse failure; jump bumped 200→300 on user complaint (3.01 m max felt too low) Any window with defaults live predicts run/jump speeds the server disagrees with — observer rubber-banding, local snap-backs retail height = (skill/(skill+1300))×22.2 + 0.05
TS-23 PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always IsPlayer EdgeSlide — for BOTH the LOCAL player mover and, as of #184 Slice 2b, every remote-PLAYER dead-reckoning mover src/AcDream.App/Input/PlayerMovementController.cs:1177; src/AcDream.App/Physics/RemotePhysicsUpdater.cs (Tick sweep, IsPlayerGuid branch) Non-PK pair walks through other non-PK players — retail's default for ACE's character-creation defaults. Slice 2b gave the remote-player mover IsPlayer (was bare EdgeSlide) so remote-vs-remote non-PK players WALK THROUGH exactly like the local player and like retail (they still collide with monsters + terrain + walls); without it Slice 2b would have de-overlapped players (MORE solid than retail) On a PK/PKLite character the client lets players walk through where retail collides — now for the local player AND remote-vs-remote — the moment PvP statuses enter play (M2+) PWD._bitfield acclient.h:6431-6463; pc:406898-406918; FindObjCollisions PvP block pc:276812 (mover IsPlayer via OBJECTINFO::init 0x0050cf30 state|=0x100)
TS-24 RawMotionState action list always empty at runtime — the packer emits num_actions (bits 1115) + per-action u16 pairs (L.2b, RawMotionState::Pack 0x0051ed10), and R3-W1 gives RawMotionState/InterpretedMotionState the retail-faithful action FIFO (AddAction/RemoveAction/ApplyMotion/RemoveMotion, src/AcDream.Core/Physics/RawMotionState.cs + MotionInterpreter.cs), but nothing calls AddAction yet — the outbound caller still builds an empty Actions list, so discrete motion events (emotes, one-shots) are still never broadcast src/AcDream.App/Rendering/GameWindow.cs:8297 (empty Actions); packer src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91; FIFO capability src/AcDream.Core/Physics/RawMotionState.cs Discrete client-initiated motions (D2) not wired yet; packer-ready, state-ready (W1), runtime emission lands with R3-W2's add_to_queue/DoInterpretedMotion population When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates RawMotionState::Pack 0x0051ed10; num_actions PackBitfield acclient.h:46487
TS-25 current_style (stance, flag bit 0x2) never populated at runtime — the packer now emits it when it differs from the retail default 0x8000003D (L.2b), but the outbound caller leaves CurrentStyle at default (stance not tracked here) src/AcDream.App/Rendering/GameWindow.cs:8286 (CurrentStyle left default); packer src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:80 Stance switching is M2 combat scope Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement RawMotionState::Pack current_style 0x0051ed10
TS-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 NARROWED 2026-07-15 — F751 teleports now resend LoginComplete only after the DAT-authored portal-space viewport and final world fade finish. Initial login still sends LoginComplete directly from the PlayerCreate (0xF746) handler and does not enter the portal-space presentation. src/AcDream.Core.Net/WorldSession.cs (PlayerCreate branch); src/AcDream.App/Rendering/GameWindow.cs (F751 FireLoginComplete) The live-session bootstrap currently needs the acknowledgement to unlock the initial authoritative object/property stream; moving initial login behind the App presentation requires an explicit session→presentation readiness contract rather than withholding it inside Core.Net Initial login can expose server updates earlier than retail and skips the wormhole presentation; recalls/portals now have retail ordering gmSmartBoxUI::UseTime @ 0x004D6E30; retail post-EnterWorld flow; holtburger client/messages.rs:391-422
TS-29 Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays src/AcDream.App/Audio/OpenAlAudioEngine.cs:331 Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) retail MIDI + ambient system (r05)
TS-30 Chat DAT elements 0x100005220x10000525 render but have no controller semantics; the older claim that they are numbered in-window filter tabs is unproven src/AcDream.App/UI/Layout/ChatWindowController.cs Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive gmMainChatUI @ 0x004CCCC0..0x004CE2A0; correction in docs/research/2026-07-10-retail-panel-behavior-pseudocode.md
TS-31 NARROWED 2026-07-13/squelch, /unsquelch, /filter, /unfilter, and /messagetypes send the exact modification events and consume the authoritative retail SquelchDB; incoming ChatLog lines are not yet filtered through that database, and clickable name-tag social actions remain absent src/AcDream.Core/Social/SquelchState.cs; src/AcDream.Core.Net/Messages/SocialStateMessages.cs; src/AcDream.App/UI/ClientCommandController.cs; src/AcDream.Core/Chat/ChatLog.cs Command/state transport is complete; enforcement belongs at the shared inbound-chat boundary so both backends remain identical A squelch appears in the list and persists server-side but matching incoming lines can still render; contextual name actions remain unavailable SquelchDB::UnPack @ 0x006B1900; ChatFilter::IsSquelched; retail right-click player name → Squelch menu
TS-32 ClientObjectTable has no pre-queue for a child CreateObject that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their ContainerId links a not-yet-known container. Retail's null_object_table + null_weenie_object_table hold unresolvable objects until the parent arrives src/AcDream.Core/Items/ClientObjectTable.cs (Ingest) PD↔CreateObject ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ A container's child object arriving before the container is ingested as a root item — it won't appear in GetContents until the next RecordMembership or a move event corrects the parent link CObjectMaint::null_object_table / null_weenie_object_table (acclient.h / named-retail pc)
TS-33 NARROWED 2026-07-15 — full AP tracker semantics are ported: MTS stamps time only; AP stamps complete cell-local Position + contact plane + time; ShouldSendPositionEvent compares cell/contact inside the interval and the complete Frame including orientation afterward. Residual: acdream's single update path snapshots the AP predicate, emits a same-update MTS first when input changed, then AP. Retail proves UseTime performs Should→AP, but MTS originates in separate input callbacks; their relative same-tick callback/wire order is not yet traced src/AcDream.App/Rendering/GameWindow.cs (outbound MTS/AP blocks); src/AcDream.App/Input/PlayerMovementController.cs (ported tracker) Preserve the pre-existing acdream wire order until a focused retail packet/breakpoint trace establishes input callback versus UseTime; do not infer it from UseTime alone In the rare update where both packets are due, ACE may observe their position timestamps/action sequences in the opposite order from retail, shifting only that correction tick; stationary target-facing is live-gated because full-frame orientation now publishes CommandInterpreter::UseTime 0x006B3BF0; SendMovementEvent 0x006B4680; SendPositionEvent 0x006B4770; ShouldSendPositionEvent 0x006B45E0; Frame::is_equal 0x00424C30
TS-40 Retail's physics_obj->cell ("placed in the world") is proxied by the explicit PhysicsBody.InWorld flag — set by SnapToCell (local player placement) and RemoteMotion construction (remotes exist only for world entities); consumed by CMotionInterp's detached-object link-strip guards (if (cell == 0) RemoveLinkAnimations, raw @305627). Replaces the UNREGISTERED CellPosition.ObjCellId == 0 proxy, which only the local player ever seeded (#145 SnapToCell), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug src/AcDream.Core/Physics/PhysicsBody.cs (InWorld); src/AcDream.Core/Physics/MotionInterpreter.cs (3 guard sites) acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag CMotionInterp::DoInterpretedMotion 0x00528360 tail @305627; CPhysicsObj::RemoveLinkAnimations
TS-35 PhysicsBody.IsFullyConstrained is a stub property (default false, never set by any physics code), read by jump_is_allowed's verbatim IsFullyConstrained gate (raw 305524-305525) src/AcDream.Core/Physics/PhysicsBody.cs (IsFullyConstrained) R3-W3 needed the read site to port jump_is_allowed's full chain. R5-V1 CORRECTED the mechanism (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the ConstraintManager server-position rubber-band leash — armed by SmartBox::HandleReceivedPosition on every inbound server position, IsFullyConstrained = max*0.9 < offset. R5-V1 ported ConstraintManager (src/AcDream.Core/Physics/Motion/ConstraintManager.cs) but does NOT arm it (no acdream SmartBox + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) CPhysicsObj::IsFullyConstrained 0x0050ec60 → ConstraintManager::IsFullyConstrained 0x005560d0; jump_is_allowed 0x005282b0; arming SmartBox::HandleReceivedPosition 0x00453fd0 (issue #167)
TS-37 RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had contact_allows_move (0x00528240) arm StandingLongJump as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; ChargeJump (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule src/AcDream.Core/Physics/MotionInterpreter.cs (contact_allows_move, ChargeJump) N/A — retired N/A — retired CMotionInterp::charge_jump 0x005281c0 @305448
TS-38 MotionInterpreter.Initted defaults to true in both constructors, not retail's false — retail's CMotionInterp is never observed pre-enter_default_state (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step src/AcDream.Core/Physics/MotionInterpreter.cs (Initted property + both constructors) Defaulting true is the C# equivalent of "the constructor already did what enter_default_state would have done to this flag" — EnterDefaultState() remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, LeaveGround tail) when a caller wants them None observed: no code path needs apply_current_movement/ReportExhaustion to no-op before an explicit EnterDefaultState() call, since nothing constructs a MotionInterpreter and defers initialization today. If a future caller DOES need staged construction (build now, EnterDefaultState() later), it must explicitly set Initted = false first CMotionInterp::enter_default_state 0x00528c80 @306124 sets initted = 1; retire if/when construction is staged through EnterDefaultState() uniformly
TS-41 RETIRED 2026-07-07 (remote-creature de-overlap #184) — the SERVERVEL synth-velocity body-drive (Body.Velocity = ServerVelocity / get_state_velocity() leg) is DELETED. Grounded NPC remotes now translate by the retail interp CATCH-UP (RemoteMotionCombiner.ComputeOffsetInterpolationManager::adjust_offset toward the MoveOrTeleport-queued server waypoint) and MovementManager::UseTime (TickRemoteMoveTo) runs UNCONDITIONALLY per tick — the retail UpdateObjectInternal shape (no wire-velocity leg-driver). The de-overlap sweep resolves the catch-up movement; the resolved position is written back into the SHADOW (AP-86) so it persists. Residual: the non-retail anim-cycle stale-stop heuristic (ApplyServerControlledVelocityCycle(Zero) on a >0.6 s velocity-staleness timer) is kept as ANIM-only and stays covered by AP-80; it no longer drives the body. src/AcDream.App/Rendering/GameWindow.cs (TickAnimations grounded NPC branch) CPhysicsObj::UpdateObjectInternal 0x005156b0 (MovementManager::UseTime @0x00515998, unconditional); MoveOrTeleport 0x00516330; InterpolationManager::adjust_offset 0x00555d30
TS-44 NPC UpdatePosition enqueue is suppressed while StickyManager is armed (PositionManager.GetStickyObjectId() != 0). Position and complete orientation otherwise share the ported InterpolateTo → Position::subtract2 → PositionManager::adjust_offset Frame, so the former orientation hard-snap residual is retired. Retail would still enqueue the server Position and let Sticky overwrite that Frame each tick; acdream retains the gate so no queued waypoint survives the stick src/AcDream.App/Rendering/GameWindow.cs (OnLivePositionUpdated NPC snapSuppressedByStick gate) Avoids replaying an old ACE waypoint immediately after a stick lease ends; all live during-stick pose ownership is now otherwise retail-shaped After unstick the body waits for the next UP instead of consuming the latest waypoint already in the queue; at low packet cadence this can pause correction for one update interval PositionManager::adjust_offset 0x00555190; CPhysicsObj::MoveOrTeleport 0x00516330; retire by allowing enqueue while Sticky overwrites the shared complete Frame
TS-46 Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by SpherePath.InitPath (foot center at radius, head center at height radius) — retail passes the Setup's SPHERE LIST verbatim (CPhysicsObj::transition 0x00512dc0 → init_sphere(GetNumSphere, GetSphere, m_scale), ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). #184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (GetSetupCylinder = setup.Radius/setup.Height × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale. RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's stepUpHeight/stepDownHeight stay a hardcoded 0.4 m, where retail derives them from setup->step_up_height/step_down_height (0x005180d0/0x005180f0, 0.04 m fallback, radius×0.5 clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) src/AcDream.Core/Physics/TransitionTypes.cs (InitPath); src/AcDream.App/Input/PlayerMovementController.cs (player, human — correct as-is); src/AcDream.App/Rendering/GameWindow.cs (remote sweep now GetSetupCylinder-fed with a shapeless→human fallback) The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii Marginal rε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers CPhysicsObj::transition 0x00512dc0; SPHEREPATH::init_sphere 0x0050c670 (≤2, ×m_scale); set_description 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into InitPath
TS-45 RETIRED 2026-07-07 — the hand-rolled SphereCollision (forced combinedR+1 cm radial de-penetration + leaked SetSlidingNormal + always-Slid, head-sphere ignored) is REPLACED by the faithful CSphere::intersects_sphere family port (branch dispatcher 0x00537A80 + step_sphere_up/slide_sphere/land_on_sphere/collide_with_point/step_sphere_down), routing the grounded slide through the shared crease SlideSphere (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. SphereCollisionFamilyTests (slide-around, block, ethereal) + docs/research/2026-07-07-csphere-collision-family-pseudocode.md. Residual AP-84 (PerfectClip TOI dead in M1.5). CSphere::intersects_sphere 0x00537A80 (pc:321678)
TS-47 NARROWED 2026-07-13 — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs; src/AcDream.App/UI/ClientCommandController.cs; src/AcDream.Core.Net/Messages/ClientCommandRequests.cs The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action ClientCommunicationSystem command-table construction around 0x00581A40..0x005850A0; docs/research/2026-07-13-retail-client-command-routing-pseudocode.md; docs/research/2026-07-13-retail-client-command-families-pseudocode.md
TS-48 Dragging an item onto another player honors the authoritative DragItemOnPlayerOpensSecureTrade option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through GiveObjectRequest 0x00CD works when the option is disabled; NPC giving is complete. src/AcDream.App/UI/ItemInteractionController.cs (PlaceIn3D, PolicyActionMessage); src/AcDream.Core/Items/ItemInteractionPolicy.cs The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. ItemHolder::AttemptPlaceIn3D @ 0x00588600; PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0; ClientTradeSystem; docs/research/2026-07-13-retail-give-item-pseudocode.md
TS-49 Hidden-object availability is bridged through TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld) because acdream has not ported retail's DetectionManager. Retail CObjCell::hide_object sends LeftDetection to detection voyeurs; acdream instead uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. src/AcDream.App/Rendering/EntityPhysicsHost.cs (NotifyHidden); src/AcDream.Core/Physics/Motion/TargetManager.cs (NotifyVoyeurOfEventAndClear) The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. Plugins or future systems listening specifically for retail detection enter/leave events receive no LeftDetection; only movement/sticky target consumers observe the equivalent availability loss. CObjCell::hide_object @ 0x0052BE30; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through LeftDetection
TS-50 AnimationDone executes semantically at each owner's retail CPhysicsObj::process_hooks boundary, but all other animation hooks are retained in AnimationHookFrameQueue until final root/part/equipped-child pose publication. Retail executes the complete hook stream before transition and the Target/Movement/PartArray/Position manager tail because its current CPartArray pose already exists in-place. Static owners correctly reach process_hooks only after their root, parts, and children are current. src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs; src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs; frame drain in src/AcDream.App/Rendering/GameWindow.cs The modern renderer publishes immutable effect-pose snapshots after all root/child composition; deferred visual sinks avoid attaching particles/lights/audio to the previous pose. Semantic AnimationDone is split out and exact, so motion completion and manager behavior are not delayed. Pose-owner lifetime tokens prevent deferred hooks from crossing delete/local-ID reuse. A non-AnimationDone hook with same-quantum semantic consequences (notably CallPES, default-script chaining, audio/particle creation relative to a transition) runs later than retail and can observe post-tail state or start one render frame late. CPhysicsObj::process_hooks @ 0x00511550; CPhysicsObj::UpdatePositionInternal @ 0x00512C30; CPhysicsObj::animate_static_object @ 0x00513DF0; retire by publishing the current per-object/child pose before hook routing or splitting semantic and presentation sinks without changing authored hook order
TS-51 Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each object's ParticleManager and ScriptManager inside every admitted UpdateObjectInternal quantum; animate_static_object also advances that static owner's managers using its whole admitted elapsed interval. src/AcDream.App/Rendering/GameWindow.cs (AdvanceLiveObjectRuntime final _particleSystem.Tick(dt) / _scriptRunner.Tick(...)) The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and manager order faithful without pretending the shared tails have per-owner timing. Splitting ownership safely requires a later effect-lifetime slice. A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static default scripts/particles use render elapsed rather than animate_static_object elapsed/discard timing. CPhysicsObj::UpdateObjectInternal @ 0x005156B0; CPhysicsObj::animate_static_object @ 0x00513DF0; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum

5. Unclear (UN) — 5 rows

These rows have a missing, contradictory, or never-argued justification. They are the highest-priority audits: each needs either a recorded equivalence argument (promote to AD/AP) or a fix.

# Divergence Where (file:line) Recorded justification (deficient) Risk if assumption breaks Retail oracle
UN-1 CheckOtherCells iterates the overlap set SORTED by cell id; retail walks the CELLARRAY in build order — and the loop halts on the first non-OK result, so order is behavior-bearing src/AcDream.Core/Physics/CellTransit.cs:1718 Justified only as "deterministic order for greppable probe logs" — no equivalence argument vs retail's array order recorded A sphere straddling two cells that would each return a different non-OK result halts on a different cell than retail — different collision normal / slide direction at multi-cell straddles CTransition::check_other_cells pc:272717-272798
UN-3 AdminEnvirons fog-override RGB tints hardcoded with no retail constant cited (RedFog 0.60/0.05/0.05 etc.); Snapshot replaces fog COLOR only, keeping keyframe distances on an unverified assumption src/AcDream.Core/World/WeatherState.cs:350 Enum semantics cite ACE EnvironChangeType + r12 §5.2; no source for the RGB values or the color-only override scope A server-forced fog event renders the wrong hue and/or wrong density vs what retail clients showed for the same packet AdminEnvirons 0xEA60; ACE EnvironChangeType.cs
UN-4 GfxObj double-sided/negative-surface handling keeps WB's legacy logic (cull-mode double-siding, no reversed-winding duplicate, different neg-surface predicate) while the CellStruct path follows the retail-cited ConstructMesh reading src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1059 (CellStruct contrast :1396-1410) No recorded justification on the GfxObj side — it is the unmodified WB extraction; the retail citation was added only to the CellStruct path GfxObj models retail draws via duplicated-reversed-winding get wrong back-face lighting (normals not inverted) or missing/extra negative faces — dark or absent faces from behind D3DPolyRender::ConstructMesh 0x0059dfa0
UN-6 Fixed 200 ms sleep between ConnectRequest and ConnectResponse; retail inserts no delay. Annotated only as "with 200ms race delay"; the 2026-06-04 audit flagged it, the follow-up refuted "forbidden workaround" but wrote no fuller rationale back src/AcDream.Core.Net/WorldSession.cs:484 Presumed ACE port+1 listener race guard — four words, no citation Every login eats a flat 200 ms; if the race needs longer on a loaded server, the handshake fails intermittently (ConnectResponse ignored → CharacterList never arrives, exit-29 shape) with no retry — a timing constant masking an unconfirmed root cause (none recorded)
UN-7 Outdoor OBJECT point lighting uses calc_point_light (wrap/norm + per-channel cap, ~1/d²) for ALL meshes including static buildings, but retail's object path is unconfirmed — config_hardware_light (0x0059ad30) sets D3D-FF point lights (Diffuse=color×intensity, Attenuation=(0,1,0)1/d, Range=falloff×1.5, material.diffuse=white) yet that math would blow walls WHITE while retail stays DIM, so static buildings may instead use the SetStaticLightingVertexColors bake. Model + the brightness-scaling factor both UNRESOLVED (issue #140 / Fix D) src/AcDream.App/Rendering/Shaders/mesh_modern.vert (pointContribution); src/AcDream.Core/Lighting/LightManager.cs (SelectForObject) Fix A/B ported calc_point_light + per-object selection for objects without confirming retail uses that model for static buildings; cdb captured the D3D-FF path but it contradicts the observed dim result Outdoor buildings blow out warm near torches (the #140 meeting-hall symptom); whichever model is wrong, the object torch contribution is too strong config_hardware_light 0x0059ad30; SetStaticLightingVertexColors 0x0059cfe0; rangeAdjust=1.5 0x00820cc4 — see docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md

6. Retire-next shortlist

Temporary-stopgap + unclear rows, ordered by risk (symptom severity × likelihood the guarding assumption breaks). Items below the line are phase-gated — they carry their trigger in their row and should land WITH that phase, not before.

  1. TS-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.
  2. 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.
  3. 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).
  4. TS-1 — PrecipiceSlide stop-at-edge — visible movement mismatch at every cliff/roof edge; diagnostic already records which ingredient is missing.
  5. 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.
  6. 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.
  7. TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12) — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
  8. UN-3 — AdminEnvirons tints — invented RGB constants + unverified color-only scope; one decomp lookup against the 0xEA60 handler.
  9. 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-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 remaining live-pose animation work lands TS-14.


Maintenance: this register is part of the definition of done for any phase that adds or removes a divergence. Sources merged 2026-06-12: 5-area code sweep, docs/architecture/worldbuilder-inventory.md, docs/ISSUES.md accepted-divergence entries (#96, #49, #50).