Files
DarkflameServer/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp
Aaron Kimbrell e3a75a689e fix(wire): send NotifyNotEnoughInvSpace with its own message ID
WIRE FIX. DLU sent NotifyNotEnoughInvSpace with the ID of
VehicleNotifyFinishedRace (1396). The 1.10.64 client registers it as
NOTIFY_NOT_ENOUGH_INV_SPACE (1516, 0x00545c90) and reads freeSlotsNeeded
followed by the optional inventoryType (0x00d8b850), which is what the
payload already was. Only the message ID changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:04 -05:00

529 lines
22 KiB
C++

#include "InventoryMessages.h"
#include "GameDependencies.h"
#include "GameMessageTestUtils.h"
#include "Legacy/InventoryMessagesLegacy.h"
#include "CDComponentsRegistryTable.h"
#include "CDItemComponentTable.h"
#include "eReplicaComponentType.h"
#include "Inventory.h"
#include "Item.h"
#include <functional>
#include <limits>
#include <vector>
#include <gtest/gtest.h>
using namespace GameMessageTestUtils;
namespace {
const std::vector<int32_t> g_Ints = { 0, 1, -1, 1727, std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::min() };
const std::vector<uint32_t> g_UInts = { 0, 1, 2, 999, std::numeric_limits<uint32_t>::max() };
const std::vector<LWOOBJID> g_Ids = { LWOOBJID_EMPTY, 0x1000000000000001LL, -1 };
const std::vector<eInventoryType> g_InventoryTypes = { eInventoryType::ITEMS, eInventoryType::VAULT_ITEMS, eInventoryType::MODELS, eInventoryType::INVALID };
const std::vector<std::u16string> g_WStrings = { u"", u"a", u"ma=1:6416+1:8092", u"été ☃", std::u16string(300, u'x') };
PacketBytes Payload(const GameMessages::NetGameMsg& msg) {
RakNet::BitStream bitStream;
msg.Serialize(bitStream);
return FromBitStream(bitStream);
}
// Serializes msg, then reads it with the legacy read sequence; both must consume exactly the same bits.
template<typename Result>
Result ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function<Result(RakNet::BitStream&)>& read) {
RakNet::BitStream wire;
msg.Serialize(wire);
RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false);
auto result = read(legacyStream);
EXPECT_EQ(legacyStream.GetReadOffset(), wire.GetNumberOfBitsUsed());
return result;
}
// Makes lot a valid item (ItemComponent componentID) with the given bind flags, without a CDClient database.
void RegisterItemLot(const LOT lot, const uint32_t componentID, const bool isBOE, const bool isBOP) {
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
registry.insert_or_assign(static_cast<uint64_t>(lot), componentID);
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::ITEM) << 32 | static_cast<uint64_t>(lot), componentID);
CDItemComponent component{};
component.id = componentID;
component.isBOE = isBOE;
component.isBOP = isBOP;
CDClientManager::GetEntriesMutable<CDItemComponentTable>().insert_or_assign(componentID, component);
}
}
class InventoryMessagesTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
TEST_F(InventoryMessagesTests, AddItemToInventoryClientSyncMatchesLegacy) {
RegisterItemLot(1727, 90001, false, false);
RegisterItemLot(6416, 90002, true, false);
RegisterItemLot(8092, 90003, false, true);
LwoNameValue emptyConfig;
LwoNameValue config;
config.Insert(u"assemblyPartLOTs", std::u16string(u"1:6416+1:8092"));
config.Insert<int32_t>(u"userModelID", 42);
uint32_t slot = 0;
for (const auto inventoryType : { eInventoryType::ITEMS, eInventoryType::MODELS }) {
Inventory inventory(inventoryType, 400, {}, nullptr);
for (const LOT lot : { 1727, 6416, 8092 }) {
for (const uint32_t count : { 0u, 1u, 7u }) {
for (const bool bound : { false, true }) {
for (const auto* itemConfig : { &emptyConfig, &config }) {
// The inventory owns (and deletes) the item.
auto* item = new Item(0x5000 + slot, lot, &inventory, slot, count, bound, *itemConfig, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE);
slot++;
for (const auto target : g_Targets) {
Entity entity(target, info);
for (const LWOOBJID newID : { LWOOBJID_EMPTY, LWOOBJID{ 0x7777 } }) {
for (const bool showFlyingLoot : { false, true }) {
for (const int itemCount : { 1, 0, -1, 5 }) {
for (const LWOOBJID subKey : { LWOOBJID_EMPTY, LWOOBJID{ 0x99 } }) {
for (const auto lootSource : { eLootSourceType::NONE, eLootSourceType::QUICKBUILD }) {
GameMessages::AddItemToInventoryClientSync msg;
msg.target = target;
msg.SetItem(*item);
msg.eLootTypeSource = lootSource;
msg.iSubkey = subKey;
msg.itemCount = itemCount;
msg.newObjID = newID;
msg.showFlyingLoot = showFlyingLoot;
ExpectSameAsLegacy([&](const SystemAddress& a) {
LegacyGameMessages::SendAddItemToInventoryClientSync(&entity, a, item, newID, showFlyingLoot, itemCount, subKey, lootSource);
}, msg, SendMode::SendToClient);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.extraInfo, msg.extraInfo);
EXPECT_EQ(copy.iObjTemplate, lot);
EXPECT_EQ(copy.invType, inventoryType);
EXPECT_EQ(copy.itemsTotal, count);
EXPECT_EQ(copy.itemCount, itemCount);
EXPECT_EQ(copy.iSubkey, subKey);
}
}
}
}
}
}
}
}
}
}
}
}
TEST_F(InventoryMessagesTests, SmallMessagesMatchLegacy) {
for (const auto target : g_Targets) {
Entity entity(target, info);
const auto toEntity = [&](const std::function<void()>& legacy, const GameMessages::NetGameMsg& msg) {
// The legacy functions sent to entity->GetSystemAddress() only.
const auto legacyPackets = Capture(legacy);
const auto ours = Capture([&] { msg.SendToClient(entity.GetSystemAddress()); });
ASSERT_EQ(legacyPackets.size(), 1);
ASSERT_EQ(ours.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacyPackets[0]), FromCapture(ours[0]));
EXPECT_EQ(legacyPackets[0].sysAddr, ours[0].sysAddr);
EXPECT_EQ(legacyPackets[0].broadcast, ours[0].broadcast);
};
for (const auto type : g_InventoryTypes) {
for (const auto size : g_Ints) {
GameMessages::SetInventorySize msg;
msg.target = target;
msg.inventoryType = type;
msg.size = size;
toEntity([&] { LegacyGameMessages::SendSetInventorySize(&entity, type, size); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.inventoryType, type);
EXPECT_EQ(copy.size, size);
}
}
for (const auto item : g_Ids) {
for (const bool success : { false, true }) {
GameMessages::ConsumeClientItem consume;
consume.target = target;
consume.bSuccess = success;
consume.item = item;
toEntity([&] { LegacyGameMessages::SendConsumeClientItem(&entity, success, item); }, consume);
const auto copy = RoundTrip(consume);
EXPECT_EQ(copy.bSuccess, success);
EXPECT_EQ(copy.item, item);
}
}
for (const auto lot : g_Ints) {
for (const bool success : { false, true }) {
GameMessages::UseItemResult result;
result.target = target;
result.itemTemplateID = lot;
result.useItemResult = success;
toEntity([&] { LegacyGameMessages::SendUseItemResult(&entity, lot, success); }, result);
const auto copy = RoundTrip(result);
EXPECT_EQ(copy.itemTemplateID, lot);
EXPECT_EQ(copy.useItemResult, success);
}
}
for (const auto response : { eUseItemResponse::NoImaginationForPet, eUseItemResponse::FailedPrecondition, static_cast<eUseItemResponse>(0) }) {
GameMessages::UseItemRequirementsResponse msg;
msg.target = target;
msg.eUseResponse = response;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUseItemRequirementsResponse(target, a, response); }, msg, SendMode::SendToClient);
EXPECT_EQ(RoundTrip(msg).eUseResponse, response);
}
for (const auto dst : g_InventoryTypes) {
for (const auto src : g_InventoryTypes) {
for (const auto code : { eReponseMoveItemBetweenInventoryTypeCode::SUCCESS, eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC, eReponseMoveItemBetweenInventoryTypeCode::FAIL_INV_FULL, eReponseMoveItemBetweenInventoryTypeCode::FAIL_CANT_MOVE_THINKING_HAT }) {
GameMessages::ResponseMoveItemBetweenInventoryTypes msg;
msg.target = target;
msg.inventoryTypeDestination = dst;
msg.inventoryTypeSource = src;
msg.response = code;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendResponseMoveItemBetweenInventoryTypes(target, a, dst, src, code); }, msg, SendMode::SendToClient);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.inventoryTypeDestination, dst);
EXPECT_EQ(copy.inventoryTypeSource, src);
EXPECT_EQ(copy.response, code);
}
}
}
for (const auto slots : g_UInts) {
for (const auto type : g_InventoryTypes) {
GameMessages::NotifyNotEnoughInvSpace msg;
msg.target = target;
msg.freeSlotsNeeded = slots;
msg.inventoryType = type;
// WIRE FIX: the legacy bytes carry VEHICLE_NOTIFY_FINISHED_RACE (1396) as the message ID; everything
// else is unchanged, so compare against them with the ID (bytes 16-17, after the header and object ID)
// swapped for NOTIFY_NOT_ENOUGH_INV_SPACE (1516).
const auto legacy = Capture([&] { LegacyGameMessages::SendNotifyNotEnoughInvSpace(target, slots, type, ClientAddress()); });
ASSERT_FALSE(legacy.empty());
auto expected = FromCapture(legacy[0]);
ASSERT_EQ(expected.bytes[16] | (expected.bytes[17] << 8), 1396);
expected.bytes[16] = 1516 & 0xFF;
expected.bytes[17] = 1516 >> 8;
EXPECT_PACKET_EQ(expected, StructPacket(msg));
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.freeSlotsNeeded, slots);
EXPECT_EQ(copy.inventoryType, type);
}
}
for (const bool active : { false, true }) {
for (const auto type : { eUnequippableActiveType::INVALID, eUnequippableActiveType::PET, eUnequippableActiveType::MOUNT }) {
for (const auto item : g_Ids) {
GameMessages::MarkInventoryItemAsActive msg;
msg.target = target;
msg.bActive = active;
msg.iType = type;
msg.itemID = item;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendMarkInventoryItemAsActive(target, active, type, item, a); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.bActive, active);
EXPECT_EQ(copy.iType, type);
EXPECT_EQ(copy.itemID, item);
}
}
}
GameMessages::UpdateInventoryUi updateUi;
updateUi.target = target;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUpdateInventoryUi(target, a); }, updateUi, SendMode::SendToClient);
}
}
TEST_F(InventoryMessagesTests, RemoveItemFromInventoryMatchesLegacy) {
for (const auto target : g_Targets) {
Entity entity(target, info);
for (const auto id : g_Ids) {
for (const LOT lot : { LOT_NULL, LOT{ 1727 } }) {
for (const auto type : g_InventoryTypes) {
for (const auto stackCount : g_UInts) {
for (const auto stackRemaining : g_UInts) {
GameMessages::RemoveItemFromInventory msg;
msg.target = target;
msg.bConfirmed = true;
msg.eInvType = type;
msg.eLootTypeSource = LOOTTYPE_NONE;
msg.iObjID = id;
msg.iObjTemplate = lot;
msg.iStackCount = stackCount;
msg.iStackRemaining = stackRemaining;
ExpectSameAsLegacy([&](const SystemAddress& a) {
LegacyGameMessages::SendRemoveItemFromInventory(&entity, a, id, lot, type, stackCount, stackRemaining);
}, msg, SendMode::SendToClient);
// What the server sends reads back the same with the old handler's read sequence.
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyRemoveItem>(msg, LegacyGameMessages::ReadRemoveItemFromInventory);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.eInvType, legacy.eInvType);
EXPECT_EQ(copy.iObjID, legacy.iObjID);
EXPECT_EQ(copy.iObjTemplate, legacy.iObjTemplate);
EXPECT_EQ(copy.iStackCount, legacy.iStackCount);
EXPECT_EQ(copy.iStackRemaining, legacy.iStackRemaining);
}
}
}
}
}
}
}
TEST_F(InventoryMessagesTests, InboundReadsLikeLegacy) {
// RemoveItemFromInventory as the client sends it (every optional field both ways is covered by the
// optional reads; the "always sent" flags only matter for what the server writes).
for (const bool flag : { false, true }) {
for (const auto& extraInfo : g_WStrings) {
for (const auto id : g_Ids) {
GameMessages::RemoveItemFromInventory msg;
msg.bConfirmed = flag;
msg.bDeleteItem = !flag;
msg.bOutSuccess = flag;
msg.extraInfo = extraInfo;
msg.forceDeletion = flag;
msg.iLootTypeSource = id;
msg.iObjID = id;
msg.iRequestingObjID = id;
msg.iSubkey = id;
msg.iTradeID = id;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyRemoveItem>(msg, LegacyGameMessages::ReadRemoveItemFromInventory);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.bConfirmed, legacy.bConfirmed);
EXPECT_EQ(copy.bDeleteItem, legacy.bDeleteItem);
EXPECT_EQ(copy.bOutSuccess, legacy.bOutSuccess);
EXPECT_EQ(copy.extraInfo, legacy.extraInfo);
EXPECT_EQ(copy.forceDeletion, legacy.forceDeletion);
EXPECT_EQ(copy.iLootTypeSource, legacy.iLootTypeSource);
EXPECT_EQ(copy.iRequestingObjID, legacy.iRequestingObjID);
EXPECT_EQ(copy.iSubkey, legacy.iSubkey);
EXPECT_EQ(copy.iTradeID, legacy.iTradeID);
ExpectTruncatedFails(msg);
}
}
}
// The same message with every optional flag clear, as a client that relies on the defaults would send it.
{
RakNet::BitStream wire;
wire.Write1(); // bConfirmed
wire.Write1(); // bDeleteItem
wire.Write0(); // bOutSuccess
wire.Write0(); // eInvType
wire.Write0(); // eLootTypeSource
wire.Write<uint32_t>(0); // extraInfo
wire.Write1(); // forceDeletion
for (int i = 0; i < 8; i++) wire.Write0(); // the eight optional ids and counts
RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false);
const auto legacy = LegacyGameMessages::ReadRemoveItemFromInventory(legacyStream);
GameMessages::RemoveItemFromInventory msg;
ASSERT_TRUE(msg.Deserialize(wire));
EXPECT_EQ(msg.eInvType, legacy.eInvType);
EXPECT_EQ(msg.eLootTypeSource, legacy.eLootTypeSource);
EXPECT_EQ(msg.iObjTemplate, legacy.iObjTemplate);
EXPECT_EQ(msg.iStackCount, legacy.iStackCount);
EXPECT_EQ(msg.iStackRemaining, legacy.iStackRemaining);
}
for (const auto id : g_Ids) {
for (const bool flag : { false, true }) {
GameMessages::EquipInventory equip;
equip.bIgnoreCooldown = !flag;
equip.bOutSuccess = flag;
equip.itemToEquip = id;
const auto legacyEquip = ReadWithLegacy<LegacyGameMessages::LegacyEquip>(equip, LegacyGameMessages::ReadEquipItem);
EXPECT_EQ(RoundTrip(equip).itemToEquip, legacyEquip.objectID);
ExpectTruncatedFails(equip);
GameMessages::UnEquipInventory unequip;
unequip.bEvenIfDead = flag;
unequip.bIgnoreCooldown = !flag;
unequip.bOutSuccess = flag;
unequip.itemToUnequip = id;
const auto legacyUnequip = ReadWithLegacy<LegacyGameMessages::LegacyEquip>(unequip, LegacyGameMessages::ReadUnequipItem);
EXPECT_EQ(RoundTrip(unequip).itemToUnequip, legacyUnequip.objectID);
ExpectTruncatedFails(unequip);
for (const auto value : g_Ints) {
GameMessages::MoveItemInInventory move;
move.destInvType = flag ? value : static_cast<int32_t>(eInventoryType::INVALID);
move.iObjID = id;
move.inventoryType = value;
move.responseCode = -value;
move.slot = value / 2;
const auto legacyMove = ReadWithLegacy<LegacyGameMessages::LegacyMoveItemInInventory>(move, LegacyGameMessages::ReadMoveItemInInventory);
const auto moveCopy = RoundTrip(move);
EXPECT_EQ(moveCopy.destInvType, legacyMove.destInvType);
EXPECT_EQ(moveCopy.iObjID, legacyMove.iObjID);
EXPECT_EQ(moveCopy.inventoryType, legacyMove.inventoryType);
EXPECT_EQ(moveCopy.responseCode, legacyMove.responseCode);
EXPECT_EQ(moveCopy.slot, legacyMove.slot);
ExpectTruncatedFails(move);
GameMessages::RequestMoveItemBetweenInventoryTypes request;
request.bAllowPartial = flag;
request.destSlot = value;
request.iStackCount = -value;
request.invTypeDst = flag ? eInventoryType::VAULT_ITEMS : eInventoryType::ITEMS;
request.invTypeSrc = flag ? eInventoryType::ITEMS : eInventoryType::MODELS;
request.itemID = id;
request.showFlyingLoot = !flag;
request.subkey = id;
request.itemLOT = value;
const auto legacyRequest = ReadWithLegacy<LegacyGameMessages::LegacyRequestMove>(request, LegacyGameMessages::ReadRequestMoveItemBetweenInventoryTypes);
const auto requestCopy = RoundTrip(request);
EXPECT_EQ(requestCopy.bAllowPartial, legacyRequest.bAllowPartial);
EXPECT_EQ(requestCopy.destSlot, legacyRequest.destSlot);
EXPECT_EQ(requestCopy.iStackCount, legacyRequest.iStackCount);
EXPECT_EQ(requestCopy.invTypeDst, legacyRequest.invTypeDst);
EXPECT_EQ(requestCopy.invTypeSrc, legacyRequest.invTypeSrc);
EXPECT_EQ(requestCopy.itemID, legacyRequest.itemID);
EXPECT_EQ(requestCopy.showFlyingLoot, legacyRequest.showFlyingLoot);
EXPECT_EQ(requestCopy.subkey, legacyRequest.subkey);
if (value != LOT_NULL) EXPECT_EQ(requestCopy.itemLOT, legacyRequest.itemLOT); // unused; the old default was 0
ExpectTruncatedFails(request);
}
for (const auto count : g_UInts) {
for (const LOT lot : { LOT_NULL, LOT{ 1727 } }) {
GameMessages::MoveItemBetweenInventoryTypes move;
move.inventoryTypeA = eInventoryType::MODELS;
move.inventoryTypeB = flag ? eInventoryType::VAULT_MODELS : eInventoryType::TEMP_MODELS;
move.objectID = id;
move.showFlyingLoot = flag;
move.stackCount = count;
move.templateID = lot;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyMoveItemBetweenInventoryTypes>(move, LegacyGameMessages::ReadMoveItemBetweenInventoryTypes);
const auto copy = RoundTrip(move);
EXPECT_EQ(copy.inventoryTypeA, legacy.inventoryTypeA);
EXPECT_EQ(copy.inventoryTypeB, legacy.inventoryTypeB);
EXPECT_EQ(copy.objectID, legacy.objectID);
EXPECT_EQ(copy.showFlyingLoot, legacy.showFlyingLoot);
EXPECT_EQ(copy.stackCount, legacy.stackCount);
EXPECT_EQ(copy.templateID, legacy.templateID);
ExpectTruncatedFails(move);
}
}
}
GameMessages::ClientItemConsumed consumed;
consumed.item = id;
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(consumed, LegacyGameMessages::ReadSingle<LWOOBJID>), id);
EXPECT_EQ(RoundTrip(consumed).item, id);
ExpectTruncatedFails(consumed);
GameMessages::UseNonEquipmentItem use;
use.itemToUse = id;
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(use, LegacyGameMessages::ReadSingle<LWOOBJID>), id);
EXPECT_EQ(RoundTrip(use).itemToUse, id);
ExpectTruncatedFails(use);
}
for (const auto lot : g_Ints) {
GameMessages::SetConsumableItem consumable;
consumable.itemTemplateID = lot;
EXPECT_EQ(ReadWithLegacy<LOT>(consumable, LegacyGameMessages::ReadSingle<LOT>), lot);
EXPECT_EQ(RoundTrip(consumable).itemTemplateID, lot);
ExpectTruncatedFails(consumable);
}
for (const std::string action : std::vector<std::string>{ "ADD", "MODIFY", "REMOVE", "", "bogus" }) {
for (const std::string& groupID : std::vector<std::string>{ "", "user_group1", std::string(200, 'g') }) {
for (const auto& groupName : g_WStrings) {
for (const auto type : { eInventoryType::ITEMS, eInventoryType::BRICKS, eInventoryType::MODELS }) {
for (const bool locked : { false, true }) {
GameMessages::UpdateInventoryGroup group;
group.action = action;
group.groupID = groupID;
group.groupName = groupName;
group.inventoryType = type;
group.locked = locked;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyGroupUpdate>(group, LegacyGameMessages::ReadUpdateInventoryGroup);
ASSERT_TRUE(legacy.ok);
const auto copy = RoundTrip(group);
EXPECT_EQ(copy.action, legacy.action);
EXPECT_EQ(copy.groupID, legacy.groupId);
EXPECT_EQ(copy.groupName, legacy.groupName);
EXPECT_EQ(copy.inventoryType, legacy.inventory);
EXPECT_EQ(copy.locked, legacy.locked);
ExpectTruncatedFails(group);
}
}
for (const auto lot : g_Ints) {
GameMessages::UpdateInventoryGroupContents contents;
contents.action = action;
contents.groupID = groupID;
contents.inventoryType = eInventoryType::BRICKS;
contents.lot = lot;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyGroupUpdate>(contents, LegacyGameMessages::ReadUpdateInventoryGroupContents);
ASSERT_TRUE(legacy.ok);
const auto copy = RoundTrip(contents);
EXPECT_EQ(copy.action, legacy.action);
EXPECT_EQ(copy.groupID, legacy.groupId);
EXPECT_EQ(copy.inventoryType, legacy.inventory);
EXPECT_EQ(copy.lot, legacy.lot);
ExpectTruncatedFails(contents);
}
}
}
}
// A group name longer than the old handler allowed is rejected.
RakNet::BitStream tooLong;
tooLong.Write<uint32_t>(0);
tooLong.Write<uint32_t>(0);
tooLong.Write<uint32_t>(MAX_MESSAGE_LENGTH / 2 + 1);
GameMessages::UpdateInventoryGroup group;
EXPECT_FALSE(group.Deserialize(tooLong));
}
// MoveInventoryBatch had no oracle (the old function was never called); it follows the client's layout
// (legouniverse.exe 1.10.64, LWOInventoryComponent_Common::msgMoveInventoryBatch reads moveLOT and moveSubKey).
TEST_F(InventoryMessagesTests, MoveInventoryBatchRoundTrips) {
GameMessages::MoveInventoryBatch batch;
EXPECT_PACKET_EQ(FromHex("00 00", 9), Payload(batch));
batch.bAllowPartial = true;
batch.count = 3;
batch.dstBag = eInventoryType::VAULT_ITEMS;
batch.moveLOT = 1727;
batch.moveSubkey = 0x22;
batch.showFlyingLoot = true;
batch.srcBag = eInventoryType::MODELS;
batch.startObjectID = 0x33;
const auto copy = RoundTrip(batch);
EXPECT_EQ(copy.count, 3);
EXPECT_EQ(copy.dstBag, eInventoryType::VAULT_ITEMS);
EXPECT_EQ(copy.moveLOT, 1727);
EXPECT_EQ(copy.moveSubkey, 0x22);
EXPECT_EQ(copy.srcBag, eInventoryType::MODELS);
EXPECT_EQ(copy.startObjectID, 0x33);
ExpectTruncatedFails(batch);
}
// Independent of the legacy code: hand computed payloads (RakNet writes MSB first).
TEST_F(InventoryMessagesTests, GoldenBytes) {
GameMessages::EquipInventory equip;
equip.bOutSuccess = true;
equip.itemToEquip = 0x11;
// 0, 1, then 11 00 00 00 00 00 00 00
EXPECT_PACKET_EQ(FromHex("44 40 00 00 00 00 00 00 00", 66), Payload(equip));
GameMessages::ResponseMoveItemBetweenInventoryTypes response;
response.inventoryTypeDestination = eInventoryType::VAULT_ITEMS; // 1
response.response = eReponseMoveItemBetweenInventoryTypeCode::SUCCESS; // 0
// 1 + 01 00 00 00, 0, 1 + 00 00 00 00
EXPECT_PACKET_EQ(FromHex("80 80 00 00 20 00 00 00 00", 67), Payload(response));
GameMessages::SetInventorySize size;
size.inventoryType = eInventoryType::ITEMS;
size.size = 20;
EXPECT_PACKET_EQ(FromHex("00 00 00 00 14 00 00 00"), Payload(size));
}