fix(B.6+B.7): re-send action on local arrival; scale indicator box by entity Scale
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).
This commit is contained in:
parent
5f83766de5
commit
2dc28bb61f
3 changed files with 132 additions and 15 deletions
|
|
@ -250,6 +250,23 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public bool IsServerAutoWalking => _autoWalkActive;
|
||||
|
||||
/// <summary>
|
||||
/// Fires once when an auto-walk reaches its destination naturally
|
||||
/// (i.e. <see cref="EndServerAutoWalk"/> called with
|
||||
/// <c>reason="arrived"</c>). Does NOT fire on user-input cancel or
|
||||
/// on a re-target (BeginServerAutoWalk overwriting state).
|
||||
///
|
||||
/// <para>
|
||||
/// Host (<see cref="Rendering.GameWindow"/>) 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -790,6 +790,15 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> _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);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </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.
|
||||
_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 /
|
||||
|
|
|
|||
|
|
@ -38,11 +38,15 @@ public sealed class TargetIndicatorPanel
|
|||
/// What the panel needs to know about the selected entity per frame.
|
||||
/// <c>ItemType</c> + <c>ObjectDescriptionFlags</c> feed
|
||||
/// <see cref="RadarBlipColors.For"/> for colour selection.
|
||||
/// <c>Scale</c> multiplies the per-type base height in
|
||||
/// <see cref="EntityHeightFor"/> — a scaled-up sign or oversized NPC
|
||||
/// gets a proportionally bigger box.
|
||||
/// </summary>
|
||||
public readonly record struct TargetInfo(
|
||||
Vector3 WorldPosition,
|
||||
uint ItemType,
|
||||
uint ObjectDescriptionFlags);
|
||||
uint ObjectDescriptionFlags,
|
||||
float Scale);
|
||||
|
||||
private readonly Func<uint?> _selectedGuidProvider;
|
||||
private readonly Func<uint, TargetInfo?> _entityResolver;
|
||||
|
|
@ -64,24 +68,68 @@ public sealed class TargetIndicatorPanel
|
|||
|
||||
/// <summary>
|
||||
/// 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 <see cref="EntityHeight"/>
|
||||
/// for entities without a recognisable type tag.
|
||||
/// indicator box. The base height per type is multiplied by the
|
||||
/// entity's <paramref name="scale"/> so an upscaled sign or NPC
|
||||
/// gets a proportionally bigger box.
|
||||
///
|
||||
/// <para>Per-type base height:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Creature (NPC / monster / player): 1.8 m (humanoid)</item>
|
||||
/// <item>Door / Lifestone / Portal: 2.4 m (door-frame tall)</item>
|
||||
/// <item>Small carry items (Money, Food, Gem, SpellComponents,
|
||||
/// Misc, Weapons, Armour, Clothing, Jewelry, Container):
|
||||
/// 0.8 m (item dropped on the ground)</item>
|
||||
/// <item>Everything else (signs, generic objects, untyped
|
||||
/// scenery interactables): 1.5 m (mid-sized object
|
||||
/// default; without mesh AABB this is a best guess)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue