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>
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>
Three review finds, all in the runtime-toggle path or edge math (the
steady-state path was confirmed correct):
1. GPU stale-slot across toggle-off/on: the disabled branch now
Disposes the GpuFrameTimer (and nulls it) instead of Stop()-ing and
keeping it — a kept instance would poll slots left pending from
BEFORE the pause on re-enable and report temporally stale GPU
samples. The re-enable branch's existing null guard rebuilds the
ring fresh. Dispose is safe there: the branch runs at the top of
OnRender with the GL context current.
2. Stale stage accumulation across mid-stage toggle-off: a StageScope
disposed after the flag flips still calls EndStage, leaving a
partial delta in _stageAccumTicks. The re-baseline branch now
Array.Clear()s it alongside the GC re-baselining.
3. FrameStatsBuffer.Max() seeded with 0, clamping all-negative windows
(the alloc channel can go negative if the boundary ever crosses
threads) and disagreeing with Percentile(). Now seeds from the
first live sample after the empty check (slots [0.._count) are
always the live window regardless of ring wraparound); empty still
returns 0 to match Percentile. TDD: Max_AllNegative_ReturnsTrueMax
failed (returned 0) before the fix, passes after.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The plan's FormatReport_IsInvariantAndComplete test ends with a
DoesNotContain(",0") guard (after stripping the gc=3/1/0 token) proving
the formatter emits period decimals regardless of the host culture.
It was dropped in the Task 3 commit; restored verbatim. Passes as-is —
FormatReport already uses CultureInfo.InvariantCulture throughout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GameWindow gets one field (_frameProfiler), one FrameBoundary() call at
the top of OnRender, three stage scopes (Update in OnUpdate, Upload
around _wbMeshAdapter?.Tick(), ImGui around _imguiBootstrap.Render()),
and one Dispose() call in teardown before _dats?.Dispose() releases the
GpuFrameTimer query ring. No new feature bodies land in GameWindow.cs —
all profiler logic lives in AcDream.App.Diagnostics.
DebugVM.FrameProf mirrors RenderingDiagnostics.FrameProfEnabled so the
DebugPanel checkbox ("Frame profiler ([frame-prof])") toggles the 5s
report live. Note: the checkbox lives in
AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs (IPanelRenderer,
backend-agnostic) alongside the other Diagnostics-section checkboxes —
not in AcDream.UI.ImGui, which holds no panel-drawing code per Code
Structure Rule 3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Permanent frame profiler: FrameBoundary() at the top of OnRender measures
CPU frame time (swap-to-swap delta), brackets the frame in a GpuFrameTimer
TimeElapsed query (self-disabled under ACDREAM_WB_DIAG=1), and samples
per-frame allocated bytes + GC collection deltas. BeginStage(FrameStage)
scopes attribute CPU time to Update/Upload/ImGui. Emits one [frame-prof]
report line every ~5s while RenderingDiagnostics.FrameProfEnabled is on;
zero-cost stage scopes when off. Report formatting is a pure static
method, unit-tested for invariant-culture formatting and the gpu=off
fallback text.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RenderingDiagnostics.FrameProfEnabled is the master toggle for the
upcoming permanent frame profiler (ACDREAM_FRAME_PROF=1, runtime
mirror via DebugVM.FrameProf). GpuFrameTimer wraps a depth-4 ring of
TimeElapsed queries for whole-frame GPU time, mirroring
WbDrawDispatcher's query idiom (including the #125 never-begun-slot
guard). Caller must not run this while ACDREAM_WB_DIAG=1 owns
TimeElapsed queries — GL forbids nested TimeElapsed queries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixed-capacity ring buffer of long samples with nearest-rank percentile
and max over the current window. Pure, allocation-free after
construction. Foundation for the MP0 frame profiler
(docs/superpowers/specs/2026-07-05-modern-pipeline-design.md).
Co-Authored-By: Claude Sonnet 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>
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>
Group-melee interpenetration + facing drift: the R5-V1-ported
StickyManager/PositionManager were Core-only — the StickTo/Unstick seams
were unbound and every arrival radius was 0, so ACE's Sticky|UseSpheres
melee chases closed ~one body-radius too deep and froze at stale arrival
poses until the next wire re-arm. Retires TS-39.
Wiring (anchors re-verified against the named decomp this session):
- EntityPhysicsHost owns a PositionManager; HandleUpdateTarget fans
MoveToManager then PositionManager (CPhysicsObj::HandleUpdateTarget
0x00512bc0 order).
- Seams bound remote + player: MoveToManager.StickTo (BeginNextNode
sticky arrival @0x00529d3a), Unstick (PerformMovement head), and
MotionInterpreter.UnstickFromObject (UM funnel head, 0x0050eaea).
- AdjustOffset at the retail UpdatePositionInternal slot (@0x00512d0e):
NPC branch composes pre-sweep (steer is swept by ResolveWithTransition);
remote-player branch chains the combiner offset through the shared
delta frame (the interp stage) so sticky OVERWRITES when armed
(0x00555430 assigns m_fOrigin, not accumulates); player inside the
30 Hz physics quantum before UpdatePhysicsInternal.
- UseTime (the 1 s lease watchdog) at the UpdateObjectInternal tail
(@0x005159b3): unconditional per remote; player gated on the physics
tick (retail's MinQuantum gate skips UseTime too).
- Real setup cylsphere radii (CPartArray::GetRadius/GetHeight
0x005180a0/0x005180b0 = setup radius/height x ObjScale from the spawn
record): own via EnsureRemoteMotionBindings + player wiring; target via
RouteServerMoveTo AND the speculative use-walk install (retail resolves
the target PartArray at EVERY MoveToObject site — ACE PhysicsObj.cs:951).
- Teardown parity: exit_world (0x00514e60) UnStick + ClearTarget before
the ExitWorld notify; player teleport fires teleport_hook's tail
(UnStick in SetPosition + EntityPhysicsHost.NotifyTeleported =
ClearTarget + NotifyVoyeurOfEvent(Teleported) @0x00514f1b) so mobs
stuck to the player drop their sticks on a recall.
- SERVERVEL arbitration also yields to a stuck entity (same starvation
class as the #170 fix — sticky owns the between-snap translation).
- StickyManager.UseTime aligned to retail's strict > deadline
(0x00555626; ACE >): two V1 tests had pinned the >= edge — corrected.
Register: TS-39 deleted; TS-41 narrowed (stickyArmed gate); TS-43 added
(remote teleport_hook gap — self-corrects within the 1 s lease); AP-23
narrowed (real radii at the speculative site; only the use-radius
buckets remain invented).
Conformance: 2 new full-stack sticky scenarios in
RemoteChaseEndToEndHarnessTests (arrive -> stick -> strafing-target
gap+facing track -> lease expiry; unstick-on-rearm -> re-stick).
Full suite 4038 green.
Pre-commit adversarial diff review (3 lenses + per-finding refuters)
confirmed and fixed 4 findings: ObjScale-dead radius read, player
UseTime order inversion, missing teleport voyeur notify, speculative-
site radius asymmetry.
Awaiting the user visual gate: pack melee side-by-side vs retail
(attackers reshuffle + keep facing; some overlap is ACE-server-side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report during the #170 gate: pack-melee monsters partly inside each
other with slightly stale facings vs retail on the same ACE. Investigation
(report-only) pinned two code-grounded causes, both already-tracked R5-V3
scope, fix APPROVED by the user:
1. TS-39 — sticky melee is a no-op: ACE arms melee chases with Sticky
(Monster_Navigation.cs:416); retail's arrival hands off to
PositionManager::StickTo -> StickyManager::adjust_offset (0x00555430,
per-tick 0.3m edge gap + facing tracking); acdream's StickTo/Unstick
seams are unbound so attackers freeze at stale arrival poses.
2. Arrival radii are zero: getOwnRadius () => 0f (the R5-V3 pin) and
RouteServerMoveTo never sets MovementStruct.Radius/Height — retail/ACE
arrive edge-to-edge with setup-derived radii (ACE PhysicsObj.MoveToObject
reads the TARGET's PartArray radius/height).
Ruled out: the between-snap collision sweep (remotes run full
ResolveWithTransition; CollisionExemption keeps creatures collidable).
Caveat recorded: ACE has no server-side monster avoidance — acceptance is
retail PARITY, not zero overlap.
ISSUES #171 filed; handoff docs/research/2026-07-04-171-sticky-melee-handoff.md
+ pickup docs/research/2026-07-04-171-pickup-prompt.md; memory index updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate result: user side-by-side vs retail — "looks good, as close to retail
now as I can see". Telemetry from the gate session (launch-170-gate2.log):
BeginMoveForward ~1:1 with MoveToObject arms for every chasing creature
(pre-fix capture was 16:1), ZERO "SERVERVEL skips UseTime while armed"
occurrences, pending_motions depth 1 at last sample.
Stripped per the #170 close-out plan: s_mvtoDiag + all [mvto] prints
(MoveToManager), s_drainDiag + [drain]/[drainq] (MotionInterpreter), the
[npc-tick] branch prints and the "UM actions" inbound-action dump
(GameWindow). The durable ACDREAM_DUMP_MOTION family stays. The
RemoteChaseEndToEndHarnessTests + RemoteChaseDrainBisectTests conformance
suites stay as the permanent regression net for this pipeline.
ISSUES: #170 -> DONE + Recently-closed entry (fix SHAs 427332ac, d2ccc80e,
1051fc83). Memory topic + index flipped to CLOSED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "sustain the run" residual. The handoff's "Ready stop-node backlog
drains a beat slower than retail" framing was DISPROVEN: a new full-stack
offline harness (RemoteChaseEndToEndHarnessTests — real MoveToManager +
MotionInterpreter + AnimationSequencer + MotionTableDispatchSink + the
manual omega integration, wired field-for-field like
EnsureRemoteMotionBindings and ticked in TickAnimations' exact phase
order) proves the Core turn/run/drain pipeline healthy: the chase turn
completes in <1 s both directions, BeginMoveForward installs per arm, the
run sustains across re-arms and attack swings, and pending_motions fully
empties (retail cdb invariant add_to_queue == MotionDone).
The real mechanism (launch-drainq.log, corrected per-guid attribution —
the previous session's timeline mis-attributed [mvto] lines that fire in
the network phase): funnel per chasing scamp was 16 mt-6 arms -> 11
dispatched turns -> ONE BeginMoveForward. Any NPC receiving
UpdatePositions gets HasServerVelocity=true (synthesized from position
deltas even when the wire carries no velocity), and the grounded per-tick
branch routed those to the SERVERVEL leg, which SKIPS
MoveToManager.UseTime — [npc-tick] literally logged
"branch=SERVERVEL (skips UseTime) mtState=MoveToObject". The armed
moveto was starved for exactly the duration of the server-side chase:
legs stayed in Ready while the body glided on synthesized velocity (the
#170 slide); the manager only woke in UP-silent gaps (creature stopped
server-side) and its stale-heading turn was interrupted by the next UM
before reaching BeginMoveForward.
Retail runs MovementManager::UseTime UNCONDITIONALLY every tick
(CPhysicsObj::UpdateObjectInternal 0x005156b0, call @0x00515998) and has
no wire-velocity leg-driver anywhere; between UPs a moveto-driven body
translates from the motion state (get_state_velocity) with UP hard-snaps
correcting drift. Fix: an armed moveto (MovementTypeState != Invalid)
always takes the MOVETO leg; SERVERVEL remains only as the legacy
fallback for entities without a moveto (scripted paths / missiles).
Register: TS-41 (the narrowed SERVERVEL stopgap), TS-42 (drain-order
divergence also pinned this session: acdream drains AnimDone->MotionDone
AFTER HandleTargetting/MoveTo.UseTime; retail's process_hooks
@0x00512d3d runs BEFORE TargetManager/MovementManager in
UpdateObjectInternal — one frame of extra latency, R6 scope).
New conformance: RemoteChaseEndToEndHarnessTests (3 scenarios + theory)
+ RemoteChaseDrainBisectTests (the drain-chain pin; its first run also
demonstrated the TS-40 InWorld=false link-strip wedge shape — harness
bodies must replicate the live RemoteMotion construction).
ISSUES #170 updated (awaiting user visual gate; probes stay until then);
handoff doc superseded-note added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Detailed handoff (docs/research/2026-07-04-170-creature-run-handoff.md) + paste
prompt (2026-07-04-170-pickup-prompt.md) for continuing #170 in a fresh session.
Records the full root-cause chain (retail cdb + acdream probes), the landed partial
fix (427332ac: per-frame apply_current_movement flood deleted → pending_motions
1.3M→~1, run installs, stuck-attack gone), the one residual (Ready-stop drain lag →
run not fully sustained), the exact next steps (cdb the retail Ready-stop drain), the
apparatus (env-gated probes + cdb scripts), the file:line map, and the DO-NOT-RETRY
list (superseded MovementManager-coexistence hypothesis; keep the velocity fix; don't
patch the retail-faithful motions_pending guard). ISSUES #170 updated to PARTIAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live retail cdb tracing + acdream probes root-caused the creature chase "slide"
end-to-end, SUPERSEDING the earlier MovementManager-coexistence hypothesis
(eb423fb7):
Chase run cycle is manufactured client-side from mt-6 MoveToObject:
HandleUpdateTarget -> MoveToObject_Internal -> (TurnToHeading node completes
via UseTime) -> BeginMoveForward -> _DoMotion(RunForward) sets substate.
BeginTurnToHeading (MoveToManager 0x00529b90) bails while
CMotionInterp.motions_pending is non-empty (retail-faithful).
acdream's pending_motions EXPLODED to ~1.3M entries (live: add=1.37M vs
done=5.7K) because the NPC per-tick called rm.Motion.apply_current_movement
EVERY FRAME, which for a remote (has a DefaultSink) re-ran
ApplyInterpretedMovement and re-dispatched the WHOLE interpreted state (stance
0x8000003C + forward=attack + sidestep/turn stops), each appending a
pending_motions node that barely drains. => MotionsPending() permanently true
=> the chase turn never started => BeginMoveForward/RunForward ~never fired =>
the creature slid in an idle+attack pose. Retail dispatches per MOTION EVENT
(per UM), never per frame — cdb drain trace: retail add_to_queue == MotionDone.
FIX: delete the per-frame apply_current_movement call in the grounded remote-NPC
dead-reckon path (GameWindow ~9992). The motion is already dispatched per UM by
the funnel (MoveToInterpretedState) + by the MoveToManager per node; body
velocity is refreshed directly by the existing get_state_velocity line.
RESULT (verified live): pending_motions depth 1.3M -> ~1 (add~=done); "stuck in
attack animation" GONE (user-confirmed); run cycle now installs (BeginMoveForward
1->10, RunForward held 0->7). PARTIAL: still not fully sustained — BeginTurnToHeading
still blocked motionsPending=True 256/272 (94%) because a residual Ready
(0x41000003) stop-node backlog keeps the queue from fully emptying between swings
(retail hits add==done exactly). acdream gets ~10 run-starts vs retail's ~21, so
it now twitches+glides instead of a clean run-then-stop. Residual = the R2/R3
Ready-stop drain (next session; see docs/research/2026-07-04-*-handoff.md).
Also includes the env-gated #170 diagnostic probes used to find this
(ACDREAM_MVTO_DIAG=1: [mvto]/[npc-tick]/[drain]/[drainq]) — TEMPORARY, strip when
#170 closes. No effect on normal runs. Core suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause (confirmed by a live ACDREAM_DUMP_MOTION capture of Mite Scamp
0x80000244 + the retail decomp): a chasing+attacking creature's attacks arrive
as the ForwardCommand of frequent mt-0 InterpretedMotionState UMs (66 attack UMs
0x62/63/64 vs 2 mt-6 chase MoveTos in the capture). Retail's get_state_velocity
(0x00527d50) computes the body's translation velocity from the current forward
command: WalkForward→3.12×spd, RunForward→4.0×spd, and 0 for everything else
(an attack) — so the creature plants its feet. acdream ALREADY has a faithful
get_state_velocity (returns 0 for a non-locomotion command; cross-checked vs
holtburger grounded_local_velocity's `_ => zero`), but it was never WIRED to the
remote body for entities with an animation sink: apply_current_movement's sink
path (all remotes have a DefaultSink) dispatches the animation and early-returns
BEFORE the set_local_velocity(get_state_velocity()) write, which lives only in
the no-sink fallback (MotionInterpreter ~1702). So a remote NPC's body.Velocity
was never recomputed from its motion state and kept the STALE run velocity from
the last chase — the body dead-reckoned forward at ~4 m/s while playing an
idle+attack pose ("glides after me").
Fix: after apply_current_movement in the grounded remote-NPC dead-reckon path
(GameWindow ~9992, restricted to remotes by serverGuid != player and to grounded
by OnWalkable), refresh rm.Body via set_local_velocity(get_state_velocity()) —
the exact write retail's apply_current_movement performs, reusing the verbatim
ported get_state_velocity. An attack forward command now resolves to 0, so the
creature stops and swings in place; RunForward still yields the run velocity.
Narrowest safe seam: the local player (which also has a sink) is excluded by the
loop's player guard, so its PlayerMovementController velocity path is untouched.
The earlier suspicion (#159 combat-command numbering) was a red herring — the
scamp's 0x62/63/64 were always in the correct block and the planner is unwired;
the wire even carries full attack variety, so "uniform" was the visual artifact
of rapid attacks over a gliding idle base, not a classification bug.
Tests: MotionVelocityPipelineTests.AttackForwardCommand_ZeroVelocity (4 cases)
pins get_state_velocity → 0 for the attack forward commands the fix depends on.
Core suite 2503 green. Visual gate pending (retail side-by-side: creature should
plant + step, not glide).
Ref: docs/research/named-retail get_state_velocity 0x00527d50; ISSUES #170.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing UM dump shows only ForwardCommand; the [CMD_LIST] dump is local-
player only and omits stamps. For a remote creature (the #170 Mite Scamp) nothing
logged the attack action stream, so a capture couldn't answer "do the attack
stamps advance (variety) or repeat (uniform)?". Add one env-gated line in the
remote funnel branch dumping each inbound action (full command + route + stamp +
autonomous + speed). Temporary #170 capture aid — strip once the mechanism is
confirmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Traced the live remote path: the mt-6 chase runs through the verbatim
MoveToManager, but every inbound mt-0 attack UM fires the unpack_movement head
interrupt (bound to MoveTo.CancelMoveTo) AND installs the UM's ForwardCommand
(Ready/idle for an attack-only UM), so the chase MoveTo is cancelled and the run
legs are replaced by idle while the body keeps dead-reckoning -> glide + repeated
idle/attack. The 15-bit ServerActionStamp gate is faithful (uniform-anim is not a
stamp bug). This chase+attack coexistence is R5/MovementManager scope, so #170
sub-bugs 2/3 are downstream of the incomplete MovementManager port (R5-V4), not
#159. Recorded as a hypothesis pending a live ACDREAM_DUMP_MOTION capture + retail
side-by-side (no guessing a fix in this revert-prone path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark ISSUES #159 done (2de5a011) and record the key finding for #170: the
CombatAnimationPlanner numbering fix is latent (the planner isn't wired into
the live dispatch) and the Mite Scamp's 0x62-0x64 attacks were always in the
correct block — so #170's visible symptom is sub-bug 2 (the pending_motions
MOTIONDONE loop), not command misclassification. Roadmap L.1b note updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CombatAnimationPlanner.CombatAnimationMotionCommands hand-transcribed the
combat MotionCommand constants from the Sept 2013 EoR decomp. The whole
late-combat block (Offhand* / Attack4-6 / Punch*) is numbered +3 lower there
than in ACE/DatReaderWriter — a contiguous low-word shift that begins around
SnowAngelState (0x115). So against a live ACE server every one of those 40
commands misclassified: the resolver returned the correct ACE value (e.g.
wire 0x0173 -> 0x10000173 OffhandSlashHigh) but the planner's constant held
the 2013 value (0x10000170), so no switch case matched. Reload was worse —
hardcoded 0x100000D4, a value absent from DRW entirely; the real ACE/DRW
Reload is the 0x40000016 SubState.
Instead of re-transcribing 40 hex constants (exactly how the drift crept in),
derive each constant directly from DatReaderWriter.Enums.MotionCommand by
name: `= (uint)Drw.Name`. The value IS the oracle by construction, can never
drift from the wire again, and ~80 magic numbers are gone. Ground truth was
taken by reflecting over the same DatReaderWriter 2.1.7 assembly the runtime
binds (409 enum values). Blocks 1-2 (stances + single melee, 0x3C-0x12A) were
already correct and are unchanged in value.
Tests: new PlanFromWireCommand_LateCombatBlock_UsesAceDrwNumbering pins the
full wire -> resolve -> classify pipeline for 14 late-block ACE wire values
(both full value and kind), plus a Reload SubState parity fact. Fixed two rows
in ClassifyMotionCommand_RecognisesRetailCombatCommands whose literal values
change meaning under DRW numbering (0x1000018E is AttackLow6/CreatureAttack,
not PunchFastLow; Reload moves to 0x40000016). Core suite 2499 green.
Note: CombatAnimationPlanner is not yet wired into the runtime dispatch (the
live path is AnimationCommandRouter -> MotionInterpreter/AnimationSequencer),
so this is a latent-correctness fix — it does not by itself change the #170
Mite Scamp symptom (whose attacks 0x62-0x64 live in the already-correct block).
Ref: docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both root-caused live + fixed this session (315af02f, 9b06a9b8). NOT R5 —
pre-existing streaming bugs surfaced while visual-gating R5-V2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (confirmed live via the landblock-load probe): the streaming window
marks every landblock "resident" at bootstrap (MarkResidentFromBootstrap) BEFORE
their async loads land. When a character spawns far from the startup center
(0xA9B4 Holtburg), the login-spawn recenter runs RecenterTo against that stale,
half-loaded window — and RecenterTo trusts _tierResidence, so it never
re-enqueues the old/new window overlap. The overlap band is marked resident but
its loads never completed, leaving a permanent HOLE of landblocks that are never
requested (zero BUILD-NULL — they're simply skipped). The player spawns in the
hole: its landblock never loads, so terrain/NPCs/player never draw — the
"world not loading / invisible / scenery behind" symptom.
Probe evidence (spawn at 0xADAF): the player's column 0xAD loads Y=0xA3-0xAA and
0xB0-0xC0 but is MISSING Y=0xAB-0xAF — and the player is at 0xADAF (Y=0xAF), dead
in the gap. 0xADAFFFFF: never [lb] ADD'd, never BUILD-NULL'd.
Fix: a far login-spawn moves the render origin exactly like an outdoor teleport
(which already calls StreamingController.ForceReloadWindow to drop the stale
window + re-bootstrap fresh around the new origin — GameWindow.cs ~5725). The
login-spawn recenter now flags the same rebuild; because the spawn handler runs
on the network thread and ForceReloadWindow is render-thread-only, the flag is
set there and consumed in OnUpdateFrame's streaming block before the Tick.
Verified live: 0xADAFFFFF now `[lb] ADD entities=216`, full world draws
(flatCount=12807), and the player transitions PENDING(hidden) -> DRAWSET PRESENT
(composes with the RelocateEntity pending-recovery fix, 315af02f). No-op for a
normal Holtburg login (spawn == startup center → guard false). Full suite 4007
green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (confirmed live via ACDREAM_PROBE_ENT): a persistent server-spawned
entity that spawns into a not-yet-loaded landblock is parked in
GpuWorldState._pendingByLandblock. RelocateEntity — called every frame to keep
the player homed to its current landblock so it draws — scanned ONLY _loaded, so
it silently no-op'd on a pending entity. The player then fell through ALL of the
recovery paths: the AddLandblock pending-drain had already run (empty) before the
cold-spawn churn re-parked the player; the server-object re-hydrate excludes the
player by design ("persistent-rescue owns it"); and RelocateEntity couldn't reach
a pending entity. Net: the player stayed stranded in pending, hidden forever.
Probe evidence (cold-spawn at 0xADAF): `[ent] APPEND guid=0x5000000A
lb=0xADAFFFFF -> PENDING(hidden)`, no later `DRAWSET PRESENT` — while the sibling
NPCs `re-hydrated 3 server object(s) into landblock 0xADAFFFFF` and drew fine.
This is the mechanism behind BOTH reported symptoms: cold-spawn "everything gone
/ invisible" AND "character disappears running out of Holtburg far enough" — both
are "player parked in pending, never recovered" (run-out crosses into a
still-streaming landblock; cold-spawn spawns into one).
Fix: RelocateEntity now removes the entity from whichever bucket it occupies
(_loaded OR _pendingByLandblock) via RemoveEntityFromAllBuckets, then re-appends
to its current landblock — promoting a stranded pending entity to drawn as soon
as its landblock is loaded. Keeps the fast-path early-return for a settled
entity (no per-frame churn).
NOT R5-V2: verified line-by-line that the voyeur/target wiring is read-only w.r.t.
position/cell/landblock/streaming — this is a pre-existing streaming bug. The
separate cold-spawn `cells=0` (outdoor spawn placed before Near-tier cells load,
no re-snap) is benign for outdoor placement (terrain-Z is used) and filed
separately; it does not block visibility, which this fix restores.
Test: GpuWorldStateTests.RelocateEntity_StrandedInPending_MovesToLoadedTarget
(red before, green after). Full suite 4007 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the AP-79 P4 TargetTracker poll adapter with the ported retail
TargetManager voyeur subscription system, wired per entity. Behaviorally a
faithful no-op refactor for the common cases (server-directed creature chase
+ auto-walk-to-object), now driven by the retail mechanism instead of the
GameWindow poll.
New: EntityPhysicsHost (App) — the per-entity IPhysicsObjHost (retail
CPhysicsObj stand-in), owning a TargetManager. Delegate-injected accessors so
it stays free of GameWindow internals (code-structure rule #1).
Wiring (GameWindow):
- _physicsHosts registry (guid → host) = retail CObjectMaint::GetObjectA,
backing the voyeur round-trip's cross-entity delivery.
- ResolvePhysicsHost lazily creates a minimal position-only host for ANY known
entity — retail lets every CPhysicsObj host a target_manager, so a STATIC
object (chest/corpse) still answers add_voyeur; without this, auto-walk to a
never-animated object would arm the moveto but never receive the immediate
target snapshot and never start.
- MoveToManager set_target/clear_target/quantum seams repointed at the host's
TargetManager (remote + player).
- HandleTargetting ticked unconditionally per entity BEFORE UseTime (retail
UpdateObjectInternal order): per-remote loop for remotes, pre-Update block for
the player. The player's tick is load-bearing for creature-chase — the player
as a watched target pushes its position to the chasing NPCs' HandleUpdateTarget
each frame, ahead of their UseTime.
- Despawn (RemoveLiveEntityByServerGuid): NotifyVoyeurOfEvent(ExitWorld) to the
entity's watchers before pruning the host — the only cleanup for a watcher
whose target already sent an Ok (past Undefined, the 10s staleness never fires).
Deleted: RemoteMotion.TrackedTarget* fields + _playerMoveToTarget* GameWindow
fields + the two manual poll blocks. AP-79 register row retired same commit
(AP section 73→72).
Delivery is now synchronous-on-set_target (retail: set_target is last in
MoveToObject, so the immediate snapshot lands with all moveto state in place)
vs AP-79's next-frame poll — more faithful. Full suite 4006 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retail movement-manager family the R4 MoveToManager port left as
do-not-invent seams (decomp §9f/§9g). Faithful C# ports of retail's
PositionManager facade + StickyManager + ConstraintManager + the
TargetManager voyeur system, with full conformance tests. NO wiring yet
— purely additive, no behavior change. Wiring (retiring TS-39 sticky +
AP-79 target adapter) is R5-V2/V3.
New Core classes (src/AcDream.Core/Physics/Motion/):
- StickyManager (0x00555400): follow-a-target steering. adjust_offset's
dense x87 mush decoded via ACE (StickyRadius 0.3, StickyTime 1.0,
follow speed ×5 / fallback 15) — speed-clamped signed-distance steer +
bounded turn-to-face; 1 s watchdog; Ok→initialized / non-Ok→teardown.
- ConstraintManager (0x00556090): the server-position rubber-band leash.
90% IsFullyConstrained jump gate + grounded linear brake taper.
Structural only — acdream never ARMS it (retail arms from
SmartBox::HandleReceivedPosition, which acdream lacks, with two x87
constants BN elided). IsFullyConstrained stays false = TS-35 behavior;
leash-arming + the unknown constants are a deferred issue.
- PositionManager facade (0x00555160): lazy Sticky/Constraint + fan-out.
- TargetManager (0x0051a370) + TargettedVoyeurInfo: the peer-to-peer
voyeur subscription system (0.5 s throttle, 10 s staleness,
send-on-drift-past-radius, dead-reckon GetInterpolatedPosition). A
faithful superset of the AP-79 adapter — SetTarget subscribes ON the
target; the target's HandleTargetting pushes updates back.
- IPhysicsObjHost: the CPhysicsObj back-pointer seam (position/velocity/
radius/contact/GetObjectA + target-tracking fan-out) the App wires per
entity in V2/V3. MotionDeltaFrame: mutable retail-Frame delta accumulator.
Supporting:
- TargetInfo extended to the full retail 10-field struct (additive
defaults keep the R4 4-arg call sites compiling).
- MoveToMath: signed CylinderDistanceNoZ, NormalizeCheckSmall,
GlobalToLocalVec.
- Rename: the misnamed AcDream.Core.Physics.PositionManager (a remote
anim+interp per-frame combiner, NOT the retail facade) → RemoteMotion
Combiner, freeing the name and removing the ambiguity that breaks every
file importing both Physics + Physics.Motion (GameWindow will in V2/V3).
Tests: 42 new conformance cases (Sticky/Constraint/Position facade +
TargetManager incl. the full cross-entity voyeur round-trip). Full suite
4006 green (+2 skipped), no regressions.
Decomp + ACE cross-ref + port plan: docs/research/2026-07-03-r5-managers/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All four visual-pass items confirmed by the user's eyes (jump/ledge
momentum, run-in-circles blend, stop settle, retail-observer view of
+Acdream). Two observations filed from the pass:
- #165: retail movers observed from acdream penetrate walls a bit
("swallowed") instead of stopping flush - remote-DR collision class,
PROBE_RESOLVE capture is the first investigative step.
- #166: downhill-jump landing glide+bounce (retail sled) absent - the
register predicted this exact gap (AD-25 risk column) - composite of
AD-25 + AP-7 + TS-4, retire together post-R6 per the user's
"polish later" call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#163: remove [autowalk-gate] (+ _lastAutowalkGateLogTime throttle clock,
PlayerMovementController), [autowalk-feed] (GameWindow player tracker
feed), and the short-lived [MOVETO-CANCEL] #162 capture probe. The
durable ProbeAutoWalk family (PhysicsDiagnostics owner + DebugPanel
toggle + the permanent [autowalk] probe sites) stays.
#162 closed WITHOUT adaptation: user A/B says retail does not glide, and
post-#161 acdream matches (user-verified walk/run-by-distance movetos +
clean mid-chain key takeover). Head-interrupt + CancelMoveTo re-audited
retail-verbatim against the raw (unpack_movement 0x00524440,
interrupt_current_movement 0x005101f0, MoveToManager::CancelMoveTo
0x00529930 armed-gate); the launch-162 capture shows ACE's mt-0
reflections carry the mover's real locomotion, so cancels never strand
the legs. The old glide was killed by the R4-V5 stack + #161's
apply-pass params fix. Evidence trail in ISSUES Recently-closed #162.
Suite 3,964 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail's apply_interpreted_movement (0x00528600) does NOT dispatch with
ctor-default MovementParameters: the BN decomp smears the bitfield store
into the mush expression at raw 305778 - (word & 0x37ff) | cancelMoveTo<<15
| disableJump<<17 - which CLEARS SetHoldKey/ModifyInterpretedState/
CancelMoveTo. ACE MotionInterp.cs:444-449 confirms independently. Three
legs fixed, all retail decode, no adaptations added:
1. ApplyInterpretedMovement now builds the pass params retail actually
uses (ModifyInterpretedState=false is load-bearing): no dispatch in the
pass writes InterpretedState, so the airborne Falling substitution
PRESERVES the wire's forward command and HitGround's re-apply
dispatches it - the motion table plays the Falling->X landing link.
The W6 entry-cache (built on the wrong "retail self-heals via hoisted
registers" theory) is deleted; live reads are retail semantics. Raw
arg3 decoded as DisableJumpDuringLink -> every (N,0) caller means
allowJump=true; all 9 caller polarities fixed. copy_movement_from's
current_style copy (raw 0051e757) added to the UM flat-copy.
2. Both GameWindow landing blocks cleared the Gravity state bit BEFORE
Motion.HitGround(), whose verbatim state&0x400 gate then no-opped the
whole retail re-apply; the UP-driven landing block never called
HitGround at all. Both now run HitGround (minterp then moveto,
MovementManager::HitGround 0x00524300 order) with Gravity still set,
then do the DR bookkeeping clear (register row AP-81 added for the
remaining flag dance, retire in R6).
3. K-fix17's forced SetCycle (both copies) deleted: it executed every
landing but read the leg-1-clobbered ForwardCommand (0x40000015) and
re-set the very Falling cycle it meant to clear.
Tests: new HitGround_AfterFall_RedispatchesPreservedForward_ExitsFalling
lifecycle pin; AirborneBody state assertion flipped (it had pinned the
bug value). Suite 3,964 green incl. the 183-case retail-observer trace.
Filed #164 (action-replay Autonomous bit, no current consumer).
Awaiting live verify: stand-still landing must exit the falling pose with
zero wire input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User observation (acdream observing a retail player's server MoveTo):
walk-distance approaches correct; run-distance plays the run cycle AND
moves toward the target in slow motion.
Root cause, raw-verified (apply_run_to_command 0x00527be0, raw
305062-305076): retail's rate chain is
weenie ? (InqRunRate() ?: my_run_rate) : 1.0
Every placed retail object HAS a weenie; a REMOTE's weenie fails
InqRunRate (no local skill data) and the chain lands on my_run_rate -
the exact field retail's mt-6/7 unpack writes with the wire's
MoveToRunRate (M13; observed 4.50). Our port had the branch verbatim
but remote interps carried NO weenie at all, taking the else-1.0
branch retail reserves for weenie-less detached objects: observer-side
run dispatches went out at speed 1.0, so the run cycle's pace AND
get_state_velocity's chase speed both crawled. Walk was unaffected -
walk speed is never rate-scaled (the discriminating symptom).
Fix: RemoteWeenie (Core) - the minimal stand-in for retail's
per-object ACCWeenieObject: InqRunRate fails (-> my_run_rate),
InqJumpVelocity fails, IsThePlayer default false (also ends the
fragile "null weenie counts as the player" reading of the A3 dual
dispatch for remotes), IsCreature default true (doors carry it too;
their force-asserted Contact keeps the creature ground-gate moot -
documented on the class). RemoteMotion's interp now constructs with
it.
Tests: RemoteWeenieRunRateTests pins both branches against the raw -
RemoteWeenie + MyRunRate=4.5 promotes WalkForward->RunForward @ 4.5;
weenie-less keeps the verbatim degenerate 1.0. Full suite green: 3,963.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>