From 520badd56653ae726cfd38adf86c82a8602046a1 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 18:29:53 +0200 Subject: [PATCH] =?UTF-8?q?docs(B.6+B.7):=20ship=20handoff=20=E2=80=94=203?= =?UTF-8?q?6=20commits,=20faithfulness=20audit,=20workaround=20retirement?= =?UTF-8?q?=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../2026-05-15-b6-b7-shipped-handoff.md | 382 ++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 docs/research/2026-05-15-b6-b7-shipped-handoff.md diff --git a/docs/research/2026-05-15-b6-b7-shipped-handoff.md b/docs/research/2026-05-15-b6-b7-shipped-handoff.md new file mode 100644 index 0000000..5d99184 --- /dev/null +++ b/docs/research/2026-05-15-b6-b7-shipped-handoff.md @@ -0,0 +1,382 @@ +# Phase B.6 + B.7 + WorldPicker tightening — handoff (visual-verified 2026-05-15) + +**Date:** 2026-05-15 (session 06:08–18: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.5–2.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:37–179` — 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.5–2.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 5–10 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 5–10 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 3–5 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 20–30 Hz with the full one). Currently `effectiveInterval = activelyMoving ? 0.1f : 1.0f` (10 Hz active / 1 Hz idle). Going to 20–30 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. ~80–120 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:37–179` (`CreateMoveToChain`) and `Player_Inventory.cs:976–1106` (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.