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:
parent
32352af583
commit
5b908bcca2
2 changed files with 95 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue