fix(B.6+B.7): flush AutonomousPosition on arrival before re-sending action

Previous re-send-on-arrival didn't actually unstick the action. Trace
showed ACE replying to the re-sent Use with another MoveToObject —
i.e. ACE's Player.Location was still the pre-walk position, so the
'I'm in range' fast-path didn't fire.

Cause: packet ordering. OnAutoWalkArrivedReSendAction was firing the
re-send immediately (sub-frame), BEFORE the next per-frame
AutonomousPosition heartbeat. ACE processed the Use against stale
location data.

Fix: SendAutonomousPositionNow() — an out-of-frame AutonomousPosition
build using the controller's current authoritative position +
rotation + cell. Called from OnAutoWalkArrivedReSendAction BEFORE the
re-send. ACE now processes 'I'm here at (target_pos)' then 'Use'
in order; CreateMoveToChain's WithinUseRadius shortcut
(Player_Move.cs:66) fires immediately and the action completes.

[autowalk-flush-ap] trace line under ACDREAM_PROBE_AUTOWALK so the
sequence is visible end-to-end:
  autowalk-end → autowalk-flush-ap → autowalk-arrived-resend → autowalk-out
This commit is contained in:
Erik 2026-05-15 07:56:02 +02:00
parent 2dc28bb61f
commit a0fa3d68a7

View file

@ -9170,19 +9170,26 @@ public sealed class GameWindow : IDisposable
/// <summary>
/// B.6+B.7 (2026-05-15). Fires when <see cref="PlayerMovementController.AutoWalkArrived"/>
/// signals natural arrival at an auto-walk target. Re-sends the
/// Use/PickUp action that started the walk so ACE completes it via
/// the WithinUseRadius shortcut even if its server-side MoveToChain
/// already gave up.
/// signals natural arrival at an auto-walk target. Force-flushes
/// the player's current authoritative position to ACE first, then
/// re-sends the Use/PickUp. Without the position flush, ACE
/// processes the re-sent Use before the next per-frame
/// AutonomousPosition heartbeat — so ACE's Player.Location is
/// still stale (back where the auto-walk started) and ACE replies
/// with another MoveToObject instead of completing the action.
/// </summary>
private void OnAutoWalkArrivedReSendAction()
{
if (_pendingPostArrivalAction is not (uint guid, bool isPickup) pending)
return;
// Clear FIRST to break any retry loop — if ACE somehow re-sends
// MoveToObject for the close-range action, we don't want
// arrival to fire a third action.
// Clear FIRST to break any retry loop.
_pendingPostArrivalAction = null;
// Send a fresh AutonomousPosition NOW so ACE's server-side
// Player.Location updates to our arrived position before ACE
// sees the re-sent action.
SendAutonomousPositionNow();
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-arrived-resend] guid=0x{guid:X8} isPickup={isPickup}"));
@ -9190,6 +9197,48 @@ public sealed class GameWindow : IDisposable
else SendUse(guid, isRetryAfterArrival: true);
}
/// <summary>
/// B.6+B.7 (2026-05-15). Send an out-of-frame AutonomousPosition
/// packet using the controller's current authoritative state.
/// Used to flush position to ACE on auto-walk arrival before
/// re-sending the Use/PickUp action; without it, ACE's
/// Player.Location is the pre-walk position and the action
/// resolves out-of-range.
/// </summary>
private void SendAutonomousPositionNow()
{
if (_liveSession is null
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld
|| _playerController is null)
return;
var pos = _playerController.Position;
int lbX = _liveCenterX + (int)MathF.Floor(pos.X / 192f);
int lbY = _liveCenterY + (int)MathF.Floor(pos.Y / 192f);
float localX = pos.X - (lbX - _liveCenterX) * 192f;
float localY = pos.Y - (lbY - _liveCenterY) * 192f;
uint wireCellId = ((uint)lbX << 24) | ((uint)lbY << 16) | (_playerController.CellId & 0xFFFFu);
var wirePos = new System.Numerics.Vector3(localX, localY, pos.Z);
var wireRot = YawToAcQuaternion(_playerController.Yaw);
byte contactByte = _playerController.IsAirborne ? (byte)0 : (byte)1;
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.AutonomousPosition.Build(
gameActionSequence: seq,
cellId: wireCellId,
position: wirePos,
rotation: wireRot,
instanceSequence: _liveSession.InstanceSequence,
serverControlSequence: _liveSession.ServerControlSequence,
teleportSequence: _liveSession.TeleportSequence,
forcePositionSequence: _liveSession.ForcePositionSequence,
lastContact: contactByte);
_liveSession.SendGameAction(body);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-flush-ap] seq={seq} cell=0x{wireCellId:X8} pos=({wirePos.X:F2},{wirePos.Y:F2},{wirePos.Z:F2})"));
}
private uint? SelectClosestCombatTarget(bool showToast)
{
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var playerEntity))