feat(vendor): Slice 6b/6c — move-to-use, buy staging, selling; the vendor arc is functionally complete
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
C1 an out-of-range Use now approaches first via the existing client-predicted BeginApproach (Pickup's far-range shape mirrored; retail's ItemHolder::UseObject @0x00588A80 has no range check and the dispatch stays immediate). C2 Add-to-List stages into the Buying tab via VendorStagingList (RemoveProfileFromList's two shapes, pc:200497-200537), Buy All sends ONE batched 0x005F and flushes staging on send exactly as retail does (SendShopEvent -> Flush, pc:204075-204076 — not UseDone-gated), and X-close over a non-empty staging list shows retail's confirm string recovered verbatim from the binary data segment (0x007b5bd8) through the existing dialog factory. C3 the Selling tab's list is the sole drop target (retail's single IsAncestorOfMe gate, pc:204229-204246); VendorSellAcceptability ports InqAcceptability with all rejection strings recovered verbatim from the raw data segment; the sell side prices with BuyPrice (retail's inverted naming: what the vendor PAYS) and 0x0060 carries no trailing currency field, unlike Buy. C4 the status-bar reproduction test PASSES against the production toolbar mount — retail's toolbar shows count + name with the split bar and NO price parenthetical (that figure is the vendor row's own cost text); no code change, the live gate referees. C5 pack order verified correct, untouched. Register: AP-161 narrowed to its two pre-existing cosmetic gaps; AP-162 extended over Buy All; AP-164 (non-sellable bitfield unmodeled), AP-165 (DescStackSize for _maxStackSize in the removal test, bounded), AP-166 (purse text + pending-sell highlight cosmetic) filed. Clean-room complete solution: 11,482 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ab3146ba88
commit
92ea3977b6
18 changed files with 2578 additions and 78 deletions
|
|
@ -33,6 +33,12 @@ public sealed class ItemInteractionControllerTests
|
|||
// TryBuy must see this as false and release the reservation
|
||||
// rather than mark it dispatched for a request nothing sent.
|
||||
public bool SendBuySucceeds = true;
|
||||
// Slice 6b/6c: the batched Buy All / Sell send delegates — same
|
||||
// "no live session" no-op simulation shape as SendBuySucceeds.
|
||||
public readonly List<(uint VendorGuid, IReadOnlyList<(int Amount, uint ItemGuid)> Items, uint AlternateCurrencyId)> BuyAlls = new();
|
||||
public bool SendBuyAllSucceeds = true;
|
||||
public readonly List<(uint VendorGuid, IReadOnlyList<(int Amount, uint ItemGuid)> Items)> Sells = new();
|
||||
public bool SendSellSucceeds = true;
|
||||
public readonly List<string> Toasts = new();
|
||||
public readonly List<string> SystemMessages = new();
|
||||
public readonly List<CombatMode> CombatModeRequests = new();
|
||||
|
|
@ -107,6 +113,20 @@ public sealed class ItemInteractionControllerTests
|
|||
return false;
|
||||
Buys.Add((vendorGuid, itemGuid, amount, alternateCurrencyId));
|
||||
return true;
|
||||
},
|
||||
sendBuyAll: (vendorGuid, items, alternateCurrencyId) =>
|
||||
{
|
||||
if (!SendBuyAllSucceeds)
|
||||
return false;
|
||||
BuyAlls.Add((vendorGuid, items, alternateCurrencyId));
|
||||
return true;
|
||||
},
|
||||
sendSell: (vendorGuid, items) =>
|
||||
{
|
||||
if (!SendSellSucceeds)
|
||||
return false;
|
||||
Sells.Add((vendorGuid, items));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2308,4 +2328,182 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.Single(h.Buys);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
// ── Slice 6b: TryBuyAll ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryBuyAll_Succeeds_SendsTheBatchAndTakesTheSharedUseReservation()
|
||||
{
|
||||
var h = new Harness();
|
||||
var items = new (int Amount, uint ItemGuid)[]
|
||||
{
|
||||
(1, 0x50002000u),
|
||||
(25, 0x50002001u),
|
||||
};
|
||||
|
||||
bool result = h.Controller.TryBuyAll(0x40001000u, items, alternateCurrencyId: 0u);
|
||||
|
||||
Assert.True(result);
|
||||
(uint vendorGuid, IReadOnlyList<(int Amount, uint ItemGuid)> sent, uint currency) = Assert.Single(h.BuyAlls);
|
||||
Assert.Equal(0x40001000u, vendorGuid);
|
||||
Assert.Equal(items, sent);
|
||||
Assert.Equal(0u, currency);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuyAll_RidesTheSameOneRequestAtATimeGateAsOrdinaryUse()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.Controller.IncrementBusyCount();
|
||||
|
||||
bool result = h.Controller.TryBuyAll(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50002000u) }, 0u);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(h.BuyAlls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u)]
|
||||
[InlineData(0x40001000u)]
|
||||
public void TryBuyAll_ZeroVendorOrEmptyList_IsRejectedWithoutTakingAReservation(uint vendorGuid)
|
||||
{
|
||||
var h = new Harness();
|
||||
var items = vendorGuid == 0u
|
||||
? new (int Amount, uint ItemGuid)[] { (1, 0x50002000u) }
|
||||
: Array.Empty<(int Amount, uint ItemGuid)>();
|
||||
|
||||
bool result = h.Controller.TryBuyAll(vendorGuid, items, 0u);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(h.BuyAlls);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuyAll_CompleteUse_ReleasesTheReservation()
|
||||
{
|
||||
var h = new Harness();
|
||||
Assert.True(h.Controller.TryBuyAll(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50002000u) }, 0u));
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
|
||||
h.Controller.CompleteUse(0);
|
||||
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuyAll_NoSessionToSendOn_ReleasesTheReservation()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SendBuyAllSucceeds = false;
|
||||
|
||||
bool result = h.Controller.TryBuyAll(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50002000u) }, 0u);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(h.BuyAlls);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
// ── Slice 6c: TrySell ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TrySell_Succeeds_SendsTheBatchAndTakesTheSharedUseReservation()
|
||||
{
|
||||
var h = new Harness();
|
||||
var items = new (int Amount, uint ItemGuid)[] { (1, 0x50003000u) };
|
||||
|
||||
bool result = h.Controller.TrySell(0x40001000u, items);
|
||||
|
||||
Assert.True(result);
|
||||
(uint vendorGuid, IReadOnlyList<(int Amount, uint ItemGuid)> sent) = Assert.Single(h.Sells);
|
||||
Assert.Equal(0x40001000u, vendorGuid);
|
||||
Assert.Equal(items, sent);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySell_RidesTheSameOneRequestAtATimeGateAsOrdinaryUse()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.Controller.IncrementBusyCount();
|
||||
|
||||
bool result = h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003000u) });
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(h.Sells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySell_ASecondSellWhileTheFirstIsInFlight_IsRejected()
|
||||
{
|
||||
var h = new Harness();
|
||||
Assert.True(h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003000u) }));
|
||||
|
||||
bool second = h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003001u) });
|
||||
|
||||
Assert.False(second);
|
||||
Assert.Single(h.Sells);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u)]
|
||||
[InlineData(0x40001000u)]
|
||||
public void TrySell_ZeroVendorOrEmptyList_IsRejectedWithoutTakingAReservation(uint vendorGuid)
|
||||
{
|
||||
var h = new Harness();
|
||||
var items = vendorGuid == 0u
|
||||
? new (int Amount, uint ItemGuid)[] { (1, 0x50003000u) }
|
||||
: Array.Empty<(int Amount, uint ItemGuid)>();
|
||||
|
||||
bool result = h.Controller.TrySell(vendorGuid, items);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(h.Sells);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySell_CompleteUse_ReleasesTheReservationAndReenablesFurtherRequests()
|
||||
{
|
||||
var h = new Harness();
|
||||
Assert.True(h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003000u) }));
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
|
||||
h.Controller.CompleteUse(0);
|
||||
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
Assert.True(h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003001u) }));
|
||||
Assert.Equal(2, h.Sells.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySell_NoSessionToSendOn_ReleasesTheReservation_AndASubsequentSellWorks()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SendSellSucceeds = false;
|
||||
|
||||
bool result = h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003000u) });
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(h.Sells);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
|
||||
h.SendSellSucceeds = true;
|
||||
bool second = h.Controller.TrySell(
|
||||
0x40001000u, new (int Amount, uint ItemGuid)[] { (1, 0x50003001u) });
|
||||
|
||||
Assert.True(second);
|
||||
Assert.Single(h.Sells);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue