Files
DarkflameServer/tests/dGameTests/dGameMessagesTests/BuildingMessagesTests.cpp
Aaron Kimbrell 0c2727ad3c refactor: building game messages and blueprint packets as structs
Converts build mode, arranging, modular build and brick by brick
messages to NetGameMsgs in BuildingMessages.{h,cpp}: StartArrangingWithItem,
FinishArrangingWithItem, ModularBuildEnd and SetBuildModeConfirmed
(sent), and StartBuildingWithItem, DoneArrangingWithItem,
ModularBuildFinish, ModularBuildMoveAndEquip, ModularBuildConvertModel,
SetBuildMode, BuildModeSet, UnUseBBBModel, BBBLoadItemRequest and
BBBSaveRequest (received, registered in GameMessageHandler's map with
their handlers' logic kept). The BLUEPRINT_SAVE_RESPONSE and
BLUEPRINT_LOAD_RESPONSE_ITEMID client packets become the LUBitStream
structs ClientPackets::BlueprintSaveResponse and BlueprintLoadItemResponse,
also used by WorldServer's level load. The never-called
SendBBBSaveResponse is gone.

SetBuildModeConfirmed keeps writing modeValue's and startPos's default
flags as always set, as DLU did. BuildModeSet and UnUseBBBModel now read
their whole client layout (DLU read only the first fields).

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions and inline packet
writes over an input grid, received messages compared with the old
handlers' read sequences with every truncated payload rejected, hand
computed golden bytes, round trips and a mutation check. Layouts
confirmed against the 1.10.64 client in Ghidra.

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

389 lines
16 KiB
C++

#include "BuildingMessages.h"
#include "ClientPackets.h"
#include "GameDependencies.h"
#include "GameMessageTestUtils.h"
#include "Legacy/BuildingMessagesLegacy.h"
#include <functional>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
using namespace GameMessageTestUtils;
namespace {
const std::vector<int32_t> g_Ints = { 0, 1, -1, 8, std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::min() };
const std::vector<NiPoint3> g_Points = { NiPoint3Constant::ZERO, NiPoint3(1.5f, -2.0f, 1e6f) };
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; returns the result and how many bits it read.
template<typename Result>
std::pair<Result, uint32_t> 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);
return { result, static_cast<uint32_t>(legacyStream.GetReadOffset()) };
}
uint32_t Bits(const GameMessages::NetGameMsg& msg) { return Payload(msg).bits; }
// A packet struct sent to one client and to UNASSIGNED must match the legacy bytes. The legacy code always did
// SEND_PACKET (never a broadcast); LUBitStream::Send broadcasts for UNASSIGNED, which none of the callers pass.
void ExpectPacketSameAsLegacy(const std::function<void(const SystemAddress&)>& legacySend, const LUBitStream& packet) {
const auto address = ClientAddress();
const auto legacy = Capture([&] { legacySend(address); });
const auto ours = Capture([&] { packet.Send(address); });
ASSERT_EQ(legacy.size(), 1);
ASSERT_EQ(ours.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacy[0]), FromCapture(ours[0]));
EXPECT_EQ(legacy[0].broadcast, ours[0].broadcast);
EXPECT_EQ(legacy[0].sysAddr, ours[0].sysAddr);
}
}
class BuildingMessagesTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
TEST_F(BuildingMessagesTests, StartArrangingWithItemMatchesLegacy) {
for (const auto target : g_Targets) {
Entity entity(target, info);
for (const bool firstTime : { false, true }) {
for (const auto area : g_Targets) {
for (const auto value : g_Ints) {
for (const auto& point : g_Points) {
GameMessages::StartArrangingWithItem msg;
msg.target = target;
msg.firstTime = firstTime;
msg.buildAreaID = area;
msg.buildStartPos = point;
msg.sourceBag = value;
msg.sourceID = ~area;
msg.sourceLot = value / 2;
msg.sourceType = -value;
msg.targetID = area / 3;
msg.targetLot = value / 5;
msg.targetPos = NiPoint3(point.z, point.y, point.x);
msg.targetType = value / 7;
ExpectSameAsLegacy([&](const SystemAddress& a) {
LegacyGameMessages::SendStartArrangingWithItem(&entity, a, firstTime, area, point, value, msg.sourceID, msg.sourceLot, msg.sourceType, msg.targetID, msg.targetLot, msg.targetPos, msg.targetType);
}, msg, SendMode::SendToClient);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.buildAreaID, area);
EXPECT_EQ(copy.targetType, msg.targetType);
ExpectTruncatedFails(msg);
}
}
}
}
// The struct's defaults are the old declaration's default arguments
GameMessages::StartArrangingWithItem defaults;
defaults.target = target;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendStartArrangingWithItem(&entity, a, true, LWOOBJID_EMPTY, NiPoint3Constant::ZERO, 0, LWOOBJID_EMPTY, 0, 8, 0, 0, NiPoint3Constant::ZERO, 0); }, defaults, SendMode::SendToClient);
}
}
TEST_F(BuildingMessagesTests, FinishArrangingAndModularBuildEndMatchLegacy) {
for (const auto target : g_Targets) {
Entity entity(target, info);
// The legacy functions sent to the entity's own address.
const auto address = entity.GetSystemAddress();
for (const auto area : g_Targets) {
const auto legacy = Capture([&] { LegacyGameMessages::SendFinishArrangingWithItem(&entity, area); });
const auto ours = Capture([&] {
GameMessages::FinishArrangingWithItem finish;
finish.target = target;
finish.buildAreaID = area;
finish.SendToClient(address);
});
ASSERT_EQ(legacy.size(), 1);
ASSERT_EQ(ours.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacy[0]), FromCapture(ours[0]));
EXPECT_EQ(legacy[0].broadcast, ours[0].broadcast);
EXPECT_EQ(legacy[0].sysAddr, ours[0].sysAddr);
GameMessages::FinishArrangingWithItem finish;
finish.buildAreaID = area;
finish.newTargetPos = NiPoint3(1, 2, 3);
finish.oldItemLot = 7;
const auto copy = RoundTrip(finish);
EXPECT_EQ(copy.buildAreaID, area);
EXPECT_EQ(copy.newTargetPos, finish.newTargetPos);
ExpectTruncatedFails(finish);
}
const auto legacyEnd = Capture([&] { LegacyGameMessages::SendModularBuildEnd(&entity); });
const auto ourEnd = Capture([&] {
GameMessages::ModularBuildEnd end;
end.target = target;
end.SendToClient(address);
});
ASSERT_EQ(legacyEnd.size(), 1);
ASSERT_EQ(ourEnd.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacyEnd[0]), FromCapture(ourEnd[0]));
EXPECT_EQ(legacyEnd[0].sysAddr, ourEnd[0].sysAddr);
}
}
TEST_F(BuildingMessagesTests, SetBuildModeConfirmedMatchesLegacy) {
for (const auto target : g_Targets) {
for (const bool start : { false, true }) {
for (const bool warn : { false, true }) {
for (const auto value : g_Ints) {
for (const auto& point : g_Points) {
GameMessages::SetBuildModeConfirmed msg;
msg.target = target;
msg.start = start;
msg.warnVisitors = warn;
msg.modePaused = !start;
msg.modeValue = value;
msg.playerId = ~target;
msg.startPos = point;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendSetBuildModeConfirmed(target, a, start, warn, !start, value, ~target, point); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.modeValue, value);
EXPECT_EQ(copy.startPos, point);
EXPECT_EQ(copy.warnVisitors, warn);
ExpectTruncatedFails(msg);
}
}
}
}
}
}
TEST_F(BuildingMessagesTests, BlueprintPacketsMatchLegacy) {
for (const auto id : g_Targets) {
for (const bool success : { false, true }) {
ClientPackets::BlueprintLoadItemResponse load;
load.success = success;
load.itemId = id;
load.destItemId = ~id;
ExpectPacketSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendBlueprintLoadItemResponse(a, success, id, ~id); }, load);
}
ClientPackets::BlueprintSaveResponse failed;
failed.reasonCode = eBlueprintSaveResponseType::PlacementFailed;
ExpectPacketSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::WriteUnUseModelSaveResponse(a); }, failed);
const std::vector<std::vector<std::string>> chunkSets = { {}, { "sd0" }, { std::string("\x73\x64\x30\x01\xff", 5), std::string(1000, '\x7f'), "" } };
std::vector<std::pair<LWOOBJID, std::vector<std::string>>> saved;
ClientPackets::BlueprintSaveResponse save;
save.localId = id;
save.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
std::vector<std::pair<LWOOBJID, std::string>> levelModels;
ClientPackets::BlueprintSaveResponse level;
level.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
for (size_t i = 0; i <= chunkSets.size(); i++) {
ExpectPacketSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::WriteBBBSaveResponse(a, id, saved); }, save);
ExpectPacketSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::WriteLevelLoadSaveResponse(a, levelModels); }, level);
if (i == chunkSets.size()) break;
saved.emplace_back(id + static_cast<LWOOBJID>(i), chunkSets[i]);
auto& model = save.models.emplace_back();
model.blueprintId = id + static_cast<LWOOBJID>(i);
for (const auto& chunk : chunkSets[i]) model.data += chunk;
levelModels.emplace_back(model.blueprintId, model.data);
level.models.push_back(model);
}
// Round trip
RakNet::BitStream bitStream;
save.WritePacket(bitStream);
ClientPackets::BlueprintSaveResponse copy;
ASSERT_TRUE(copy.ReadHeader(bitStream));
ASSERT_TRUE(copy.Deserialize(bitStream));
EXPECT_EQ(bitStream.GetNumberOfUnreadBits(), 0);
ASSERT_EQ(copy.models.size(), save.models.size());
for (size_t i = 0; i < copy.models.size(); i++) EXPECT_EQ(copy.models[i].data, save.models[i].data);
}
}
TEST_F(BuildingMessagesTests, InboundReadsLikeLegacy) {
for (const auto id : g_Targets) {
for (const auto value : g_Ints) {
for (const bool flag : { false, true }) {
GameMessages::StartBuildingWithItem start;
start.firstTime = flag;
start.success = !flag;
start.sourceBag = value;
start.sourceId = id;
start.sourceLot = value / 2;
start.sourceType = value / 3;
start.targetId = ~id;
start.targetLot = -value;
start.targetPos = NiPoint3(1, 2, 3);
start.targetType = value / 5;
const auto [legacy, bits] = ReadWithLegacy<LegacyGameMessages::LegacyStartBuildingWithItem>(start, LegacyGameMessages::ReadStartBuildingWithItem);
EXPECT_EQ(bits, Bits(start));
const auto copy = RoundTrip(start);
EXPECT_EQ(copy.firstTime, legacy.firstTime);
EXPECT_EQ(copy.success, legacy.success);
EXPECT_EQ(copy.sourceBag, legacy.sourceBag);
EXPECT_EQ(copy.sourceId, legacy.sourceId);
EXPECT_EQ(copy.sourceLot, legacy.sourceLot);
EXPECT_EQ(copy.sourceType, legacy.sourceType);
EXPECT_EQ(copy.targetId, legacy.targetId);
EXPECT_EQ(copy.targetLot, legacy.targetLot);
EXPECT_EQ(copy.targetPos, legacy.targetPosition);
EXPECT_EQ(copy.targetType, legacy.targetType);
ExpectTruncatedFails(start);
for (const auto& point : g_Points) {
GameMessages::SetBuildMode mode;
mode.start = flag;
mode.distanceType = value;
mode.modePaused = !flag;
mode.modeValue = -value;
mode.playerId = id;
mode.startPos = point;
const auto [legacyMode, modeBits] = ReadWithLegacy<LegacyGameMessages::LegacySetBuildMode>(mode, LegacyGameMessages::ReadSetBuildMode);
EXPECT_EQ(modeBits, Bits(mode));
const auto modeCopy = RoundTrip(mode);
EXPECT_EQ(modeCopy.start, legacyMode.start);
EXPECT_EQ(modeCopy.distanceType, legacyMode.distanceType);
EXPECT_EQ(modeCopy.modePaused, legacyMode.modePaused);
EXPECT_EQ(modeCopy.modeValue, legacyMode.modeValue);
EXPECT_EQ(modeCopy.playerId, legacyMode.playerId);
EXPECT_EQ(modeCopy.startPos, legacyMode.startPosition);
ExpectTruncatedFails(mode);
// BuildModeSet has SetBuildMode's layout; DLU only ever read its first field.
GameMessages::BuildModeSet set;
set.start = flag;
set.distanceType = value;
set.modeValue = -value;
set.playerId = id;
set.startPos = point;
EXPECT_EQ(ReadWithLegacy<bool>(set, LegacyGameMessages::ReadBuildModeSet).first, flag);
EXPECT_EQ(RoundTrip(set).startPos, point);
ExpectTruncatedFails(set);
}
}
GameMessages::DoneArrangingWithItem done;
done.newSourceBag = value;
done.newSourceID = id;
done.newSourceLot = value / 2;
done.newSourceType = value / 3;
done.newTargetID = ~id;
done.newTargetLot = -value;
done.newTargetType = value / 5;
done.newTargetPosition = NiPoint3(4, 5, 6);
done.oldItemBag = value / 7;
done.oldItemID = id / 3;
done.oldItemLot = value / 11;
done.oldItemType = value / 13;
const auto [legacyDone, doneBits] = ReadWithLegacy<LegacyGameMessages::LegacyDoneArranging>(done, LegacyGameMessages::ReadDoneArrangingWithItem);
EXPECT_EQ(doneBits, Bits(done));
const auto doneCopy = RoundTrip(done);
EXPECT_EQ(doneCopy.newSourceBag, legacyDone.newSourceBAG);
EXPECT_EQ(doneCopy.newSourceID, legacyDone.newSourceID);
EXPECT_EQ(doneCopy.newSourceLot, legacyDone.newSourceLOT);
EXPECT_EQ(doneCopy.newSourceType, legacyDone.newSourceTYPE);
EXPECT_EQ(doneCopy.newTargetID, legacyDone.newTargetID);
EXPECT_EQ(doneCopy.newTargetLot, legacyDone.newTargetLOT);
EXPECT_EQ(doneCopy.newTargetType, legacyDone.newTargetTYPE);
EXPECT_EQ(doneCopy.newTargetPosition, legacyDone.newTargetPOS);
EXPECT_EQ(doneCopy.oldItemBag, legacyDone.oldItemBAG);
EXPECT_EQ(doneCopy.oldItemID, legacyDone.oldItemID);
EXPECT_EQ(doneCopy.oldItemLot, legacyDone.oldItemLOT);
EXPECT_EQ(doneCopy.oldItemType, legacyDone.oldItemTYPE);
ExpectTruncatedFails(done);
GameMessages::ModularBuildMoveAndEquip move;
move.templateID = value;
EXPECT_EQ(ReadWithLegacy<LOT>(move, LegacyGameMessages::ReadModularBuildMoveAndEquip).first, value);
EXPECT_EQ(RoundTrip(move).templateID, value);
ExpectTruncatedFails(move);
}
GameMessages::ModularBuildConvertModel convert;
convert.modelID = id;
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(convert, LegacyGameMessages::ReadObjectId).first, id);
EXPECT_EQ(RoundTrip(convert).modelID, id);
ExpectTruncatedFails(convert);
GameMessages::BBBLoadItemRequest load;
load.itemID = id;
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(load, LegacyGameMessages::ReadObjectId).first, id);
EXPECT_EQ(RoundTrip(load).itemID, id);
ExpectTruncatedFails(load);
for (const bool hasTransform : { false, true }) {
GameMessages::UnUseBBBModel unUse;
unUse.bHasWorldTransform = hasTransform;
unUse.modelID = id;
const auto legacyUnUse = ReadWithLegacy<std::pair<bool, LWOOBJID>>(unUse, LegacyGameMessages::ReadUnUseModel).first;
const auto unUseCopy = RoundTrip(unUse);
EXPECT_EQ(unUseCopy.bHasWorldTransform, legacyUnUse.first);
EXPECT_EQ(unUseCopy.modelID, legacyUnUse.second);
ExpectTruncatedFails(unUse);
unUse.worldPos = NiPoint3(1, 2, 3);
unUse.worldRot = NiQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
const auto withTransform = RoundTrip(unUse);
EXPECT_EQ(withTransform.worldPos, unUse.worldPos);
EXPECT_EQ(withTransform.worldRot, unUse.worldRot);
}
for (const auto& data : { std::string(), std::string("\x73\x64\x30\x01\xff\x00\x10", 7), std::string(5000, 'z') }) {
GameMessages::BBBSaveRequest save;
save.localID = id;
save.lxfmlDataCompressed = data;
save.timeTakenInMs = 1234;
const auto [legacySave, saveBits] = ReadWithLegacy<LegacyGameMessages::LegacyBBBSaveRequest>(save, LegacyGameMessages::ReadBBBSaveRequest);
EXPECT_EQ(saveBits, Bits(save));
const auto saveCopy = RoundTrip(save);
EXPECT_EQ(saveCopy.localID, legacySave.localId);
EXPECT_EQ(saveCopy.lxfmlDataCompressed, legacySave.sd0);
EXPECT_EQ(saveCopy.timeTakenInMs, legacySave.timeTaken);
}
GameMessages::BBBSaveRequest small;
small.lxfmlDataCompressed = "abc";
ExpectTruncatedFails(small);
}
// ModularBuildFinish: the old handler read the parts only for 3 to 7 of them.
for (const std::vector<LOT> modules : { std::vector<LOT>{ 1, 2, 3 }, std::vector<LOT>{ 8129, 8129, -1, 4, 5, 6, 7 } }) {
GameMessages::ModularBuildFinish finish;
finish.modules = modules;
const auto [legacy, bits] = ReadWithLegacy<std::vector<uint32_t>>(finish, LegacyGameMessages::ReadModularBuildFinish);
EXPECT_EQ(bits, Bits(finish));
ASSERT_EQ(legacy.size(), modules.size());
for (size_t i = 0; i < modules.size(); i++) EXPECT_EQ(static_cast<LOT>(legacy[i]), modules[i]);
EXPECT_EQ(RoundTrip(finish).modules, modules);
ExpectTruncatedFails(finish);
}
}
// Independent of the legacy code: hand computed payloads (RakNet writes MSB first).
TEST_F(BuildingMessagesTests, GoldenBytes) {
GameMessages::SetBuildModeConfirmed confirmed;
confirmed.start = true;
// 1, 1, 0, flag 1, 01 00 00 00, 8 zero bytes (player), flag 1, 12 zero bytes (position)
EXPECT_PACKET_EQ(FromHex("d0 10 00 00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 00", 197), Payload(confirmed));
GameMessages::ModularBuildFinish finish;
finish.modules = { 6416, 1, 2 };
EXPECT_PACKET_EQ(FromHex("03 10 19 00 00 01 00 00 00 02 00 00 00"), Payload(finish));
ClientPackets::BlueprintLoadItemResponse load;
load.success = true;
load.itemId = 515;
load.destItemId = 990;
RakNet::BitStream bitStream;
load.WritePacket(bitStream);
EXPECT_PACKET_EQ(FromHex("53 05 00 17 00 00 00 00 01 03 02 00 00 00 00 00 00 de 03 00 00 00 00 00 00"), FromBitStream(bitStream));
}