feat(retail): Commit B — retail-faithful AP cadence + screen-rect picker
Retires divergences flagged in the 2026-05-16 faithfulness audit: 1. AP cadence. Replaces the 1 Hz idle / 10 Hz active flat heartbeat with a diff-driven model gated on `Contact && OnWalkable` (acclient_2013_pseudo_c.txt:700327 SendPositionEvent). Sends on position or cell change while grounded on walkable, plus a 1 sec heartbeat; suppressed entirely airborne. PlayerMovementController exposes `NotePositionSent(pos, cellId, now)` which GameWindow stamps after each AutonomousPosition / MoveToState send — mirrors retail's shared `last_sent_position_time` between SendPositionEvent (0x006b4770) and SendMovementEvent (0x006b4680). Known divergence from retail: ours is per-frame-while-moving, retail's effective rate is ~1 Hz during smooth motion (cell/plane checks). Filed as #74, blocked by #63 — when #63 lands we revert to retail's narrower gate. 2. Workaround retirement. Removes TinyMargin (0.05 m inside arrival) and the AP-flush before re-send (`SendAutonomousPositionNow`). The diff-driven cadence makes both obsolete. Close-range turn-first deferred Use is kept (it IS retail — ACE Player_Move.cs:66-87 mirrors retail's CreateMoveToChain pre-callback rotation), renamed `OnAutoWalkArrivedSendDeferredAction` to clarify it's a FIRST send. `isRetryAfterArrival` parameter dropped. 3. Far-range Use/PickUp retry. Restored — was load-bearing, not the "redundant cleanup" the Group 2 audit thought. Issue #63 means ACE drops the first Use as too-far without re-polling on subsequent APs; the arrival re-send is what makes far-range Use complete. Logs include `(queued for arrival re-send pending #63)` to make this explicit. Removes when #63 closes. 4. Screen-rect picker. New `AcDream.Core.Selection.ScreenProjection` helper shared by `WorldPicker` and `TargetIndicatorPanel`. The `Setup.SelectionSphere` projects to a screen-space square (retail anchor `SmartBox::GetObjectBoundingBox` 0x00452e20); picker hit-tests the mouse pixel against the same rect the indicator draws, inflated by 8 px (`TriangleSize`). Guarantees what-you-see is what-you-click — including rect corners that were dead zones under the old ray-sphere picker. Per-type radius (1.0/1.6/2.0 m) and vertical-offset (0.2/0.9/1.0/1.5 m) heuristic lambdas retired; `IsTallSceneryGuid` deleted; `EntityHeightFor` trimmed to 1.5 m × scale defensive default. No defensive sphere synth — entities without a baked `SelectionSphere` are skipped, matching retail's `GfxObjUnderSelectionRay` (0x0054c740). 5. Rotation rate run multiplier (Commit A precursor). `TurnRateFor(running)` helper applies retail's `run_turn_factor = 1.5f` (PDB-named 0x007c8914) under HoldKey.Run, matching `apply_run_to_command` at 0x00527be0 (line 305098). Effective: walking ≈ 90°/s, running ≈ 135°/s. Keyboard A/D + ApplyAutoWalkOverlay both use it. 6. Useability gate (Commit A precursor). `IsUseableTarget` corrected to `useability != 0` per `ItemUses::IsUseable` at 256455 — ANY non-zero passes (USEABLE_NO=1, USEABLE_CONTAINED=8, etc.), not just the USEABLE_REMOTE bit. Cross-checked against 4 call sites in retail (ItemHolder::UseObject 0x00588a80, DetermineUseResult 0x402697, UsingItem 0x367638, disable-button-state 0x198826). Added `ProbeUseabilityFallbackEnabled` diagnostic (`ACDREAM_PROBE_USEABILITY_FALLBACK=1`) to measure how often the creature/BF_DOOR fallback fires for ACE-seed-DB entities with null useability. CLAUDE.md updated with the graceful-shutdown rule for relaunch: Stop-Process bypasses the logout packet, leaving ACE's session marked logged-in for ~3+ min. CloseMainWindow() sends WM_CLOSE so the shutdown hook runs and the logout packet reaches ACE. Tests: +3 ScreenProjectionTests + 6 WorldPickerRectOverloadTests = +9. Core.Net 294/294 pass; Core 1073/1081 (8 pre-existing Physics failures unchanged). Visual-verified 2026-05-16: rotation rate, useability, screen-rect click area, double-click + R-key + F-key Use/PickUp at short and long range — dialogue/door/pickup fire on arrival. Filed follow-ups #70 (triangle apex/size DAT sprite), #71 (picker Stage B polygon refine), #72 (cdb omega.z probe), #73 (retail-message sweep pattern), #74 (per-frame AP chattier than retail — blocked by #63). Old ray-sphere `WorldPicker.Pick(origin, direction, ...)` overload kept for back-compat; no callers in acdream proper. Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2bc3a9e99
commit
b5da17db76
10 changed files with 1348 additions and 573 deletions
|
|
@ -791,13 +791,11 @@ public sealed class GameWindow : IDisposable
|
|||
// 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).
|
||||
// B.6/B.7 (2026-05-16): pending close-range action that will be fired
|
||||
// once the local auto-walk overlay reports arrival (body has finished
|
||||
// rotating to face the target). Only set for close-range Use/PickUp;
|
||||
// far-range sends fire the wire packet immediately at SendUse/SendPickUp
|
||||
// time. Cleared before the deferred send fires — single-fire, no retry.
|
||||
private (uint Guid, bool IsPickup)? _pendingPostArrivalAction;
|
||||
private readonly record struct LiveEntityInfo(
|
||||
string? Name,
|
||||
|
|
@ -6445,6 +6443,14 @@ public sealed class GameWindow : IDisposable
|
|||
DumpMovementTruthOutbound(
|
||||
"MTS", seq, result, wirePos, wireCellId, contactByte);
|
||||
_liveSession.SendGameAction(body);
|
||||
// B.6/B.7 (2026-05-16): stamp the diff-driven heartbeat clock so
|
||||
// HeartbeatDue resets its interval from THIS send — mirrors retail's
|
||||
// SendMovementEvent (acclient_2013_pseudo_c.txt:0x006b4680) writing
|
||||
// last_sent_position_time + last_sent_position after each MTS send.
|
||||
_playerController.NotePositionSent(
|
||||
worldPos: _playerController.Position,
|
||||
cellId: _playerController.CellId,
|
||||
nowSeconds: _playerController.SimTimeSeconds);
|
||||
}
|
||||
|
||||
if (_playerController.HeartbeatDue)
|
||||
|
|
@ -6463,6 +6469,14 @@ public sealed class GameWindow : IDisposable
|
|||
DumpMovementTruthOutbound(
|
||||
"AP", seq, result, wirePos, wireCellId, contactByte);
|
||||
_liveSession.SendGameAction(body);
|
||||
// B.6/B.7 (2026-05-16): stamp the diff-driven heartbeat clock so
|
||||
// HeartbeatDue resets its interval from THIS send — mirrors retail's
|
||||
// SendPositionEvent (acclient_2013_pseudo_c.txt:700345-700348)
|
||||
// writing last_sent_position_time + last_sent_position after each AP.
|
||||
_playerController.NotePositionSent(
|
||||
worldPos: _playerController.Position,
|
||||
cellId: _playerController.CellId,
|
||||
nowSeconds: _playerController.SimTimeSeconds);
|
||||
}
|
||||
|
||||
if (result.JumpExtent.HasValue && result.JumpVelocity.HasValue)
|
||||
|
|
@ -9007,69 +9021,41 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (_cameraController is null || _window is null) return;
|
||||
|
||||
// 2026-05-16 — retail-faithful screen-rect picker. The hit area
|
||||
// is the same screen-space rect the target indicator draws
|
||||
// (computed via the shared AcDream.Core.Selection.ScreenProjection
|
||||
// helper). Per user feedback 2026-05-16: clicking the indicator
|
||||
// brackets — including the rect corners — must select the entity.
|
||||
// The per-type radius/offset heuristics retired here (1.0/1.6/2.0
|
||||
// m radii, 0.2/0.9/1.0/1.5 m vertical offsets, IsTallSceneryGuid)
|
||||
// existed to make a 3D ray-sphere picker approximate the visible
|
||||
// rect; the new picker doesn't need them.
|
||||
var camera = _cameraController.Active;
|
||||
var (origin, direction) = AcDream.Core.Selection.WorldPicker.BuildRay(
|
||||
mouseX: _lastMouseX, mouseY: _lastMouseY,
|
||||
viewportW: _window.Size.X, viewportH: _window.Size.Y,
|
||||
view: camera.View, projection: camera.Projection);
|
||||
|
||||
if (direction.LengthSquared() < 1e-6f) return; // degenerate ray
|
||||
var viewport = new System.Numerics.Vector2((float)_window.Size.X, (float)_window.Size.Y);
|
||||
|
||||
var picked = AcDream.Core.Selection.WorldPicker.Pick(
|
||||
origin, direction,
|
||||
_entitiesByServerGuid.Values,
|
||||
mouseX: _lastMouseX, mouseY: _lastMouseY,
|
||||
view: camera.View, projection: camera.Projection,
|
||||
viewport: viewport,
|
||||
candidates: _entitiesByServerGuid.Values,
|
||||
skipServerGuid: _playerServerGuid,
|
||||
maxDistance: 50f,
|
||||
// B.7 (2026-05-15): widen the pick sphere for large flat
|
||||
// objects (doors, lifestones, portals, corpses) so their
|
||||
// visible surface stays clickable even though the entity
|
||||
// origin is a single point. 0.7 m default is fine for
|
||||
// humanoids and most items; doors / portals need ~2 m
|
||||
// to cover the doorframe.
|
||||
//
|
||||
// 2026-05-15 sign-class extension: post-mounted scenery
|
||||
// (Holtburg town sign etc.) needs the sphere TALLER than
|
||||
// wider. We classify "non-creature, non-flat, non-small-item"
|
||||
// as tall scenery and grow the sphere to 1.6 m radius lifted
|
||||
// to 1.5 m vertical offset — covers a 3 m post from
|
||||
// ground to top. Mirrors TargetIndicatorPanel.EntityHeightFor's
|
||||
// 3 m default so the click sphere matches the visible box.
|
||||
radiusForGuid: g =>
|
||||
{
|
||||
if (_lastSpawnByGuid.TryGetValue(g, out var s)
|
||||
&& s.ObjectDescriptionFlags is { } odf)
|
||||
{
|
||||
// BF_DOOR = 0x1000, BF_LIFESTONE = 0x4000,
|
||||
// BF_PORTAL = 0x40000, BF_CORPSE = 0x2000
|
||||
// (acclient.h:6431-6463)
|
||||
const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
|
||||
if ((odf & LargeFlatMask) != 0) return 2.0f;
|
||||
}
|
||||
if (IsTallSceneryGuid(g)) return 1.6f;
|
||||
// 1.0 m sphere centred at chest height (see
|
||||
// verticalOffsetForGuid) covers a 1.8 m humanoid from
|
||||
// shin to crown without overlapping neighbours.
|
||||
return 1.0f;
|
||||
},
|
||||
verticalOffsetForGuid: g =>
|
||||
{
|
||||
// Lift the pick sphere to mid-body so chest/head clicks
|
||||
// hit instead of missing past the top of a feet-anchored
|
||||
// sphere. WorldEntity.Position is at feet level
|
||||
// (Z=ground); humanoids reach ~1.8 m, items sit close
|
||||
// to the ground (~0.2 m above their feet).
|
||||
if (_liveEntityInfoByGuid.TryGetValue(g, out var info)
|
||||
&& (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0)
|
||||
return 0.9f; // humanoid mid-body
|
||||
if (_lastSpawnByGuid.TryGetValue(g, out var s)
|
||||
&& s.ObjectDescriptionFlags is { } odf)
|
||||
{
|
||||
const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
|
||||
if ((odf & LargeFlatMask) != 0) return 1.0f; // mid-door
|
||||
}
|
||||
if (IsTallSceneryGuid(g)) return 1.5f; // mid-pole height
|
||||
return 0.2f; // small ground item — sphere just above feet
|
||||
});
|
||||
// Resolver: Setup's SelectionSphere is the ONLY input. If the
|
||||
// entity's Setup didn't bake a SelectionSphere, return null —
|
||||
// the picker skips it, which matches retail behaviour
|
||||
// (Render::GfxObjUnderSelectionRay at 0x0054c740 skips
|
||||
// candidates with no drawing_sphere data). Earlier defensive
|
||||
// 1.5 m × scale synth was removed 2026-05-16 — it made
|
||||
// dat-incomplete entities click as phantom hitboxes the size
|
||||
// of an NPC, diverging from retail and masking real Setup-
|
||||
// loading bugs.
|
||||
sphereForEntity: e =>
|
||||
TryGetEntitySelectionSphere(e.ServerGuid, out var c, out var r)
|
||||
? ((System.Numerics.Vector3, float)?)(c, r)
|
||||
: null,
|
||||
// Match the indicator's TriangleSize (8 px) so the click area
|
||||
// extends out to the bracket corners — what the user perceives
|
||||
// as "selectable extent."
|
||||
inflatePixels: 8f);
|
||||
|
||||
if (picked is uint guid)
|
||||
{
|
||||
|
|
@ -9102,7 +9088,7 @@ public sealed class GameWindow : IDisposable
|
|||
string radStr = pickUseRadius.HasValue ? pickUseRadius.Value.ToString("F2", System.Globalization.CultureInfo.InvariantCulture) : "null";
|
||||
string setupStr = pickSetupId.HasValue ? $"0x{pickSetupId.Value:X8}" : "null";
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[B.7] pick-info guid=0x{guid:X8} itemType=0x{rawItemType:X8} pwd=0x{pwdBits:X8} use={useStr} useRadius={radStr} scale={pickScale:F2} setup={setupStr} tallScenery={IsTallSceneryGuid(guid)} color=({col.R},{col.G},{col.B})"));
|
||||
$"[B.7] pick-info guid=0x{guid:X8} itemType=0x{rawItemType:X8} pwd=0x{pwdBits:X8} use={useStr} useRadius={radStr} scale={pickScale:F2} setup={setupStr} color=({col.R},{col.G},{col.B})"));
|
||||
_debugVm?.AddToast($"Selected: {label}");
|
||||
if (useImmediately) SendUse(guid);
|
||||
}
|
||||
|
|
@ -9161,7 +9147,7 @@ public sealed class GameWindow : IDisposable
|
|||
else SendUse(sel);
|
||||
}
|
||||
|
||||
private void SendUse(uint guid, bool isRetryAfterArrival = false)
|
||||
private void SendUse(uint guid)
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
|
|
@ -9169,69 +9155,64 @@ public sealed class GameWindow : IDisposable
|
|||
_debugVm?.AddToast("Not in world");
|
||||
return;
|
||||
}
|
||||
// 2026-05-15: defense-in-depth useability gate. Double-click flows
|
||||
// directly through SendUse without passing UseCurrentSelection's
|
||||
// dispatcher gate, so re-check here. Silent ignore matches retail
|
||||
// (acclient.h:6478 ITEM_USEABLE — USEABLE_REMOTE bit required).
|
||||
// isRetryAfterArrival bypasses the gate because we only retry an
|
||||
// action we previously gated through.
|
||||
if (!isRetryAfterArrival && !IsUseableTarget(guid))
|
||||
|
||||
// Retail-faithful useability gate (acclient_2013_pseudo_c.txt:256455
|
||||
// ItemUses::IsUseable). Signs / banners with useability=0 silently
|
||||
// ignore Use.
|
||||
if (!IsUseableTarget(guid))
|
||||
{
|
||||
// Retail-style client-side toast for unusable targets
|
||||
// (signs, decorative scenery with USEABLE_NO / USEABLE_UNDEF).
|
||||
// Retail string at acclient_2013_pseudo_c.txt:1033115
|
||||
// (data_7e2a70): "The %s cannot be used" (no trailing period).
|
||||
string label = DescribeLiveEntity(guid);
|
||||
_debugVm?.AddToast(AcDream.Core.Ui.RetailMessages.CannotBeUsed(label));
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.4b] SendUse ignored — not useable guid=0x{guid:X8}");
|
||||
return;
|
||||
}
|
||||
// B.6 (2026-05-15): install a speculative auto-walk on the local
|
||||
// player toward the target. For far targets ACE will overwrite
|
||||
// this with its own MovementType=6 wire payload (and a better
|
||||
// wire-supplied use-radius). For close-range targets ACE skips
|
||||
// MoveToChain entirely and just rotates server-side; our
|
||||
// overlay provides the matching local rotation. Either way the
|
||||
// alignment-gated arrival ensures the body finishes facing
|
||||
// the target before stopping.
|
||||
|
||||
// B.6 (2026-05-15): install speculative local auto-walk against
|
||||
// the target so close-range Use rotates the body to face before
|
||||
// the action fires. For FAR targets, ACE's CreateMoveToChain
|
||||
// (Player_Move.cs:37-179) takes over via inbound MovementType=6
|
||||
// and our overlay is overwritten by ACE's wire-supplied radius.
|
||||
//
|
||||
// User feedback (2026-05-15): 'first is rotation, when you are
|
||||
// facing, then using.' For close-range we DEFER the wire packet
|
||||
// until our local overlay arrives (turn-then-fire). The
|
||||
// _pendingPostArrivalAction handler will re-fire SendUse with
|
||||
// isRetryAfterArrival=true after the body finishes turning.
|
||||
// For far range we still send immediately so ACE can start
|
||||
// its MoveToChain.
|
||||
if (!isRetryAfterArrival)
|
||||
// 2026-05-16: simplified — close-range deferral now fires the
|
||||
// wire packet ONCE on AutoWalkArrived (turn-first done), not a
|
||||
// retry of an earlier failed send. No re-send path.
|
||||
bool closeRange = IsCloseRangeTarget(guid);
|
||||
InstallSpeculativeTurnToTarget(guid);
|
||||
|
||||
if (closeRange)
|
||||
{
|
||||
InstallSpeculativeTurnToTarget(guid);
|
||||
// Defer the wire packet — OnAutoWalkArrivedSendDeferredAction
|
||||
// will fire it after rotation completes.
|
||||
_pendingPostArrivalAction = (guid, false);
|
||||
if (IsCloseRangeTarget(guid))
|
||||
{
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
|
||||
return; // wait for AutoWalkArrived to fire the wire send
|
||||
}
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Far range: send Use immediately so ACE has the request,
|
||||
// AND queue an arrival re-send. Issue #63 (server-initiated
|
||||
// MoveToObject not honored) means ACE's first-Use response
|
||||
// is dropped as too-far and ACE doesn't re-poll
|
||||
// WithinUseRadius when the speculative local walk gets us in
|
||||
// range. The arrival re-send fires a second Use packet once
|
||||
// the body reaches the target — at which point ACE accepts
|
||||
// and executes the action. The retail-faithful path is to
|
||||
// honor MoveToObject and let ACE complete the Use server-
|
||||
// side; until #63 lands, this client-side retry is the
|
||||
// workaround that keeps far-range Use working.
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.4b] use guid=0x{guid:X8} seq={seq}");
|
||||
_pendingPostArrivalAction = (guid, false);
|
||||
Console.WriteLine($"[B.4b] use guid=0x{guid:X8} seq={seq} (queued for arrival re-send pending #63)");
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
{
|
||||
string label = DescribeLiveEntity(guid);
|
||||
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, bool isRetryAfterArrival = false)
|
||||
private void SendPickUp(uint itemGuid)
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
|
|
@ -9245,7 +9226,7 @@ public sealed class GameWindow : IDisposable
|
|||
// "cannot pick up creatures!" message instead of the generic
|
||||
// "can't be picked up!".
|
||||
// Retail string acclient_2013_pseudo_c.txt:401642 (data_7e22b4).
|
||||
if (!isRetryAfterArrival && IsLiveCreatureTarget(itemGuid))
|
||||
if (IsLiveCreatureTarget(itemGuid))
|
||||
{
|
||||
_debugVm?.AddToast(AcDream.Core.Ui.RetailMessages.CannotPickUpCreatures);
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
|
|
@ -9256,7 +9237,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Generic non-pickupable gate (signs, banners, decorative scenery).
|
||||
// Retail string acclient_2013_pseudo_c.txt:401589 (sprintf
|
||||
// "The %s can't be picked up!").
|
||||
if (!isRetryAfterArrival && !IsPickupableTarget(itemGuid))
|
||||
if (!IsPickupableTarget(itemGuid))
|
||||
{
|
||||
string label = DescribeLiveEntity(itemGuid);
|
||||
_debugVm?.AddToast(AcDream.Core.Ui.RetailMessages.CantBePickedUp(label));
|
||||
|
|
@ -9264,66 +9245,75 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine($"[B.5] SendPickUp ignored — not pickupable item=0x{itemGuid:X8}");
|
||||
return;
|
||||
}
|
||||
|
||||
// B.6 (2026-05-15): same speculative turn-to-target + deferral as
|
||||
// SendUse — close-range pickup rotates locally to face the
|
||||
// item first, then the wire packet fires when the local
|
||||
// overlay reports arrival.
|
||||
if (!isRetryAfterArrival)
|
||||
//
|
||||
// 2026-05-16: simplified — FIRST send on arrival, not a retry.
|
||||
bool closeRange = IsCloseRangeTarget(itemGuid);
|
||||
InstallSpeculativeTurnToTarget(itemGuid);
|
||||
|
||||
if (closeRange)
|
||||
{
|
||||
InstallSpeculativeTurnToTarget(itemGuid);
|
||||
_pendingPostArrivalAction = (itemGuid, true);
|
||||
if (IsCloseRangeTarget(itemGuid))
|
||||
{
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}");
|
||||
return;
|
||||
}
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Far range: same arrival-retry pattern as SendUse — fire
|
||||
// PickUp immediately AND queue for arrival re-send. ACE's
|
||||
// first PickUp is dropped if we're outside the use-radius
|
||||
// and isn't re-polled (issue #63 workaround).
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
||||
seq, itemGuid, _playerServerGuid, placement: 0);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
|
||||
_pendingPostArrivalAction = (itemGuid, true);
|
||||
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq} (queued for arrival re-send pending #63)");
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
{
|
||||
string label = DescribeLiveEntity(itemGuid);
|
||||
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. 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.
|
||||
/// 2026-05-16. Fires the deferred close-range Use/PickUp action
|
||||
/// once the local auto-walk overlay reports arrival (i.e. the body
|
||||
/// has finished rotating to face the target). Unlike the old
|
||||
/// <c>OnAutoWalkArrivedReSendAction</c>, this is a FIRST send — not a
|
||||
/// retry of an earlier failed send. Far-range Use/PickUp paths
|
||||
/// fire the wire packet immediately at <see cref="SendUse"/>/<see cref="SendPickUp"/> time
|
||||
/// and never touch <c>_pendingPostArrivalAction</c>.
|
||||
/// </summary>
|
||||
private void OnAutoWalkArrivedReSendAction()
|
||||
private void OnAutoWalkArrivedSendDeferredAction()
|
||||
{
|
||||
if (_pendingPostArrivalAction is not (uint guid, bool isPickup) pending)
|
||||
return;
|
||||
// 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 (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
return;
|
||||
|
||||
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);
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
if (isPickup)
|
||||
{
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
||||
seq, guid, _playerServerGuid, placement: 0);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.5] pickup-deferred item=0x{guid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.4b] use-deferred guid=0x{guid:X8} seq={seq}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -9421,48 +9411,6 @@ public sealed class GameWindow : IDisposable
|
|||
walkRunThreshold: 15.0f);
|
||||
}
|
||||
|
||||
/// <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))
|
||||
|
|
@ -9515,74 +9463,6 @@ public sealed class GameWindow : IDisposable
|
|||
return (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-05-15. True when the entity is "tall scenery" — has a known
|
||||
/// non-zero ItemType that is NOT in the small-carry-item mask AND
|
||||
/// has no door/lifestone/portal/corpse PWD bits AND is not a
|
||||
/// creature. The Holtburg town sign is the canonical example: a
|
||||
/// 3 m post-mounted entity that needs the pick sphere lifted to
|
||||
/// mid-pole with a wider radius so the user can click any part of
|
||||
/// the visible mesh, not just the pole base.
|
||||
///
|
||||
/// <para>
|
||||
/// Mirrors <see cref="UI.TargetIndicatorPanel.EntityHeightFor"/>'s
|
||||
/// classification — both fall into the "everything else: 3 m default"
|
||||
/// branch — so the visible indicator box and the click sphere
|
||||
/// match.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private bool IsTallSceneryGuid(uint guid)
|
||||
{
|
||||
// Creatures are never "tall scenery" — picker uses humanoid sphere.
|
||||
if (_liveEntityInfoByGuid.TryGetValue(guid, out var info)
|
||||
&& (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0)
|
||||
return false;
|
||||
|
||||
if (!_lastSpawnByGuid.TryGetValue(guid, out var spawn))
|
||||
return false;
|
||||
|
||||
// Doors / lifestones / portals / corpses → LargeFlatMask branch.
|
||||
if (spawn.ObjectDescriptionFlags is { } odf)
|
||||
{
|
||||
const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
|
||||
if ((odf & LargeFlatMask) != 0) return false;
|
||||
}
|
||||
|
||||
uint it = spawn.ItemType ?? 0u;
|
||||
|
||||
// 2026-05-15 — useability-based discriminator. Mirrors
|
||||
// TargetIndicatorPanel.EntityHeightFor exactly so the click
|
||||
// sphere matches the visible box. A small-item ItemType is
|
||||
// a REAL pickup item only if it is also useable from the
|
||||
// world (USEABLE_REMOTE bit, acclient.h:6478). Otherwise
|
||||
// (Misc + USEABLE_UNDEF — the Holtburg sign case) it's tall
|
||||
// scenery and gets the lifted+widened sphere.
|
||||
const uint USEABLE_REMOTE_BIT = 0x20u;
|
||||
bool useableFromWorld = spawn.Useability is uint u
|
||||
&& (u & USEABLE_REMOTE_BIT) != 0;
|
||||
|
||||
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);
|
||||
// Real pickup item: small-item-class AND useable. NOT tall scenery.
|
||||
if ((it & SmallItemMask) != 0 && useableFromWorld) return false;
|
||||
|
||||
// Everything else (signs / banners / untyped scenery /
|
||||
// Misc-typed-but-non-useable): tall scenery.
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-05-16 — retail-faithful port of
|
||||
|
|
@ -9955,15 +9835,10 @@ 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;
|
||||
// B.6/B.7 (2026-05-16): fire the deferred close-range Use/PickUp
|
||||
// action (first send, not a retry) when the local auto-walk overlay
|
||||
// reports arrival (body finished rotating to face the target).
|
||||
_playerController.AutoWalkArrived += OnAutoWalkArrivedSendDeferredAction;
|
||||
|
||||
// K-fix7 (2026-04-26): if PlayerDescription already arrived, the
|
||||
// server's Run / Jump skill values are cached here — push them
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue