acdream/docs/superpowers/plans/2026-05-02-l3-1-interpolation-manager.md
Erik f28240ad19 docs(plan): Phase L.3.1 — InterpolationManager core implementation plan
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>
2026-05-02 18:26:02 +02:00

38 KiB
Raw Blame History

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 for InterpolationManager::UseTime near 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. Find InterpolationManager::UseTime (search by exact string InterpolationManager::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.md sections "L.3.1 architecture" → "New file" through the unit-test list. Read the research at docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md for the constants table.

Create the file src/AcDream.Core/Physics/InterpolationManager.cs matching 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 Position type in the signature, but acdream's PhysicsBody uses Vector3 Position separately from uint CellId. So:

  • Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo) — caller is responsible for resolving cell deltas
  • AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp) returns the world-space delta to add to body.Position this frame

Implement the spec's AdjustOffset algorithm 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). Use LinkedList<InterpolationNode> for the queue.

Create tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs with 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-level using block, namespace AcDream.Core.Tests.Physics;, then test methods. Use file sealed class for any test-only helpers.

Build with cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo and dotnet 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.md section "L.3.1 architecture" → "Modified — MotionInterpreter".

Add a public method GetMaxSpeed() to src/AcDream.Core/Physics/MotionInterpreter.cs. It must port retail's CMotionInterp::get_max_speed semantics: return the motion-table-derived max speed for the current InterpretedState.ForwardCommand. Acdream's MotionInterpreter already knows the constants RunAnimSpeed = 4.0f and WalkAnimSpeed = 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.txt for CMotionInterp::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_ReturnsRunAnimSpeedTimesRunRate
  • GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed
  • GetMaxSpeed_Idle_ReturnsZero

Use the existing FakeWeenie test 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 — RemoteMotion class)

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 Interp field

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:

  1. Walk forward 5 sec
  2. Walk backward 5 sec
  3. Strafe left/right 5 sec each
  4. Stop
  5. Run forward 5 sec
  6. Jump from standstill 2-3x
  7. Jump while running 2-3x
  8. 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 17a9ff1 still 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:

  1. In OnLivePositionUpdated (added in Task 4): delete the if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") wrapper. Keep ONLY the routing block inside it (the new path). Delete the legacy hard-snap fall-through.

  2. In the per-frame remote tick block (modified in Task 5): same — delete the if/else env-var gate. Keep ONLY the new path (Interp.AdjustOffset). Delete the legacy apply_current_movement + force-OnWalkable + Euler-extrapolate code in the else branch.

  3. In the RemoteMotion class (~line 224): delete SnapResidualDecayRate and any soft-snap residual fields (search for _snapResidual, SnapResidualDecayRate, SoftSnap, etc.). Also delete any related code in OnLivePositionUpdated and 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).

  4. Search for any remaining ACDREAM_INTERP_MANAGER references 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.md to understand the existing format. Find the spot between ### Phase L.2 — Movement & Collision Conformance and ### 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 to for 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 --nologo

Commit:

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 IsStaleSequence is 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.HasContact field name is a guess — verify against CreateObject.ServerPosition definition. If absent, file a parser-gap follow-up; for L.3.1 default to hasContact = true (allow all to enqueue).
  • Task 5's dt source — the existing per-frame block already has dt from the render loop or computes it from nowSec - lastTime. Reuse whatever's there.
  • Task 6's update.Omega field — verify against VectorUpdate.Parsed. If named AngularVelocity use 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.