From d063ac884d3e7085e5469eb25a932528ef4361c8 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 3 May 2026 10:10:16 +0200 Subject: [PATCH] docs(plan): Phase L.3.1+L.3.2 PositionManager + retail-faithful jump plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6-task plan with subagent dispatch on Tasks 1, 3, 5: - Task 1: PositionManager class + 6 unit tests (subagent) - Task 2: Plumb IsGrounded through EntityPositionUpdate (parent, ~5 lines) - Task 3: Retail-faithful per-frame remote tick (subagent — biggest: RemoteMotion.Position field + OnLivePositionUpdated rewrite [airborne no-op + landing transition + grounded routing] + TickAnimations rewrite [PositionManager.ComputeOffset + UpdatePhysicsInternal]) - Task 4: USER GATE (visual verification with retail observer) - Task 5: Cleanup commit (subagent, parallel with 6) - Task 6: Roadmap + spec status update (parent, parallel with 5) Each task has TDD-style steps with exact file paths, code blocks, and commit messages. Spec at c4446e7 lists L.3.1's already-shipped 6 commits; this plan picks up from the revert at 1641d6e. Co-Authored-By: Claude Opus 4.7 --- .../2026-05-02-l3-positionmanager-jump.md | 785 ++++++++++++++++++ 1 file changed, 785 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md diff --git a/docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md b/docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md new file mode 100644 index 0000000..2091058 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md @@ -0,0 +1,785 @@ +# Phase L.3.1+L.3.2 Combined — PositionManager + Retail-Faithful Remote Tick + +> **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. + +**Goal:** Add the PositionManager combiner (animation root motion + InterpolationManager corrections) that was originally deferred to L.3.2, plumb `IsGrounded` through `EntityPositionUpdate`, and rewrite the per-frame remote tick + `OnLivePositionUpdated` env-var-on branches to match retail's `MoveOrTeleport` semantics. This eliminates the 1-Hz chop and endless-jump bugs surfaced during Task 7 visual verification. + +**Architecture:** Pure-data `PositionManager.ComputeOffset(dt, body.Position, seqVel, ori, interp, maxSpeed) → Vector3` returns the per-frame world-space delta to add to body.Position. Combines (a) animation root motion = `seqVel * dt` rotated by body orientation with (b) `InterpolationManager.AdjustOffset` correction. Per-frame tick always runs all steps (matches retail `UpdateObjectInternal`). `OnLivePositionUpdated` routes per `MoveOrTeleport`: airborne → no-op; landing transition → snap + clear flags; grounded → enqueue or slide-snap. Server is authoritative for airborne arcs (no local prediction fights gravity). + +**Tech Stack:** C# / .NET 10 / xUnit. 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`](../specs/2026-05-02-l3-remote-entity-motion-design.md) (committed `c4446e7`). + +**Already shipped (do NOT rebuild):** +- `f43f168` + `927636e` Task 1 — InterpolationManager +- `9c5634a` + `5b26d28` Task 2 — MotionInterpreter.GetMaxSpeed +- `517a3ce` Task 3 — RemoteMotion.Interp field +- `062e19f` Task 4 — OnLivePositionUpdated env-var routing v1 +- `ae79e34` Task 5 — Per-frame Interp.AdjustOffset v1 +- `e08accf` Task 6 — VectorUpdate.Omega +- `1641d6e` revert of band-aids +- `c4446e7` spec revision + +--- + +## File Structure + +| File | Action | Responsibility | +|---|---|---| +| `src/AcDream.Core/Physics/PositionManager.cs` | **CREATE** | Pure-function combiner: animation root motion + Interp correction. ~50 lines including XML docs. | +| `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` | **CREATE** | 6 unit tests against pure `ComputeOffset`. | +| `src/AcDream.Core.Net/Messages/UpdatePosition.cs` | **MODIFY** | Add `IsGrounded` to `Parsed` record, populate from `flags & PositionFlags.IsGrounded`. ~3 lines. | +| `src/AcDream.Core.Net/WorldSession.cs` | **MODIFY** | Add `IsGrounded` to `EntityPositionUpdate` record, pass through in PositionUpdated invoke. ~2 lines. | +| `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | (a) `RemoteMotion` gains `Position` field; (b) rewrite `OnLivePositionUpdated` env-var-on branch (airborne no-op + landing transition + grounded routing); (c) rewrite `TickAnimations` env-var-on branch (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). | +| (cleanup commit) `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | Delete env-var dual paths; delete `RemoteMotion` soft-snap residual fields. | +| `docs/plans/2026-04-11-roadmap.md` | **MODIFY** (cleanup phase) | Update Phase L.3 entry to reflect L.3.1+L.3.2 combined. | +| `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` | **MODIFY** (cleanup phase) | Mark L.3.1+L.3.2 as SHIPPED. | + +--- + +## Task Decomposition Overview + +``` + Task 1 — PositionManager class + 6 tests (subagent) + ↓ + Task 2 — Plumb IsGrounded through EntityPositionUpdate (parent, 2 files, ~5 lines) + ↓ + Task 3 — Retail-faithful per-frame remote tick (subagent — biggest change) + ↓ + Task 4 — USER GATE: visual verification with retail observer + ↓ (after sign-off) + ┌─ DISPATCH IN PARALLEL ──────────────────┐ + │ Task 5: Cleanup commit (subagent) │ + │ Task 6: Roadmap + spec status (parent) │ + └──────────────────────────────────────────┘ +``` + +--- + +## Task 1 — PositionManager class + 6 unit tests + +**Owner:** Sonnet subagent (general-purpose). + +**Files:** +- Create: `src/AcDream.Core/Physics/PositionManager.cs` +- Create: `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` + +**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet): + +> You are implementing Task 1 of Phase L.3.1+L.3.2 in the acdream codebase. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` section "L.3.2 architecture" → "New file — `src/AcDream.Core/Physics/PositionManager.cs`". +> +> **What to build:** +> +> Create `src/AcDream.Core/Physics/PositionManager.cs`: +> +> ```csharp +> using System.Numerics; +> +> namespace AcDream.Core.Physics; +> +> /// +> /// Per-frame combiner for remote-entity motion: animation root motion +> /// + InterpolationManager catch-up correction. Pure function — no +> /// side effects, no hidden state. +> /// +> /// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730): +> /// rootOffset = CPartArray::Update(dt) // animation +> /// PositionManager::adjust_offset(rootOffset) // adds correction +> /// frame.origin += rootOffset +> /// +> /// In acdream the animation root motion is sourced from +> /// AnimationSequencer.CurrentVelocity (body-local velocity from the +> /// active locomotion cycle). We rotate that by the body's orientation +> /// to get a world-space delta, then add the InterpolationManager's +> /// world-space correction. +> /// +> public sealed class PositionManager +> { +> /// +> /// Compute the per-frame world-space delta to add to body.Position. +> /// +> /// Per-frame delta time, seconds. +> /// Body's current world-space position. +> /// +> /// Body-local velocity from the active animation cycle +> /// (from AnimationSequencer.CurrentVelocity); pass +> /// Vector3.Zero if the entity has no sequencer or is on a +> /// non-locomotion cycle. +> /// +> /// Body orientation; used to rotate seqVel from body-local to world. +> /// The remote's InterpolationManager (for AdjustOffset call). +> /// From MotionInterpreter.GetMaxSpeed() — passed to AdjustOffset for the catch-up clamp. +> public Vector3 ComputeOffset( +> double dt, +> Vector3 currentBodyPosition, +> Vector3 seqVel, +> Quaternion ori, +> InterpolationManager interp, +> float maxSpeed) +> { +> // Step 1: animation root motion (body-local → world). +> Vector3 rootMotionLocal = seqVel * (float)dt; +> Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori); +> +> // Step 2: interpolation correction (world-space already). +> Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed); +> +> // Step 3: combined delta. +> return rootMotionWorld + correction; +> } +> } +> ``` +> +> Create `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` with EXACTLY these 6 test names (these are the contract): +> +> 1. `ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion` +> - seqVel = Vector3.Zero, no enqueued nodes in interp +> - Assert: returned offset == Vector3.Zero +> +> 2. `ComputeOffset_AnimationOnly_Forward_BodyAdvances` +> - seqVel = (0, 4, 0) (4 m/s forward), ori = Quaternion.Identity, dt = 0.1 +> - Assert: returned offset == (0, 0.4, 0) (forward 0.4m) +> +> 3. `ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth` +> - seqVel = (0, 4, 0), ori = quaternion rotating +Y → -Y (180° around Z), dt = 0.1 +> - Assert: returned offset.Y ≈ -0.4 (south) +> +> 4. `ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue` +> - seqVel = Vector3.Zero, interp has 1 enqueued node 1m ahead, dt = 0.1, maxSpeed = 4f +> - Expected: AdjustOffset returns the catch-up step (≤ 1m, clamped); ComputeOffset returns same +> +> 5. `ComputeOffset_BothActive_Combined` +> - seqVel = (0, 4, 0) — root motion (0, 0.4, 0) +> - interp has node 1m ahead — AdjustOffset returns ~Vector3.UnitY * step +> - Assert: returned offset == rootMotion + correction +> +> 6. `ComputeOffset_LocalToWorldRotation_Yaw90` +> - seqVel = (0, 1, 0) (forward 1 m/s in body frame) +> - ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f) (yaw +90°) +> - dt = 1 +> - Verify the rotation is applied correctly. With yaw +90° around Z, body-local +Y rotates to world... compute the expected and assert with precision: 4. +> +> Use xUnit, `namespace AcDream.Core.Tests.Physics;`, file-private fakes via `file sealed class` if needed. Read `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` for the existing pattern. +> +> Note: Tests #4 and #5 need a real `InterpolationManager` (not a fake) because PositionManager calls AdjustOffset directly. Construct one inline in each test, Enqueue what you need, and call ComputeOffset. +> +> **Build + test:** +> +> ```bash +> 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~PositionManager" +> ``` +> +> Both green. 6 tests pass. +> +> **Commit:** +> +> ```bash +> git add src/AcDream.Core/Physics/PositionManager.cs tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs +> git commit -m "$(cat <<'EOF' +> feat(physics): PositionManager combiner class + 6 unit tests (L.3.2) +> +> Pure-function ComputeOffset(dt, pos, seqVel, ori, interp, maxSpeed) → +> Vector3. Combines animation root motion (seqVel × dt rotated by body +> orientation) with InterpolationManager.AdjustOffset world-space +> correction. Mirrors retail CPhysicsObj::UpdateObjectInternal +> (acclient @ 0x00513730). +> +> Composed into RemoteMotion in subsequent task (L.3.1+L.3.2 Task 3); +> not yet consumed. +> +> Co-Authored-By: Claude Opus 4.7 +> EOF +> )" +> ``` +> +> **Self-review checklist:** +> - [ ] `PositionManager` is public sealed class +> - [ ] `ComputeOffset` is the only public method (no other API) +> - [ ] All 6 tests have the exact names listed +> - [ ] Tests #4 and #5 use a real `InterpolationManager` +> - [ ] No game/window/sequencer dependencies — only `System.Numerics` + `AcDream.Core.Physics.InterpolationManager` +> - [ ] Build clean, all 6 tests pass +> - [ ] Commit references "L.3.2" +> +> **Report:** +> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT +> - What you built (1-2 sentences) +> - Test results (count, any deviations) +> - Files changed +> - Concerns (if any) + +**Steps for the parent (controller):** + +- [ ] **Step 1.1: Dispatch the implementer subagent** using the prompt above. +- [ ] **Step 1.2: Verify the commit landed** + ```bash + cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.Core/Physics/PositionManager.cs + ``` + Expected: commit message starts with `feat(physics): PositionManager combiner class`. +- [ ] **Step 1.3: Re-run tests in parent** + ```bash + cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager" + ``` + Expected: 6 tests pass. +- [ ] **Step 1.4: Dispatch spec compliance reviewer** (use `general-purpose`, Sonnet). Verify the 6 tests have the EXACT names listed and verify `ComputeOffset` algorithm matches the spec's pseudocode. +- [ ] **Step 1.5: Dispatch code quality reviewer** (use `superpowers:code-reviewer`). Check for: API surface (only ComputeOffset public), test quality, no superfluous deps. +- [ ] **Step 1.6: Address review issues if any.** If issues found, dispatch fix subagent. Re-review. + +--- + +## Task 2 — Plumb `IsGrounded` through `EntityPositionUpdate` + +**Owner:** Parent. Mechanical edit, ~5 lines across 2 files. + +**Files:** +- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:62-69` (add `IsGrounded` to `Parsed` record) +- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:166` (populate `IsGrounded` in the constructor call) +- Modify: `src/AcDream.Core.Net/WorldSession.cs:110-113` (add `IsGrounded` to `EntityPositionUpdate` record) +- Modify: `src/AcDream.Core.Net/WorldSession.cs:711-714` (pass `posUpdate.Value.IsGrounded` through) + +**Steps:** + +- [ ] **Step 2.1: Read existing `UpdatePosition.Parsed` record + TryParse return** + ```bash + grep -n "public readonly record struct Parsed\|return new Parsed" "C:/Users/erikn/source/repos/acdream/src/AcDream.Core.Net/Messages/UpdatePosition.cs" + ``` + +- [ ] **Step 2.2: Add `IsGrounded` field to `UpdatePosition.Parsed`** + + Edit `src/AcDream.Core.Net/Messages/UpdatePosition.cs` (~line 62): + + Change: + ```csharp + public readonly record struct Parsed( + uint Guid, + CreateObject.ServerPosition Position, + System.Numerics.Vector3? Velocity, + uint? PlacementId, + ushort InstanceSequence = 0, + ushort TeleportSequence = 0, + ushort ForcePositionSequence = 0); + ``` + To: + ```csharp + public readonly record struct Parsed( + uint Guid, + CreateObject.ServerPosition Position, + System.Numerics.Vector3? Velocity, + uint? PlacementId, + bool IsGrounded, + ushort InstanceSequence = 0, + ushort TeleportSequence = 0, + ushort ForcePositionSequence = 0); + ``` + +- [ ] **Step 2.3: Populate `IsGrounded` in the `Parsed` constructor call (~line 166)** + + Find the line `return new Parsed(guid, serverPos, velocity, placementId,` (~line 166) and change to pass `(flags & PositionFlags.IsGrounded) != 0` as the new IsGrounded argument. Looks roughly like: + + ```csharp + return new Parsed(guid, serverPos, velocity, placementId, + (flags & PositionFlags.IsGrounded) != 0, + instSeq, teleSeq, forceSeq); + ``` + + (Verify the trailing-arg layout against what's actually there; preserve any existing trailing arguments.) + +- [ ] **Step 2.4: Add `IsGrounded` field to `WorldSession.EntityPositionUpdate`** + + Edit `src/AcDream.Core.Net/WorldSession.cs:110`: + + Change: + ```csharp + public readonly record struct EntityPositionUpdate( + uint Guid, + CreateObject.ServerPosition Position, + System.Numerics.Vector3? Velocity); + ``` + To: + ```csharp + public readonly record struct EntityPositionUpdate( + uint Guid, + CreateObject.ServerPosition Position, + System.Numerics.Vector3? Velocity, + bool IsGrounded); + ``` + +- [ ] **Step 2.5: Pass `IsGrounded` through in PositionUpdated invoke (~line 711)** + + Change: + ```csharp + PositionUpdated?.Invoke(new EntityPositionUpdate( + posUpdate.Value.Guid, + posUpdate.Value.Position, + posUpdate.Value.Velocity)); + ``` + To: + ```csharp + PositionUpdated?.Invoke(new EntityPositionUpdate( + posUpdate.Value.Guid, + posUpdate.Value.Position, + posUpdate.Value.Velocity, + posUpdate.Value.IsGrounded)); + ``` + +- [ ] **Step 2.6: Build + test** + ```bash + cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo + dotnet test --no-build --nologo 2>&1 | tail -6 + ``` + Expected: 0 build errors. Same 4 pre-existing test failures, no new failures. + +- [ ] **Step 2.7: Commit** + ```bash + git add src/AcDream.Core.Net/Messages/UpdatePosition.cs src/AcDream.Core.Net/WorldSession.cs + git commit -m "$(cat <<'EOF' + feat(net): plumb IsGrounded through EntityPositionUpdate (L.3.2) + + PositionFlags.IsGrounded (0x04) was already parsed by UpdatePosition + but not exposed through Parsed record or EntityPositionUpdate. + Adds the bool field to both records so OnLivePositionUpdated can + consume it for retail-faithful MoveOrTeleport routing + (acclient @ 0x00516330: has_contact=false → no-op during airborne arc). + + Consumed in subsequent task (L.3.1+L.3.2 Task 3). + + Co-Authored-By: Claude Opus 4.7 + EOF + )" + ``` + +--- + +## Task 3 — Retail-faithful per-frame remote tick + +**Owner:** Sonnet subagent (general-purpose). Largest task — touches 3 distinct sites in `GameWindow.cs`. + +**Files:** +- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (RemoteMotion class line ~224 + OnLivePositionUpdated env-var branch + TickAnimations env-var branch) + +**Subagent dispatch prompt:** + +> You are implementing Task 3 of Phase L.3.1+L.3.2 in the acdream codebase. This task rewrites two env-var-gated branches in `src/AcDream.App/Rendering/GameWindow.cs` to consume the new PositionManager (Task 1) and IsGrounded plumbing (Task 2). +> +> **Repo:** `C:/Users/erikn/source/repos/acdream` — main branch — direct-to-main per CLAUDE.md. +> +> **Spec:** `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` "L.3.2 architecture" sections. +> +> **Three changes in `GameWindow.cs`:** +> +> ### Change 1: `RemoteMotion` class gains `Position` field +> +> Find the existing `Interp` field (added in commit `517a3ce`). Right after it, add: +> +> ```csharp +> /// +> /// Per-frame combiner for animation root motion + InterpolationManager +> /// correction (Phase L.3.2). Consumed in TickAnimations to compute the +> /// per-frame body.Position delta. +> /// +> public AcDream.Core.Physics.PositionManager Position { get; } = +> new AcDream.Core.Physics.PositionManager(); +> ``` +> +> ### Change 2: Rewrite `OnLivePositionUpdated` env-var-on branch +> +> Find the existing env-var-on block in `OnLivePositionUpdated` (was added at commit `062e19f`). It currently looks roughly like: +> ```csharp +> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") +> { +> rmState.Body.Orientation = rot; +> // teleport check, dist check, etc. +> return; +> } +> ``` +> +> Replace the env-var-on body with this new logic: +> +> ```csharp +> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") +> { +> // Orientation always snaps on receipt — the InterpolationManager +> // walks position only; heading would otherwise lag the queue. +> rmState.Body.Orientation = rot; +> +> // ── AIRBORNE NO-OP ──────────────────────────────────────────── +> // Mirrors retail CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330): +> // when has_contact==0, return false (don't touch body, don't queue). +> // body.Velocity (set once by OnLiveVectorUpdated at jump start) keeps +> // integrating gravity via per-frame UpdatePhysicsInternal. Server is +> // authoritative for the arc; we don't predict it locally. +> if (!update.IsGrounded) +> return; +> +> // ── LANDING TRANSITION ───────────────────────────────────────── +> // First IsGrounded=true UP after rmState.Airborne signals landed. +> // Clear airborne flags, hard-snap to authoritative landing position, +> // clear interpolation queue (any pre-jump waypoints are stale). +> if (rmState.Airborne) +> { +> rmState.Airborne = false; +> rmState.Body.Velocity = System.Numerics.Vector3.Zero; +> rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity; +> rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact +> | AcDream.Core.Physics.TransientStateFlags.OnWalkable; +> rmState.Interp.Clear(); +> rmState.Body.Position = worldPos; +> return; +> } +> +> // ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ──────────── +> const float MaxPhysicsDistance = 96f; +> var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero; +> float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos); +> +> if (dist > MaxPhysicsDistance) +> { +> // Beyond view bubble: SetPositionSimple slide-snap. Clear queue. +> rmState.Interp.Clear(); +> rmState.Body.Position = worldPos; +> } +> else +> { +> // Within view bubble: enqueue waypoint for adjust_offset to walk to. +> // PositionManager (called per-frame in TickAnimations) handles the +> // actual body advancement — mix of animation root motion + queue +> // correction. +> float headingFromQuat = ExtractYawFromQuaternion(rot); +> rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false); +> } +> return; +> } +> ``` +> +> The legacy `else` branch (env-var unset) STAYS UNCHANGED. +> +> If `ExtractYawFromQuaternion` doesn't exist anymore (it might have been removed in the revert), re-add it near the original location (search for it in commit `062e19f`'s diff). The body is: +> ```csharp +> private static float ExtractYawFromQuaternion(System.Numerics.Quaternion q) +> { +> // Standard z-up yaw extraction: atan2(2(wz + xy), 1 - 2(y² + z²)) +> return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y), +> 1f - 2f * (q.Y * q.Y + q.Z * q.Z)); +> } +> ``` +> +> ### Change 3: Rewrite `TickAnimations` env-var-on branch +> +> Find the existing env-var-on block in the per-frame remote tick (added at commit `ae79e34`). It currently looks roughly like: +> ```csharp +> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") +> { +> if (rm.Interp.IsActive) { +> float maxSpeed = rm.Motion.GetMaxSpeed(); +> Vector3 delta = rm.Interp.AdjustOffset((double)dt, rm.Body.Position, maxSpeed); +> rm.Body.Position += delta; +> } +> rm.Body.UpdatePhysicsInternal(dt); +> // entity write-back +> } +> ``` +> +> Replace with PositionManager call: +> +> ```csharp +> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") +> { +> // Always-run-all-steps per retail CPhysicsObj::UpdateObjectInternal +> // (acclient @ 0x00513730): +> // 1+2. animation root motion + interpolation correction (combined) +> // 3. physics integration (gravity for airborne; no-op for grounded) +> System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity +> ?? System.Numerics.Vector3.Zero; +> float maxSpeed = rm.Motion.GetMaxSpeed(); +> System.Numerics.Vector3 offset = rm.Position.ComputeOffset( +> dt: (double)dt, +> currentBodyPosition: rm.Body.Position, +> seqVel: seqVel, +> ori: rm.Body.Orientation, +> interp: rm.Interp, +> maxSpeed: maxSpeed); +> rm.Body.Position += offset; +> rm.Body.UpdatePhysicsInternal(dt); +> // KEEP whatever entity write-back lines were here (ae.Entity.Position = ..., etc.) +> } +> else +> { +> // EXISTING legacy path UNCHANGED +> } +> ``` +> +> The `else` branch (legacy path) stays UNCHANGED. +> +> **Build + test:** +> +> ```bash +> cd C:/Users/erikn/source/repos/acdream +> dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo +> dotnet test --no-build --nologo 2>&1 | tail -6 +> ``` +> +> Expected: 0 build errors. Same 4 pre-existing failures (`DispatcherToMovementIntegrationTests` + `BSPStepUpTests` — these are not related to L.3 work). No NEW failures. +> +> **Commit:** +> +> ```bash +> git add src/AcDream.App/Rendering/GameWindow.cs +> git commit -m "$(cat <<'EOF' +> feat(motion): retail-faithful per-frame remote tick (L.3.1+L.3.2) +> +> Combines PositionManager (Task 1) + IsGrounded plumbing (Task 2) into +> the per-frame remote motion path. Three changes in GameWindow.cs, +> all gated behind ACDREAM_INTERP_MANAGER=1: +> +> 1. RemoteMotion gains Position field (PositionManager instance). +> +> 2. OnLivePositionUpdated env-var branch rewritten to mirror retail +> CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330): +> - orientation snap-on-receipt (PositionManager handles position only) +> - airborne (!IsGrounded) → no-op (server is authoritative for arc; +> body.Velocity from VectorUpdate integrates gravity locally) +> - landing transition (first IsGrounded=true after Airborne) → +> clear airborne flags, hard-snap to landing pos, clear queue +> - grounded routing: dist > 96m → slide-snap; dist ≤ 96m → enqueue +> +> 3. TickAnimations env-var branch rewritten to use PositionManager: +> body.Position += PositionManager.ComputeOffset(dt, pos, seqVel, +> ori, interp, maxSpeed); body.UpdatePhysicsInternal(dt) for gravity. +> +> Replaces the L.3.1-only AdjustOffset-only path. Legacy (env-var off) +> path unchanged. +> +> Cleanup commit (next sub-task) deletes the env-var dual paths after +> visual verification. +> +> Co-Authored-By: Claude Opus 4.7 +> EOF +> )" +> ``` +> +> **Self-review checklist:** +> - [ ] `RemoteMotion.Position` field added (alongside existing `Interp`) +> - [ ] `OnLivePositionUpdated` env-var branch has 3 sub-branches: airborne return, landing transition, grounded routing (snap or enqueue) +> - [ ] `OnLivePositionUpdated` legacy `else` branch UNCHANGED +> - [ ] `TickAnimations` env-var branch uses `PositionManager.ComputeOffset` exclusively (no direct `AdjustOffset` call) +> - [ ] `TickAnimations` legacy `else` branch UNCHANGED +> - [ ] `ExtractYawFromQuaternion` helper present (re-add if missing) +> - [ ] `OnLiveVectorUpdated` UNTOUCHED (it already does the right thing) +> - [ ] Build clean, same 4 pre-existing failures +> +> **Report:** +> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT +> - Lines changed (with file:line refs) +> - Test count +> - Concerns (if any) +> +> If the existing legacy `else` path is so tangled that you can't safely rewrite the env-var branch without disturbing it, REPORT BLOCKED with specifics. + +**Steps for the parent:** + +- [ ] **Step 3.1: Dispatch the implementer subagent** using the prompt above. +- [ ] **Step 3.2: Verify the commit landed** + ```bash + cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.App/Rendering/GameWindow.cs + ``` +- [ ] **Step 3.3: Build + test in parent** + ```bash + cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo 2>&1 | tail -6 + ``` + Expected: 0 build errors. Same 4 pre-existing failures. +- [ ] **Step 3.4: Spec compliance review** (general-purpose subagent). Verify the rewrite matches the spec's pseudocode exactly. Verify legacy `else` paths are byte-for-byte unchanged. +- [ ] **Step 3.5: Code quality review** (`superpowers:code-reviewer`). Specifically check: orientation snap is in ALL routing paths; airborne no-op is the FIRST gate; landing transition resets all the right flags; ExtractYawFromQuaternion is correct. +- [ ] **Step 3.6: Address review issues if any.** Fix subagent + re-review. + +--- + +## Task 4 — USER GATE: visual verification + +**Owner:** User. Cannot be automated. + +**Steps:** + +- [ ] **Step 4.1: Kill any running acdream** + ```powershell + Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force + Start-Sleep -Seconds 8 + ``` + +- [ ] **Step 4.2: Launch acdream with `ACDREAM_INTERP_MANAGER=1`** + ```powershell + $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 4.3: Visual test matrix** with parallel retail observer of `+Acdream`. On the retail side, walk + run + jump + turn the toon and verify: + + | Scenario | Expected | + |---|---| + | Walk forward 5 sec | acdream observer sees smooth glide, NO 1-Hz popping | + | Walk backward 5 sec | smooth glide backward (regression check vs commit `17a9ff1`) | + | Strafe left/right 5 sec each | smooth glide sideways | + | Stop, then run forward 5 sec | smooth glide at run speed | + | Jump from standstill 2-3× | curved arc, lands cleanly, NO endless rise | + | Jump while running 2-3× | arc preserves forward motion, lands cleanly | + | Turn quickly while running | heading tracks smoothly (not stuck at login direction) | + +- [ ] **Step 4.4: User signs off OR files a regression** + - If smooth + jumps land + turning works → proceed to Tasks 5+6. + - If anything regresses → describe the symptom; parent dispatches a fix subagent or unsets the env-var for instant rollback. + +--- + +## Task 5 — Cleanup commit (parallel with Task 6) + +**Owner:** Sonnet subagent (general-purpose). Independent of Task 6. + +**Files:** +- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (delete env-var dual paths + soft-snap fields) + +**Subagent dispatch prompt:** + +> You are implementing Task 5 of Phase L.3.1+L.3.2: cleanup. The user has visually verified that `ACDREAM_INTERP_MANAGER=1` works correctly. Now collapse the dual-path scaffolding. +> +> **Repo:** `C:/Users/erikn/source/repos/acdream` — main — direct-to-main per CLAUDE.md. +> +> **What to do in `src/AcDream.App/Rendering/GameWindow.cs`:** +> +> 1. **In `OnLivePositionUpdated`**: delete the `if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") { ... return; }` wrapper. Keep ONLY the new logic inside it. Delete the legacy hard-snap path that came after. +> +> 2. **In `TickAnimations` (per-frame remote tick)**: delete the `if/else` env-var gate. Keep ONLY the new path (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). 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`. Also delete any related code in the call sites. +> +> 4. **Search for any remaining `ACDREAM_INTERP_MANAGER` references** in the codebase and confirm zero remain: +> ```bash +> grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1 +> ``` +> Expected: no output. +> +> **Build + test:** +> ```bash +> cd C:/Users/erikn/source/repos/acdream +> dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo +> dotnet test --no-build --nologo 2>&1 | tail -6 +> ``` +> Expected: 0 build errors. Same 4 pre-existing failures, no new ones. +> +> **Commit:** +> ```bash +> git add src/AcDream.App/Rendering/GameWindow.cs +> git commit -m "$(cat <<'EOF' +> chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead legacy paths (L.3.1+L.3.2 cleanup) +> +> User has visually verified the new PositionManager + IsGrounded +> routing path works correctly. Collapses the env-var dual-path: +> deletes legacy hard-snap + apply_current_movement + Euler-extrapolate +> code from OnLivePositionUpdated and the per-frame remote tick. +> Deletes SnapResidualDecayRate + soft-snap residual fields from +> RemoteMotion. +> +> Single retail-faithful path remains. ~80 lines net deletion. +> +> Co-Authored-By: Claude Opus 4.7 +> EOF +> )" +> ``` +> +> **Report:** +> - Status, line counts deleted, files touched, test results. + +**Steps for the parent:** + +- [ ] **Step 5.1: Dispatch the cleanup subagent in parallel with Task 6** (one message, two Agent tool calls). +- [ ] **Step 5.2: Verify the commit landed** + ```bash + cd C:/Users/erikn/source/repos/acdream && git log -1 --stat + ``` +- [ ] **Step 5.3: Confirm zero env-var references remain** + ```bash + grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1 + ``` + Expected: no output. +- [ ] **Step 5.4: Re-run all tests in parent** + ```bash + cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo 2>&1 | tail -6 + ``` + Expected: same baseline. + +--- + +## Task 6 — Roadmap + spec status update (parallel with Task 5) + +**Owner:** Parent. Mechanical doc updates. + +**Files:** +- Modify: `docs/plans/2026-04-11-roadmap.md` (Phase L.3 entry — mark L.3.1+L.3.2 SHIPPED) +- Modify: `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` (add SHIPPED status banner) + +**Steps:** + +- [ ] **Step 6.1: Find the Phase L.3 entry in the roadmap** + ```bash + grep -n "Phase L.3\|L.3.1\|L.3.2\|L.3.3" "C:/Users/erikn/source/repos/acdream/docs/plans/2026-04-11-roadmap.md" + ``` + If the roadmap doesn't yet have a Phase L.3 entry, add one between L.2 and M with the L.3.1+L.3.2 combined status = SHIPPED, L.3.3 status = PLANNED. + +- [ ] **Step 6.2: Update the spec doc's status** + + In `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`, near the top (after the title / methodology), add or update a status line: + + ```markdown + **Status:** L.3.1+L.3.2 SHIPPED 2026-05-02. L.3.3 PLANNED. + ``` + +- [ ] **Step 6.3: Commit (combined doc update)** + ```bash + git add docs/plans/2026-04-11-roadmap.md docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md + git commit -m "$(cat <<'EOF' + docs(roadmap+spec): Phase L.3.1+L.3.2 shipped (L.3.3 pending) + + Roadmap Phase L.3 entry updated. Spec status banner reflects the + combined L.3.1+L.3.2 deliverable as shipped after visual verification. + L.3.3 (MoveToManager) remains a separate sub-lane to be specced and + scheduled. + + Co-Authored-By: Claude Opus 4.7 + EOF + )" + ``` + +--- + +## Verification Plan + +End-to-end smoke test after Task 6: + +```bash +cd C:/Users/erikn/source/repos/acdream +dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo # green +dotnet test --no-build --nologo # 4 pre-existing failures only +git log --oneline -10 # see commits in order +grep -rn "ACDREAM_INTERP_MANAGER" src/ # zero hits (cleanup confirmed) +grep -rn "SnapResidualDecayRate" src/ # zero hits (deleted) +``` + +User can re-run the visual test matrix WITHOUT setting `ACDREAM_INTERP_MANAGER` (default behavior is now the new path) and confirm parity. + +If everything's green → Phase L.3.1+L.3.2 done; brainstorm L.3.3 (MoveToManager) as the next sub-lane. + +--- + +## Self-Review Notes + +- **Spec coverage:** every section of the spec maps to a task here. PositionManager → Task 1; IsGrounded plumbing → Task 2; per-frame tick rewrite + RemoteMotion field + OnLivePositionUpdated rewrite → Task 3; cleanup → Task 5; doc updates → Task 6. +- **Already-shipped commits NOT rebuilt.** L.3.1's first 6 commits (f43f168 → e08accf) already provide InterpolationManager + GetMaxSpeed + Interp field + v1 routing + v1 tick + Omega. +- **Reverted commits** (5154a3e + f199a6a) were band-aids; their replacements live in Task 3. +- **Subagent failure handling:** if a subagent reports BLOCKED on Task 3 (the largest), break it into smaller pieces (3a: RemoteMotion field; 3b: OnLivePositionUpdated rewrite; 3c: TickAnimations rewrite) and dispatch sequentially. Don't let a confused subagent leave broken code in main. +- **Task 4's visual verification is the gate.** Tasks 5+6 only fire after user sign-off. If visual fails, dispatch a fix subagent before Tasks 5+6.