acdream/docs/superpowers/plans/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk.md
Erik d640ed74e1 feat(retail): Phase B.6 — server-driven auto-walk done right
Closes #63, #69, #74, #75. Replaces the chain of Commit-B workarounds
that compensated for ACE's MoveToChain getting cancelled by a leaked
user-MoveToState packet during inbound auto-walk. The fix is
architectural — auto-walk drives the body directly from the
server-supplied path data, no player-input synthesis, no spurious
wire-packet transitions, no grace-period band-aid.

Architectural change (closes #75):
  PlayerMovementController.ApplyAutoWalkOverlay → DriveServerAutoWalk.
  - Steps Yaw toward target at retail-faithful turn rates.
  - Computes desired forward velocity from path runRate.
  - Calls _motion.DoMotion(WalkForward, speed) directly for the
    motion-interpreter state (drives animation cycle).
  - Sets _body.set_local_velocity directly when grounded.
  - Returns true to gate the user-input motion + velocity section
    in Update so user-input flow doesn't overwrite auto-walk
    velocity or motion state.
  Mirrors retail's MovementManager::PerformMovement case 6 (decomp
  0x00524440) which never touches the user-input pipeline during
  server-controlled auto-walk.

Wire-layer guard at GameWindow.cs:6419 retained as a SEMANTIC
statement (`if (result.MotionStateChanged && !IsServerAutoWalking)`):
user-MoveToState packets are for user-driven motion intent. During
server-controlled auto-walk, the motion-state transitions caused by
the animation override (RunForward / WalkForward / TurnLeft /
TurnRight cycles) must not leak as user-cancellation packets. This
is NOT the deleted 500ms grace-period band-aid; it's the wire-layer
expressing the user-vs-server motion split.

Animation plumbed for auto-walk phases (closes #69):
  - Moving forward → WalkForward (speed=1.0) / RunForward (speed=runRate)
  - Turn-first phase → TurnLeft / TurnRight (sign of yawStep)
  - Aligned-but-pre-step / arrival → no override (idle)
Driven via _autoWalkMovingForwardThisFrame + _autoWalkTurnDirectionThisFrame
fields set in DriveServerAutoWalk and read in the MovementResult
construction at the bottom of Update. UpdatePlayerAnimation picks up
the localAnimCmd as the highest-priority animation source.

Walk/run threshold = 1.0m, retail-observed. ACE's wire-default of
15.0f is too generous; ACE's own physics layer uses 1.0f at
MovementParameters.cs:50 (with the 15.0f line commented out) and
Creature.cs:312 notes "default 15 distance seems too far". The
formula matches retail's MovementParameters::get_command at decomp
0x0052aa00: running = (initialDist - distance_to_object) >=
threshold, evaluated ONCE at chain start and held for the rest of
the auto-walk (matches retail "runs all the way / walks all the way"
behaviour). Wire-supplied threshold is ignored.

Pickup gate (IsPickupableTarget) now uses BF_STUCK
(acclient.h:6435, bit 0x4) to discriminate immovable scenery from
real pickup items that share a Misc ItemType. Sign (pwd=0x14 with
BF_STUCK) → blocked; spell component (pwd=0x10, no BF_STUCK) →
allowed. ACE's PutItemInContainer (Player_Inventory.cs:831-836)
responds with WeenieError.Stuck (0x29) on stuck items so the gate
prevents wasted wire packets + a UX dead-end.

R-key dispatch by target type. UseCurrentSelection's top-level
IsUseableTarget gate was wrong (blocked USEABLE_NO=1 items that
ARE pickupable). Reordered:
  1. Creature → SendUse
  2. Pickupable → SendPickUp
  3. Useable → SendUse
  4. Otherwise → "cannot be used" toast
Each handler keeps its own gate. Matches retail's per-action
server-side validation.

AP cadence revert (closes #74). With the MoveToChain race fixed,
the per-frame "send while moving" cadence is no longer load-bearing.
Reverted to retail's two-branch ShouldSendPositionEvent gate
(acclient_2013_pseudo_c.txt:700233-700285):
  Interval NOT elapsed (< 1 sec): send if cell or contact-plane changed.
  Interval elapsed (>= 1 sec):    send if cell or position frame changed.
Adds _lastSentContactPlane field + ApproxPlaneEqual helper +
PlayerMovementController.ContactPlane public accessor. Extended
NotePositionSent(Vector3, uint, Plane, float) — both outbound sites
(MoveToState + AP) pass _playerController.ContactPlane.
Effective rates: 0 Hz idle, ~1 Hz smooth motion, per-event on
cell/plane changes, 0 Hz airborne.

CLAUDE.md updated with no-workarounds rule (commit `da126f9` on
the worktree branch). Saved as feedback memory at
memory/feedback_no_workarounds.md.

Tests: build green; Core.Net 294/294; Core 1073/1081 (baseline,
8 pre-existing Physics failures unchanged). Visual-verified
end-to-end on 2026-05-16 for far/near Use + PickUp on NPCs,
doors, items, spell components, signs (correctly blocked), corpses,
turn-first animation, run/walk thresholds, idle quiet, smooth-
motion 1Hz.

Spec: docs/superpowers/specs/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk-design.md
Plan: docs/superpowers/plans/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:14:44 +02:00

631 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase B.6 — Suppress MoveToState during inbound auto-walk 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.
**Goal:** Stop sending outbound `MoveToState` while ACE's server-initiated auto-walk is driving the player, then retire the Commit B workarounds that compensated for the resulting `MoveToChain` cancellation. ACE's `TryUseItem` callback fires on arrival; client sends Use exactly once.
**Architecture:** One-line guard against the misleading wire packet, two retry-assignment deletions, one revert of the AP-cadence gate to retail's narrow shape. No new files, no new tests (behavioral change is wire-level integration; covered by existing Core.Net suite + user visual verify).
**Tech Stack:** C# .NET 10. Edits touch `AcDream.App/Rendering/GameWindow.cs`, `AcDream.App/Input/PlayerMovementController.cs`, `docs/ISSUES.md`.
**Spec:** [docs/superpowers/specs/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk-design.md](../specs/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk-design.md).
**Retail anchors:**
- `Player_Use.cs:205` (ACE) — `CreateMoveToChain(item, (success) => TryUseItem(item, success))`.
- `Player_Move.cs:150` (ACE) — chain polls and fires `callback(true)` when within use radius.
- `acclient_2013_pseudo_c.txt:700233-700285` — retail `ShouldSendPositionEvent`: narrow gate (cell-or-plane change during sub-interval; frame change after interval; gated on `Contact && OnWalkable`).
- `acclient_2013_pseudo_c.txt:700327` — retail `SendPositionEvent`: `(state & 1) != 0 && (state & 2) != 0`.
---
## File structure
| File | Responsibility | Touched in tasks |
|---|---|---|
| `src/AcDream.App/Rendering/GameWindow.cs` | Outbound wire layer. Guard MoveToState build (Task 1); delete two retry assignments + log strings (Task 1); update NotePositionSent call sites to pass contact plane (Task 3). | 1, 3 |
| `src/AcDream.App/Input/PlayerMovementController.cs` | Diff-driven cadence state + the auto-walk overlay. Add `_lastSentContactPlane` field; extend `NotePositionSent` signature; replace per-frame `positionChanged` gate with retail's narrow gate (Task 3). | 3 |
| `docs/ISSUES.md` | Close `#63` and `#74` to "Recently closed" (Task 5). | 5 |
---
## Task 1: Suppress outbound MoveToState during server auto-walk + delete the retry workarounds
**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` line ~6410 (the MoveToState send block).
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` line ~9203 (SendUse far-range path).
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` line ~9265 (SendPickUp far-range path).
### Step 1: Guard the outbound MoveToState build
Find the block at `GameWindow.cs:6410` that reads:
```csharp
if (result.MotionStateChanged)
{
// HoldKey axis values — retail enum (holtburger types.rs HoldKey):
```
Change the condition to:
```csharp
// 2026-05-16 (Phase B.6): suppress outbound MoveToState while
// ACE's server-initiated auto-walk is driving the player.
// Synthesized Forward+Run input in ApplyAutoWalkOverlay leaks
// to MotionStateChanged=true; sending the resulting "user is
// RunForward" wire packet makes ACE cancel its own MoveToChain
// (Player_Move.cs:150 callback never fires). Retail and
// holtburger walk the body locally during inbound MoveToObject
// WITHOUT sending an outbound MoveToState — AutonomousPosition
// alone is enough for ACE's WithinUseRadius poll.
if (result.MotionStateChanged && !_playerController.IsServerAutoWalking)
{
// HoldKey axis values — retail enum (holtburger types.rs HoldKey):
```
(Only the `if` line changes; the comment above is new. Leave the rest of the block untouched.)
- [ ] **Step 2: Delete the SendUse far-range retry assignment**
Find the SendUse method's far-range block. Search:
```
grep -n "Far range:" src/AcDream.App/Rendering/GameWindow.cs
```
Expected: line ~9197. The block reads (paraphrased — find the exact text in the file):
```csharp
// Far range: send Use immediately so ACE has the request,
// AND queue an arrival re-send. Issue #63 (server-initiated
// MoveToObject not honored) means ACE's first-Use response
// is dropped as too-far and ACE doesn't re-poll
// WithinUseRadius when the speculative local walk gets us in
// range. The arrival re-send fires a second Use packet once
// the body reaches the target — at which point ACE accepts
// and executes the action. The retail-faithful path is to
// honor MoveToObject and let ACE complete the Use server-
// side; until #63 lands, this client-side retry is the
// workaround that keeps far-range Use working.
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
_liveSession.SendGameAction(body);
_pendingPostArrivalAction = (guid, false);
Console.WriteLine($"[B.4b] use guid=0x{guid:X8} seq={seq} (queued for arrival re-send pending #63)");
```
Replace with:
```csharp
// Far range: send Use ONCE. ACE's CreateMoveToChain
// (Player_Use.cs:205) holds a callback (TryUseItem) and fires
// it server-side when WithinUseRadius passes during the
// MoveToChain poll (Player_Move.cs:150). No client-side retry
// needed — the Phase B.6 MoveToState-suppression fix
// (GameWindow.cs:6410) keeps ACE's chain alive during the
// walk.
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
_liveSession.SendGameAction(body);
Console.WriteLine($"[B.4b] use guid=0x{guid:X8} seq={seq}");
```
(Removes the `_pendingPostArrivalAction = (guid, false);` line and trims the log to drop the `(queued for arrival re-send pending #63)` suffix.)
- [ ] **Step 3: Delete the SendPickUp far-range retry assignment**
Find the SendPickUp method's far-range block. Search:
```
grep -n "Far range: same arrival-retry pattern" src/AcDream.App/Rendering/GameWindow.cs
```
Expected: line ~9255. Replace the block:
```csharp
// Far range: same arrival-retry pattern as SendUse — fire
// PickUp immediately AND queue for arrival re-send. ACE's
// first PickUp is dropped if we're outside the use-radius
// and isn't re-polled (issue #63 workaround).
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
seq, itemGuid, _playerServerGuid, placement: 0);
_liveSession.SendGameAction(body);
_pendingPostArrivalAction = (itemGuid, true);
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq} (queued for arrival re-send pending #63)");
```
With:
```csharp
// Far range: send PickUp ONCE. Same auto-fire-via-MoveToChain
// callback pattern as SendUse — ACE's chain fires
// PutItemInContainer/Move server-side when in range. No
// client-side retry; Phase B.6 MoveToState suppression keeps
// ACE's chain alive.
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
seq, itemGuid, _playerServerGuid, placement: 0);
_liveSession.SendGameAction(body);
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
```
- [ ] **Step 4: Build**
```
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug
```
Expected: 0 errors, 0 warnings. The deletions remove the only assignment of `_pendingPostArrivalAction` for far-range paths; the close-range path still assigns it (line ~9191 and ~9258). The field declaration at line ~799 stays.
- [ ] **Step 5: Run existing tests**
```
dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj -c Debug --nologo
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --nologo
```
Expected: Core.Net 294/294 pass. Core 1073/1081 pass (baseline; 8 pre-existing physics failures unchanged).
- [ ] **Step 6: Visual verification (user-driven)**
User stops any running AcDream.App gracefully via the close-window button, waits ~3 seconds, launches:
```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_DEVTOOLS = "1"
$env:ACDREAM_PROBE_AUTOWALK = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch.log"
```
Scenarios to test:
1. **Far-range Use NPC.** Double-click a Royal Guard / Pathwarden ~8 m away. Expected log shape:
```
[B.4b] use guid=0x... seq=X
[autowalk-mt] mt=0x06 isMoveTo=True ...
[autowalk-begin] dest=...
[autowalk-end] reason=arrived
```
Expected: NO `[B.4b] use-deferred` follow-up. Dialogue fires on arrival from ACE's `TryUseItem` callback.
2. **Far-range PickUp.** F-key a ground item ~5 m away. Same shape — single `[B.5] pickup` line, no `pickup-deferred`, item enters inventory.
3. **Close-range Use NPC behind player.** Within 3 m, press R. Body turns 180°. Expected log:
```
[B.4b] use deferred (close-range, turn-first) guid=0x...
[autowalk-end] reason=arrived
[B.4b] use-deferred guid=0x... seq=X
```
(Close-range deferred path is unchanged; `use-deferred` is correct here.)
4. **Open inn door from across the room.** ONE `[B.4b] use` line, no retry, door opens once.
5. **User takes manual control mid-auto-walk.** Click far NPC → press W during the walk. Auto-walk cancels (`EndServerAutoWalk("user-input")`). Action does NOT fire on arrival.
**STOP and wait for user confirmation that scenarios 15 pass.**
- [ ] **Step 7: Commit**
```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
fix(retail): suppress outbound MoveToState during inbound auto-walk
Phase B.6 — retire the Commit B issue-#63 workarounds by plugging the
underlying leak that caused them.
ApplyAutoWalkOverlay synthesizes Forward+Run input during inbound
MoveToObject so the existing motion-interpreter pipeline drives body
position + animation locally. That synth set MotionStateChanged=true,
so the outbound wire layer (GameWindow.cs:6410) built a MoveToState
packet with forwardCommand=RunForward and sent it to ACE. ACE read
the packet as "user took manual control" and cancelled its own
CreateMoveToChain (Player_Use.cs:205 → Player_Move.cs:150), so the
TryUseItem callback never fired on arrival. Our workaround sent Use
a second time at local-arrival to bypass ACE's cancelled chain.
Fix: one-line guard. The MoveToState send only fires when
!_playerController.IsServerAutoWalking. AutonomousPosition keeps
flowing during the walk (so ACE's WithinUseRadius poll sees the
player approach); ACE's chain runs uninterrupted; callback fires
when in range. Retail and holtburger (simulation.rs:178-206) follow
the same pattern — no outbound MoveToState during inbound MoveToObject.
Deletes the retry workarounds:
- SendUse far-range: `_pendingPostArrivalAction = (guid, false);`
+ the `(queued for arrival re-send pending #63)` log
- SendPickUp far-range: same shape
Close-range turn-first deferred path (separate code, retail-faithful
pre-callback rotation) is unchanged.
Spec: docs/superpowers/specs/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk-design.md
Plan: docs/superpowers/plans/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 2: Visual checkpoint — confirm Task 1's fix before touching cadence
This is not a code task. The Step 6 visual verification in Task 1 establishes that ACE's `MoveToChain` callback fires correctly with the MoveToState suppression in place. Only proceed to Task 3 if all five scenarios pass. If any regresses, STOP and revert Task 1's commit before continuing.
---
## Task 3: Revert AP cadence to retail's narrow gate
**Files:**
- Modify: `src/AcDream.App/Input/PlayerMovementController.cs` line ~254 (field block), line ~441-449 (`NotePositionSent`), line ~1240-1275 (the gate logic), line ~289-296 (`AutoWalkArrived` doc-comment cleanup).
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` line ~6450 (MTS NotePositionSent call), line ~6476 (AP NotePositionSent call).
- [ ] **Step 1: Add `_lastSentContactPlane` field**
In `PlayerMovementController.cs`, find the diff-tracking field block (around line 535-540 — search for `_lastSentPos`):
```csharp
private System.Numerics.Vector3 _lastSentPos;
private uint _lastSentCellId;
private float _lastSentTime;
private bool _lastSentInitialized;
```
Add a new field immediately after `_lastSentCellId`:
```csharp
private System.Numerics.Vector3 _lastSentPos;
private uint _lastSentCellId;
private System.Numerics.Plane _lastSentContactPlane;
private float _lastSentTime;
private bool _lastSentInitialized;
```
- [ ] **Step 2: Extend `NotePositionSent` to accept the contact plane**
Find `NotePositionSent` in `PlayerMovementController.cs` (around line 441). Replace:
```csharp
public void NotePositionSent(System.Numerics.Vector3 worldPos,
uint cellId,
float nowSeconds)
{
_lastSentPos = worldPos;
_lastSentCellId = cellId;
_lastSentTime = nowSeconds;
_lastSentInitialized = true;
}
```
With:
```csharp
/// <summary>
/// 2026-05-16 (Phase B.6). Called by the network outbound layer
/// after every AutonomousPosition or MoveToState that carries the
/// player's position. Resets the diff-driven heartbeat clock so the
/// next <see cref="HeartbeatDue"/> evaluation requires a fresh
/// state change. Mirrors retail's SendPositionEvent
/// (acclient_2013_pseudo_c.txt:700345-700348) which writes
/// `last_sent_position`, `last_sent_position_time`, and
/// `last_sent_contact_plane` after every send.
/// </summary>
public void NotePositionSent(System.Numerics.Vector3 worldPos,
uint cellId,
System.Numerics.Plane contactPlane,
float nowSeconds)
{
_lastSentPos = worldPos;
_lastSentCellId = cellId;
_lastSentContactPlane = contactPlane;
_lastSentTime = nowSeconds;
_lastSentInitialized = true;
}
```
- [ ] **Step 3: Replace the per-frame gate with retail's narrow gate**
Find the cadence block in `PlayerMovementController.cs` (around line 1240-1275 — search for `retail diff-driven AP cadence`). Replace the block starting at the `// 2026-05-16 — retail diff-driven AP cadence` comment through the `HeartbeatDue =` line with:
```csharp
// 2026-05-16 (Phase B.6) — retail-faithful AP cadence per
// CommandInterpreter::ShouldSendPositionEvent at
// acclient_2013_pseudo_c.txt:700233-700285. Two-branch:
//
// Branch 1 — interval NOT yet elapsed (< 1 sec since
// last send): send only if cell changed OR contact-plane
// changed (mid-walk events that matter).
//
// Branch 2 — interval HAS elapsed (>= 1 sec): send only
// if cell OR position frame changed. Truly idle = no
// send (retail's `last_sent.frame == player.frame` check
// at line 700248-700265).
//
// SendPositionEvent (line 700327) gates the actual send on
// (state & 1) != 0 && (state & 2) != 0 — Contact AND
// OnWalkable both set. We mirror that gate here so airborne
// and wall-contact-without-walkable states suppress AP
// entirely; MoveToState carries jump/fall snapshots while
// airborne.
//
// Effective rates:
// - Truly idle (grounded, no movement) : 0 Hz
// - Smooth movement (no cell/plane changes) : ~1 Hz (interval-driven)
// - Cell crossings + stair/hill steps : per-event
// - Airborne : 0 Hz
//
// Bootstrap: when NotePositionSent has never been called
// (_lastSentInitialized=false), treat every frame as
// "anything to send" so the first AP gets a chance to fire.
bool intervalElapsed = !_lastSentInitialized
|| (_simTimeSeconds - _lastSentTime) >= HeartbeatInterval;
bool cellChanged = !_lastSentInitialized
|| _lastSentCellId != CellId;
bool planeChanged = !_lastSentInitialized
|| !_lastSentContactPlane.Equals(_body.ContactPlane);
bool frameChanged = !_lastSentInitialized
|| !ApproxPositionEqual(_lastSentPos, _body.Position);
bool sendThisFrame = intervalElapsed
? (cellChanged || frameChanged)
: (cellChanged || planeChanged);
// Grounded-on-walkable gate per acclient_2013_pseudo_c.txt:700327
// (`(state & 1) != 0 && (state & 2) != 0`). Both flags must be
// set simultaneously, NOT a bitwise-OR mask test.
bool groundedOnWalkable = _body.InContact && _body.OnWalkable;
HeartbeatDue = groundedOnWalkable && sendThisFrame;
```
- [ ] **Step 4: Update the MTS site to pass `contactPlane`**
In `GameWindow.cs`, find the MoveToState `NotePositionSent` call (around line 6450). Replace:
```csharp
_playerController.NotePositionSent(
worldPos: _playerController.Position,
cellId: _playerController.CellId,
nowSeconds: _playerController.SimTimeSeconds);
```
With:
```csharp
_playerController.NotePositionSent(
worldPos: _playerController.Position,
cellId: _playerController.CellId,
contactPlane: _playerController.PhysicsBody.ContactPlane,
nowSeconds: _playerController.SimTimeSeconds);
```
If `_playerController.PhysicsBody` doesn't exist as a public accessor, search:
```
grep -n "public.*_body\|public PhysicsBody\|public.*Body" src/AcDream.App/Input/PlayerMovementController.cs
```
If no public accessor, add one in `PlayerMovementController.cs` near the existing public properties (around line 130-160):
```csharp
/// <summary>2026-05-16. Read-only access to the controller's
/// physics body — needed by the network outbound layer to stamp
/// the contact plane into NotePositionSent.</summary>
public AcDream.Core.Physics.PhysicsBody PhysicsBody => _body;
```
(Verify the field name is `_body` first — search `private.*PhysicsBody`.)
- [ ] **Step 5: Update the AP site to pass `contactPlane`**
Find the AutonomousPosition `NotePositionSent` call (around line 6476). Apply the same edit:
```csharp
_playerController.NotePositionSent(
worldPos: _playerController.Position,
cellId: _playerController.CellId,
contactPlane: _playerController.PhysicsBody.ContactPlane,
nowSeconds: _playerController.SimTimeSeconds);
```
- [ ] **Step 6: Build**
```
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug
```
Expected: 0 errors. Any compile error here is a wiring mistake — the field name (`_body` vs `_physicsBody`), the property accessor, or the `Plane` namespace.
- [ ] **Step 7: Run existing tests**
```
dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj -c Debug --nologo
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --nologo
```
Expected: Core.Net 294/294, Core 1073/1081 (baseline unchanged).
- [ ] **Step 8: Visual verification (user-driven)**
User restarts the client (graceful close + ~3 sec wait + launch). Runs scenarios:
1. **Idle test.** Stand still on flat ground in Holtburg for 10 sec. Watch the dev console / `[autowalk-up]` lines or any outbound packet trace.
- Old behavior: 1 AP/sec heartbeat.
- New behavior: ZERO outbound packets while truly idle.
2. **Smooth-running test.** Hold W and run in a straight line for 5 sec on flat ground (no cell crossings).
- Old behavior: ~60 AP/sec (per-frame while position changed).
- New behavior: ~1 AP/sec (interval-driven; cell/plane don't change every frame).
- **The character should still appear to remote observers as moving smoothly** — ACE's dead-reckoning fills in the gaps between sparse APs. If remote view becomes jittery, the cadence is too sparse and we'll need to tune.
3. **Cell-crossing test.** Run across a landblock boundary (every ~192 m). Should see a burst of AP packets at the crossing — both the `cellChanged` path and the `intervalElapsed && frameChanged` path can fire here.
4. **Far-range Use re-test.** Repeat Task 1 Step 6 scenario 1 (far-range NPC Use). Should still work — ACE's `MoveToChain` callback fires on arrival, dialogue plays, single Use packet.
5. **Hill / stair test.** Walk up a slope or stairs. Contact-plane changes per step → sub-interval AP sends fire on plane change. Behavior should look smooth to remote observers.
**STOP and wait for user confirmation that scenarios 15 pass.** If scenario 2 produces visible remote-jitter, the spec's `ApproxPositionEqual` epsilon may need tightening, or we may need a higher heartbeat rate; document the finding and tune before continuing.
- [ ] **Step 9: Commit**
```bash
git add src/AcDream.App/Input/PlayerMovementController.cs src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
fix(retail): revert AP cadence to retail's narrow gate
Phase B.6 — closes #74. With the MoveToState suppression fix in
place, the per-frame "send while moving" cadence is no longer needed
to compensate for ACE's MoveToChain cancellation. Reverts to retail's
two-branch gate per CommandInterpreter::ShouldSendPositionEvent at
acclient_2013_pseudo_c.txt:700233-700285:
Interval NOT elapsed (< 1 sec): send if cell or contact-plane changed.
Interval elapsed (>= 1 sec): send if cell or position frame changed.
Bootstrap fires every frame until the first NotePositionSent.
Grounded-on-walkable gate (Contact && OnWalkable) unchanged from
700327.
Effective rates:
Truly idle (grounded, no movement) : 0 Hz (was 1 Hz)
Smooth straight-line run : ~1 Hz (was ~60 Hz)
Cell crossings + stair/hill steps : per-event
Airborne : 0 Hz (unchanged)
Adds _lastSentContactPlane field + extends NotePositionSent to accept
System.Numerics.Plane. Adds PhysicsBody public accessor so the wire
layer can read _body.ContactPlane to pass into NotePositionSent. Both
outbound sites (MoveToState at GameWindow.cs:6450, AP at ~6476)
updated to pass the plane.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 4: Visual checkpoint — confirm Task 3's cadence revert before closing issues
Same as Task 2. Only proceed to Task 5 if all five scenarios in Task 3 Step 8 pass cleanly.
---
## Task 5: Close issues #63 and #74
**Files:**
- Modify: `docs/ISSUES.md`.
- [ ] **Step 1: Move issue #63 to "Recently closed"**
Find `## #63 — Server-initiated auto-walk (MoveToObject) not honored` in `docs/ISSUES.md` (around line 425). Currently `Status: OPEN`. Cut the entire `#63` block from the active issues section and paste it into the "Recently closed" section near the bottom of the file with these changes:
1. Change the title line from:
```markdown
## #63 — Server-initiated auto-walk (MoveToObject) not honored
```
to:
```markdown
## #63 — [DONE 2026-05-16 · `<TASK1_SHA>`] Server-initiated auto-walk (MoveToObject) not honored
```
(Replace `<TASK1_SHA>` with the actual commit SHA from Task 1. Get it via `git log --oneline -5`.)
2. Change `Status: OPEN` to `Status: DONE`.
3. Append a "Resolution" paragraph after the existing "Acceptance":
```markdown
**Resolution (2026-05-16):** B.6 slice 2 (2026-05-14) shipped the inbound-MoveToObject auto-walk handling. The remaining "MoveToChain callback never fires on arrival" half was tracked to ApplyAutoWalkOverlay's synthesized Forward+Run input leaking to the wire as an outbound MoveToState packet (forwardCommand=RunForward), which ACE read as "user took manual control" and used to cancel its own MoveToChain. Fix in `<TASK1_SHA>` adds a single guard at `GameWindow.cs:6410`: outbound MoveToState only fires when `!_playerController.IsServerAutoWalking`. With ACE's chain running uninterrupted, the `TryUseItem` callback (Player_Use.cs:205) fires server-side on arrival; no client retry needed. Retired the `_pendingPostArrivalAction` retry workarounds from SendUse + SendPickUp far-range paths.
```
- [ ] **Step 2: Move issue #74 to "Recently closed"**
Find `## #74 — AP cadence is per-frame-while-moving, more chatty than retail`. Same shape: cut the block, paste in "Recently closed", change title to:
```markdown
## #74 — [DONE 2026-05-16 · `<TASK3_SHA>`] AP cadence is per-frame-while-moving, more chatty than retail
```
Change `Status: OPEN` to `Status: DONE`. Append:
```markdown
**Resolution (2026-05-16):** With #63 closed (MoveToState no longer cancels ACE's MoveToChain), the per-frame-while-moving cadence workaround is unnecessary. Reverted to retail's two-branch ShouldSendPositionEvent gate per `acclient_2013_pseudo_c.txt:700233-700285` in `<TASK3_SHA>`. Effective rate during smooth motion drops from ~60 Hz to ~1 Hz; truly idle drops from 1 Hz to 0 Hz. Cell crossings + contact-plane changes still fire mid-interval. Matches retail bit-for-bit.
```
- [ ] **Step 3: Commit**
```bash
git add docs/ISSUES.md
git commit -m "$(cat <<'EOF'
docs: close #63 (MoveToObject not honored) + #74 (AP chattier than retail)
Both retired by Phase B.6. #63 fixed in <TASK1_SHA> (MoveToState
suppression during inbound auto-walk + retry workaround retirement).
#74 fixed in <TASK3_SHA> (AP cadence reverted to retail's two-branch
ShouldSendPositionEvent gate now that the workaround that needed
per-frame APs is gone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
(Replace `<TASK1_SHA>` and `<TASK3_SHA>` with the actual SHAs.)
---
## Self-review checklist
**Spec coverage:**
| Spec section | Plan task |
|---|---|
| Wire-level changes: `IsServerAutoWalking` guard at `GameWindow.cs:6410` | Task 1 Step 1 ✅ |
| Far-range Use retry deletion | Task 1 Step 2 ✅ |
| Far-range PickUp retry deletion | Task 1 Step 3 ✅ |
| Log string cleanup | Task 1 Steps 2+3 ✅ |
| `_lastSentContactPlane` field + `NotePositionSent` signature | Task 3 Steps 1+2 ✅ |
| Retail-narrow gate | Task 3 Step 3 ✅ |
| MTS site contactPlane wiring | Task 3 Step 4 ✅ |
| AP site contactPlane wiring | Task 3 Step 5 ✅ |
| Testing plan (visual scenarios) | Task 1 Step 6 + Task 3 Step 8 ✅ |
| Close `#63` + `#74` | Task 5 ✅ |
| Out-of-scope `#75` (status messages) | Filed as deferred — not in this plan ✅ |
**Placeholder scan:** No "TBD" / "implement later" / vague phrases. Every step has actual code or actual commands.
**Type consistency:**
- `IsServerAutoWalking` referenced Task 1 Step 1 — already exists in code (verified at PlayerMovementController.cs:273). ✅
- `_lastSentContactPlane : System.Numerics.Plane` defined Task 3 Step 1, used Task 3 Steps 2+3. ✅
- `NotePositionSent(Vector3, uint, Plane, float)` defined Task 3 Step 2, called Task 3 Steps 4+5. ✅
- `_playerController.PhysicsBody` property defined Task 3 Step 4 (conditional add if missing), used Task 3 Steps 4+5. ✅
- `_lastSentPos`, `_lastSentCellId`, `_lastSentTime`, `_lastSentInitialized` — pre-existing from Commit B. ✅
**Risk / rollback:**
- Task 1 commit: simple revert restores the workaround.
- Task 3 commit: simple revert restores the per-frame cadence.
- Task 5 commit: docs-only; trivial.
Each task is independently revertable. If Task 3 introduces remote-view jitter (scenario 2), revert Task 3 and re-evaluate (e.g., dial down `HeartbeatInterval` from 1 s to 0.5 s).
---
## Execution handoff
Plan saved to `docs/superpowers/plans/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk.md`.
Two options for the controller:
1. **Subagent-Driven (recommended)** — Dispatch fresh subagent per task. Two-stage review (spec compliance + code quality) between tasks. ~3 implementer + 6 reviewer dispatches.
2. **Inline Execution** — Execute tasks in this session using `superpowers:executing-plans`. Two visual-verify checkpoints (between Task 1 & Task 3, between Task 3 & Task 5).
Given the small scope (~30 LOC of behavior change + docs) and the two mandatory user-driven visual checkpoints, inline execution may be simpler — the subagent overhead exceeds the implementation time for tasks this small.