From 2dc28bb61f575e7fa65b6c94b647c69d297790f9 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 07:45:27 +0200 Subject: [PATCH] fix(B.6+B.7): re-send action on local arrival; scale indicator box by entity Scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: 'It still however just approach it and does not use it.' Root cause: local auto-walk arrives at the target visually, but ACE's server-side MoveToChain may have timed out before our position was recognised as in-range (we don't echo authoritative position back to ACE during the walk yet). The action never fires. Fix (re-send on arrival): * PlayerMovementController.AutoWalkArrived event fires once when EndServerAutoWalk(reason='arrived') is called. * GameWindow tracks _pendingPostArrivalAction = (guid, isPickup) on each SendUse / SendPickUp. * OnAutoWalkArrivedReSendAction (subscribed at EnterPlayerModeNow) re-sends the action with isRetryAfterArrival=true. The retry flag prevents the re-sent action from itself setting a new pending action — breaks any potential re-fire loop. * The re-sent action is close-range from the local body's perspective, so ACE's CreateMoveToChain hits the WithinUseRadius shortcut (Player_Move.cs:66) and completes immediately — dialogue opens, item picks up. User report: 'items dropped on the ground now have a smaller triangle box, perhaps too small. Also now other stuff like signs also have a very small triangle box, should not have it should scale to the size of the object.' Fix (scale-aware indicator height): * TargetIndicatorPanel.TargetInfo now carries entity Scale. * EntityHeightFor multiplies the per-type base by Scale so an upscaled NPC / sign / lifestone gets a proportionally larger box. * Per-type table refined: Creature : 1.8 m * scale Door/Lifestone/Portal : 2.4 m * scale Small carry items (weapon/armor/clothing/jewelry/food/money/ misc/missile-weapon/container/gem/spellcomp/writable/key/ caster — most pickup-able): 0.8 m * scale (up from 0.5 m) Everything else (signs / scenery interactables / untyped): 1.5 m * scale (up from 0.5 m default) Deferred to follow-up: exact mesh-AABB-derived box (need to read each entity's actual rendered bounds at registration time). --- .../Input/PlayerMovementController.cs | 19 +++++ src/AcDream.App/Rendering/GameWindow.cs | 56 ++++++++++++++- src/AcDream.App/UI/TargetIndicatorPanel.cs | 72 +++++++++++++++---- 3 files changed, 132 insertions(+), 15 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 55bb81f..71f3239 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -250,6 +250,23 @@ public sealed class PlayerMovementController /// public bool IsServerAutoWalking => _autoWalkActive; + /// + /// Fires once when an auto-walk reaches its destination naturally + /// (i.e. called with + /// reason="arrived"). Does NOT fire on user-input cancel or + /// on a re-target (BeginServerAutoWalk overwriting state). + /// + /// + /// Host () subscribes to re-send + /// the Use/PickUp action that triggered the auto-walk — without + /// this, ACE's server-side MoveToChain may have already timed out + /// by the time our local body arrives, so the action wouldn't + /// fire. Re-sending the action close-range hits ACE's WithinUseRadius + /// fast-path and completes immediately. + /// + /// + public event Action? AutoWalkArrived; + public PlayerMovementController(PhysicsEngine physics) { _physics = physics; @@ -384,6 +401,8 @@ public sealed class PlayerMovementController _autoWalkActive = false; if (PhysicsDiagnostics.ProbeAutoWalkEnabled) Console.WriteLine($"[autowalk-end] reason={reason}"); + if (reason == "arrived") + AutoWalkArrived?.Invoke(); } /// diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b3bc3a9..45ab2c9 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -790,6 +790,15 @@ public sealed class GameWindow : IDisposable private readonly Dictionary _lastSpawnByGuid = new(); // Current selection: written by Q-cycle (combat) and LMB click (interact); cleared on entity despawn. private uint? _selectedGuid; + + // B.6+B.7 (2026-05-15): pending action that triggered an auto-walk. + // When the local body arrives at the auto-walk target, + // OnAutoWalkArrivedReSendAction re-sends the action close-range so + // ACE completes it via WithinUseRadius even if its server-side + // MoveToChain already timed out. Cleared before each re-send to + // prevent infinite loops (the re-sent action's auto-walk would + // arrive immediately at the same position, infinite re-fire). + private (uint Guid, bool IsPickup)? _pendingPostArrivalAction; private readonly record struct LiveEntityInfo( string? Name, AcDream.Core.Items.ItemType ItemType); @@ -1177,7 +1186,7 @@ public sealed class GameWindow : IDisposable && spawn.ObjectDescriptionFlags is { } odf) pwdBits = odf; return new AcDream.App.UI.TargetIndicatorPanel.TargetInfo( - entity.Position, rawItemType, pwdBits); + entity.Position, rawItemType, pwdBits, entity.Scale); }, cameraProvider: () => { @@ -9096,7 +9105,7 @@ public sealed class GameWindow : IDisposable else SendUse(sel); } - private void SendUse(uint guid) + private void SendUse(uint guid, bool isRetryAfterArrival = false) { if (_liveSession is null || _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld) @@ -9114,9 +9123,13 @@ public sealed class GameWindow : IDisposable Console.WriteLine(System.FormattableString.Invariant( $"[autowalk-out] op=use target=0x{guid:X8} name=\"{label}\" seq={seq}")); } + // Remember this action so OnAutoWalkArrivedReSendAction can + // re-fire it close-range. Skip when this IS the re-send. + if (!isRetryAfterArrival) + _pendingPostArrivalAction = (guid, false); } - private void SendPickUp(uint itemGuid) + private void SendPickUp(uint itemGuid, bool isRetryAfterArrival = false) { if (_liveSession is null || _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld) @@ -9149,6 +9162,32 @@ public sealed class GameWindow : IDisposable Console.WriteLine(System.FormattableString.Invariant( $"[autowalk-out] op=pickup target=0x{itemGuid:X8} name=\"{label}\" seq={seq}")); } + // Remember this action so OnAutoWalkArrivedReSendAction can + // re-fire it close-range. Skip when this IS the re-send. + if (!isRetryAfterArrival) + _pendingPostArrivalAction = (itemGuid, true); + } + + /// + /// 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. + /// + 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. + _pendingPostArrivalAction = null; + if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled) + Console.WriteLine(System.FormattableString.Invariant( + $"[autowalk-arrived-resend] guid=0x{guid:X8} isPickup={isPickup}")); + if (isPickup) SendPickUp(guid, isRetryAfterArrival: true); + else SendUse(guid, isRetryAfterArrival: true); } private uint? SelectClosestCombatTarget(bool showToast) @@ -9320,6 +9359,17 @@ public sealed class GameWindow : IDisposable } _playerController = new AcDream.App.Input.PlayerMovementController(_physicsEngine); + + // B.6+B.7 (2026-05-15): re-send the Use/PickUp action on local + // auto-walk arrival. ACE's server-side MoveToChain may have + // already timed out by the time the local body arrives (we + // walk locally but don't send tracking position updates to + // ACE during the walk yet, so ACE's WithinUseRadius check may + // never have passed). Resending close-range hits ACE's + // CreateMoveToChain WithinUseRadius shortcut (Player_Move.cs:66) + // and completes the action immediately. + _playerController.AutoWalkArrived += OnAutoWalkArrivedReSendAction; + // K-fix7 (2026-04-26): if PlayerDescription already arrived, the // server's Run / Jump skill values are cached here — push them // into the freshly-constructed controller so the runRate / diff --git a/src/AcDream.App/UI/TargetIndicatorPanel.cs b/src/AcDream.App/UI/TargetIndicatorPanel.cs index 21d86ae..e657f40 100644 --- a/src/AcDream.App/UI/TargetIndicatorPanel.cs +++ b/src/AcDream.App/UI/TargetIndicatorPanel.cs @@ -38,11 +38,15 @@ public sealed class TargetIndicatorPanel /// What the panel needs to know about the selected entity per frame. /// ItemType + ObjectDescriptionFlags feed /// for colour selection. + /// Scale multiplies the per-type base height in + /// — a scaled-up sign or oversized NPC + /// gets a proportionally bigger box. /// public readonly record struct TargetInfo( Vector3 WorldPosition, uint ItemType, - uint ObjectDescriptionFlags); + uint ObjectDescriptionFlags, + float Scale); private readonly Func _selectedGuidProvider; private readonly Func _entityResolver; @@ -64,24 +68,68 @@ public sealed class TargetIndicatorPanel /// /// Resolve the world-space height to use for a given entity's - /// indicator box. Humanoids (Creature flag) use 1.8 m; doors / - /// lifestones / portals use 2.4 m (door-frame tall); ground items - /// use 0.5 m so the box hugs the item rather than ballooning out - /// to humanoid height. Falls back to - /// for entities without a recognisable type tag. + /// indicator box. The base height per type is multiplied by the + /// entity's so an upscaled sign or NPC + /// gets a proportionally bigger box. + /// + /// Per-type base height: + /// + /// Creature (NPC / monster / player): 1.8 m (humanoid) + /// Door / Lifestone / Portal: 2.4 m (door-frame tall) + /// Small carry items (Money, Food, Gem, SpellComponents, + /// Misc, Weapons, Armour, Clothing, Jewelry, Container): + /// 0.8 m (item dropped on the ground) + /// Everything else (signs, generic objects, untyped + /// scenery interactables): 1.5 m (mid-sized object + /// default; without mesh AABB this is a best guess) + /// + /// + /// + /// Future refinement (deferred): read the entity's actual mesh + /// AABB at registration time and use the projected silhouette + /// for an exact-fit box. Issue #66-ish. + /// /// - public float EntityHeightFor(uint itemType, uint pwdBitfield) + public float EntityHeightFor(uint itemType, uint pwdBitfield, float scale) { + if (scale <= 0f) scale = 1f; // defensive bool isCreature = (itemType & (uint)AcDream.Core.Items.ItemType.Creature) != 0; - if (isCreature) return 1.8f; + if (isCreature) return 1.8f * scale; // BF_DOOR = 0x1000, BF_LIFESTONE = 0x4000, BF_PORTAL = 0x40000 // (acclient.h:6431-6463) const uint TallStructureMask = 0x1000u | 0x4000u | 0x40000u; - if ((pwdBitfield & TallStructureMask) != 0) return 2.4f; + if ((pwdBitfield & TallStructureMask) != 0) return 2.4f * scale; - // Default: small ground item / object. - return 0.5f; + // Small carry items: weapons / armour / clothing / jewellery / + // money / food / misc / weapons / containers / gems / spell + // components / writable / keys / casters / lockables / promissory + // notes / mana stones / craft bases / craft intermediates / + // tinkering tools / tinkering materials / gameboard. + // Per AcDream.Core.Items.ItemType — most non-zero values in + // the lower 28 bits map to carryable items. + const uint SmallItemMask = + (uint)(AcDream.Core.Items.ItemType.MeleeWeapon + | AcDream.Core.Items.ItemType.Armor + | AcDream.Core.Items.ItemType.Clothing + | AcDream.Core.Items.ItemType.Jewelry + | AcDream.Core.Items.ItemType.Food + | AcDream.Core.Items.ItemType.Money + | AcDream.Core.Items.ItemType.Misc + | AcDream.Core.Items.ItemType.MissileWeapon + | AcDream.Core.Items.ItemType.Container + | AcDream.Core.Items.ItemType.Gem + | AcDream.Core.Items.ItemType.SpellComponents + | AcDream.Core.Items.ItemType.Writable + | AcDream.Core.Items.ItemType.Key + | AcDream.Core.Items.ItemType.Caster); + if ((itemType & SmallItemMask) != 0) return 0.8f * scale; + + // Everything else (signs, scenery interactables, untyped objects): + // 1.5 m default — bigger than a small item but smaller than a + // humanoid, splitting the difference until we have real mesh + // bounds to project. + return 1.5f * scale; } /// @@ -132,7 +180,7 @@ public sealed class TargetIndicatorPanel // projection. if (!TryProjectToScreen(info.WorldPosition, viewProj, viewport, out var feetScreen)) return; - float entityHeight = EntityHeightFor(info.ItemType, info.ObjectDescriptionFlags); + float entityHeight = EntityHeightFor(info.ItemType, info.ObjectDescriptionFlags, info.Scale); var headWorld = new Vector3( info.WorldPosition.X, info.WorldPosition.Y,