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>
198 lines
7.5 KiB
C#
198 lines
7.5 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using AcDream.Core.Net.Messages;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
/// <summary>
|
|
/// Slice 6.3 golden-byte coverage for the outbound Buy (<c>0x005F</c>)
|
|
/// builder — research doc §A.1's four-way-confirmed wire layout, including
|
|
/// the trailing <c>alternateCurrencyId</c> field the real retail client
|
|
/// sends but ACE's reader currently ignores.
|
|
/// </summary>
|
|
public sealed class VendorRequestsTests
|
|
{
|
|
[Fact]
|
|
public void BuildBuy_SingleItem_WritesEnvelopeSequenceOpcodeVendorCountAndItem()
|
|
{
|
|
byte[] body = VendorRequests.BuildBuy(
|
|
gameActionSequence: 9,
|
|
vendorGuid: 0x40001000u,
|
|
amount: 1,
|
|
itemGuid: 0x50002000u,
|
|
alternateCurrencyId: 0u);
|
|
|
|
// envelope(4) + seq(4) + opcode(4) + vendorGuid(4) + itemCount(4)
|
|
// + 1*(amount(4)+guid(4)) + trailing currency(4) = 32.
|
|
Assert.Equal(32, body.Length);
|
|
Assert.Equal(VendorRequests.GameActionEnvelope,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0)));
|
|
Assert.Equal(9u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
|
|
Assert.Equal(VendorRequests.BuyOpcode,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
|
Assert.Equal(0x005Fu, VendorRequests.BuyOpcode);
|
|
Assert.Equal(0x40001000u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
|
Assert.Equal(1u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
|
Assert.Equal(1,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20)));
|
|
Assert.Equal(0x50002000u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(24)));
|
|
// Trailing alternateCurrencyId — present even for a pyreal (0) vendor,
|
|
// matching retail's CM_Vendor::Event_Buy which writes it unconditionally.
|
|
Assert.Equal(0u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(28)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildBuy_StackedItem_WritesTheSplitSliderQuantityAsAPlainPositiveAmount()
|
|
{
|
|
// amount is NOT ItemProfile's packed sign-extended supply-count
|
|
// field -- it's a plain positive int32 (research doc §A.1).
|
|
byte[] body = VendorRequests.BuildBuy(
|
|
gameActionSequence: 1,
|
|
vendorGuid: 0x40001000u,
|
|
amount: 25,
|
|
itemGuid: 0x50002001u,
|
|
alternateCurrencyId: 0u);
|
|
|
|
Assert.Equal(25,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildBuy_AlternateCurrencyVendor_WritesTheVendorsTradeWcidTrailing()
|
|
{
|
|
byte[] body = VendorRequests.BuildBuy(
|
|
gameActionSequence: 1,
|
|
vendorGuid: 0x40001000u,
|
|
amount: 1,
|
|
itemGuid: 0x50002000u,
|
|
alternateCurrencyId: 0x12345678u);
|
|
|
|
Assert.Equal(0x12345678u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(28)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildBuy_ListOverload_MultipleItems_WritesEachAmountGuidPairInOrder()
|
|
{
|
|
byte[] body = VendorRequests.BuildBuy(
|
|
gameActionSequence: 4,
|
|
vendorGuid: 0x40001000u,
|
|
items: new (int Amount, uint ItemGuid)[]
|
|
{
|
|
(1, 0x50002000u),
|
|
(10, 0x50002001u),
|
|
},
|
|
alternateCurrencyId: 0u);
|
|
|
|
// 24 + 2*8 = 40.
|
|
Assert.Equal(40, body.Length);
|
|
Assert.Equal(2u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
|
Assert.Equal(1,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20)));
|
|
Assert.Equal(0x50002000u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(24)));
|
|
Assert.Equal(10,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(28)));
|
|
Assert.Equal(0x50002001u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(32)));
|
|
// Trailing currency still lands after every item pair.
|
|
Assert.Equal(0u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(36)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildBuy_SingleItemOverload_MatchesTheGeneralListOverload()
|
|
{
|
|
byte[] viaSingle = VendorRequests.BuildBuy(
|
|
gameActionSequence: 3,
|
|
vendorGuid: 0x40001000u,
|
|
amount: 5,
|
|
itemGuid: 0x50002000u,
|
|
alternateCurrencyId: 7u);
|
|
byte[] viaList = VendorRequests.BuildBuy(
|
|
gameActionSequence: 3,
|
|
vendorGuid: 0x40001000u,
|
|
items: new (int Amount, uint ItemGuid)[] { (5, 0x50002000u) },
|
|
alternateCurrencyId: 7u);
|
|
|
|
Assert.Equal(viaList, viaSingle);
|
|
}
|
|
|
|
// ---- Slice 6c: BuildSell (0x0060) — no trailing currency field ---------
|
|
|
|
[Fact]
|
|
public void BuildSell_SingleItem_WritesEnvelopeSequenceOpcodeVendorCountAndItemWithNoTrailer()
|
|
{
|
|
byte[] body = VendorRequests.BuildSell(
|
|
gameActionSequence: 9,
|
|
vendorGuid: 0x40001000u,
|
|
items: new (int Amount, uint ItemGuid)[] { (1, 0x50002000u) });
|
|
|
|
// envelope(4) + seq(4) + opcode(4) + vendorGuid(4) + itemCount(4)
|
|
// + 1*(amount(4)+guid(4)) = 28. Note: 4 bytes SHORTER than the
|
|
// equivalent single-item Buy payload (32) — Sell has no trailing
|
|
// alternateCurrencyId.
|
|
Assert.Equal(28, body.Length);
|
|
Assert.Equal(VendorRequests.GameActionEnvelope,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0)));
|
|
Assert.Equal(9u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
|
|
Assert.Equal(VendorRequests.SellOpcode,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
|
Assert.Equal(0x0060u, VendorRequests.SellOpcode);
|
|
Assert.Equal(0x40001000u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
|
Assert.Equal(1u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
|
Assert.Equal(1,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20)));
|
|
Assert.Equal(0x50002000u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(24)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildSell_MultipleItems_WritesEachAmountGuidPairInOrderWithNoTrailer()
|
|
{
|
|
byte[] body = VendorRequests.BuildSell(
|
|
gameActionSequence: 4,
|
|
vendorGuid: 0x40001000u,
|
|
items: new (int Amount, uint ItemGuid)[]
|
|
{
|
|
(1, 0x50002000u),
|
|
(10, 0x50002001u),
|
|
});
|
|
|
|
// 20 + 2*8 = 36.
|
|
Assert.Equal(36, body.Length);
|
|
Assert.Equal(2u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
|
Assert.Equal(1,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20)));
|
|
Assert.Equal(0x50002000u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(24)));
|
|
Assert.Equal(10,
|
|
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(28)));
|
|
Assert.Equal(0x50002001u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(32)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildSell_EmptyList_WritesAZeroCountAndNoItemPairs()
|
|
{
|
|
byte[] body = VendorRequests.BuildSell(
|
|
gameActionSequence: 1,
|
|
vendorGuid: 0x40001000u,
|
|
items: Array.Empty<(int Amount, uint ItemGuid)>());
|
|
|
|
Assert.Equal(20, body.Length);
|
|
Assert.Equal(0u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
|
}
|
|
}
|