From 5b908bcca25162fd0f54adc22d6e86f6b648e7a9 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 12:05:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(B.6):=20close-range=20turn-to-face=20?= =?UTF-8?q?=E2=80=94=20install=20overlay=20on=20Use/PickUp=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: 'It should always face the NPC. When I'm close I'm not facing. But now it turns if I'm far away.' Cause: ACE skips MoveToChain when the player is already within WithinUseRadius (Player_Move.cs:66 shortcut) — it rotates the body server-side via Rotate(target) but doesn't broadcast a MovementType=6 to us, so our auto-walk overlay never installs. The local body never turns; the player remains facing wherever the camera/mouse last left them. Fix has two pieces: 1. PlayerMovementController.ApplyAutoWalkOverlay: arrival is now gated on BOTH within-radius AND aligned. Previously a body that started already in-range ended the overlay before turning. Now it turns in place, then ends once facing. Also: forward motion stays suppressed while withinArrival (we just need to finish the turn, no point stepping forward into a target we're already touching). 2. GameWindow.SendUse / SendPickUp: install a speculative auto-walk overlay at send time via new InstallSpeculativeTurnToTarget helper. For far targets ACE's MovementType=6 arrives moments later and overwrites with its wire-supplied use-radius. For close targets our overlay is the only thing that runs — body turns, then ends. The per-type use-radius mirrors the picker's heuristic (3 m creature / 2 m large flat / 0.6 m item). --- .../Input/PlayerMovementController.cs | 31 +++++--- src/AcDream.App/Rendering/GameWindow.cs | 72 +++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 29b68fe..0cf7b4b 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -451,21 +451,25 @@ public sealed class PlayerMovementController // 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. + // + // ARRIVAL IS GATED ON ALIGNMENT: we only end the auto-walk once + // the body is BOTH within use-radius AND facing the target. + // Without the alignment gate, a Use on a close target while + // facing away would end immediately and the body wouldn't turn + // at all (user feedback 2026-05-15: 'when I'm close I'm not + // facing'). The alignment check is computed below in the same + // block as the heading-step; we defer the arrival fire-and-end + // until after we've inspected `aligned`. float arrivalThreshold = _autoWalkMoveTowards ? _autoWalkDistanceToObject : _autoWalkMinDistance; const float TinyMargin = 0.05f; float effectiveArrival = MathF.Max(arrivalThreshold - TinyMargin, 0.1f); - bool arrived = + bool withinArrival = (_autoWalkMoveTowards && dist <= effectiveArrival) || (!_autoWalkMoveTowards && dist >= arrivalThreshold + RemoteMoveToDriver.ArrivalEpsilon); - if (arrived) - { - EndServerAutoWalk("arrived"); - return input; // falls through to Ready (no Forward) → body stops - } // Step Yaw toward target. Convention from Update line 364: // _body.Orientation = Quaternion.CreateFromAxisAngle(Z, Yaw - π/2), @@ -506,6 +510,16 @@ public sealed class PlayerMovementController aligned = MathF.Abs(delta) <= WalkWhileTurningRad; } + // End the auto-walk once the body is BOTH within use radius + // AND aligned with the target. This is the alignment-gated + // arrival the comment above flagged: a close-range Use on a + // target behind the player still rotates the body first. + if (withinArrival && aligned) + { + EndServerAutoWalk("arrived"); + return input; + } + // Walk vs run decided ONCE at BeginServerAutoWalk based on // initial distance — held for the rest of the auto-walk so the // character keeps running all the way to the target instead of @@ -516,8 +530,9 @@ public sealed class PlayerMovementController // 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; + // walking an arc. Also suppress when already within arrival — + // we just turned to face it; no need to step forward into it. + bool moveForward = aligned && !withinArrival; // Synthesize "moving forward" input. The rest of Update reads // Yaw + input.Forward + input.Run to drive _motion + body diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b54eb60..d2d960d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9113,6 +9113,17 @@ public sealed class GameWindow : IDisposable _debugVm?.AddToast("Not in world"); return; } + // B.6 (2026-05-15): install a speculative auto-walk on the local + // player toward the target. For far targets ACE will overwrite + // this with its own MovementType=6 wire payload (and a better + // wire-supplied use-radius). For close-range targets ACE skips + // MoveToChain entirely and just rotates server-side; our + // overlay provides the matching local rotation. Either way the + // alignment-gated arrival ensures the body finishes facing + // the target before stopping. + if (!isRetryAfterArrival) + InstallSpeculativeTurnToTarget(guid); + var seq = _liveSession.NextGameActionSequence(); var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid); _liveSession.SendGameAction(body); @@ -9137,6 +9148,11 @@ public sealed class GameWindow : IDisposable _debugVm?.AddToast("Not in world"); return; } + // B.6 (2026-05-15): same speculative turn-to-target install as + // SendUse — close-range pickup still rotates locally to face + // the item before the pickup resolves server-side. + if (!isRetryAfterArrival) + InstallSpeculativeTurnToTarget(itemGuid); // B.5 polish (2026-05-14): silently block client-side when the // selected entity is a creature/NPC. ACE's @@ -9197,6 +9213,62 @@ public sealed class GameWindow : IDisposable else SendUse(guid, isRetryAfterArrival: true); } + /// + /// B.6 (2026-05-15). Install a local auto-walk overlay against the + /// given target entity at Use / PickUp send time. ACE's response + /// branches by distance: + /// + /// Far target → ACE broadcasts a MovementType=6 + /// MoveToObject which arrives shortly after and overwrites + /// our speculative state with ACE's wire-supplied use-radius + /// and origin. No conflict; same target either way. + /// Close target → ACE skips MoveToChain (WithinUseRadius + /// shortcut at Player_Move.cs:66) and rotates the body + /// server-side via Rotate(target). ACE doesn't broadcast + /// anything actionable to us, so our pre-installed overlay + /// handles the local rotation — body turns to face the target + /// in place, then ends. + /// + /// + /// Per-type use radius mirrors the picker's radius heuristic: + /// 3 m for creatures, 2 m for doors / lifestones / portals, + /// 0.6 m for everything else (ground items). + /// + /// + private void InstallSpeculativeTurnToTarget(uint targetGuid) + { + if (_playerController is null) return; + if (!_entitiesByServerGuid.TryGetValue(targetGuid, out var entity)) + return; + + // Per-type use radius — same heuristic as the picker's + // radiusForGuid callback. + float useRadius = 0.6f; + if (_liveEntityInfoByGuid.TryGetValue(targetGuid, out var info) + && (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0) + { + useRadius = 3.0f; + } + else if (_lastSpawnByGuid.TryGetValue(targetGuid, out var spawn) + && spawn.ObjectDescriptionFlags is { } odf) + { + // BF_DOOR | BF_LIFESTONE | BF_PORTAL | BF_CORPSE + const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u; + if ((odf & LargeFlatMask) != 0) useRadius = 2.0f; + } + + _playerController.BeginServerAutoWalk( + destinationWorld: entity.Position, + minDistance: 0f, + distanceToObject: useRadius, + moveTowards: true, + // High walkRunThreshold → walk mode by default. If the + // body is far enough to actually need to run, ACE's + // subsequent MovementType=6 will overwrite with the + // wire's real threshold. + walkRunThreshold: 9999f); + } + /// /// B.6+B.7 (2026-05-15). Send an out-of-frame AutonomousPosition /// packet using the controller's current authoritative state.