fix(vendor): grand-gate findings — wire-truth container counts, the live split bar, arrival-gated use, prepend-order race
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Four live findings, each with the paper-verification failure named: G1 the container-capacity guard counted containers by a local type/capacity heuristic that over-classifies ordinary items; retail buckets from the wire's ContainerProperties at insert. Now reads ClientObjectTable's existing ContainerTypeHint (AP-168 narrowed to the shop-stock half; a pre-check must never false-block). G2 the amount bar never showed live because ACE never sets StackSize on browse listings — DescStackSize is null for every real vendor item and the C4 paper test hand-set the field, bypassing the materializer. The materializer now falls back to the packed supply count (AP-169, ACE adaptation); the new test drives the REAL materializer. G3 an out-of-range Use now dispatches ON ARRIVAL (pickup's shape): ACE's HandleActionUseItem only opens the vendor when the Use finds the player in range — a click-time send is greeted and dropped (AP-170, ACE adaptation; retail's server walks the player, ACE does not). G4 bought items appended because ACE's placement echo (UIQueue) can beat the CreateObject (SmartboxQueue) — cross-queue, no ordering guarantee — and the early echo was silently dropped. ClientObjectTable now stashes unresolved placements and replays them at Ingest: buys land at the retail list head. No register row — this RESTORES parity. Clean-room complete solution: 11,521 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c68ad1e646
commit
68568a3a59
12 changed files with 1140 additions and 90 deletions
|
|
@ -31,6 +31,23 @@ public readonly record struct RuntimePendingPickup(
|
|||
ulong PendingPlacementToken,
|
||||
RuntimeInteractionApproachToken ApproachToken);
|
||||
|
||||
/// <summary>
|
||||
/// G3 (grand-gate finding): an out-of-range Use armed to dispatch on
|
||||
/// arrival, mirroring <see cref="RuntimePendingPickup"/>'s shape. Holds the
|
||||
/// eligibility snapshot computed at request time (retail's own
|
||||
/// <c>ItemHolder::UseObject</c> eligibility test runs once, before the
|
||||
/// walk-in) plus the caller's <see cref="ItemUseRequestReservation"/>, which
|
||||
/// crosses the approach boundary unresolved until arrival (dispatch) or
|
||||
/// cancellation.
|
||||
/// </summary>
|
||||
public readonly record struct RuntimePendingUse(
|
||||
ulong Token,
|
||||
uint ServerGuid,
|
||||
bool OwnedByPlayer,
|
||||
bool Useable,
|
||||
ItemUseRequestReservation? Reservation,
|
||||
RuntimeInteractionApproachToken ApproachToken);
|
||||
|
||||
public readonly record struct RuntimeAppraisalResponseAcceptance(
|
||||
bool Accepted,
|
||||
bool FirstResponse);
|
||||
|
|
@ -53,7 +70,13 @@ public readonly record struct RuntimeInteractionTransactionSnapshot(
|
|||
int OutboundCount,
|
||||
bool HasPendingPickup,
|
||||
ulong PendingPickupToken,
|
||||
long DispatchFailureCount)
|
||||
long DispatchFailureCount,
|
||||
// G3 (grand-gate finding): an armed out-of-range Use holds a live
|
||||
// ItemUseRequestReservation (a busy-count reference) until arrival or
|
||||
// cancellation resolves it — it must reach zero at teardown exactly
|
||||
// like HasPendingPickup.
|
||||
bool HasPendingUse = false,
|
||||
ulong PendingUseToken = 0u)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
|
|
@ -62,7 +85,8 @@ public readonly record struct RuntimeInteractionTransactionSnapshot(
|
|||
&& AwaitingAppraisalId == 0u
|
||||
&& CurrentAppraisalId == 0u
|
||||
&& OutboundCount == 0
|
||||
&& !HasPendingPickup;
|
||||
&& !HasPendingPickup
|
||||
&& !HasPendingUse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,10 +95,23 @@ public readonly record struct RuntimeInteractionTransactionSnapshot(
|
|||
/// <see cref="InventoryTransactionState"/> and is borrowed exactly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ordinary Use follows <c>ItemHolder::UseObject @ 0x00588A80</c>: consume the
|
||||
/// strict 0.2-second gate, send immediately, then transfer the busy reference
|
||||
/// to <c>ClientUISystem::Handle_Item__UseDone @ 0x00564900</c>. Pickup keeps
|
||||
/// the existing local approach transaction and exact post-arrival token.
|
||||
/// Ordinary (already-in-range) Use follows <c>ItemHolder::UseObject @
|
||||
/// 0x00588A80</c>: consume the strict 0.2-second gate, send immediately,
|
||||
/// then transfer the busy reference to <c>ClientUISystem::Handle_Item__UseDone
|
||||
/// @ 0x00564900</c>. Pickup keeps the existing local approach transaction and
|
||||
/// exact post-arrival token.
|
||||
/// <para>
|
||||
/// G3 (grand-gate finding, 2026-08-08): an OUT-OF-RANGE Use does NOT send
|
||||
/// immediately — ACE's <c>Player.HandleActionUseItem</c>
|
||||
/// (<c>Player_Use.cs:176-215</c>) only calls <c>ActOnUse</c> once its own
|
||||
/// <c>CreateMoveToChain</c> confirms the player is within the target's use
|
||||
/// radius; a Use that arrives while still out of range never opens the
|
||||
/// vendor panel (see register AP-170). <see cref="TryArmPostArrivalUse"/>/
|
||||
/// <see cref="TryResolveUseApproachCompletion"/> mirror
|
||||
/// <see cref="RuntimePendingPickup"/>'s exact arrival-gated shape for this
|
||||
/// case; an already-in-range Use is unaffected and still dispatches via
|
||||
/// <see cref="TryDispatchUse"/> immediately.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class RuntimeInteractionTransactionState : IDisposable
|
||||
{
|
||||
|
|
@ -89,6 +126,8 @@ public sealed class RuntimeInteractionTransactionState : IDisposable
|
|||
private uint _currentAppraisalId;
|
||||
private RuntimePendingPickup? _pendingPickup;
|
||||
private ulong _nextPickupToken;
|
||||
private RuntimePendingUse? _pendingUse;
|
||||
private ulong _nextUseToken;
|
||||
private uint _clearEpoch;
|
||||
private long _revision;
|
||||
private long _dispatchFailureCount;
|
||||
|
|
@ -106,6 +145,7 @@ public sealed class RuntimeInteractionTransactionState : IDisposable
|
|||
public uint CurrentAppraisalId => _currentAppraisalId;
|
||||
public int OutboundCount => _outbound.Count;
|
||||
public bool HasPendingPickup => _pendingPickup is not null;
|
||||
public bool HasPendingUse => _pendingUse is not null;
|
||||
public bool IsDisposed => _disposed;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public long DispatchFailureCount =>
|
||||
|
|
@ -122,7 +162,9 @@ public sealed class RuntimeInteractionTransactionState : IDisposable
|
|||
_outbound.Count,
|
||||
_pendingPickup is not null,
|
||||
_pendingPickup?.Token ?? 0u,
|
||||
DispatchFailureCount);
|
||||
DispatchFailureCount,
|
||||
_pendingUse is not null,
|
||||
_pendingUse?.Token ?? 0u);
|
||||
|
||||
public bool TryConsumeUseThrottle(long nowMs)
|
||||
{
|
||||
|
|
@ -517,6 +559,124 @@ public sealed class RuntimeInteractionTransactionState : IDisposable
|
|||
return dispatched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G3: arms an out-of-range Use to dispatch once the approach completes
|
||||
/// naturally, mirroring <see cref="TryArmPostArrivalPickup"/>. The
|
||||
/// <paramref name="reservation"/> (if any) crosses the approach boundary
|
||||
/// unresolved — it is released only by
|
||||
/// <see cref="TryResolveUseApproachCompletion"/> (via the caller's
|
||||
/// dispatch/cancel), <see cref="TryCancelPendingUse(out RuntimePendingUse)"/>,
|
||||
/// or a reset/dispose, never here.
|
||||
/// </summary>
|
||||
public bool TryArmPostArrivalUse(
|
||||
uint serverGuid,
|
||||
bool ownedByPlayer,
|
||||
bool useable,
|
||||
ItemUseRequestReservation? reservation,
|
||||
RuntimeInteractionApproachToken approachToken,
|
||||
out RuntimePendingUse pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (serverGuid == 0u
|
||||
|| approachToken.ControllerLifetime == 0u
|
||||
|| approachToken.ApproachGeneration == 0u)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
if (_pendingUse is not null)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong token = ++_nextUseToken;
|
||||
if (token == 0u)
|
||||
token = ++_nextUseToken;
|
||||
pending = new RuntimePendingUse(
|
||||
token,
|
||||
serverGuid,
|
||||
ownedByPlayer,
|
||||
useable,
|
||||
reservation,
|
||||
approachToken);
|
||||
_pendingUse = pending;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G3: resolves an armed Use's approach completion, mirroring
|
||||
/// <see cref="TryResolveApproachCompletion"/>. The caller is responsible
|
||||
/// for dispatching (via <see cref="TryDispatchUse"/>, which itself
|
||||
/// resolves <see cref="RuntimePendingUse.Reservation"/>) or cancelling
|
||||
/// (<see cref="ItemUseRequestReservation.CancelBeforeDispatch"/>)
|
||||
/// depending on the returned bool and whether the target is still
|
||||
/// current.
|
||||
/// </summary>
|
||||
public bool TryResolveUseApproachCompletion(
|
||||
RuntimeInteractionApproachToken approachToken,
|
||||
bool natural,
|
||||
out RuntimePendingUse pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_pendingUse is not { } current
|
||||
|| current.ApproachToken != approachToken)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
_pendingUse = null;
|
||||
pending = current;
|
||||
IncrementRevision();
|
||||
return natural;
|
||||
}
|
||||
|
||||
public bool TryGetPendingUse(out RuntimePendingUse pending)
|
||||
{
|
||||
if (_pendingUse is { } current)
|
||||
{
|
||||
pending = current;
|
||||
return true;
|
||||
}
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Unconditional cancel — used when a NEW approach supersedes whatever was armed.</summary>
|
||||
public bool TryCancelPendingUse(out RuntimePendingUse pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_pendingUse is not { } current)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
_pendingUse = null;
|
||||
pending = current;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Guid-matching cancel — used when the target itself vanishes (hidden/removed).</summary>
|
||||
public bool TryCancelPendingUse(
|
||||
uint serverGuid,
|
||||
out RuntimePendingUse pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_pendingUse is not { } current
|
||||
|| current.ServerGuid != serverGuid)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
_pendingUse = null;
|
||||
pending = current;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
|
@ -538,16 +698,25 @@ public sealed class RuntimeInteractionTransactionState : IDisposable
|
|||
|| _currentAppraisalId != 0u
|
||||
|| _outbound.Count != 0
|
||||
|| _pendingPickup is not null
|
||||
|| _pendingUse is not null
|
||||
|| _lastUseSourceId != 0u
|
||||
|| _lastUseTargetId != 0u
|
||||
|| _lastUseMs != long.MinValue / 2;
|
||||
|
||||
// G3: an armed Use's reservation is a live busy-count reference —
|
||||
// release it here unconditionally so a reset/dispose that runs
|
||||
// without a preceding CancelPendingApproach() (e.g. a headless/
|
||||
// no-window teardown with no SelectionInteractionController) can
|
||||
// never leak it. Idempotent: a no-op if already resolved.
|
||||
_pendingUse?.Reservation?.CancelBeforeDispatch();
|
||||
|
||||
_lastUseSourceId = 0u;
|
||||
_lastUseTargetId = 0u;
|
||||
_awaitingAppraisalId = 0u;
|
||||
_currentAppraisalId = 0u;
|
||||
_outbound.Clear();
|
||||
_pendingPickup = null;
|
||||
_pendingUse = null;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
_clearEpoch++;
|
||||
if (resetInventory)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue