docs(B.6+B.7): ship handoff — 36 commits, faithfulness audit, workaround retirement plan

Covers the 2026-05-15 session in full: B.6 local-player auto-walk on
inbound MoveToObject (issue #63 working), B.7 Vivid Target Indicator
MVP, WorldPicker tightening (#59 closed).

Includes:
- 36-commit table cf22f9c..e49c704
- Wire-format facts (MovementType 6/7/8, WalkRunThreshold, heartbeat cadence)
- Auto-walk state machine current shape + GameWindow wiring
- Picker + target-indicator current shapes
- Honest faithfulness audit (user-requested) — workarounds are our bugs not ACE's
- Closed issues (#59, #62, #67) + open follow-ups (#65, #66, #68, #69)
- Reproducibility commands + diagnostic env vars
- Files touched
- Workaround retirement plan (single fix retires four workarounds: per-tick outbound)
- Next-session entry points (sign indicator size, #66 MovementType=8, etc.)

Single biggest lesson surfaced in session: when a workaround starts feeling
load-bearing, find the heartbeat-cadence root cause first. The 10 Hz
AutonomousPosition bump (301281d) closed #67 and tightened firing distance
for every other interaction in one commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-15 18:29:53 +02:00
parent e49c704b39
commit 520badd566

View file

@ -0,0 +1,382 @@
# Phase B.6 + B.7 + WorldPicker tightening — handoff (visual-verified 2026-05-15)
**Date:** 2026-05-15 (session 06:0818:21).
**Branch:** commits live on `main` from `cf22f9c..e49c704` (36 commits).
**Predecessors:**
- [docs/research/2026-05-14-b5-shipped-handoff.md](2026-05-14-b5-shipped-handoff.md) — B.5 (pickup) shipped immediately before.
- [docs/superpowers/specs/2026-05-14-phase-b6-design.md](../superpowers/specs/2026-05-14-phase-b6-design.md) — B.6 design with retail anchors + trace findings + 4-slice plan.
- [docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md](../superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md) — B.7 design (Vivid Target Indicator).
---
## TL;DR
Three coupled improvements shipped end-to-end this session:
- **Phase B.6 — Local-player auto-walk on inbound `MoveToObject` (issue #63 OPEN → working).** When the user double-clicks a far target or presses R/F on an out-of-range target, ACE sends `UpdateMotion (0xF74D)` with `MovementType=6` carrying the destination guid. We now synthesize `Forward+Run` input into `PlayerMovementController` to walk the body to the target, then fire the deferred Use/PickUp once the position has arrived AND the body has rotated to face. Smooth rotation (no snap), dual alignment thresholds (30° walk-while-turning, 5° fully aligned). 10 Hz position heartbeat while moving keeps ACE's server-side `WithinUseRadius` poll converging fast enough that doors / NPCs / items all complete the action.
- **Phase B.7 — Vivid Target Indicator (MVP).** Four small corner triangles drawn around the selected entity, colour-coded by entity type using a port of `gmRadarUI::GetBlipColor` (`0x004d76f0`). Box size scales with projected entity height × scale, per-type base height (humanoid 1.8 m, door/lifestone/portal 2.4 m, small item 0.8 m, default 1.5 m). Drawn via ImGui background draw list — no new GL infrastructure. **Selection bug is now self-correcting**: the user can see *what* they actually clicked before pressing R/F.
- **WorldPicker tightening (#59 closed).** 5 m fixed sphere radius → 0.7 m default → 1.0 m default with 0.9 m vertical offset (chest-height sphere centre). Per-entity radius/offset callbacks let doors / lifestones / portals get bigger spheres (1.52.0 m) and small items get tighter ones (0.4 m).
The M1 demo target *"click an NPC"* + *"open the inn door"* is now reachable from ANY range (close-range or far-range). The visual flow matches retail: you click → indicator appears → you press R → if far, character walks to within use radius, turns to face, action fires; if near, character turns to face, action fires.
**Honest faithfulness note (user-requested audit).** Several workarounds remain — arrival safety margin, deferred-wire-Use packet, AutonomousPosition flush on arrival, retry flag — all rooted in our 1 Hz position heartbeat. The 10 Hz bump retires the worst of them in practice. Per-tick outbound (or fixing whatever causes ACE to lose our position between heartbeats) would retire ALL of them. Documented below in **Workaround retirement plan**.
---
## What shipped on this branch (36 commits)
Ordered oldest → newest. Each commit subject is its own retail-faithful unit; the "fix(B.6+B.7)" pairing means a single workaround serves both phases.
| # | Commit | Subject |
|---|---|---|
| 1 | `87ba5c9` | `feat(B.5): pickup feedback chat line + toast ("You pick up the X.")` |
| 2 | `7be1393` | `docs(M1): record all 4 demo targets met, list deferred polish` |
| 3 | `20ecb23` | `Revert "feat(B.5): pickup feedback chat line + toast …"` — retail-faithful: no feedback. |
| 4 | `a01ebd5` | `fix(B.5): block pickup of creatures client-side; show 'Can't pick that up' toast` |
| 5 | `ab7c04f` | `docs(M1): reflect chat/toast revert + the actual B.5 polish (creature pickup guard)` |
| 6 | `e55ad48` | `fix(B.5): make creature-pickup guard silent (retail-faithful)` |
| 7 | `ec9fd52` | `fix #62: null-guard the PARTSDIAG read of ae.Animation` |
| 8 | `5053e40` | `docs: close #62 — PARTSDIAG null-guard landed in ec9fd52` |
| 9 | `281d125` | `docs(B.6): design spec for local-player MoveToObject auto-walk (issue #63)` |
| 10 | `9e1d33a` | `docs(B.6): retail decomp settles Option A; revise spec with 4-slice plan` |
| 11 | `eda8278` | `feat(B.6 slice 1): ACDREAM_PROBE_AUTOWALK diagnostic baseline` |
| 12 | `1b4f3ba` | `feat(B.6 slice 1): DebugPanel mirror for ProbeAutoWalk checkbox` |
| 13 | `d82b064` | `docs(B.6): record Slice 1 trace findings — ACE sends mtRun=0.00, no UP echo` |
| 14 | `b936ef8` | `feat(B.6 slice 2): local-player auto-walk on inbound MoveToObject` — core feature lands. |
| 15 | `f18de7c` | `fix(B.6 slice 2): don't cancel autowalk on the companion InterpretedMotionState` |
| 16 | `5612ce7` | `feat(B.6): honor wire WalkRunThreshold — walk vs run per retail semantics` |
| 17 | `37177a4` | `docs(B.7): design spec for Vivid Target Indicator (selection feedback)` |
| 18 | `8544a78` | `feat(B.7): RadarBlipColors — port of gmRadarUI::GetBlipColor` |
| 19 | `c7e5f9f` | `feat(B.7): TargetIndicatorPanel — corner triangles around selected entity` |
| 20 | `4bc95ec` | `fix(B.7): scale indicator box from projected entity height, not fixed pixels` |
| 21 | `5e29773` | `fix #59: tighten WorldPicker radius from 5 m to 0.7 m` |
| 22 | `631571a` | `docs: close #59 — picker radius tightened in 5e29773` |
| 23 | `23cb1e9` | `fix(B.7): square indicator box + bigger pick sphere for doors/lifestones/portals + diag` |
| 24 | `1a0656a` | `fix(picker): lift sphere centre to mid-body so chest/head clicks hit` |
| 25 | `211fe24` | `fix(B.6+B.7): run-all-the-way auto-walk, per-type indicator height, R = smart interact` |
| 26 | `5f83766` | `docs: file #65 — local player doesn't turn to face on close-range Use` |
| 27 | `2dc28bb` | `fix(B.6+B.7): re-send action on local arrival; scale indicator box by entity Scale` |
| 28 | `a0fa3d6` | `fix(B.6+B.7): flush AutonomousPosition on arrival before re-sending action` |
| 29 | `39ff3a5` | `fix(B.6+B.7): arrival predicate uses safety margin INSIDE ACE's WithinUseRadius` |
| 30 | `64c9793` | `fix(B.6+B.7): shrink arrival safety margin; file #66 rotation, #67 door` |
| 31 | `301281d` | `fix(B.6+B.7): bump AutonomousPosition heartbeat 1Hz -> 10Hz while moving`**single biggest fix** |
| 32 | `32352af` | `fix(B.6): turn-first auto-walk + tiny margin; close #67 doors; file #68 remote arrival` |
| 33 | `5b908bc` | `fix(B.6): close-range turn-to-face — install overlay on Use/PickUp send` |
| 34 | `cffb10f` | `fix(B.6): tighter 5° alignment + defer Use until rotation completes; file #69 turn anim` |
| 35 | `7158c46` | `fix(B.6): smooth local rotation — remove 20° snap-on-approach (not retail)` |
| 36 | `e49c704` | `fix(B.6): speculative auto-walk uses WalkRunThreshold=15 to match ACE` |
**Build:** clean.
**Tests:** `dotnet test -c Debug` shows the new RadarBlipColors tests (8) and the existing B.5 BuildPickUp tests passing. Failure count unchanged at the 8 pre-existing baseline in `AcDream.Core.Tests`.
---
## Wire-format facts (what ACE sends, what we parse)
| Wire | Field | Value | Our handling |
|---|---|---|---|
| `UpdateMotion (0xF74D)` | `MovementType` | `6` MoveToObject | Local: `BeginServerAutoWalk(...)` + speculative turn overlay. Remote: existing `RemoteMoveToDriver`. |
| `UpdateMotion (0xF74D)` | `MovementType` | `7` MoveToPosition | Local: `BeginServerAutoWalk(...)` with a synthetic guid (positional destination only). |
| `UpdateMotion (0xF74D)` | `MovementType` | `8` TurnToObject | **NOT YET PARSED** — issue #66. ACE sends this on close-range Use against an off-facing target. Our parser falls into the locomotion path and silently drops the rotation. |
| `UpdateMotion (0xF74D)` | `MovementType` | `0` Interpreted | Companion locomotion echo after a MovementType=6 (RunForward command). We do NOT treat this as a cancel signal (commit f18de7c). |
| `UpdateMotion (0xF74D)` | `WalkRunThreshold` | float meters | If `distance > threshold` → run, else walk. ACE default `15.0 m`. We honor it (commit 5612ce7) for inbound; we use it as the speculative-overlay walk/run gate too (commit e49c704). |
| `GameAction (0xF7B1)` outbound | `0x0036 Use` | guid | Sent on R-key / double-click + close-range. For far-range we install the speculative overlay and defer the wire packet until arrival (commit cffb10f). |
| `GameAction (0xF7B1)` outbound | `0x0019 PutItemInContainer` | item guid + container guid + placement | Sent on F-key. Same defer-on-far-range pattern. |
| `AutonomousPosition (0xF7B1 0x0007)` outbound | position + heading | every 1 Hz idle / **10 Hz while moving** | The 10 Hz bump (commit 301281d) is what unblocks doors (#67) and lets ACE's `MoveToChain` see us arrive at the use radius without timing out. |
**Retail anchors for the above:**
- `MovementManager::PerformMovement` at `0x00524440` — dispatch switch on MovementType.
- `MoveToManager::HandleMoveToObject` — MovementType=6 driver (turn-to-face → walk → stop).
- `MoveToManager::HandleMoveToPosition` — MovementType=7 driver.
- `MoveToManager::HandleTurnToHeading` at `0x0052a0c0` — turn-only driver used by MovementType=8.
- `CPhysicsObj::MoveToObject` at `0x00512860` — high-level entry from physics.
- `Player_Move.CreateMoveToChain` (ACE) at `Player_Move.cs:37179` — server-side state machine that depends on our heartbeat to detect arrival.
---
## Local auto-walk state machine (current shape)
`src/AcDream.App/Input/PlayerMovementController.cs`:
```csharp
// State
private bool _autoWalkActive;
private Vector3 _autoWalkDestination;
private float _autoWalkMinDistance; // ACE's WithinUseRadius (per-type)
private float _autoWalkDistanceToObject; // initial distance, used for run/walk decision
private bool _autoWalkMoveTowards;
private bool _autoWalkInitiallyRunning; // decided ONCE at Begin
public event Action? AutoWalkArrived; // GameWindow re-sends Use/PickUp on this
// Per-frame overlay, called from top of Update
private MovementInput ApplyAutoWalkOverlay(float dt, MovementInput input)
{
if (!_autoWalkActive) return input;
// User-input override → cancel
if (input.Forward || input.Back || input.StrafeL || input.StrafeR)
{
EndServerAutoWalk("user-input");
return input;
}
// Compute delta yaw, distance, alignment
Vector3 toTarget = _autoWalkDestination - Position;
float dist = toTarget.Length();
float targetYaw = MathF.Atan2(toTarget.Y, toTarget.X) - MathF.PI / 2f;
float delta = NormalizeAngle(targetYaw - Yaw);
// SMOOTH rotation (commit 7158c46 — no snap)
float maxStep = RemoteMoveToDriver.TurnRateRadPerSec * dt;
Yaw += MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), maxStep);
// Dual alignment thresholds (commit cffb10f)
const float WalkWhileTurningRad = 30f * MathF.PI / 180f;
const float FullyAlignedRad = 5f * MathF.PI / 180f;
bool walkAligned = MathF.Abs(delta) <= WalkWhileTurningRad;
bool aligned = MathF.Abs(delta) <= FullyAlignedRad;
// Arrival predicate uses TIGHT 0.05 m safety margin INSIDE ACE's radius
// (commit 39ff3a5 + 64c9793; works because of 10 Hz heartbeat from 301281d)
bool withinArrival = dist <= (_autoWalkMinDistance - 0.05f);
if (withinArrival && aligned)
{
EndServerAutoWalk("arrived"); // fires AutoWalkArrived event
return input;
}
bool moveForward = walkAligned && !withinArrival;
return input with
{
Forward = moveForward,
Run = moveForward && _autoWalkInitiallyRunning,
// Strafes left clear so we don't combine with other input
};
}
```
`src/AcDream.App/Rendering/GameWindow.cs`:
```csharp
// Wired in ctor:
_playerController.AutoWalkArrived += OnAutoWalkArrivedReSendAction;
private (uint Guid, bool IsPickup)? _pendingPostArrivalAction;
// On Use/PickUp send: install speculative overlay + defer wire packet if close
private void SendUse(uint guid, bool isRetryAfterArrival = false)
{
if (!isRetryAfterArrival)
{
InstallSpeculativeTurnToTarget(guid); // BeginServerAutoWalk with tiny radius
_pendingPostArrivalAction = (guid, false);
if (IsCloseRangeTarget(guid))
return; // wire packet deferred until arrival
}
// ... build + send 0xF7B1/0x0036
}
private void OnAutoWalkArrivedReSendAction()
{
if (_pendingPostArrivalAction is not (uint guid, bool isPickup)) return;
_pendingPostArrivalAction = null;
SendAutonomousPositionNow(); // flush position so ACE sees us at radius
if (isPickup) SendPickUp(guid, isRetryAfterArrival: true);
else SendUse(guid, isRetryAfterArrival: true);
}
```
---
## Picker (current shape)
`src/AcDream.Core/Selection/WorldPicker.cs`:
- `DefaultRadius = 1.0f` (up from 0.7 m to compensate for vertical-offset lift, commit 1a0656a).
- `DefaultVerticalOffset = 0.9f` (chest-height humanoid mid-body — fixes the bug where clicking the head/chest of an NPC missed because the sphere was at the feet).
- Per-entity callbacks (`radiusForGuid`, `verticalOffsetForGuid`) supplied by `GameWindow`:
- Doors / lifestones / portals: **radius 1.52.0 m, vertical offset 1.2 m** (commit 23ce1e9).
- Small dropped items (BF_ITEM-class): **radius 0.4 m, vertical offset 0.1 m** (item lies on ground).
- Default (NPC / creature / sign / other): defaults 1.0 m / 0.9 m.
- Inside-sphere origin handled (commit 5821bdc, pre-session): if `t_near < 0` use `t_far` so the entity is still pickable at point-blank range.
---
## Target indicator (current shape)
`src/AcDream.App/UI/TargetIndicatorPanel.cs` + `src/AcDream.Core/Ui/RadarBlipColors.cs`:
- `TargetInfo(WorldPosition, ItemType, ObjectDescriptionFlags, Scale)` record carries the inputs.
- Box height = `EntityHeightFor(itemType, pwdBitfield, scale)`:
- Creature (NPC / monster / player): **1.8 m × scale** (humanoid baseline).
- Door / Lifestone / Portal (BF_DOOR=0x1000 | BF_LIFESTONE=0x4000 | BF_PORTAL=0x40000): **2.4 m × scale** (door-frame tall).
- Small carry items (Weapon | Armor | Clothing | Jewelry | Food | Money | Misc | MissileWeapon | Container | Gem | SpellComponents | Writable | Key | Caster): **0.8 m × scale**.
- Default (signs, scenery interactables, untyped): **1.5 m × scale**. ⚠️ User reports signs still feel too small — see follow-up below.
- Box is **square** (`WidthHeightRatio = 1.0`, matches retail) — width = height.
- Projection: project feet + head world points to screen, draw 4 right-angle triangles at corners via ImGui background draw list.
- Min screen height clamp: 16 px (prevents collapse on far entities).
- Off-screen / behind-camera: returns early; ±20% NDC margin so a tall entity whose feet are just off-screen still gets head projected.
- Colour: from `RadarBlipColors.For(itemType, pwdBitfield)`. Port of `gmRadarUI::GetBlipColor (0x004d76f0)` — Portal → Vendor → Creature (yellow) → PlayerKiller (red) → PKLite → FriendlyPlayer → default Item (white-ish).
- 8 unit tests in `tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs`.
**MVP scope — explicitly deferred (per spec §3):** off-screen edge arrow (`m_pOffScreen`), DAT-loaded triangle sprite (today's are procedural), mesh-tint highlight on the target, player-option toggle.
---
## Faithfulness audit (user-requested honest comparison)
This was the most important conversation thread of the session. User asked: *"How faithful are we to retail in this?"* and pushed back on every workaround I'd introduced. Direct answer: **The data path is retail-faithful, the timing isn't, and the workarounds are our bugs not ACE's.**
| Piece | What retail did | What we do | Why we diverged | Resolution path |
|---|---|---|---|---|
| MoveToObject wire parse | `MovementManager::PerformMovement` switch on `MovementType` | We parse 6/7, miss 8 | Incremental delivery — B.6 scope didn't include TurnToObject | **Issue #66** — port MovementType=8 |
| Local turn-to-face | Smooth interpolation animation (legs+arms cycle while body pivots) | Smooth Yaw step; no animation cycle (statue-pivot) | Motion interpreter not fed TurnLeft/TurnRight when overlay turns | **Issue #69** — synthesize TurnLeft/TurnRight |
| Arrival predicate | Exact: stop when `dist ≤ radius` | `dist ≤ radius 0.05 m` safety margin | Our client+server position drift would let retail's exact predicate fall through. 1 Hz heartbeat was the root cause; with 10 Hz it's mostly redundant. | **Drop the margin** once per-tick outbound lands. |
| Action send timing | Once on player intent | Twice: once on intent (deferred if far) + once on arrival | Our `MoveToChain` poll on ACE side races against our position heartbeat. With 1 Hz we always lost. 10 Hz helps. | **Single-send** once per-tick outbound + a server-side action queue replaces the retry. |
| AutonomousPosition flush | Continuous broadcast | One forced AP on arrival + 10 Hz during move | Same root cause: ACE polls at 0.1 s, we broadcast at 1 s default. | **Per-tick outbound** retires this entirely. |
| Local-player TurnToObject | Server broadcasts MovementType=8; client rotates body | We drop the wire MovementType=8 | Incremental delivery — B.6 was scoped to MovementType=6 only | **Issue #66** — same fix as the parse gap above. |
| Remote-player arrival animation | Client detects arrival from wire's distance threshold and transitions cycle | RemoteMoveToDriver `Arrived` state set but consumer doesn't flip cycle | Cycle-routing layer never wired to the arrival event | **Issue #68** — add `SetCycle(NonCombat, Ready)` on arrival. |
| Local-player pickup animation | Server-initiated `Motion(Pickup)` broadcast → animates locally | Self-echo filter drops it | Pre-existing (B.5) | **Issue #64** — admit server-initiated one-shots through the filter. |
**Bottom line: every workaround in this list traces to the position heartbeat or the missing MovementType=8 path. Neither is "ACE's bug" — they are gaps in our client.**
---
## Open follow-up issues filed this session
| ID | Severity | Summary |
|---|---|---|
| **#66** | LOW-MED | Local + remote rotation flip-back / NPCs don't turn — port MovementType=8 TurnToObject |
| **#68** | LOW-MED | Remote players' running animation doesn't stop on auto-walk arrival |
| **#69** | LOW | Local player rotation isn't animated (statue-pivot vs leg-shuffle) — synthesize TurnLeft/TurnRight |
| **#65** | LOW | Local-player no turn-to-face on close-range Use — superseded by #66 |
**Closed this session:**
- **#59** — `WorldPicker` 5 m over-pick → 1.0 m + vertical offset (`5e29773`, `1a0656a`, `23ce1e9`).
- **#62** — PARTSDIAG null-guard for sequencer-driven entities (`ec9fd52`).
- **#67** — Door Use action doesn't complete after auto-walk arrival → fixed by 10 Hz heartbeat (`301281d`).
**Visual gripe still open (not filed as an issue yet):** signs still feel too small. Default 1.5 m × scale should probably be 2.5 m for sign-class objects — they're tall posts in retail. Wiring is there (just bump the constant in `EntityHeightFor` for the "everything else" branch, or add a sign-detection rule). User's most recent feedback before context-out was *"still when I select a sign the box is way to small."*
---
## Reproducibility — how to verify the shipped behaviour
```powershell
# From C:\Users\erikn\source\repos\acdream
Stop-Process -Name AcDream.App -ErrorAction SilentlyContinue
Start-Sleep -Seconds 3
$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"
# Optional diagnostic env vars (heavy)
# $env:ACDREAM_PROBE_AUTOWALK = "1" # one [autowalk] line per MoveToObject inbound + transition
# $env:ACDREAM_DUMP_MOTION = "1"
dotnet build -c Debug; if ($?) {
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
Tee-Object -FilePath "launch.log"
}
```
**Test scenarios (each verified visually during the session):**
1. **Far-range Use on NPC.** Stand 510 m from Tirenia in the Holtburg inn. Click NPC → corner triangles appear (yellow). Press R. Character runs to within ~3 m, decelerates, turns to face, Use fires, dialogue appears.
2. **Far-range pickup on item.** Stand 510 m from a dropped taper. Click item → corner triangles appear (white-ish). Press F. Character runs to within ~0.6 m, turns to face, PickUp fires, item despawns, inventory updates.
3. **Door open.** Stand 35 m from the inn front door. Click door → corner triangles appear (white-ish, scaled to 2.4 m × scale). Press R. Character walks to within ~2 m, turns to face, Use fires, ACE broadcasts `SetState (ETHEREAL)`, character walks through.
4. **Close-range Use.** Already within ~1 m of an NPC, facing away. Press R. Character turns to face → Use fires → dialogue. (Close-range branch — exercises `IsCloseRangeTarget` + deferred-wire-packet path.)
5. **Far-range double-click on item.** Same as (2) but double-click — should behave identically to F-key (double-click activation passes through `OnInputAction` after `58b95bc`).
**Diagnostic env vars active for this work:**
- `ACDREAM_PROBE_AUTOWALK=1` — one `[autowalk]` line per inbound MoveToObject + state transition (commit `eda8278`). Also toggleable via DebugPanel.
- `ACDREAM_DUMP_MOTION=1` — every inbound `UpdateMotion` (guid, stance, cmd, speed).
- `ACDREAM_REMOTE_VEL_DIAG=1``[UPCYCLE]` traces for remote-arrival debug (relates to #68).
---
## Files touched this session
**New files:**
- `src/AcDream.Core/Ui/RadarBlipColors.cs` — colour table port.
- `src/AcDream.App/UI/TargetIndicatorPanel.cs` — corner-triangle renderer.
- `tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs` — 8 unit tests.
- `docs/superpowers/specs/2026-05-14-phase-b6-design.md` — B.6 design + 4-slice plan.
- `docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md` — B.7 design.
**Modified:**
- `src/AcDream.App/Input/PlayerMovementController.cs` — auto-walk overlay, smooth rotation, dual alignment, 10 Hz heartbeat.
- `src/AcDream.App/Rendering/GameWindow.cs``SendUse`/`SendPickUp` defer logic, `_pendingPostArrivalAction`, `OnAutoWalkArrivedReSendAction`, `InstallSpeculativeTurnToTarget`, `IsCloseRangeTarget`, `SendAutonomousPositionNow`, `TargetIndicatorPanel` wiring, per-entity picker callbacks, `UseCurrentSelection` smart-R dispatch.
- `src/AcDream.Core/Selection/WorldPicker.cs` — radius/vertical-offset callbacks, inside-sphere origin handling documented.
- `src/AcDream.Core.Net/WorldSession.cs` — MovementType=6 routing into local auto-walk path.
- `src/AcDream.Core/Physics/PhysicsDiagnostics.cs``ProbeAutoWalkEnabled` static property, `ACDREAM_PROBE_AUTOWALK` env var.
- `src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs` + `DebugPanel.cs` — DebugPanel checkbox mirror.
- `docs/ISSUES.md` — closed #59, #62, #67; filed #65, #66, #68, #69; updated #63 with B.6 status.
- `docs/plans/2026-05-12-milestones.md` — M1 four-of-four status reflected.
- `CLAUDE.md` — updated "Currently in Phase…" line with B.6/B.7 ship facts.
---
## Workaround retirement plan
The four workarounds that should NOT survive a per-tick-outbound + MovementType=8 phase:
1. **Arrival safety margin (currently 0.05 m).** `ApplyAutoWalkOverlay` stops at `dist <= _autoWalkMinDistance - 0.05f`. Retail stops at `dist <= radius`. Drop the margin when our outbound position is fresh enough that ACE's `WithinUseRadius` poll always sees us inside the radius the moment we get there.
2. **Re-send on arrival.** `_pendingPostArrivalAction` + `OnAutoWalkArrivedReSendAction` re-fire `SendUse`/`SendPickUp` after the body arrives. Retail's client sends the action once and lets the server-side `MoveToChain` complete. Drop when ACE consistently completes the chain from a single send.
3. **AutonomousPosition flush on arrival.** `SendAutonomousPositionNow()` explicitly broadcasts position the moment we arrive. With per-tick outbound this happens naturally.
4. **`isRetryAfterArrival` flag.** Branch in `SendUse`/`SendPickUp` to skip the speculative-overlay install on the retry. Goes away when the retry goes away.
**Single fix that retires all four:** per-tick outbound position broadcast (probably at the physics tick rate of 60 Hz with a smaller payload, or 2030 Hz with the full one). Currently `effectiveInterval = activelyMoving ? 0.1f : 1.0f` (10 Hz active / 1 Hz idle). Going to 2030 Hz active would likely close the gap; per-tick is the upper bound.
**Reference for retail's outbound cadence:** `docs/research/named-retail/` — search for `CPhysicsObj::send_movement_event` and `AutonomousPosition` send-site. Holtburger's `client/movement/system.rs` also sends at higher cadence than our default.
---
## Next-session entry points (in rough priority order)
1. **Fix the sign indicator box.** User's last gripe. Bump `EntityHeightFor` default from 1.5 m to ~2.5 m, or add an explicit sign-detection rule. ~5 LOC. Verify in Holtburg by selecting one of the inn signs.
2. **Issue #66 — MovementType=8 TurnToObject (local + remote).** Two-direction fix: stop local-player flip-back AND make NPCs turn to face. ~80120 LOC + tests. Spec template is the B.6 spec with MovementType=8 substituted.
3. **Issue #69 — animate rotation.** Synthesize `TurnLeft`/`TurnRight` input flags while the overlay turns the body. ~30 LOC in `ApplyAutoWalkOverlay` + verify retail's human motion table has the cycle. Pairs with #66 nicely.
4. **Issue #68 — Remote players don't stop run animation on arrival.** Wire `RemoteMoveToDriver.Arrived` to `SetCycle(NonCombat, Ready)`. ~20 LOC. Small standalone fix.
5. **Issue #64 — Local-player pickup animation.** Pre-existing B.5 gap; the self-echo filter drops `UpdateMotion(Pickup)`. Either (a) admit server-initiated one-shots through the filter, or (b) generate locally on send.
6. **Per-tick outbound position broadcast.** The big one. Retires the four B.6 workarounds and probably fixes a class of "ACE doesn't see us" bugs we haven't even noticed yet. Probably its own design phase (call it B.8 or M.x). Read `docs/research/named-retail/` for retail's cadence first.
7. **Investigate the running-in-circles bug.** User reported during B.6 slice 2 testing that auto-walk would occasionally "run in circles" before going straight. The fix in `211fe24` (run-all-the-way) appears to have fixed it but no regression test exists. Worth a one-session investigation with `ACDREAM_PROBE_AUTOWALK=1`.
---
## Predecessor reading order for a fresh session
1. **This document** — the full picture of what's in main.
2. [`docs/superpowers/specs/2026-05-14-phase-b6-design.md`](../superpowers/specs/2026-05-14-phase-b6-design.md) — retail anchors + decomp citations for auto-walk.
3. [`docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md`](../superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md) — B.7 design + deferred-MVP list.
4. [`docs/research/2026-05-14-b5-shipped-handoff.md`](2026-05-14-b5-shipped-handoff.md) — B.5 (pickup, close-range path) preceded this work.
5. [`docs/research/2026-05-13-b4b-shipped-handoff.md`](2026-05-13-b4b-shipped-handoff.md) — B.4b (Use outbound + WorldPicker) preceded that.
**Retail decomp anchors for auto-walk:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` searched by:
- `MovementManager::PerformMovement` (`0x00524440`)
- `MoveToManager::HandleMoveToObject`
- `MoveToManager::HandleMoveToPosition`
- `MoveToManager::HandleTurnToHeading` (`0x0052a0c0`)
- `CPhysicsObj::MoveToObject` (`0x00512860`)
- `VividTargetIndicator::SetSelected` (`0x004f5ce0`)
- `gmRadarUI::GetBlipColor` (`0x004d76f0`)
**ACE anchors:** `references/ACE/Source/ACE.Server/WorldObjects/Player_Move.cs:37179` (`CreateMoveToChain`) and `Player_Inventory.cs:9761106` (pickup chain).
---
## Session-specific morale note
This was a long session — 43 user messages, ~12 hours wall-clock, 36 commits. The pattern was: implement, user tests against retail, user reports specific divergence, fix, repeat. The user pushed back hard on workarounds twice ("Why workarounds? Nothing wrong with ACE, our client is wrong" + "did you verify with retail?") and both times the right move was to drop the workaround and chase the root cause. The 10 Hz heartbeat (`301281d`) was the highest-leverage commit of the session — it closed #67 and tightened the firing distance for every other interaction. **Lesson: when a workaround starts feeling load-bearing, find the heartbeat-cadence-style root cause behind it before adding more layers.**
The B.6 four-slice plan in the spec was the right shape — Slice 1 (diagnostic) revealed `mtRun=0.00 + no UP echo`, which directly informed Slice 2 (treat MovementType=0 InterpretedMotionState as a companion not a cancel signal, `f18de7c`). Slice 3 + 4 (walk vs run + turn-first) emerged from visual testing. **Lesson: diagnostic-first slicing pays off when you don't actually know what ACE will send.**
— Session ended at user request to write this handoff before context compaction.