User decision 2026-07-05: the FPS/ms wild-fluctuation complaint is
resolved (MP0 profiler + MP1a Content extraction + MP-Alloc safe batch,
all shipped + merged to local main, user-confirmed steady FPS). ECS
(MP3) and the rest are deferred to the future - ECS is the throughput
lever (higher avg FPS), a different goal from the steadiness we fixed;
revisit only if a higher raw FPS number is specifically wanted. Rust
rejected. Rationale captured in memory project_mp_track_findings.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port retail's per-frame light collection: the point-light pool is built from ONLY the
currently-visible cells' lights, matching CObjCell::add_*_to_global_lights
(0x0052b350/0x0052b390) walked over CEnvCell::visible_cell_table (0x0052d410) — not a
flat world-space set capped at 128-nearest-camera.
- LightSource.CellId (retail insert_light arg6 -> RenderLight +0x6c); tagged at both
registration sites from entity.ParentCellId (live weenie fixtures + dat EnvCell statics).
- LightManager.BuildPointLightSnapshot(camPos, visibleCells): a light joins the pool iff
CellId==0 (viewer/global) or its cell is in the flood. 128 cap kept as a now-non-biting
backstop (retail's is 40 static + 7 dynamic, 0x0081ec94/8).
- Threaded via RetailPViewDrawContext.RebuildScopedLights, invoked in DrawInside after the
flood resolves prepareCells and before the draws (renderers select from the same
in-place-rebuilt PointSnapshot; EnvCellRenderer clears its per-cell cache each pass).
- [indoor-light] probe (ACDREAM_PROBE_INDOOR_LIGHT=1) dumps the scoped-pool SET COMPOSITION.
Un-skips LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant.
CORRECTION: the handoff called the camera-cap the "confirmed" #176/#177 mechanism. The probe
PROVES scoping works (291 Hub fixtures -> pool of 1-9, ~285 through-floor lights dropped/frame,
CellIds match the flood), but the user's VISUAL GATE showed BOTH symptoms unchanged. So pool
composition is NOT the cause. #176 real cause = an over-bright purple point light
(intensity=100, color 0.784,0,0.784 -- from [light-detail]); #177 = a portal-visibility miss
(stairs not drawn looking back). Both stay OPEN. This change is retail-faithful and retires the
camera-eviction latent bug; kept as such, not as the symptom fix. Register AP-85 corrected;
ISSUES #176/#177 re-diagnosed; render digest banner updated.
Decomp: insert_light 0x0054d1b0, minimize_object_lighting 0x0054d480, calc_point_light
0x0059c8b0; pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md.
Suites green: Core 2595 + 2 skip, App 719 + 2 skip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dense-town before/after (frame profiler + user eyes): frame-time max
20-87ms -> 6-10ms, single-frame alloc spikes (30-75MB) eliminated,
gen2 GC 5-11/window -> ~0, user confirms the FPS counter now holds
steady instead of swinging. The residual steady ~1.6MB/frame (from the
deferred harder sites: EnvCell rebuild gate + physics Transition
pooling) is small gen0-only churn that no longer causes swings; those
are now an OPTIONAL follow-up that would also lower the median.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User goal clarified: the wildly fluctuating FPS/ms IS the target. Root cause is per-frame throwaway allocations triggering periodic gen2 GC pauses = the spikes. Plan takes only the easy, bit-identical, non-faithfulness-sensitive buffer-reuse sites from the 54-site audit (animation pose buffers, particle draw-lists/iterator, interior partition, trivial per-frame HashSets); defers the two risky sites (EnvCell settled-camera gate, physics Transition pooling). Gate = the existing frame profiler before/after (alloc_kb down, cpu_ms max/p99 tighten) + identical visuals. Loading/pak work parked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54-site adversarial allocation audit: town GC churn (1.5-3 MB/frame) is ~90-95 pct OUTSIDE the MP3 draw-submission surface (visibility compute, EnvCell rebuild, particle draw-lists, per-entity anim pose) and independent of the pak. So MP3 will NOT fix the steady-state stutter, and the triage is separable + mostly easy faithfulness-neutral buffer reuse. New MP-Alloc phase pulled ahead of MP1c (load-time smoothness is the rarer hitch). Also flips MP1b to blocked-on-dedup and records the pview-runs-outdoors scenario. Findings in project_mp_track_findings.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session handoff for the next pickup. #176 (purple seam flash) + #177
(stair-room light pop-in) are ONE bug, root-caused to the camera-nearest
MaxGlobalLights=128 snapshot cap evicting in-range lights of visible
cells (Hub = 366 fixtures). Fix deferred to A7 because uncapping exposes
unported per-cell light-reach (through-floor light) + the fixture
falloff-curve misassignment + an unattributed striped-floor artifact.
- New handoff: docs/research/2026-07-06-176-177-handoff-A7-lighting.md
(root cause, why-deferred, the A7 fix order, tooling inventory).
- Roadmap A7 updated: now owns #176/#177; the 'light visibility culling'
hypothesis layer is CONFIRMED (no per-cell insert_light registration);
fixture-curve layer added. Analysis pre-paid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-bake gate baked all 729,888 EnvCells as separate fileId-keyed blobs = 865 GB pak in 52 min, 97 pct near-duplicate cell geometry. The prior 'v1-acceptable duplication' call was wrong by ~100x. Bake-time dedup (compute geomId, extract each unique geometry once, alias all sharing cell-fileIds to one blob offset) is now a v1 requirement; needs no format or reader change and collapses size AND bake time together. MP1b not shipped until the dedup slice lands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MaxGlobalLights 128->1024 fix (4d25e04d) was live-tested and made
the eviction pops stop — but with the full 366-fixture pool active,
three unported retail lighting semantics dominate the Facility Hub:
(a) lights reach THROUGH solid floors/walls: retail registers lights
per-CELL (insert_light 0x0054d1b0) so the under-room portals'
purple light never touches the corridor above; our flat
sphere-overlap selection has no reach/occlusion notion — rooms
washed magenta (user screenshot).
(b) stationary weenie fixtures ride the DYNAMIC 1/d falloff (~9x
retail's static 1/d3 bake curve at 3m) — the #143 isDynamic
assignment is wrong for ACE-served world fixtures.
(c) an unexplained striped z-fight-like artifact on lit floor regions
(user screenshot; no coincident dat geometry — the coplanar-pair
sweep came back empty; not a striped texture — all corridor
surfaces are plain Base1Image stone).
Reverted to 128. The cap is now documented as a LOAD-BEARING STOPGAP:
it accidentally approximates per-cell reach by keeping the pool local
to the camera. The #176/#177 root cause (cap eviction popping per-cell
light sets) stays CONFIRMED and fully documented; the real fix is the
A7 dungeon-lighting arc: per-cell light registration + the static
fixture curve + the stripe hunt, THEN uncap. The desired-end-state pin
is kept as Skip with the full pointer. Register row AP-85 rewritten to
match reality; ISSUES #176/#177 back to OPEN with the complete
mechanism story.
Suites: Core 2591+3skip / App 719 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MP1b review finding, verified against EnvCellRenderer.GetEnvCellGeomId:
runtime dedups interior geometry under a 64-bit content hash (bit 33),
not the cell fileId the pak keys by. MP1c maps at the RegisterCell seam
(read pak[cellFileId], register under geomId, dedup post-load as today);
duplicate blob content on disk is v1-acceptable. GeomId-keyed baking is
not viable in format v1 (hash exceeds PakKey's 56 usable bits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The probe launch discriminated it: the user reproduced the purple floor
flash while [light] (ambient branch) and [pv-input] (portal flood) read
provably healthy — eliminating the last CPU-side theories and exposing
the one channel the probes could not see: per-cell 8-light set
composition.
BuildPointLightSnapshot kept the MaxGlobalLights=128 point lights
nearest THE CAMERA; the Facility Hub registers 366 fixtures, so 238
were evicted per frame by camera distance. SelectForObject (faithfully
camera-independent, and unit-pinned as such) could only choose from the
surviving 128 — an in-range torch of a visible cell that ranked past
the cap dropped out of that cell's 8-set, so per-cell Gouraud lighting
flipped as the chase boom swung the camera:
- #176: the flipping unit is a CELL -> discontinuity lines at exactly
cell-seam granularity; a torch-losing floor drops to dim blue-grey
stone at 0.2 ambient (the perceived purple), camera-angle dependent.
- #177: a stair room whose torches all ranked past the cap rendered at
bare 0.2 ambient (near-black = 'not visible'); approach re-admitted
them ('pops into existence'); the sweeping boundary dropped the
ramp's lights mid-descent ('disappears on the last step'). The
geometry never vanished - its lights did.
Retail's minimize_object_lighting (0x0054d480) has NO global
camera-nearest pool cap (lights register per cell, insert_light
0x0054d1b0). Fix: MaxGlobalLights 128 -> 1024, a non-biting safety
valve (GlobalLightPacker grows to fit; 64 B/light). Register row AP-85.
TDD pin: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(RED at 128 with a Hub-scale 401-light layout, GREEN at 1024). The
pre-existing camera-independence pin covered the SELECTOR but not the
SNAPSHOT it selects from - the pop re-entered one stage upstream.
Suites: Core 2588 / App 719 / UI 425 / Net 385 green. Pending user gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dungeon render pair (purple seam flash + stair pop-in) resisted
desk root-causing, but the investigation narrowed the space to two
live theories and shipped permanent apparatus:
- Issue176177DungeonSeamInspectionTests: dat truth — corridor floors
ARE textured drawn PortalSide portal polys; the reciprocal is NoPos;
the 'stairs' are 0x8A020182's ramp shell (vertical portals, zero
statics); CellBSP partitions exactly at portal planes; DXT1 textures
carry zero transparent-mode texels.
- Issue176177FacilityHubFloodReplayTests: production-matched flood
replays — approach/descent/gaze-sweep/walk all healthy with coherent
inputs; ScenarioE pins the flood's collapse-to-root sensitivity under
incoherent (root, eye) pairs.
- Issue176177SeamTransitLagTests: the resolver flips cells within one
tick-step of the portal plane — the 0.33-0.47m [cell-transit] 'lag'
in the gate logs is speed*tick quantization, not membership error.
Refuted (do NOT retry — ledger in the research doc): placeholder
texture, reciprocal z-fight, seal z-fight (seals only fire for
OtherCellId==0xFFFF), root/eye incoherence (production camera sweep is
mm-exact at planes), flood bistability, #119-class statics, undefined
DXT mips (both paths decode DXT->RGBA8; the compressed-array branch is
dead code), DXT1 alpha, fog mix (ramp ~538m), lightning leak (flash==0
in production), viewer-light pops (smooth (1-d/range) ramp).
Filed #178 (A8 double-sided shell stopgap still live) and #179
(lightning flash lacks an indoor gate — dormant). The purple can only
be the fog clear color (undrawn pixels) or the outdoor ambient+sun
tint; discrimination needs ONE probe launch (ACDREAM_PROBE_LIGHT +
ACDREAM_PROBE_PVINPUT + ACDREAM_PROBE_CELL) — protocol in
docs/research/2026-07-06-176-177-render-pair-investigation.md.
Suites: Core 2587 / App 719 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Normative v1 format (ACPK header with dat-iteration stamps, sorted
24-byte TOC entries with crc32, 64-byte-aligned uncompressed blobs,
deterministic ObjectMeshData serialization), the bake CLI that drives
MP1a MeshExtractor in parallel, the mmap zero-copy PakReader, and a
dat-gated live-vs-pak equivalence suite. Scope-fenced to the
ObjectMeshData asset classes (the decode-storm content from the MP0
baseline); terrain/BSP/scenery/degrade blobs are later slices. Also
amends the spec: PakReader lives in AcDream.Content (Content->Core
direction from MP1a makes a Core home circular).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Launch smoke passed: world renders identically (user-confirmed), zero
dat-layer tripwires in the session log, frame profile identical to the
pre-extraction baseline (town p50 2.1-2.2ms, same GC cadence) - the
move was perf-neutral as a pure relocation should be. Roadmap Track MP
table updated: MP1 split into shipped MP1a and upcoming MP1b (pak +
bake tool + reader) / MP1c (streaming cutover + hitch gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coordinator-directed follow-up. The buffer-and-drain seam diverged from
the original on the exception path: pre-MP1a, CollectEmittersFromScript
enqueued particle-preload meshes DIRECTLY into _stagedMeshData
mid-Prepare, so preloads staged before a later throw in the same
Prepare* call (reachable via PrepareEnvCellMeshData side-staging during
its StaticObjects loop, then PrepareCellStructMeshData throwing on a
malformed-dat texture decode) were already safely enqueued. The drain
version only flushed after a successful return — on throw, entries
stranded on the shared extractor until an unrelated successful call
flushed them, and were silently dropped on dispose.
Fix: MeshExtractor takes an Action<ObjectMeshData>? sideStagedSink
constructor parameter; the two CollectEmittersFromScript sites become
_sideStagedSink?.Invoke(meshData) — the original code shape (immediate
hand-off) at those exact lines. ObjectMeshManager wires the sink to
_stagedMeshData.Enqueue, restoring the original immediate-enqueue
semantics including on mid-Prepare throw. _sideStaged buffer,
DrainSideStaged(), and the ProcessQueueAsync drain loop are deleted.
The MP1b bake tool passes its own collector.
Inventory doc updated: MP1a note now records the sink seam and the
Content-owned upload enums, so its no-behavior-change claim is accurate.
dotnet build green; full test suite 4059 passed / 0 failed / 4 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MP1a ground truth: the runtime atlas consumes decoded RGBA8 via
TextureBatchData, so baking to BC7/BC1 (lossy) would change delivered
pixels and contradict the phase's own byte-identical conformance gate.
V1 stores RGBA8 exactly as the runtime produces; BC becomes an explicit
post-conformance option with its own visual gate. Mip chains stay
runtime-generated in v1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corrected 1.835m collision capsule blocks the window alcove like
retail; doorways, seams, and stairs unaffected by the taller head.
Remaining #137 scope: the doors half (block/pass per open state, the
#175 pose fix's pending gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical-move plan: lift TextureKey out of the GL atlas class, move the
ObjectMeshData family, extract the Prepare* CPU pipeline verbatim into a
new MeshExtractor in AcDream.Content (net10.0, no Silk.NET), App keeps
queue/worker/upload lifecycle and delegates. Binding rules: verbatim
bodies (no SurfaceDecoder switch - byte-identity is MP1b conformance
foundation), compiler enumerates dependencies, BLOCKED on any hidden GL
dependency, zero divergence rows. Gates: full suite green per task +
user launch smoke. Boundary facts verified against source this session:
the Prepare* region touches GL only via the TextureKey struct; first GL
call is UploadGfxObjMeshData.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'run into the last corridor window and pop up through its roof'
report: the live callers passed sphereHeight: 1.2f into
SpherePath.InitPath, whose head-sphere formula (height - radius) put the
head sphere center at 0.72 - the capsule top at 1.2m. The top 0.63m of a
1.83m character had NO collision, so at the corridor-end window alcove
(0x8A020179 -> 0x8A02017E: 0.70m sill face, 1.3m opening, sloped funnel
behind) the step-up's placement never saw the head overlapping the
lintel solids and let the player climb in head-through-roof.
Dat truth (HumanSetup_CollisionSpheres_DatTruth): Setup 0x02000001
spheres = (0,0,0.475) r=0.48 and (0,0,1.350) r=0.48 - capsule top 1.83 =
Setup.Height 1.835. Retail collides with that sphere list verbatim
(CPhysicsObj::transition 0x00512dc0 -> init_sphere(GetNumSphere,
GetSphere, m_scale)).
Fix: PlayerMovementController + the GameWindow remote resolve now pass
sphereHeight: 1.835f (capsule top; head center 1.355 vs dat 1.350).
InitPath unchanged - captured-input replay fixtures (recorded 1.2
inputs) stay byte-identical. Register TS-46: the (radius, capsule-top)
scalar approximation of the Setup sphere list (5mm foot/head offsets;
remotes use human dims) with the retire path (plumb the sphere list).
Pins: WindowOpening_HeadCannotFit_EntryBlocked (22-frame walked approach
wall-slides at the sill, never enters 0x8A02017E) +
WindowAlcove_RaisedPlacement_HeadInLintelSolid_Collides (Path-1
placement rejects the raised head in the lintel solids) + the
WindowShaft_FullPolyDump / HumanSetup dat inspections.
Suites: Core 2562 / App 713 / UI 425 / Net 385, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-route capture (Holtburg -> dungeon 0x0007 -> town 0xCE94 -> dungeon
-> Holtburg, 4 teleports) plus the supplemental town run. Verdict: the
render-side-CPU split is confirmed (GPU <= ~2.7ms, upd/upl/imgui ~= 0),
steady medians beat the spec assumption (worst town p50 3.6ms), and the
smoothness gap is GC: 1.5-3 MB allocated per frame drives gen2
collections 1-2/s and every town p99/max violation, while dungeon
windows (zero gen1/gen2) are spike-free at ~2000 fps. Teleport hitch
quantified at 211ms worst frame / 75.7MB single-frame allocation.
Gate decision: PROCEED to MP1 unchanged, with one recorded amendment -
a bounded post-MP1 allocation-triage session for churn sites outside
the MP3 rewrite surface. Fort Tethana axiom view still to be
re-measured at the MP2/MP3 gates.
MP0 complete: profiler shipped (7d74c68c..4b44a152) + baseline + gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User gate 2026-07-06 evening: 'not collision anymore. Good.' — the
corridor-phantom arc (stub sliding-normal leak, slide_sphere opposing
branch, CheckOtherCells stale footCenter) is user-verified fixed.
Same gate surfaced three follow-ups:
- #137 remaining scope: the window/opening climb — running into the last
corridor window steps the player up into the opening, head+camera
through its roof. Log anchor: transit 0x8A020179 -> 0x8A02017E at
(90.209,-41.774,-5.209) — sill ~0.8m > stepUpHeight 0.6, climbed anyway.
- #176 (NEW, render): purple floor flashing at cell seams, camera-angle
dependent — survives the physics fix, so render-side; the seam floors
ARE portal polygons to the under-rooms (placeholder-surface suspicion).
- #177 (NEW, render): stairs between levels pop in/out — invisible from
the corridor, appear on entering the room, vanish on the last step
down. The #119 visibility class, dungeon edition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap gains the Track MP phase table (MP0 in progress, MP1-MP5 ahead);
milestones doc records the side track + the explicit user-authorized
freeze exception (MP may reopen frozen streaming/WB-rendering subsystems
under its own gated phases only); CLAUDE.md Current state gets the
one-line pointer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The P2 cellar-lip lesson one loop deeper. CheckOtherCells takes footCenter
by value and used it for every cell in the loop — but a mid-loop query can
MOVE the sphere: at the Facility Hub cell boundaries the neighbor's ramp
floor full-hits, step_sphere_up climbs the foot +0.6mm and returns OK, and
the loop continued querying the REMAINING cells (including the under-room,
portal-ring-3) at the pre-climb height — 0.4mm inside the double-faced
floor slab, grazing its underside (the under-room's ceiling) within the
near-miss window. That dispatched a neg-poly step-up with a DOWNWARD
normal, whose failure funneled into slide_sphere's opposing branch ->
synthetic reversed-movement collision -> Collided -> revert, every frame:
the seam shake (and, pre-mechanism-2-fix, the original absorbing wedge's
entry).
Retail check_other_cells reads the LIVE sphere_path.global_sphere for
every cell (each cell's find_collisions receives the transition itself,
pc:272717+). Fix: re-read footCenter = sp.GlobalSphere[0].Origin at the
top of each loop iteration.
All three Issue137CorridorSeamReplayTests repros un-skipped: the
snapshot-exact west-boundary crossing (capture tick 4101), the east
deep-straddle, and the clean-run lifecycle all GREEN. Full suites: Core
2556 / App 713 / UI 425 / Net 385, 0 failures.
Visual gate pending: corridor run + the purple seam flashing re-check
(expected to be the render exposing the same per-frame oscillation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corridor gate FAILED with a changed symptom: shaking at cell seams
(+ purple floor flashing there) instead of the dead stop. Deep-probe
session (step-walk/push-back/indoor-bsp + full resolve capture) traced
the complete chain; docs/ISSUES.md #137 carries it. Short form:
- Corridor floors are double-faced portal slabs over under-rooms; the
resting foot sphere lives within half a millimeter of three hit/straddle
thresholds there.
- Crossing a boundary, the foot penetrates the neighbor ramp slab by
~0.4mm, steps up onto it successfully (+0.6mm lift, stepped=True) —
and the lifted check position is then LOST: the following pass runs at
the unlifted height (the P2 stale-snapshot class; retail step_up
0x0050b6cc restores only on FAILURE).
- The unlifted re-test grazes the under-room's ceiling (the slab
underside) within the near-miss window, dispatches a neg-poly step-up
with a DOWNWARD normal, whose nested step-down finds no walkable at
exact tangency -> StepUpSlide -> slide_sphere opposing branch ->
reversed-movement collision -> Collided -> revert. Every frame = shake.
Apparatus committed:
- Issue137CorridorSeamReplayTests: 3 deterministic offline repros
(snapshot-exact west-boundary from the capture, east deep-straddle,
the clean-run pin), currently Skip='#137 seam shake' pending the fix.
Key: THREE portal-ring hydration (the under-room 0x8A020166 is ring-3;
with fewer rings the flood can't add it and the bug vanishes) + live
Setup step heights (0.6/1.5) + probe-buffer capture for line-diffing
offline vs live traces.
- Issue137CorridorSeamInspectionTests: portal-poly world spans (exposed
the visual-vs-physics polygon-id conflation and the floor-portal
topology), physics-BSP leaf membership walk, hit-normal candidate
sweep (|align| both windings), downward-poly sweep.
NEXT: read TransitionalInsert's attempt loop against retail 0x0050b6f0,
find the restore that clobbers the successful step-up position, fix,
un-skip. The purple seam flashing is expected to be the render exposing
the same per-frame oscillation - re-check after the physics fix.
Suites: Core 2553 / App 713 / UI 425 / Net 385, 0 failures (5 skips =
2 pre-existing + the 3 parked repros).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five bite-sized tasks: FrameStatsBuffer (TDD), FrameProfEnabled flag +
GpuFrameTimer TimeElapsed ring, FrameProfiler facade (TDD on the report
formatter), GameWindow wiring (one boundary call + three stage scopes)
with DebugPanel mirror, and the user-driven baseline capture that gates
MP1. Records two spec deviations: GPU per-stage timestamps deferred
(frame is CPU-bound at ~0.5ms GPU), and the toggle lives in
RenderingDiagnostics per the diagnostic-owner rule, not RuntimeOptions.
Encodes the discovered GL constraint: whole-frame TimeElapsed is
mutually exclusive with ACDREAM_WB_DIAG per-pass queries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Umbrella design for the user-commissioned performance side track:
MP0 honest frame profiler -> MP1 baked asset pak (acdream-bake CLI,
mmap zero-copy reader) -> MP2 retail distance-degrade (hide-only cut)
-> MP3 Arch ECS render world + delta submission -> MP4 zero-alloc +
flat physics data (queued behind M1.5 #137) -> MP5 jobs (stretch,
evidence-gated).
Decisions recorded: C# not Rust (bottleneck is architectural, not
language); ECS scoped to the render world only (simulation keeps its
retail-mirroring OO structure); Arch framework per user choice; bake
as CLI now, client auto-detect later; smoothness gated before the
300+ FPS throughput target. Every phase has a numeric + visual gate;
legacy paths delete at each gate (no lingering dual pipelines).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mechanism-1 theory (PortalSide portal polys solid in our physics set)
is REFUTED for the corridor repro, and the remaining half of the phantom
is fixed — no cdb session needed:
- The live hit normal (-1.00,0.03,-0.03) matches NO dat polygon: a
world-space sweep of both seam cells + every portal-adjacent neighbor
(CorridorSeam_FindPolygonMatchingLiveHit) returns zero candidates. The
normal is the negated movement direction — the SYNTHETIC value
slide_sphere's opposing-normals branch records (reversed = -gDelta).
- Cell 0x8A02016E has IDENTITY rotation (the prior session's 'rotation
maps the portal planes into the -X wall' was a misattribution). The
PortalSide polys to 0x011E are +-Y planes 1.4 m beside the player's
track, perpendicular to the +X run — pos_hits_sphere's directional
cull rejects them for that movement. They ARE referenced by the dat's
physics-BSP leaves (CorridorCell_PhysicsBspLeafMembership), so retail
tests them too when approached into their plane; the dat's
keep-PortalSide / strip-ExactMatch asymmetry reads as intentional
(solid window/grate-class portals). No portal-poly filter — exactly
the blanket-skip the pickup warned against.
- Port fix: CSphere::slide_sphere's opposing-normals branch
(0x005375d7-0x0053762c) records the reversed displacement and returns
COLLIDED_TS; our port returned OK ('retail returns OK here' was a
decomp misread), letting the step complete as-is with the synthetic
collision normal that validate's epilogue then persisted as the
sliding normal the wedge absorbed on. TransitionTypes opposing branch
now returns Collided; pinned by
SlideSphere_OpposingNormals_ReturnsCollided_WithReversedDisplacementNormal
(RED->GREEN).
- Dat-backed replay (Issue137CorridorSeamReplayTests) reproduces the
live hit frame verbatim (same in/out to the millimeter, same 016E->017A
transit, same +8mm settle) and runs the corridor CLEAN: hit=no, no
sliding normal persisted, six further forward frames advance freely.
- Inspection tests extended: physics-BSP leaf membership walk +
hit-normal candidate sweep + downward-poly sweep (all report-style,
dat-gated). Pickup prompt banner'd SUPERSEDED; ISSUES #137 updated
(door half stays open); audit doc extended with the resolution.
Suites: Core 2551 / App 713 / UI 425 / Net 385, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Facility Hub corridor dead-stop's second half: after one seam hit,
every forward resolve returned ok=False hit=no with zero advance. The
body-persisted SlidingNormal (-1,0,0) projected the exactly-anti-parallel
corridor push to zero in AdjustOffset and the step loop aborted at step 0
before any collision test could refresh the state.
Audit (docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md):
retail's only in-transition sliding-normal writer is validate_transition
(0x0050ac21); the whole sphere/BSP layer writes NONE (grep-verified), and
the body persistence (SetPositionInternal 0x005154c2, SLIDING_TS bit sync
0x005154e1) runs only on transition success. Our BSPQuery Contact-branch
full-hit responses were stubs (SetCollisionNormal + SetSlidingNormal +
return Slid) where retail dispatches the real slide_sphere — so the seam
hit (a SUCCESSFUL full-advance resolve per the live log) persisted the
phantom wall's normal, which retail's lifecycle structurally cannot do.
- BSPQuery Contact foot full-hit fallback + head full-hit now route
through Transition.SlideSphereInternal (CSphere::slide_sphere
0x00537440 — in-frame slide, no sliding-normal write; ACE
BSPTree.cs:202,310-316). The dead stub is rewritten as the faithful
BSPTREE::slide_sphere wrapper.
- PhysicsEngine sliding writeback gated on ok (retail success-only
placement; behaviorally latent, removes the failed-frame leak class).
- Register: TS-4 amended (Path-6 steep-tangent sites still write the
normal — now documented), TS-45 added (SphereCollision's write — same
leak class, left for a follow-up out of #171's blast radius).
- Pins: Issue137SlidingNormalLifecycleTests — both site pins RED->GREEN,
plus the retail persist/absorb/clear wall lifecycle (validate-write
persistence, faithful absorbed anti-parallel frame, oblique escape
clears the bit). BSPQueryTests full-hit pin updated to the real slide.
Mechanism 1 (PortalSide portal polys solid in the physics set) stays
OPEN - #137 not closed; the corridor re-test rides that session.
Suites: Core 2545 / App 713 / UI 425 / Net 385, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both mechanisms + evidence pointers + the done-already oracle greps,
ordered mechanism-2-first (the sliding-normal wedge, pure acdream-side)
then the mechanism-1 retail cdb trace (needs the user running retail
at the Facility Hub corridor).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CCellStruct::UnPack loads physics polys + BSP verbatim (no portal
stripping); the CPolygon test chain is pure geometry; CCellPortal
carries portal_side/exact_match but nothing in the BSP tests consults
it. Retail's PortalSide passability is therefore not a load filter or
poly flag — remaining candidates are the transit/membership test
order or a stippling/sidedness interaction. Next: cdb-attach retail
at the 0x8A02016E corridor per the step -1 protocol.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Facility Hub corridor repro (probe + dat evidence, both mechanisms
pinned):
1. PortalSide portal polygons live IN CellStruct.PhysicsPolygons and
acdream collides with them as plain solid geometry. Cell
0x8A02016E's portals to 0x011E (polys 1/3/5, flags=PortalSide) are
present in the physics set while every ExactMatch portal in the
same cell is absent — the cell's rotation maps those portal planes
to the world -X wall the player hit mid-corridor
(launch-175-verify2.log:42858, n=(-1,0,0)). Retail must honor the
portal side; oracle grep required before fixing.
2. After the single seam hit, the body-persisted SlidingNormal
projects every subsequent forward offset to exactly zero in
AdjustOffset - the step aborts BEFORE any collision test can
update state (ok=False hit=no, zero advance), an absorbing wedge
escaped only by strafing ("push through on the side"). The #116
slide-response family: retail re-derives slide state per frame
(get_object_info pc:279992).
Inspection fixture: Issue137CorridorSeamInspectionTests (dumps
physics-vs-portal polygon membership for the two corridor cells).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Facility Hub double door (Setup 0x02000C9D) embeds the player into
the visual panel from one side and blocks with a phantom slab on the
other: its Setup PLACEMENT frames pose the two panels AJAR (yaw
-150/-30 deg, 0.44 m behind the doorway plane — dat-confirmed by the
Issue175 inspection) while the rendered door poses them CLOSED from
the wire-supplied motion table via the sequencer. ShadowShapeBuilder
read placement frames, so the 1.66x0.29x2.95 m physics slabs
registered at the ajar pose. Retail tests each part's LIVE
CPhysicsPart pose — for an idle door, the motion table's default
(closed) state.
Fix: ShadowShapeBuilder.FromSetup gains partPoseOverride (BSP part
shapes only); RegisterServerEntityCollision derives it from the spawn
MotionTableId via GameWindow.MotionTableDefaultPose (default style ->
first cycle -> LowFrame part frames). Null/short poses fall back
per-part to placement frames — table-less entities and landblock
statics unchanged. One-shot snapshot vs retail's per-frame live pose
is register row AP-84 (equivalent for the door lifecycle: closed ==
default pose; open == ETHEREAL bypasses collision, #150).
Pins: FromSetup_PartPoseOverride_ReplacesPlacementFrames /
_NoOverride_KeepsPlacementFrames / _ShortOverride_FallsBackPerPart.
Suites: Core 2539 / App 713 / UI 425 / Net 385 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report at the Facility Hub double door (Setup 0x02000C9D): embed
into the visual panel from one side, phantom wall on the other. Dat
inspection (Issue175HubDoorPoseInspectionTests, kept as the evidence
fixture) confirms the mechanism: the Setup's Default PLACEMENT frames
pose the two panels AJAR (yaw -150/-30, origin (+-0.88, -0.44, 1.37))
while the rendered door poses them CLOSED from the wire-supplied
motion table via the sequencer. ShadowShapeBuilder reads placement
frames, so the 1.66x0.29x2.95 m physics slabs register at the ajar
pose — displaced behind the visual door. Retail tests each part's
LIVE pose (closed == motion-table default; the open swing is ETHEREAL,
#150). Fix shape filed: BSP shadow shapes for sequencer-bearing
entities must use the sequencer's part transforms, placement frames
only as the no-animation fallback. Holtburg's single door never
surfaced this because its placement pose ~= closed pose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail CPhysicsObj::RemoveLinkAnimations (0x0050fe20) is a tailcall to
CPartArray::HandleEnterWorld (0x00517d70) -> MotionTableManager::
HandleEnterWorld (0x0051bdd0): remove_all_link_animations PLUS a full
pending_animations drain (while (head) AnimationDone(0)), each pop
relaying MotionDone so CMotionInterp pops its pending_motions node in
lockstep. acdream bound the seam to the bare sequence strip, so every
jump's LeaveGround removed the animations that queued manager nodes
were counting down on — orphaning them (NumAnims>0, anims gone) and
permanently damming BOTH queues. MotionsPending() then never drained
(probe round: last player pending=False at the first MovementJump
press; old jump motions still completing at rest minutes later) and
BeginTurnToHeading/BeginMoveForward's verbatim motions_pending gates
starved every armed moveto: ACE's mt-6 walk-to-door armed but the body
never walked (wire-proven, seqs 98-101); the close-range use turn
never completed so the deferred action was silently eaten. Doors only
worked on a fresh session (shallow queue).
Rebind both production sites (remote EnsureRemoteMotionBindings +
the player's EnterPlayerModeNow block) to Manager.HandleEnterWorld();
the sequencer wrapper was a pure passthrough so the manager call is a
strict superset. All six interp seam sites (LeaveGround, HitGround,
Dead, and the detached-object guards) are the same retail chain.
Harness mirrors updated; pins: Issue174LinkStripDrainTests (the seam
drains both queues; fresh motions queue and complete after). Suites:
Core 2535 / App 713 / UI 425 / Net 385 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probe round (ACDREAM_PROBE_AUTOWALK) discriminated the candidates: the
local player's pending-motion queue drains at ~1 node/sec and backs up
minutes deep during active play (last pending=False at the first
MovementJump press; old jump motions still completing at rest minutes
later). MotionsPending() then starves BeginTurnToHeading/
BeginMoveForward (verbatim retail gates): far-range Uses ARE sent and
ACE replies mt-6 walk-to-door but the body never walks; close-range
Uses park on a turn that never completes — both faces of "door does
not work", while a fresh session (shallow queue) works perfectly.
Retail's queue stays shallow (the #170 cdb drain trace: adds == dones)
— this is the #170 pending_motions family, local-player DRAIN-rate
edition. Fix path: decomp CheckForCompletedMotions (0x0051bfd0) pop
semantics + queue-dump probe first; no MotionsPending bypass, no
deferral-skipping band-aids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Facility Hub door investigation evidence: retail's byte-identical
UseItem works on the same ACE (door F74C/F74B captured, broadcast to
both clients; acdream renders the observed swing), while acdream's
later attempts never reach the wire — the AP-23 close-range deferral
parks the action on MoveToComplete(None) and the player's speculative
TurnToObject never completes after the first use of the session.
Candidates: per-attempt user-input cancel vs MotionsPending starvation
(the #170 class, local edition). Also noted: GameEventType.UseDone is
parsed nowhere, so ACE rejections are invisible. Captures:
door-use3/4.pcapng (session scratchpad). Next: ACDREAM_PROBE_AUTOWALK
round, then fix per the retail client use flow (§9a/§9b) — no
deferral-skipping band-aid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An observed character jumping into a dungeon ceiling hovered at the
roof until its ballistic arc decayed, landing visibly late (user
report, 0x0007). The remote DR tick sweeps collision (position pinned
at the ceiling — no clip-through) but retail's post-transition velocity
response, CPhysicsObj::handle_all_collisions (pc:282699-282715:
v -= (1+elasticity)*dot(v,n)*n), was only ported for the LOCAL player
(L.3a). The remote body kept its +Z launch velocity and re-integrated
it into the roof every tick — the position was clamped but the
timeline was pure ballistics.
Retail runs handle_all_collisions after every SetPositionInternal for
every physics object, remotes included. Mirror the local reflection
block in the remote sweep's post-resolve path: same formula, same
AD-25 airborne-before-AND-after suppression (corridor slides and
landings don't reflect; the landing snap's Velocity.Z <= 0 gate stays
intact), same Inelastic zero-out for future missiles. AD-25 register
row extended to cover both sites.
Suites green: Core 2533 / App 713 / UI 425 / Net 385.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Holtburg town-network portal platform (stab 0xC0A9B465, Setup
0x020019E3, CylSphere r=2.597m h=0.256m) blocked the player with an
endless rim slide instead of retail's step-up-onto-top — gating the
whole #137 dungeon repro. Surfaced when #149 started registering
BSP-less stab CylSpheres: the collision SHAPE became right while the
RESPONSE was still the hand-rolled AP-6 approximation (step-up gate +
radial wall-slide only).
Root cause: no cylinder-TOP support anywhere. DoStepUp's internal
step-down probe needs retail's step_sphere_down (0x0053a9b0) to land on
the flat top — a cylinder has no polygons for the walkable search — so
every step-up onto a wide cylinder failed into StepUpSlide and the
player orbited the rim (probe-confirmed: [cyl-test] result=Slid with
horizontal rim normals, launch-137-repro.log).
Port the full family verbatim: dispatcher intersects_sphere 0x0053b440
(placement/ethereal detection, step-down cap landing, walkable probe,
grounded step_sphere_up 0x0053b310, PathClipped collide_with_point
0x0053acb0, airborne land_on_cylinder 0x0053b3d0, Collide-flag
exact-TOI cap rest) + collides_with_sphere 0x0053a880 +
normal_of_collision 0x0053ab50 + slide_sphere 0x0053b2a0. Pseudocode +
settled BN x87 ambiguities (via ACE cross-ref) + two ACE bugs found and
NOT copied (head-slide foot-disp; see doc §8):
docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md
Ethereal cylinders now flow through retail's Layer-2 override
(pc:276961) instead of the early-OK consume — same net #150 behavior,
plus retail placement-blocked-by-cylinder semantics. SlideSphere gains
a sphereNum param (retail slides the head sphere by its own
displacement, 0x0053b843).
Register: AP-6 retired; AP-83 added (PerfectClip TOI tail decoded per
ACE, dead code until missiles). Tests: CylSphereFamilyTests (grounded
step-up onto the exact platform shape, tall-cylinder block, airborne
top landing, ethereal guard); the #42 self-shadow control assertion
updated to the retail observable (denied movement — the old ~1m radial
self-push was the approximation's artifact, not retail). Suites: Core
2533 / App 713 / UI 425 / Net 385 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Work order: #137 collision (oracle-first, repro before fix) -> #153
apparatus-first (hold shape needs explicit user approval per the
no-holds feedback) -> #138 acceptance run -> A7 lighting closer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structural capstone of the R5 movement-manager arc; zero behavior change.
Retail MovementManager (acclient.h /* 3463 */, 16 bytes / four pointers)
gives every CPhysicsObj ONE owner for its motion_interpreter +
moveto_manager. acdream carried them as loose per-entity objects wired by
hand at three sites. This slice:
- New src/AcDream.Core/Physics/Motion/MovementManager.cs — owns
MotionInterpreter + lazy MoveToManager (MakeMoveToManager 0x00524000 via
a MoveToFactory closure, the acdream stand-in for the physics_obj/
weenie_obj backpointers) + the relays with retail call shapes:
PerformMovement 0x005240d0 (types 1-5 -> minterp, 6-9 ->
MakeMoveToManager + moveto, (type-1)>8 -> 0x47), UseTime 0x005242f0
(moveto only), HitGround 0x00524300 (minterp FIRST then moveto),
HandleExitWorld 0x00524350 (minterp only), CancelMoveTo 0x005241b0,
HandleUpdateTarget 0x00524790, IsMovingTo 0x00524260.
- RemoteMotion.Movement + PlayerMovementController.Movement hold the ONE
facade; Motion/MoveTo become child views so the comment-dense call sites
read unchanged. The three wiring sites (EnsureRemoteMotionBindings,
EnterPlayerModeNow, the chase harness — same commit per the mirror rule)
construct through MoveToFactory + MakeMoveToManager(), preserving the
pre-facade eager timing (side-effect-free ctor = unobservable either way).
- Relay call sites repointed: both remote landing HitGround pairs + the
player landing pair, despawn HandleExitWorld, TickRemoteMoveTo + the
player Update UseTime, RouteServerMoveTo (takes the facade; routes via
the retail PerformMovement dispatch), InstallSpeculativeTurnToTarget,
host HandleUpdateTarget/InterruptCurrentMovement closures (retail
CPhysicsObj::HandleUpdateTarget @0x00512bf0 fan head + the TS-36
interrupt chain, now the literal facade relays).
- NOT absorbed per the slice spec: unpack_movement stays App
(RouteServerMoveTo + UM heads; Core.Net types stay out of Core.Physics);
TS-42 per-tick order untouched (R6); #170/#171 gate-passed machinery
untouched. PerformMovement's set_active(1) head not re-asserted (spawn
asserts Active; status quo — no new register row).
- Register: TS-41/TS-42 source wording freshened to the facade shape;
AD-36 retire note corrected (facade half closed; residue = entities
that never get a RemoteMotion). No new rows.
- Conformance: 15 new MovementManagerTests pin the dispatch table, lazy
create, relay targets/order, null tolerance. Suite 4052 green; the
183-case/funnel/moveto/chase/sticky suites UNMODIFIED (harness
construction mirrors production, test bodies untouched).
Decomp: docs/research/2026-07-03-r5-managers/r5-movementmanager-decomp.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handoff status updated. The R5 arc's remaining work is the structural
MovementManager facade only (own slice, fresh session — pickup at
r5-wiring-handoff §V4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three unpack_movement parity items (facade deferred per the handoff's
own optional clause — see r5-wiring-handoff §V4 status):
1. HEAD style-on-change (0x00524440 @00524502-0052452c): both GameWindow
routing heads (remote + player) now dispatch DoMotion(style, ctor
defaults) when the UM's stance differs from the interp's current
style, BEFORE the movement-type routing — for EVERY type. Previously
style applied only through the mt-0 funnel copy, so a chase/turn UM
(mt 6-9) carrying a stance change started the move in the OLD stance
(a monster charged in NonCombat posture until the next mt-0). The
RetailObserverTraceConformanceTests exclusion note updated — the
trace filter stays (head calls can't appear in a
MoveToInterpretedState replay) but the production gap it pointed at
is closed.
2. #164 (closes): the action-replay loop threads each action's autonomy
into the dispatch params (Autonomous = the 0x1000 splice, raw
305982) — replayed actions enter the interpreted actions list with
their real autonomy instead of ctor-default false.
3. mt-0 wire flags consumed (UpdateMotion parsed them since R4-V3):
0x1 StickToObject → CPhysicsObj::stick_to_object port (0x005127e0:
resolve target, PartArray radii — 0 when shapeless, guid-as-is for
acdream's flat entity table — → PositionManager.StickTo; unresolvable
target → no stick), at BOTH case-0 tails in retail order
(@00524583-0052458e: funnel apply → stick → longjump flag);
0x2 StandingLongJump → Motion.StandingLongJump, UNCONDITIONAL write
(absent flag clears — retail @0052458e). ServerMotionState gains the
StandingLongJump field.
Conformance: ChaseArm_WithStanceChange_AppliesStanceBeforeTheChase
(harness mirrors the routing-head dispatch) +
Actions_ReplayCarriesAutonomyIntoTheInterpretedList. Suite 4041 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Discharges the 2026-06-22 banner-reconcile note: M1.5 stays the single
active milestone (remaining: A7 dungeon lighting, #137 dungeon collision,
#138 teleport-OUT). The D.2b retail-UI track and the R5 movement-manager
arc are recorded in the milestones doc as user-report-driven,
issue-level parity tracks subordinate to M1.5 — not milestones. Banner
rewritten within the 6-line budget; R5-V3/#170/#171 closes and the
R5-V4 pickup pointer included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate 2: pack behavior good except "sometimes the monster is in the
character / too close vs retail". The ACDREAM_PROBE_STICKY capture
nailed it frame-by-frame: 1661 deep-overlap AdjustOffset ticks, ALL
steering inward (dist -0.65 -> -0.78 -> ... -> -1.9 at +0.13/tick),
monsters converging to centerDist~0 — while the suppressed-snap lines
show ACE's authoritative positions stayed properly OUTSIDE (drift up to
7.7 m). The radii were correct (tgtR=0.68, ownR=0.59-0.98).
Root cause: ACE's literal decode of StickyManager::adjust_offset
(`if (delta >= |dist|) delta = dist;`) leaves delta POSITIVE when the
overlap exceeds one tick's step — steering TOWARD the target center, a
runaway whose equilibrium is centers-coincident. ACE servers virtually
never reach that branch (quantum >=1/30 -> threshold ~1 m); at
render-rate quanta the threshold is ~0.13 m and pack jostle trips it
constantly. The BN mush (0x00555554-0x00555597) is unreadable on
exactly this compare; the retail oracle (side-by-side on the same ACE:
monsters separate) refutes the ACE-literal reading.
Pin: sign-correct clamp — `else if (dist < 0) delta = -delta` (back off
rate-limited). Identical to ACE-literal in every shallow/outside case.
Register row AP-82 (same commit) with the cdb verification note.
Conformance: StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_
RateLimited. Full suite 4039 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate 1 (2026-07-04): "better in general" + three residuals — monsters
pushed INTO the player, attacks with stale facing, position
"flashing/flapping instead of gliding". All three are ONE mechanism:
the legacy NPC UpdatePosition handler hard-snaps position, orientation,
and velocity/cycle UNCONDITIONALLY, fighting the armed sticky every UP
(ACE's authoritative rest pose sits ~0.6 m out and its server-side
facing lags the strafing target; the client stick holds 0.3 m + live
facing — oscillation at UP cadence).
Retail is immune by architecture, not by tuning: UP corrections flow
through the InterpolationManager into the SAME per-tick
PositionManager::adjust_offset chain where StickyManager::adjust_offset
OVERWRITES them while armed (0x00555190 chain order; 0x00555430 assigns
m_fOrigin). A server correction can never fight an armed stick
frame-by-frame. The remote-player branch already has exactly that
(queue -> combiner -> sticky overwrite); the legacy NPC path snaps
outside the chain.
Fix: suppress the NPC UP position/orientation/velocity-adoption snaps
while the entity is stuck (PositionManager.GetStickyObjectId() != 0) —
the retail chain semantics translated to the snap architecture.
LastServerPos/Time + cell bookkeeping still record; server truth
reasserts on the first UP after unstick, bounded by the 1 s sticky
lease. Register row TS-44 (same commit); retires with the S6/R6
interp-queue unification of the NPC path.
Apparatus: ACDREAM_PROBE_STICKY=1 — per-guid [sticky] lifecycle lines
(STICK / UNSTICK / LEASE-EXPIRE / TARGET-status teardown), per-armed-
tick steer lines (signed gap dist, applied delta, heading delta, live
resolve), and [sticky-snap-skip] at the suppressed-snap site.
PhysicsDiagnostics.ProbeStickyEnabled owns the flag (rule #5).
Full suite 4038 green. Awaiting gate 2 (pack melee vs retail).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>