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

29 KiB
Raw Blame History

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:


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 movingsingle 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:

// 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:

// 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:

  • #59WorldPicker 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

# 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.csSendUse/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.csProbeAutoWalkEnabled 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 — retail anchors + decomp citations for auto-walk.
  3. docs/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 — B.5 (pickup, close-range path) preceded this work.
  5. docs/research/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.