acdream/docs/research/2026-05-15-b6-b7-shipped-handoff.md
Erik 520badd566 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>
2026-05-15 18:29:53 +02:00

382 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 + 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.