10-task incremental plan with explicit subagent dispatch points: - Tasks 0+1+2 dispatched in parallel (3 concurrent Sonnet subagents): Task 0 = decomp dive to settle UseTime head-vs-tail blip ambiguity Task 1 = InterpolationManager class + ~13 unit tests Task 2 = MotionInterpreter.GetMaxSpeed() + ~3 unit tests - Tasks 3-6 sequential GameWindow edits (env-var gated, dual-path): Task 3 = RemoteMotion gains Interp field Task 4 = OnLivePositionUpdated MoveOrTeleport routing Task 5 = per-frame remote tick Interp.AdjustOffset add Task 6 = OnLiveVectorUpdated.Omega application - Task 7 = USER GATE (visual verification) - Tasks 8+9 dispatched in parallel after sign-off (2 subagents): Task 8 = cleanup commit (delete env-var, dead paths, soft-snap residual) Task 9 = roadmap update (insert Phase L.3 entry) Each task has TDD-style steps with exact file paths, code blocks, build/test commands, and commit messages. Plan honors CLAUDE.md direct-to-main + commit-after-each-step + visual-verify-on-motion. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
38 KiB
Phase L.3.1 — InterpolationManager Core + MoveOrTeleport Routing — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.Heavy subagent use is the user's explicit request. Tasks marked
[PARALLEL-A],[PARALLEL-B], etc. can be dispatched simultaneously as concurrent Sonnet subagents and the parent reviews each return before integrating. The plan flags every parallelization opportunity.
Goal: Replace acdream's hard-snap-then-Euler-extrapolate remote-entity motion with retail's queued position-waypoint pipeline (InterpolationManager + MoveOrTeleport routing). Apply parsed-but-ignored VectorUpdate.Omega. Tear out the now-redundant RemoteMotion soft-snap residual code. Ship behind ACDREAM_INTERP_MANAGER=1 env-var gate, then collapse the dual paths after visual verification.
Architecture: New pure-data InterpolationManager class (FIFO queue cap 20 + AdjustOffset(dt, maxSpeed) → Vector3 per-frame catch-up) composed into the existing per-remote RemoteMotion container. Inbound 0xF748 UpdatePosition handler (OnLivePositionUpdated) replaced by retail-faithful router (stale-seq → ignore; teleport-seq newer → snap; within 96 m → enqueue; beyond 96 m → slide-snap). Per-frame remote tick adds Interp.AdjustOffset(dt) → body.Position. Single-keyword env-var rollback during dev; cleanup commit after sign-off.
Tech Stack: C# / .NET 10 / xUnit. Edits in AcDream.App + AcDream.Core. No new NuGet deps. Tests at tests/AcDream.Core.Tests/Physics/*Tests.cs.
Spec: docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md (committed 08cb7f9).
Research baseline: docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md.
File Structure
| File | Action | Responsibility |
|---|---|---|
src/AcDream.Core/Physics/InterpolationManager.cs |
CREATE | Pure-data FIFO position-queue + adjust_offset math. No game/window deps. Composed into RemoteMotion. |
tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs |
CREATE | ~13 unit tests covering queue mechanics, AdjustOffset math, stall detection. |
src/AcDream.Core/Physics/MotionInterpreter.cs |
MODIFY | Add public GetMaxSpeed() returning motion-table-derived max for current InterpretedState. |
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs |
MODIFY | Add ~3 tests covering GetMaxSpeed for Walk/Run/Idle. |
src/AcDream.App/Rendering/GameWindow.cs |
MODIFY | (a) RemoteMotion class gains Interp field. (b) OnLivePositionUpdated env-var gated routing. (c) Per-frame remote tick env-var gated Interp.AdjustOffset add. (d) OnLiveVectorUpdated applies Omega to body. |
docs/plans/2026-04-11-roadmap.md |
MODIFY | Insert Phase L.3 entry between L.2 and M. |
(cleanup commit) src/AcDream.App/Rendering/GameWindow.cs |
MODIFY | Delete env-var dual-path branches; delete old hard-snap path; delete RemoteMotion soft-snap residual fields. |
Open Precision Item
The spec flags one ambiguity: does retail's InterpolationManager::UseTime (acclient @ 0x00555F20) blip-to-HEAD or blip-to-TAIL on stall? The two agent reports disagreed. Default for the initial port = HEAD. Task 0 below resolves this via a 30-second cdb static decomp dive (no live attach needed).
Task Decomposition Overview
┌──────────────────────────────┐
│ Task 0 [PARALLEL-A] │
│ Resolve UseTime head/tail │
│ (decomp read, ~5 min) │
└──────────────────────────────┘
┌──────────────────────────────┐
│ Task 1 [PARALLEL-B] │
┌─ DISPATCH 3 SUBAGENTS IN PARALLEL ──────────────►│ InterpolationManager + tests│
│ └──────────────────────────────┘
│ ┌──────────────────────────────┐
│ │ Task 2 [PARALLEL-C] │
│ │ MotionInterpreter.GetMaxSpeed│
│ └──────────────────────────────┘
┌─ AFTER 0+1+2 LAND ────────────────────────────────────────────────────────────────────┐
│ │
│ Task 3 — RemoteMotion.Interp field (sequential, single edit) │
│ ↓ │
│ Task 4 — OnLivePositionUpdated env-var routing (sequential) │
│ ↓ │
│ Task 5 — Per-frame remote tick env-var Interp.AdjustOffset (sequential) │
│ ↓ │
│ Task 6 — OnLiveVectorUpdated.Omega (sequential, 3 lines) │
│ │
│ Task 7 — Visual verification (USER GATE) │
│ ↓ user signs off │
│ │
│ ┌─ DISPATCH 2 SUBAGENTS IN PARALLEL ──────┐ │
│ │ Task 8: Cleanup commit │ │
│ │ Task 9: Roadmap update │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────────────┘
Task 0 — [PARALLEL-A] Resolve UseTime head-vs-tail via static decomp
Owner: Sonnet subagent (general-purpose). Read-only; no code changes.
Files:
- Read:
docs/research/named-retail/acclient_2013_pseudo_c.txt(search forInterpolationManager::UseTimenear line ~352261-353375)
Subagent dispatch prompt (use general-purpose agent type, Sonnet):
Read the named retail decomp at
docs/research/named-retail/acclient_2013_pseudo_c.txt. FindInterpolationManager::UseTime(search by exact stringInterpolationManager::UseTime). It should appear around line 352261-353375. Read the body of that function (~100 lines).The function decides what to do when the per-5-frame stall counter shows the entity isn't catching up to its queued waypoints (
node_fail_counter > 3). The two prior research agents disagreed on whether the resulting "blip" snaps the body to the HEAD of the queue (the next intended waypoint) or to the TAIL (the most recent server-sent position).Report under 200 words: which is it (HEAD or TAIL), with the line range from the decomp that proves it. If the decompile is ambiguous (e.g. comparison polarity artifact), flag that and recommend a default. No code edits.
Steps:
- Step 0.1: Dispatch the subagent
Use the Agent tool with subagent_type=general-purpose, model sonnet, prompt above.
- Step 0.2: Read subagent report; record decision in implementation note
Append a one-line note to the InterpolationManager source comment (created in Task 1) recording the resolution.
Task 1 — [PARALLEL-B] InterpolationManager class + ~13 unit tests
Owner: Sonnet subagent (general-purpose). Independent of Tasks 0 + 2.
Files:
- Create:
src/AcDream.Core/Physics/InterpolationManager.cs - Create:
tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs
Subagent dispatch prompt (use general-purpose agent type, Sonnet):
You are implementing Phase L.3.1 Task 1. Read the spec at
docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.mdsections "L.3.1 architecture" → "New file" through the unit-test list. Read the research atdocs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.mdfor the constants table.Create the file
src/AcDream.Core/Physics/InterpolationManager.csmatching the spec's API:public sealed class InterpolationManager { void Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo); Vector3 AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp); bool IsActive { get; } void Clear(); // constants from spec }The spec uses retail's
Positiontype in the signature, but acdream's PhysicsBody usesVector3 Positionseparately fromuint CellId. So:
Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo)— caller is responsible for resolving cell deltasAdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp)returns the world-space delta to add to body.Position this frameImplement the spec's
AdjustOffsetalgorithm exactly (steps 1-9 as written). For the stall-blip branch, use HEAD as the default (Task 0 may override this; Task 0's report should be available — if it says TAIL, use TAIL). UseLinkedList<InterpolationNode>for the queue.Create
tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cswith the 13 tests listed in the spec under "L.3.1 unit tests" → "Queue mechanics", "AdjustOffset math", "Stall detection". Use xUnit. Match the test-file pattern of existing files (e.g.tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs): top-levelusingblock,namespace AcDream.Core.Tests.Physics;, then test methods. Usefile sealed classfor any test-only helpers.Build with
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologoanddotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~InterpolationManager". Both must be green.Commit with
feat(physics): InterpolationManager core (L.3.1 Task 1)and Co-Authored-By Claude Opus 4.7. Direct-to-main per CLAUDE.md.Report under 300 words: what you built, test results, any deviations from the spec (if you had to deviate, justify).
Steps:
- Step 1.1: Dispatch the subagent in parallel with Tasks 0 and 2
Use the Agent tool with subagent_type=general-purpose, model=sonnet. Send all 3 dispatch calls in a single message so they run concurrently.
- Step 1.2: Verify subagent's commit
git log -1 --stat src/AcDream.Core/Physics/InterpolationManager.cs
Expected: commit message starts with feat(physics): InterpolationManager core (L.3.1 Task 1). Files changed include InterpolationManager.cs + InterpolationManagerTests.cs.
- Step 1.3: Re-run tests in parent session to confirm green
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~InterpolationManager"
Expected: all ~13 tests pass.
- Step 1.4: Spot-check the implementation file
Read the created file. Verify: API surface matches spec exactly; constants are public consts with the spec's values; AdjustOffset algorithm follows spec steps 1-9; stall-blip uses HEAD (or TAIL per Task 0 outcome).
If anything diverges materially from the spec without justification in the subagent's report, dispatch a fix subagent. If the deviation is minor and harmless, accept it.
Task 2 — [PARALLEL-C] MotionInterpreter.GetMaxSpeed() + ~3 unit tests
Owner: Sonnet subagent (general-purpose). Independent of Tasks 0 + 1.
Files:
- Modify:
src/AcDream.Core/Physics/MotionInterpreter.cs(add one public method, ~10-15 lines) - Modify:
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs(add ~3 tests)
Subagent dispatch prompt (use general-purpose agent type, Sonnet):
You are implementing Phase L.3.1 Task 2. Read the spec at
docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.mdsection "L.3.1 architecture" → "Modified —MotionInterpreter".Add a public method
GetMaxSpeed()tosrc/AcDream.Core/Physics/MotionInterpreter.cs. It must port retail'sCMotionInterp::get_max_speedsemantics: return the motion-table-derived max speed for the currentInterpretedState.ForwardCommand. Acdream'sMotionInterpreteralready knows the constantsRunAnimSpeed = 4.0fandWalkAnimSpeed = 3.12f(search the file for these). Approximate retail logic:public float GetMaxSpeed() { return InterpretedState.ForwardCommand switch { MotionCommand.RunForward => RunAnimSpeed * (WeenieObj?.InqRunRate(out var r) == true ? r : MyRunRate), MotionCommand.WalkForward => WalkAnimSpeed, MotionCommand.WalkBackward => WalkAnimSpeed * 0.65f, // BackwardsFactor _ => 0f, // idle / non-locomotion }; }If retail decomp suggests a different formula, prefer that — search the named decomp at
docs/research/named-retail/acclient_2013_pseudo_c.txtforCMotionInterp::get_max_speed(around line 305235-305280). Report what you found.Add ~3 unit tests to
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs:
GetMaxSpeed_RunForward_ReturnsRunAnimSpeedTimesRunRateGetMaxSpeed_WalkForward_ReturnsWalkAnimSpeedGetMaxSpeed_Idle_ReturnsZeroUse the existing
FakeWeenietest helper from MotionInterpreterTests.cs.Build + test:
cd C:/Users/erikn/source/repos/acdream dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~MotionInterpreter"Both green.
Commit with
feat(physics): MotionInterpreter.GetMaxSpeed for InterpolationManager (L.3.1 Task 2)and Co-Authored-By Claude Opus 4.7. Direct-to-main.Report under 200 words: what the formula is, decomp reference if found, test results.
Steps:
-
Step 2.1: Dispatch in parallel with Tasks 0 and 1 (single message, three concurrent Agent tool calls)
-
Step 2.2: Verify subagent's commit
git log -1 --stat src/AcDream.Core/Physics/MotionInterpreter.cs
Expected: commit message feat(physics): MotionInterpreter.GetMaxSpeed (L.3.1 Task 2).
- Step 2.3: Re-run tests
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~MotionInterpreter"
Expected: existing tests + new ~3 tests all pass.
Task 3 — Add Interp field to RemoteMotion class
Owner: Parent (you). Tiny mechanical edit; not worth a subagent.
Files:
- Modify:
src/AcDream.App/Rendering/GameWindow.cs(line ~224 —RemoteMotionclass)
Steps:
- Step 3.1: Read the RemoteMotion class definition
grep -n "private sealed class RemoteMotion" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
Then Read tool from the line returned, ~120 lines.
- Step 3.2: Add
Interpfield
In the RemoteMotion class body (after the existing field declarations, before the constructor public RemoteMotion()), add:
/// <summary>
/// Per-remote position-waypoint queue + catch-up math (retail's
/// InterpolationManager). Replaces the hard-snap-then-Euler-extrapolate
/// path when ACDREAM_INTERP_MANAGER=1 — see L.3.1 spec.
/// </summary>
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
new AcDream.Core.Physics.InterpolationManager();
- Step 3.3: Build and verify
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
Expected: 0 warnings, 0 errors, "Build succeeded."
- Step 3.4: Run all tests
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
Expected: existing tests still pass (no behavior change yet).
- Step 3.5: Commit
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(motion): RemoteMotion gains InterpolationManager field (L.3.1 Task 3)
Composes the new InterpolationManager (Task 1) into the per-remote
container. Field exists but is not consumed yet — Tasks 4 and 5 wire
it into the routing + per-frame tick.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
Task 4 — Env-var gated routing in OnLivePositionUpdated
Owner: Parent. Manual edit because the surrounding handler is complex (~70 lines) and we need to wrap it without disrupting the legacy path.
Files:
- Modify:
src/AcDream.App/Rendering/GameWindow.cs(line ~3151 —OnLivePositionUpdated)
Steps:
- Step 4.1: Read the entire OnLivePositionUpdated method to understand current structure
grep -n "OnLivePositionUpdated\b" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
Then Read tool from OnLivePositionUpdated start, ~100 lines (until the next method declaration).
Note: the method currently does (a) lazy-create RemoteMotion if not in dict, (b) hard-snap body.Position and body.Orientation, (c) update RemoteMotion.SnapResidualDecayRate / soft-snap residual fields, (d) clear airborne / set Z-fields if has-velocity changed.
- Step 4.2: Locate the specific point where the hard-snap happens
Look for rm.Body.Position = ... (or body.Position = ...) inside this handler. Mark its surrounding context.
- Step 4.3: Wrap the snap block in env-var conditional
Pseudocode of the change:
private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
{
// ... existing lazy-create + parse + identification (unchanged) ...
if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) {
rm = new RemoteMotion();
_remoteDeadReckon[update.Guid] = rm;
}
var targetPos = ...; // existing extraction (Vector3)
var targetOri = ...; // existing extraction (Quaternion)
// NEW: env-var gated retail-faithful routing (L.3.1)
if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
{
// CPhysicsObj::MoveOrTeleport router (acclient @ 0x00516330):
// - stale-seq: ignore (TODO: implement IsStaleSequence wrapping uint16 compare on the four sequence counters; for now allow all to land)
// - teleport-seq newer or no-cell: SetPosition (hard-snap)
// - has_contact false: no-op
// - has_contact true && distance ≤ 96: Interp.Enqueue
// - has_contact true && distance > 96: SetPositionSimple (slide-snap)
// Distance source: retail uses entity->[+0x20] (entity-to-local-player).
// Acdream computes equivalent via local player position.
Vector3 localPlayerPos = _playerController?.Position ?? Vector3.Zero;
float dist = Vector3.Distance(targetPos, localPlayerPos);
bool teleportFlag = false; // TODO: source from update sequence comparison once IsStaleSequence is in
bool hasContact = update.Position.HasContact; // verify field name in CreateObject.ServerPosition
if (teleportFlag) {
rm.Body.Position = targetPos;
rm.Body.Orientation = targetOri;
rm.Interp.Clear();
}
else if (!hasContact) {
// no-op
}
else if (dist > 96f) {
rm.Interp.Clear();
rm.Body.Position = targetPos;
rm.Body.Orientation = targetOri;
}
else {
float headingFromQuat = ExtractYawFromQuaternion(targetOri); // see helper below
rm.Interp.Enqueue(targetPos, headingFromQuat, isMovingTo: false);
}
return;
}
// EXISTING hard-snap path (unchanged) — kept until cleanup commit (Task 8)
rm.Body.Position = targetPos;
rm.Body.Orientation = targetOri;
// ... rest of existing soft-snap + residual fields ...
}
// Helper (place near OnLivePositionUpdated):
private static float ExtractYawFromQuaternion(Quaternion q)
{
// Inverse of YawToAcQuaternion: extract Z-axis rotation angle.
// Acdream's player Yaw convention: Yaw=0 faces +X.
return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y), 1f - 2f * (q.Y * q.Y + q.Z * q.Z));
}
If update.Position.HasContact doesn't exist, look for the equivalent on CreateObject.ServerPosition — likely update.Position.IsGrounded or similar based on parsed PositionPack flags. Use whatever's there; acceptable to file a TODO to plumb it through if it's not currently parsed.
- Step 4.4: Build
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
Expected: 0 errors, 0 warnings.
- Step 4.5: Run tests
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
Expected: all existing tests pass (env-var off by default → existing behavior unchanged).
- Step 4.6: Commit
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(motion): MoveOrTeleport routing in OnLivePositionUpdated (L.3.1 Task 4)
Wraps the hard-snap path in ACDREAM_INTERP_MANAGER=1 env-var guard.
When set, runs retail-faithful routing (acclient!CPhysicsObj::
MoveOrTeleport @ 0x00516330): teleport-seq → SetPosition; within 96m
→ Interp.Enqueue; beyond 96m → SetPositionSimple slide-snap.
Existing hard-snap behavior preserved when flag is unset (default).
Old path will be removed in cleanup commit after visual verification.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
Task 5 — Env-var gated per-frame Interp.AdjustOffset add
Owner: Parent. Touches the per-frame remote tick (~line 5680-5760).
Files:
- Modify:
src/AcDream.App/Rendering/GameWindow.cs(per-frame remote tick block)
Steps:
- Step 5.1: Locate the remote tick block
grep -n "_remoteDeadReckon.TryGetValue.*serverGuid" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
Look for the line ~5689 entry; read 80 lines forward to see the whole tick block (where apply_current_movement and body.UpdatePhysicsInternal are called).
- Step 5.2: Wrap the legacy tick body in an if/else against the env-var
Pseudocode of the change:
if (ae.Sequencer is not null
&& serverGuid != 0
&& serverGuid != _playerServerGuid
&& _remoteDeadReckon.TryGetValue(serverGuid, out var rm))
{
if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
{
// NEW PATH: queued position-chase via InterpolationManager.
// Walking remotes have m_velocityVector == 0 in retail; all visible
// motion comes from adjust_offset walking the body toward queue head
// at 2 × motion_max_speed × dt.
if (rm.Interp.IsActive)
{
float maxSpeed = rm.Motion.GetMaxSpeed(); // Task 2 method
Vector3 delta = rm.Interp.AdjustOffset((float)dt, rm.Body.Position, maxSpeed);
rm.Body.Position += delta;
}
// For airborne remotes, OnLiveVectorUpdated has set body.Velocity;
// body.UpdatePhysicsInternal below applies gravity. No queue
// adjustment competes with the arc.
rm.Body.UpdatePhysicsInternal((float)dt);
}
else
{
// EXISTING hard-snap + Euler path (unchanged) — kept until cleanup
if (!rm.Airborne) {
// ... existing apply_current_movement, force-OnWalkable, etc.
}
rm.Body.UpdatePhysicsInternal((float)dt);
// ... existing post-physics processing ...
}
}
The exact shape depends on the existing code — preserve everything in the else branch verbatim. The if branch is the new one.
- Step 5.3: Build
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
Expected: 0 errors.
- Step 5.4: Run tests
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
Expected: all pass (flag off → existing behavior).
- Step 5.5: Commit
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(motion): per-frame Interp.AdjustOffset in remote tick (L.3.1 Task 5)
When ACDREAM_INTERP_MANAGER=1, the per-frame remote tick uses
InterpolationManager.AdjustOffset to walk body.Position toward the
queue head at 2 × motion-max-speed × dt (retail's
acclient!InterpolationManager::adjust_offset @ 0x00555D30).
Legacy apply_current_movement + Euler dead-reckoning preserved when
flag is unset.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
Task 6 — Apply VectorUpdate.Omega in OnLiveVectorUpdated
Owner: Parent. Tiny edit, no env-var gate (this is a strict bug-fix that improves both old and new paths).
Files:
- Modify:
src/AcDream.App/Rendering/GameWindow.cs(line ~3064 —OnLiveVectorUpdated)
Steps:
- Step 6.1: Read the existing handler
grep -n "OnLiveVectorUpdated" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
Read from line returned, ~30 lines.
- Step 6.2: Find the velocity-application line and add omega next to it
Find:
if (update.Velocity is { } v)
rm.Body.Velocity = v;
Add immediately after:
if (update.Omega is { } w)
rm.Body.Omega = w;
Verify the field name on VectorUpdate.Parsed — it might be Omega or AngularVelocity. If it's not present at all, that's a parser gap — file as a follow-up issue and skip this task. Most likely it's already parsed because the spec confirmed "currently parsed-but-ignored".
- Step 6.3: Build + test
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo
Expected: green.
- Step 6.4: Commit
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
fix(motion): apply VectorUpdate.Omega to remote body (L.3.1 Task 6)
VectorUpdate.Omega was parsed by WorldSession but never written to
the remote body's Omega field, leaving remote jumping/turning
arcs flat. Apply it alongside the existing Velocity assignment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
Task 7 — Visual verification (USER GATE)
Owner: User. Cannot be automated.
Steps:
- Step 7.1: Kill any running acdream
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 6
- Step 7.2: Launch acdream with ACDREAM_INTERP_MANAGER=1
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_INTERP_MANAGER = "1"
dotnet run --project C:\Users\erikn\source\repos\acdream\src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "C:\Users\erikn\source\repos\acdream\.claude\worktrees\jovial-blackburn-773942\launch.log"
- Step 7.3: User performs the visual test matrix
Have a parallel retail observer toon watching +Acdream. On the retail observer side:
- Walk forward 5 sec
- Walk backward 5 sec
- Strafe left/right 5 sec each
- Stop
- Run forward 5 sec
- Jump from standstill 2-3x
- Jump while running 2-3x
- Turn quickly while running
For each, verify (against the acceptance criteria in the spec):
-
Walking remotes glide smoothly (no 1-Hz popping)
-
Backward / strafe / turn behaviors from commit
17a9ff1still work -
Jump arcs are curved (Omega applied)
-
Step 7.4: User signs off OR files a regression
If anything regresses, file the specifics and either fix forward (parent dispatches a focused-fix subagent) or revert the env-var to legacy mode while debugging.
If everything looks right, proceed to Tasks 8 + 9 (parallel cleanup + roadmap).
Task 8 — [PARALLEL-D] Cleanup commit
Owner: Sonnet subagent (general-purpose). Independent of Task 9.
Files:
- Modify:
src/AcDream.App/Rendering/GameWindow.cs(delete env-var dual paths in 4 + 5; delete RemoteMotion soft-snap residual fields)
Subagent dispatch prompt (use general-purpose agent type, Sonnet):
You are implementing Phase L.3.1 Task 8: cleanup. The user has visually verified that ACDREAM_INTERP_MANAGER=1 works correctly. Now collapse the dual-path scaffolding into a single retail-faithful path.
In
src/AcDream.App/Rendering/GameWindow.cs:
In
OnLivePositionUpdated(added in Task 4): delete theif (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")wrapper. Keep ONLY the routing block inside it (the new path). Delete the legacy hard-snap fall-through.In the per-frame remote tick block (modified in Task 5): same — delete the
if/elseenv-var gate. Keep ONLY the new path (Interp.AdjustOffset). Delete the legacyapply_current_movement+ force-OnWalkable + Euler-extrapolate code in theelsebranch.In the
RemoteMotionclass (~line 224): deleteSnapResidualDecayRateand any soft-snap residual fields (search for_snapResidual,SnapResidualDecayRate,SoftSnap, etc.). Also delete any related code inOnLivePositionUpdatedand the per-frame tick that touched those fields (it should already be gone if Task 4/5 wrapped them in the env-var gate, but double-check).Search for any remaining
ACDREAM_INTERP_MANAGERreferences in the codebase and confirm zero remain.Build:
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo. 0 warnings, 0 errors. Test:dotnet test --no-build --nologo. All green.Commit:
chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead soft-snap path (L.3.1 Task 8) User has visually verified the new InterpolationManager-based remote motion (commits f2 + f5 + f6 from L.3.1). Collapses the env-var dual-path: deletes legacy hard-snap + Euler-extrapolate code from OnLivePositionUpdated and the per-frame remote tick, deletes the SnapResidualDecayRate + soft-snap residual fields from RemoteMotion. Net diff: ~50 lines deletion. Single retail-faithful path remains. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Report under 200 words: line counts deleted, files touched, test results.
Steps:
- Step 8.1: Dispatch the subagent in parallel with Task 9
Use Agent tool, general-purpose, sonnet. Send simultaneously with Task 9's dispatch.
- Step 8.2: Verify the commit landed and the diff is sensible
git log -1 --stat
git show HEAD -- src/AcDream.App/Rendering/GameWindow.cs | head -100
Expected: ~50 lines deleted, no ACDREAM_INTERP_MANAGER in the diff (only its removal).
- Step 8.3: Re-run all tests in parent session
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
Expected: green.
- Step 8.4: Confirm zero env-var references remain
grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
Expected: no output.
Task 9 — [PARALLEL-D] Roadmap update
Owner: Sonnet subagent (general-purpose). Independent of Task 8.
Files:
- Modify:
docs/plans/2026-04-11-roadmap.md
Subagent dispatch prompt (use general-purpose agent type, Sonnet):
You are implementing Phase L.3.1 Task 9: add the Phase L.3 entry to the roadmap.
Read
docs/plans/2026-04-11-roadmap.mdto understand the existing format. Find the spot between### Phase L.2 — Movement & Collision Conformanceand### Phase M — Network Stack Conformance(search for those exact headings).Insert a new Phase L.3 entry with this content:
### Phase L.3 — Remote Entity Motion Conformance **Status:** L.3.1 IN PROGRESS / SHIPPED (depending on whether cleanup commit has landed when you read this). **Goal:** Replace acdream's hard-snap-then-Euler-extrapolate remote-entity motion with retail's queued position-waypoint pipeline (`InterpolationManager` + `MoveOrTeleport` routing). Apply parsed-but-ignored `VectorUpdate.Omega`. Drop the parallel soft-snap residual scaffolding `RemoteMotion` was carrying. **Why now:** Live cdb traces (2026-05-02) confirmed retail uses a per-physobj FIFO position queue with `adjust_offset(dt)` walking the body at 2× motion-table-max-speed toward the head, NOT velocity-based dead-reckoning. acdream's chop comes from the wrong algorithm category, not just bad parameters. **Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md). **Plan (L.3.1):** [`docs/superpowers/plans/2026-05-02-l3-1-interpolation-manager.md`](../superpowers/plans/2026-05-02-l3-1-interpolation-manager.md). **Sub-lanes:** - **L.3.1 — InterpolationManager core + routing.** New `InterpolationManager` class, `MoveOrTeleport` routing replacing the hard-snap, `VectorUpdate.Omega` application, deletion of `RemoteMotion` soft-snap residual. - **L.3.2 — PositionManager.** Combines per-frame animation root-motion offset with the InterpolationManager's catch-up offset before writing the body's frame. Mirrors retail `CPhysicsObj::UpdateObjectInternal`. Spec to be drafted after L.3.1 ships. - **L.3.3 — MoveToManager.** Replaces `RemoteMoveToDriver` MVP with full retail-faithful port: retracking, sticky-to-target, fail-distance progress checks, sphere-cylinder distance variants. Spec to be drafted after L.3.2 ships.Also update the file's top-line
**Status:** Living document. Updated YYYY-MM-DD for ...line — change the date to today (2026-05-02) and the trailing reason tofor Phase L.3 remote-entity motion planning.Build (no code changed but verify nothing broke):
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologoCommit:
docs(roadmap): Phase L.3 — Remote Entity Motion Conformance (L.3.1 Task 9) Adds the Phase L.3 entry between L.2 (collision) and M (network). Lists the three sub-lanes (L.3.1 in progress, L.3.2 + L.3.3 sketched). Cross-references the design spec and the L.3.1 implementation plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Report under 100 words: that the entry is inserted, location, build green.
Steps:
-
Step 9.1: Dispatch in parallel with Task 8 (single message)
-
Step 9.2: Verify the commit
git log -1 --stat docs/plans/2026-04-11-roadmap.md
Expected: commit message docs(roadmap): Phase L.3 — Remote Entity Motion Conformance. File diff shows the new section in the right location.
- Step 9.3: Optional final push
cd C:/Users/erikn/source/repos/acdream && git push origin main
(Per CLAUDE.md, ask user before pushing.)
Verification Plan
End-to-end smoke test after L.3.1 fully lands (post-Task 9):
cd C:/Users/erikn/source/repos/acdream
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo # green
dotnet test --no-build --nologo # all green (~110 tests)
git log --oneline -10 # see L.3.1 commits in order
grep -rn "ACDREAM_INTERP_MANAGER" src/ # zero hits (cleanup confirmed)
grep -rn "SnapResidualDecayRate" src/ # zero hits (deleted)
Then user re-runs the visual test matrix from Task 7.3 with no env-var set (default behavior is now the new path).
If everything's green: L.3.1 done. Brainstorm L.3.2 next (PositionManager).
Self-Review Notes (for the executor)
- Task 4's
IsStaleSequenceis intentionally deferred — the legacy code doesn't check sequences either. Filing a follow-up TODO is acceptable; not a blocker for L.3.1. - Task 4's
update.Position.HasContactfield name is a guess — verify againstCreateObject.ServerPositiondefinition. If absent, file a parser-gap follow-up; for L.3.1 default tohasContact = true(allow all to enqueue). - Task 5's
dtsource — the existing per-frame block already hasdtfrom the render loop or computes it fromnowSec - lastTime. Reuse whatever's there. - Task 6's
update.Omegafield — verify againstVectorUpdate.Parsed. If namedAngularVelocityuse that. - Subagent failure handling: if a subagent reports a deviation that breaks the spec contract, dispatch a fix subagent or take it over manually. Don't let a confused subagent leave broken code in main.