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>
29 KiB
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 — B.5 (pickup) shipped immediately before.
- docs/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 — 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 sendsUpdateMotion (0xF74D)withMovementType=6carrying the destination guid. We now synthesizeForward+Runinput intoPlayerMovementControllerto 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-sideWithinUseRadiuspoll 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::PerformMovementat0x00524440— dispatch switch on MovementType.MoveToManager::HandleMoveToObject— MovementType=6 driver (turn-to-face → walk → stop).MoveToManager::HandleMoveToPosition— MovementType=7 driver.MoveToManager::HandleTurnToHeadingat0x0052a0c0— turn-only driver used by MovementType=8.CPhysicsObj::MoveToObjectat0x00512860— high-level entry from physics.Player_Move.CreateMoveToChain(ACE) atPlayer_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:
// 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, commit1a0656a).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 byGameWindow:- 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): ift_near < 0uset_farso 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 ofgmRadarUI::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 —
WorldPicker5 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):
- 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.
- 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.
- 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. - 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.) - Far-range double-click on item. Same as (2) but double-click — should behave identically to F-key (double-click activation passes through
OnInputActionafter58b95bc).
Diagnostic env vars active for this work:
ACDREAM_PROBE_AUTOWALK=1— one[autowalk]line per inbound MoveToObject + state transition (commiteda8278). Also toggleable via DebugPanel.ACDREAM_DUMP_MOTION=1— every inboundUpdateMotion(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/SendPickUpdefer logic,_pendingPostArrivalAction,OnAutoWalkArrivedReSendAction,InstallSpeculativeTurnToTarget,IsCloseRangeTarget,SendAutonomousPositionNow,TargetIndicatorPanelwiring, per-entity picker callbacks,UseCurrentSelectionsmart-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—ProbeAutoWalkEnabledstatic property,ACDREAM_PROBE_AUTOWALKenv 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:
- Arrival safety margin (currently 0.05 m).
ApplyAutoWalkOverlaystops atdist <= _autoWalkMinDistance - 0.05f. Retail stops atdist <= radius. Drop the margin when our outbound position is fresh enough that ACE'sWithinUseRadiuspoll always sees us inside the radius the moment we get there. - Re-send on arrival.
_pendingPostArrivalAction+OnAutoWalkArrivedReSendActionre-fireSendUse/SendPickUpafter the body arrives. Retail's client sends the action once and lets the server-sideMoveToChaincomplete. Drop when ACE consistently completes the chain from a single send. - AutonomousPosition flush on arrival.
SendAutonomousPositionNow()explicitly broadcasts position the moment we arrive. With per-tick outbound this happens naturally. isRetryAfterArrivalflag. Branch inSendUse/SendPickUpto 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)
- Fix the sign indicator box. User's last gripe. Bump
EntityHeightFordefault 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. - 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.
- Issue #69 — animate rotation. Synthesize
TurnLeft/TurnRightinput flags while the overlay turns the body. ~30 LOC inApplyAutoWalkOverlay+ verify retail's human motion table has the cycle. Pairs with #66 nicely. - Issue #68 — Remote players don't stop run animation on arrival. Wire
RemoteMoveToDriver.ArrivedtoSetCycle(NonCombat, Ready). ~20 LOC. Small standalone fix. - 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. - 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. - 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 withACDREAM_PROBE_AUTOWALK=1.
Predecessor reading order for a fresh session
- This document — the full picture of what's in main.
docs/superpowers/specs/2026-05-14-phase-b6-design.md— retail anchors + decomp citations for auto-walk.docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md— B.7 design + deferred-MVP list.docs/research/2026-05-14-b5-shipped-handoff.md— B.5 (pickup, close-range path) preceded this work.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::HandleMoveToObjectMoveToManager::HandleMoveToPositionMoveToManager::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.