fix(B.6): close-range turn-to-face — install overlay on Use/PickUp send

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).
This commit is contained in:
Erik 2026-05-15 12:05:37 +02:00
parent 32352af583
commit 5b908bcca2
2 changed files with 95 additions and 8 deletions

View file

@ -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);
}
/// <summary>
/// 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:
/// <list type="bullet">
/// <item>Far target → ACE broadcasts a <c>MovementType=6</c>
/// 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.</item>
/// <item>Close target → ACE skips MoveToChain (WithinUseRadius
/// shortcut at <c>Player_Move.cs:66</c>) and rotates the body
/// server-side via <c>Rotate(target)</c>. 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.</item>
/// </list>
/// <para>
/// 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).
/// </para>
/// </summary>
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);
}
/// <summary>
/// B.6+B.7 (2026-05-15). Send an out-of-frame AutonomousPosition
/// packet using the controller's current authoritative state.