From a0fa3d68a7ad281385358b1d5305e841699dc8c9 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 07:56:02 +0200 Subject: [PATCH] fix(B.6+B.7): flush AutonomousPosition on arrival before re-sending action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/AcDream.App/Rendering/GameWindow.cs | 63 ++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) 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))