diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 45ab2c9..b54eb60 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -9170,19 +9170,26 @@ public sealed class GameWindow : IDisposable
///
/// B.6+B.7 (2026-05-15). Fires when
- /// 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.
///
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);
}
+ ///
+ /// 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.
+ ///
+ 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))