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
|
|
@ -1,3 +1,4 @@
|
|||
using System.Linq;
|
||||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -765,6 +766,208 @@ public sealed class ClientObjectTableTests
|
|||
Assert.Equal(new[] { item }, table.GetContents(pack));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SCRATCH G4 reproduction: a vendor BUY's real wire shape is a fresh
|
||||
/// CreateObject (a BRAND NEW guid our client has never seen before —
|
||||
/// ACE's Player_Commerce.cs FinalizeBuyTransaction/ItemProfileToWorldObjects
|
||||
/// mints a new WorldObject for common stock, distinct from the shop-list
|
||||
/// guid) carrying ContainerId=player already (TryAddToInventory sets it
|
||||
/// before TryCreateInInventoryWithNetworking sends CreateObject), followed
|
||||
/// by the SAME InventoryPutObjInContainer (0x0022) placement=0 message a
|
||||
/// pickup gets. This drives that EXACT two-message sequence — Ingest
|
||||
/// (simulating CreateObject) THEN ApplyConfirmedServerMove (simulating
|
||||
/// the 0x0022 echo) — against a pack that already has other items, to see
|
||||
/// whether the fresh-guid case actually prepends the way
|
||||
/// AuthoritativePickup_PlacementZeroInsertsAtRetailListHead's
|
||||
/// already-known-guid case (built via ReplaceContents, which creates the
|
||||
/// object FIRST) already proves.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SCRATCH_BuyShapedFreshGuidCreateObjectThenContainId_FinalOrder()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint pack = 0x50000001u;
|
||||
const uint existingA = 0xA01u;
|
||||
const uint existingB = 0xA02u;
|
||||
const uint boughtItem = 0xB01u;
|
||||
|
||||
table.InitializeInventoryManifest(pack, new[]
|
||||
{
|
||||
new ContainerContentEntry(existingA, 0u),
|
||||
new ContainerContentEntry(existingB, 0u),
|
||||
});
|
||||
|
||||
// Simulates the buy's CreateObject: a guid NEVER SEEN BEFORE,
|
||||
// already carrying ContainerId=pack (matching TryAddToInventory's
|
||||
// pre-send assignment), routed through the SAME ApplyEntitySpawn ->
|
||||
// Ingest path production CreateObject handling uses.
|
||||
table.Ingest(new WeenieData(
|
||||
Guid: boughtItem,
|
||||
Name: "Arrow",
|
||||
Type: ItemType.MissileWeapon,
|
||||
WeenieClassId: 5u,
|
||||
IconId: 0u,
|
||||
IconOverlayId: 0u,
|
||||
IconUnderlayId: 0u,
|
||||
Effects: 0u,
|
||||
Value: 100,
|
||||
StackSize: 100,
|
||||
StackSizeMax: null,
|
||||
Burden: 1,
|
||||
ContainerId: pack,
|
||||
WielderId: 0u,
|
||||
ValidLocations: null,
|
||||
CurrentWieldedLocation: null,
|
||||
Priority: null,
|
||||
ItemsCapacity: null,
|
||||
ContainersCapacity: null,
|
||||
Structure: null,
|
||||
MaxStructure: null,
|
||||
Workmanship: null));
|
||||
|
||||
// Simulates the SAME wire echo a pickup gets (0x0022,
|
||||
// InventoryPutObjInContainer / Item_ServerSaysContainId), which ACE
|
||||
// sends unconditionally after TryAddToInventory for a buy exactly
|
||||
// the same way it does for a pickup — with Placement=0.
|
||||
Assert.True(table.ApplyConfirmedServerMove(
|
||||
boughtItem,
|
||||
pack,
|
||||
newWielderId: 0u,
|
||||
newSlot: 0,
|
||||
containerTypeHint: 0u));
|
||||
|
||||
Assert.Equal(
|
||||
new[] { boughtItem, existingA, existingB },
|
||||
table.GetContents(pack));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G4 (grand-gate finding): ACE's <c>GameMessageCreateObject</c> rides
|
||||
/// <c>GameMessageGroup.SmartboxQueue</c>
|
||||
/// (references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageCreateObject.cs:8)
|
||||
/// while <c>GameEventItemServerSaysContainId</c> (our
|
||||
/// InventoryPutObjInContainer, 0x0022) rides
|
||||
/// <c>GameMessageGroup.UIQueue</c> — TWO independent reliable queues
|
||||
/// with no cross-queue ordering guarantee. A vendor buy of common stock
|
||||
/// mints a brand-new guid (ACE's <c>ItemProfileToWorldObjects</c>) the
|
||||
/// client has never seen, so unlike an ordinary pickup (whose item was
|
||||
/// already visible/known), its final resting slot depends entirely on
|
||||
/// which queue's message the client happens to process first. This
|
||||
/// drives the 0x0022 echo BEFORE its item's CreateObject — root cause,
|
||||
/// live-verified: before the fix this returned <c>applied=false</c> and
|
||||
/// silently dropped the placement, leaving the item appended at the
|
||||
/// list tail once its later CreateObject-only <c>Ingest</c> ran.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ContainIdArrivingBeforeCreateObject_StillInsertsAtRetailListHead()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint pack = 0x50000001u;
|
||||
const uint existingA = 0xA01u;
|
||||
const uint existingB = 0xA02u;
|
||||
const uint boughtItem = 0xB02u;
|
||||
|
||||
table.InitializeInventoryManifest(pack, new[]
|
||||
{
|
||||
new ContainerContentEntry(existingA, 0u),
|
||||
new ContainerContentEntry(existingB, 0u),
|
||||
});
|
||||
|
||||
// The 0x0022 echo arrives FIRST — boughtItem does not exist in the
|
||||
// table yet. ApplyConfirmedServerMove itself still reports failure
|
||||
// (nothing to move YET) — the fix is that it no longer drops the
|
||||
// request on the floor.
|
||||
Assert.False(table.ApplyConfirmedServerMove(
|
||||
boughtItem,
|
||||
pack,
|
||||
newWielderId: 0u,
|
||||
newSlot: 0,
|
||||
containerTypeHint: 0u));
|
||||
|
||||
// THEN the CreateObject arrives.
|
||||
table.Ingest(new WeenieData(
|
||||
Guid: boughtItem,
|
||||
Name: "Arrow",
|
||||
Type: ItemType.MissileWeapon,
|
||||
WeenieClassId: 5u,
|
||||
IconId: 0u,
|
||||
IconOverlayId: 0u,
|
||||
IconUnderlayId: 0u,
|
||||
Effects: 0u,
|
||||
Value: 100,
|
||||
StackSize: 100,
|
||||
StackSizeMax: null,
|
||||
Burden: 1,
|
||||
ContainerId: pack,
|
||||
WielderId: 0u,
|
||||
ValidLocations: null,
|
||||
CurrentWieldedLocation: null,
|
||||
Priority: null,
|
||||
ItemsCapacity: null,
|
||||
ContainersCapacity: null,
|
||||
Structure: null,
|
||||
MaxStructure: null,
|
||||
Workmanship: null));
|
||||
|
||||
// The stashed placement replays: the bought item lands at the head
|
||||
// (retail slot 0), matching the in-order case exactly.
|
||||
Assert.Equal(
|
||||
new[] { boughtItem, existingA, existingB },
|
||||
table.GetContents(pack));
|
||||
Assert.Equal(pack, table.Get(boughtItem)!.ContainerId);
|
||||
Assert.Equal(0u, table.Get(boughtItem)!.ContainerTypeHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G4 companion: a stashed placement for a guid that never actually
|
||||
/// arrives (e.g. a stale/unrelated echo) must not leak forever, or
|
||||
/// silently resurrect a placement for some LATER, unrelated recycled
|
||||
/// guid. <see cref="ClientObjectTable.Clear"/> (session teardown) drops
|
||||
/// it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PendingUnresolvedPlacement_IsDroppedByClear()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint pack = 0x50000001u;
|
||||
const uint neverArrives = 0xB03u;
|
||||
|
||||
Assert.False(table.ApplyConfirmedServerMove(
|
||||
neverArrives, pack, newWielderId: 0u, newSlot: 0, containerTypeHint: 0u));
|
||||
|
||||
table.Clear();
|
||||
|
||||
// A later, unrelated Ingest of the SAME (recycled) guid must not
|
||||
// resurrect the old stashed placement — it should append normally
|
||||
// (no other contents to reposition ahead of, so this just proves no
|
||||
// exception/misplacement occurs after a session boundary).
|
||||
table.Ingest(new WeenieData(
|
||||
Guid: neverArrives,
|
||||
Name: "Something Else",
|
||||
Type: ItemType.Misc,
|
||||
WeenieClassId: 9u,
|
||||
IconId: 0u,
|
||||
IconOverlayId: 0u,
|
||||
IconUnderlayId: 0u,
|
||||
Effects: 0u,
|
||||
Value: 1,
|
||||
StackSize: null,
|
||||
StackSizeMax: null,
|
||||
Burden: 1,
|
||||
ContainerId: pack,
|
||||
WielderId: 0u,
|
||||
ValidLocations: null,
|
||||
CurrentWieldedLocation: null,
|
||||
Priority: null,
|
||||
ItemsCapacity: null,
|
||||
ContainersCapacity: null,
|
||||
Structure: null,
|
||||
MaxStructure: null,
|
||||
Workmanship: null));
|
||||
|
||||
Assert.Equal(new[] { neverArrives }, table.GetContents(pack));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthoritativePickup_PlacementZeroInsertsAtRetailListHead()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue