diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 60184ae..0111505 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,35 +46,44 @@ Copy this block when adding a new issue: # Active issues -## #67 — Door Use action doesn't complete after auto-walk arrival +## #68 — Remote players don't stop running animation on auto-walk arrival **Status:** OPEN -**Severity:** MEDIUM (M1-affecting — doors are M1 demo target 2) +**Severity:** LOW-MEDIUM (visual only — server-side action completes correctly) **Filed:** 2026-05-15 (B.7 visual verification) -**Component:** net / interaction +**Component:** motion / remote dead-reckoning / animation cycle -**Description:** After B.6's auto-walk + B.7's flush-AP-and-re-send, NPC -dialogue fires correctly on out-of-range Use. But Use on a door from -out-of-range still doesn't open the door — the player walks to the -door, sends the re-send, and nothing happens. +**Description:** Observing a retail player from acdream as they approach +an NPC at a distance: the remote body's run animation keeps cycling +even after the body has visibly stopped at the NPC. Retail-side the +character stopped; the action (dialogue) fired; but our client's +animation never transitioned RunForward → Ready. -**Suspected causes (need investigation):** -1. Door's `objDist` on the wire might be different from what we - parse, so the local arrival lands outside ACE's actual - door-use radius. -2. ACE's `Door.ActOnUse` may require additional client-side state - (specific stance, motion, facing) that our re-sent Use lacks. -3. The door's ObjectDescriptionFlags `BF_DOOR (0x1000)` may not - be properly captured / forwarded for our picker's per-type - radius logic to fire. +**Suspected:** `RemoteMoveToDriver` detects arrival via +`DriveResult.Arrived`, but the consumer site (per-tick loop in +`GameWindow.TickAnimations` or wherever the remote body's cycle is +driven) doesn't flip the animation cycle back to Ready on arrival. +Alternatively the cycle persists because ACE doesn't broadcast a +follow-up `UpdateMotion(Ready)` — relying on the client to detect +arrival from the wire's distance threshold instead. -**Acceptance:** F or R-key on a selected door from > 0.6 m runs the -auto-walk, arrives, opens the door (player walks through; B.4c swing -animation plays). +**Files (likely):** +- `src/AcDream.App/Rendering/GameWindow.cs` — wherever per-tick motion + for remote entities reads `RemoteMoveToDriver`'s state. Need to + call `SetCycle(NonCombat, Ready)` on arrival. -**Files (likely):** `src/AcDream.App/Rendering/GameWindow.cs` -`SendUse`; `src/AcDream.App/Input/PlayerMovementController.cs` -arrival; `references/ACE/Source/ACE.Server/WorldObjects/Door.cs`. +**Acceptance:** Retail player observed running up to an NPC visibly +stops running animation at arrival distance, transitions to idle. + +--- + +## #67 — [DONE 2026-05-15 · `301281d`] Door Use action doesn't complete after auto-walk arrival + +**Status:** DONE — fixed by `301281d` (10 Hz heartbeat during motion). +With ACE seeing our position in near-real-time, its `CreateMoveToChain` +converges normally for doors as well as NPCs. Root cause was 1 Hz +position sync on our side, not anything door-specific. User confirmed +doors work after the heartbeat bump. --- diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index ada4855..29b68fe 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -445,22 +445,17 @@ public sealed class PlayerMovementController float dy = _autoWalkDestination.Y - pos.Y; float dist = MathF.Sqrt(dx * dx + dy * dy); - // Arrival predicate. CRITICAL: ACE's server-side WithinUseRadius - // is strict (dist <= radius), so arriving exactly at the radius - // boundary fails — ACE rejects the action and replies with - // another MoveToObject. We walk slightly INSIDE the boundary so - // the re-sent action lands safely in-range. - // - // The margin is small — user feedback says retail fires Use - // from longer range, so we minimise the over-walk: 0.2 m at - // typical NPC radii (3 m → arrive at 2.8 m), tapered for tight - // pickup radii (0.6 m → arrive at 0.48 m) so the body stays - // reachable but always inside ACE's strict check. + // Arrival predicate. With the 10 Hz heartbeat from 301281d the + // server-side Player.Location tracks our body within ~100 ms, so + // the previous "subtract 0.2 m safety margin" workaround is no + // longer needed. Tiny 0.05 m margin remains to absorb the + // sub-tick race between local arrival-fire and the next + // heartbeat's outbound packet. float arrivalThreshold = _autoWalkMoveTowards ? _autoWalkDistanceToObject : _autoWalkMinDistance; - float safetyMargin = MathF.Min(0.2f, arrivalThreshold * 0.2f); - float effectiveArrival = MathF.Max(arrivalThreshold - safetyMargin, 0.1f); + const float TinyMargin = 0.05f; + float effectiveArrival = MathF.Max(arrivalThreshold - TinyMargin, 0.1f); bool arrived = (_autoWalkMoveTowards && dist <= effectiveArrival) @@ -476,6 +471,14 @@ public sealed class PlayerMovementController // _body.Orientation = Quaternion.CreateFromAxisAngle(Z, Yaw - π/2), // so local-forward (+Y) maps to world (cos Yaw, sin Yaw, 0). // Therefore Yaw that faces (dx,dy) is atan2(dy, dx). + // + // User feedback (2026-05-15): 'I should face that object and then + // start moving. Now it starts running before facing is complete.' + // Track the current heading delta — if we're more than the + // walk-while-turning threshold off, suppress Forward this frame + // so the body turns IN PLACE first. Once we're within the + // threshold, the synthesised Forward+Run kicks in below. + bool aligned = true; if (dist > 1e-4f) { float desiredYaw = MathF.Atan2(dy, dx); @@ -493,6 +496,14 @@ public sealed class PlayerMovementController } while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI; while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI; + + // 30° "walk-while-turning" threshold: outside this, body + // turns in place. Inside, body walks forward while finishing + // any remaining alignment. Matches retail-feel observation; + // exact retail value is in MoveToManager but ~30° is a + // sensible heuristic for now. + const float WalkWhileTurningRad = 30f * MathF.PI / 180f; + aligned = MathF.Abs(delta) <= WalkWhileTurningRad; } // Walk vs run decided ONCE at BeginServerAutoWalk based on @@ -503,6 +514,11 @@ public sealed class PlayerMovementController // all the way to the object and then stop"). bool shouldRun = _autoWalkInitiallyRunning; + // Turn-first gate: if not yet aligned with the target, suppress + // forward motion so the body turns in place rather than + // walking an arc. + bool moveForward = aligned; + // Synthesize "moving forward" input. The rest of Update reads // Yaw + input.Forward + input.Run to drive _motion + body // velocity exactly as it does for user-driven W (+ optional Shift). @@ -510,8 +526,8 @@ public sealed class PlayerMovementController // steering. return input with { - Forward = true, - Run = shouldRun, + Forward = moveForward, + Run = moveForward && shouldRun, Backward = false, StrafeLeft = false, StrafeRight = false,