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
|
|
@ -378,6 +378,143 @@ public sealed class RuntimeInteractionTransactionStateTests
|
|||
state.Dispose();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// G3 (grand-gate finding) — arrival-gated Use, mirroring the pickup
|
||||
// coverage above.
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PostArrivalUseRequiresItsExactApproachTokenAndDispatchesTheHeldReservation()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
ItemUseRequestReservation reservation = state.BeginUseRequestReservation();
|
||||
var approach = new RuntimeInteractionApproachToken(3u, 7u);
|
||||
|
||||
Assert.True(state.TryArmPostArrivalUse(
|
||||
Item,
|
||||
ownedByPlayer: false,
|
||||
useable: true,
|
||||
reservation,
|
||||
approach,
|
||||
out RuntimePendingUse pending));
|
||||
Assert.True(state.HasPendingUse);
|
||||
// Nothing sent yet — armed, not dispatched.
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
|
||||
// The wrong token must not resolve it.
|
||||
Assert.False(state.TryResolveUseApproachCompletion(
|
||||
new RuntimeInteractionApproachToken(3u, 8u),
|
||||
natural: true,
|
||||
out _));
|
||||
Assert.True(state.HasPendingUse);
|
||||
|
||||
Assert.True(state.TryResolveUseApproachCompletion(
|
||||
approach,
|
||||
natural: true,
|
||||
out RuntimePendingUse ready));
|
||||
Assert.Equal(pending.Token, ready.Token);
|
||||
Assert.False(state.HasPendingUse);
|
||||
|
||||
var transport = new Transport();
|
||||
RuntimeInteractionDispatchResult result = state.TryDispatchUse(
|
||||
ready.ServerGuid,
|
||||
ready.OwnedByPlayer,
|
||||
ready.Useable,
|
||||
ready.Reservation,
|
||||
transport,
|
||||
out uint sequence);
|
||||
|
||||
Assert.Equal(RuntimeInteractionDispatchResult.Dispatched, result);
|
||||
Assert.Equal(1u, sequence);
|
||||
Assert.Equal(new[] { Item }, transport.Uses);
|
||||
// The reservation transferred to the authoritative UseDone wait,
|
||||
// exactly like an ordinary immediate Use.
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseApproachCompletionCancellationReleasesTheHeldReservationWithoutSending()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
ItemUseRequestReservation reservation = state.BeginUseRequestReservation();
|
||||
var approach = new RuntimeInteractionApproachToken(1u, 1u);
|
||||
Assert.True(state.TryArmPostArrivalUse(
|
||||
Item, ownedByPlayer: false, useable: true, reservation, approach, out _));
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
|
||||
// natural: false (cancellation, e.g. moved away) — TryResolveUseApproachCompletion
|
||||
// itself returns false; the caller is responsible for releasing the
|
||||
// reservation (mirrors SelectionInteractionController.HandleUseApproachCompletion).
|
||||
Assert.False(state.TryResolveUseApproachCompletion(
|
||||
approach, natural: false, out RuntimePendingUse cancelled));
|
||||
Assert.False(state.HasPendingUse);
|
||||
cancelled.Reservation?.CancelBeforeDispatch();
|
||||
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelPendingUseByGuidOnlyMatchesTheArmedTarget()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
ItemUseRequestReservation reservation = state.BeginUseRequestReservation();
|
||||
Assert.True(state.TryArmPostArrivalUse(
|
||||
Item, ownedByPlayer: false, useable: true, reservation,
|
||||
new RuntimeInteractionApproachToken(1u, 1u), out _));
|
||||
|
||||
Assert.False(state.TryCancelPendingUse(Container, out _));
|
||||
Assert.True(state.HasPendingUse);
|
||||
|
||||
Assert.True(state.TryCancelPendingUse(Item, out RuntimePendingUse cancelled));
|
||||
Assert.False(state.HasPendingUse);
|
||||
cancelled.Reservation?.CancelBeforeDispatch();
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArmingASecondUseWhileOneIsAlreadyArmedFails()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
Assert.True(state.TryArmPostArrivalUse(
|
||||
Item, ownedByPlayer: false, useable: true, reservation: null,
|
||||
new RuntimeInteractionApproachToken(1u, 1u), out _));
|
||||
|
||||
Assert.False(state.TryArmPostArrivalUse(
|
||||
Container, ownedByPlayer: false, useable: true, reservation: null,
|
||||
new RuntimeInteractionApproachToken(2u, 2u), out _));
|
||||
Assert.Equal(Item, state.TryGetPendingUse(out RuntimePendingUse stillArmed)
|
||||
? stillArmed.ServerGuid
|
||||
: 0u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingUseReservationIsReleasedByResetSessionAndDisposal()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
var state = new RuntimeInteractionTransactionState(inventory);
|
||||
ItemUseRequestReservation reservation = state.BeginUseRequestReservation();
|
||||
Assert.True(state.TryArmPostArrivalUse(
|
||||
Item, ownedByPlayer: false, useable: true, reservation,
|
||||
new RuntimeInteractionApproachToken(1u, 1u), out _));
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
|
||||
state.ResetSession();
|
||||
|
||||
// G3: ResetCore releases a live pending-use reservation even when no
|
||||
// caller explicitly cancelled it first — must not leak a busy-count
|
||||
// reference.
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
Assert.False(state.HasPendingUse);
|
||||
|
||||
state.Dispose();
|
||||
Assert.True(state.CaptureOwnership().IsConverged);
|
||||
state.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstancesDoNotShareThrottleQueueAppraisalOrPickupState()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -116,6 +116,58 @@ public sealed class VendorShopItemMaterializerTests
|
|||
Assert.Equal(5, objects.Get(ItemA)!.StackSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G2 (grand-gate finding): a REAL ACE vendor listing carries
|
||||
/// <c>DescStackSize=null</c> (ACE's <c>Vendor.LoadInventoryItem</c> never
|
||||
/// calls <c>wo.SetStackSize</c> on the browse-list WorldObject) and only
|
||||
/// the packed <see cref="VendorShopItem.StackSize"/> supply-count field
|
||||
/// (<c>VendorShopCreateListStackSize</c>) names a real quantity. Before
|
||||
/// the fix, <c>ToWeenieData</c> read only <c>DescStackSize</c>, so
|
||||
/// <see cref="ClientObject.StackSize"/> came back 1 for every vendor
|
||||
/// item — the toolbar split slider (which gates on
|
||||
/// <c>stackSize > 1</c>) never appeared for ANY vendor stack. This
|
||||
/// pins the fallback: no <c>DescStackSize</c>, packed
|
||||
/// <c>StackSize=100</c> -> <c>ClientObject.StackSize</c> resolves to
|
||||
/// 100, not 1.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Apply_NoDescStackSize_FallsBackToPackedSupplyCount()
|
||||
{
|
||||
var vendor = new VendorState();
|
||||
var objects = new ClientObjectTable();
|
||||
using var materializer = new VendorShopItemMaterializer(vendor, objects);
|
||||
|
||||
vendor.Apply(VendorGuid, default, new[]
|
||||
{
|
||||
new VendorShopItem(
|
||||
ItemA, StackSize: 100, WeenieClassId: 1u, Name: "Arrow",
|
||||
ItemType: (uint)ItemType.MissileWeapon, IconId: 0x1234u, Value: 100,
|
||||
DescStackSize: null),
|
||||
});
|
||||
|
||||
Assert.Equal(100, objects.Get(ItemA)!.StackSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G2 companion: the packed field's -1 "unlimited supply" sentinel has
|
||||
/// no bounded per-row purchase cap in the wire shape today, so it must
|
||||
/// fall through to the safe non-splittable default (1) rather than
|
||||
/// literally propagating -1 (which would read as a huge unsigned
|
||||
/// "stack size" to <see cref="SelectedObjectController"/>'s
|
||||
/// <c>stackSize > 1</c> gate).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Apply_UnlimitedSupplySentinel_FallsBackToNonSplittableDefault()
|
||||
{
|
||||
var vendor = new VendorState();
|
||||
var objects = new ClientObjectTable();
|
||||
using var materializer = new VendorShopItemMaterializer(vendor, objects);
|
||||
|
||||
vendor.Apply(VendorGuid, default, new[] { Item(ItemA, "Bread") }); // StackSize: -1, DescStackSize: null
|
||||
|
||||
Assert.Equal(1, objects.Get(ItemA)!.StackSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Refreshed_ItemNoLongerListed_IsRemoved()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue