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
|
|
@ -123,6 +123,28 @@ public sealed class ClientObjectTable
|
|||
// the optimistic MoveItem; restored by RollbackMove on InventoryServerSaveFailed (0x00A0),
|
||||
// cleared by ConfirmMove on the InventoryPutObjInContainer (0x0022) echo.
|
||||
private readonly Dictionary<uint, (ClientObjectPlacement placement, int outstanding)> _pendingMoves = new();
|
||||
|
||||
/// <summary>
|
||||
/// G4 (grand-gate finding, 2026-08-08, register AP-171): an authoritative
|
||||
/// placement (<see cref="ApplyServerMove"/>/<see cref="ApplyConfirmedServerMove"/>)
|
||||
/// whose item guid does not exist in <see cref="_objects"/> YET, keyed by
|
||||
/// that guid — stashed instead of silently dropped, and replayed by
|
||||
/// <see cref="Ingest"/> the moment that guid's CreateObject arrives.
|
||||
/// <para>
|
||||
/// ACE's <c>GameMessageCreateObject</c> rides <c>GameMessageGroup.SmartboxQueue</c>
|
||||
/// while <c>Item_ServerSaysContainId</c>/<c>InventoryPutObjInContainer</c>
|
||||
/// (0x0022) rides <c>GameMessageGroup.UIQueue</c> — two independent
|
||||
/// reliable queues with NO cross-queue ordering guarantee. An ordinary
|
||||
/// pickup's item guid is already known (it was visible in the 3D world
|
||||
/// first), so this race can't bite it; a vendor BUY of common stock
|
||||
/// mints a brand-new guid the client has never seen
|
||||
/// (<c>Player_Commerce.cs</c> <c>ItemProfileToWorldObjects</c>), so its
|
||||
/// existence depends entirely on which queue's message the client
|
||||
/// processes first.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, (ClientObjectPlacement Placement, uint? ContainerTypeHint)>
|
||||
_pendingUnresolvedPlacements = new();
|
||||
private ulong _mutationRevision;
|
||||
|
||||
public ClientObjectTable()
|
||||
|
|
@ -451,6 +473,19 @@ public sealed class ClientObjectTable
|
|||
/// request is reconciled before <see cref="ObjectMoved"/> is published,
|
||||
/// matching retail <c>ServerSaysMoveItem</c>: a reentrant listener may start
|
||||
/// a new request without the old confirmation consuming it afterward.
|
||||
/// <para>
|
||||
/// G4 (grand-gate finding, register AP-171): if <paramref name="itemId"/>
|
||||
/// does not exist yet — the cross-queue race documented on
|
||||
/// <see cref="_pendingUnresolvedPlacements"/>, most visibly a vendor
|
||||
/// buy's brand-new guid whose <c>InventoryPutObjInContainer</c> (UIQueue)
|
||||
/// echo can arrive before its own <c>CreateObject</c> (SmartboxQueue) —
|
||||
/// the requested placement is STASHED rather than silently dropped, and
|
||||
/// replayed the moment <see cref="Ingest"/> creates that guid. Without
|
||||
/// this, the item's placement=0 request is lost, and its later
|
||||
/// CreateObject-only <c>Ingest</c> leaves it wherever the naive
|
||||
/// container-index append put it (the list tail) instead of retail's
|
||||
/// requested slot 0 (list head).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool ApplyConfirmedServerMove(
|
||||
uint itemId,
|
||||
|
|
@ -472,6 +507,17 @@ public sealed class ClientObjectTable
|
|||
return true;
|
||||
}
|
||||
|
||||
if (itemId != 0u && newContainerId != 0u && !_objects.ContainsKey(itemId))
|
||||
{
|
||||
_pendingUnresolvedPlacements[itemId] = (
|
||||
new ClientObjectPlacement(
|
||||
newContainerId,
|
||||
newSlot,
|
||||
newWielderId,
|
||||
newEquipLocation),
|
||||
containerTypeHint);
|
||||
}
|
||||
|
||||
ObjectMoved?.Invoke(new ClientObjectMove(
|
||||
itemId,
|
||||
Item: null,
|
||||
|
|
@ -929,6 +975,28 @@ public sealed class ClientObjectTable
|
|||
UpdateEquipmentIndex(obj.ObjectId, previous, ClientObjectPlacement.From(obj));
|
||||
if (!existed) ObjectAdded?.Invoke(obj); else ObjectUpdated?.Invoke(obj);
|
||||
PublishContainerContentsChanges(changedContainers);
|
||||
|
||||
// G4: this guid's CreateObject just arrived. If an authoritative
|
||||
// placement (Item_ServerSaysContainId/InventoryPutObjInContainer)
|
||||
// for it had already arrived and been stashed — the cross-queue
|
||||
// race documented on _pendingUnresolvedPlacements — replay it now
|
||||
// via the SAME ordered-insert path ApplyServerMove already uses, so
|
||||
// the retail-requested slot (e.g. 0 — the pack head) wins over the
|
||||
// naive append Reindex just performed above. Only ever possible for
|
||||
// a brand-new guid: an existing object's ApplyConfirmedServerMove
|
||||
// would have found it immediately and never stashed anything.
|
||||
if (!existed
|
||||
&& _pendingUnresolvedPlacements.Remove(
|
||||
d.Guid, out var pending))
|
||||
{
|
||||
ApplyServerMove(
|
||||
d.Guid,
|
||||
pending.Placement.ContainerId,
|
||||
pending.Placement.WielderId,
|
||||
pending.Placement.ContainerSlot,
|
||||
pending.Placement.EquipLocation,
|
||||
pending.ContainerTypeHint);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -1506,6 +1574,7 @@ public sealed class ClientObjectTable
|
|||
_containerIndex.Clear();
|
||||
_equipmentIndex.Clear();
|
||||
_pendingMoves.Clear(); // B-Drag: drop in-flight optimistic snapshots (a recycled guid must not mis-rollback)
|
||||
_pendingUnresolvedPlacements.Clear(); // G4: drop stashed placements for a session that's ending anyway
|
||||
Cleared?.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue