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>
This commit is contained in:
Aaron Kimbrell
2026-09-26 23:21:34 -05:00
parent 1e4df609b0
commit db5af641cd
14 changed files with 1807 additions and 858 deletions

View File

@@ -0,0 +1,388 @@
#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));
}

View File

@@ -1,5 +1,6 @@
SET(DGAMEMESSAGES_TESTS
"ActivityMessagesTests.cpp"
"BuildingMessagesTests.cpp"
"GameMessageTests.cpp"
"GameMsgSplitTests.cpp"
"MissionMessagesTests.cpp"

View File

@@ -1,6 +1,7 @@
#include "Action.h"
#include "Amf3.h"
#include "AMFDeserialize.h"
#include "ClientPackets.h"
#include "GameMessages.h"
#include "GameDependencies.h"
@@ -51,7 +52,11 @@ protected:
*
*/
TEST_F(GameMessageTests, SendBlueprintLoadItemResponse) {
GameMessages::SendBlueprintLoadItemResponse(UNASSIGNED_SYSTEM_ADDRESS, true, 515, 990);
ClientPackets::BlueprintLoadItemResponse response;
response.success = true;
response.itemId = 515;
response.destItemId = 990;
response.Send(UNASSIGNED_SYSTEM_ADDRESS);
auto* bitStream = static_cast<dServerMock*>(Game::server)->GetMostRecentBitStream();
ASSERT_NE(bitStream, nullptr);
ASSERT_EQ(bitStream->GetNumberOfUnreadBits(), 200);

View File

@@ -0,0 +1,354 @@
#ifndef BUILDINGMESSAGESLEGACY_H
#define BUILDINGMESSAGESLEGACY_H
// FROZEN ORACLE - DO NOT EDIT.
// Verbatim copies of the hand written GameMessages functions that BuildingMessages.h and the ClientPackets blueprint
// packets replaced (dGame/dGameMessages/GameMessages.cpp and dWorldServer/WorldServer.cpp, branched from origin/main
// 129199e4). Only the namespace changed. The Write* functions are the BLUEPRINT_SAVE_RESPONSE write sequences that
// were inline in HandleUnUseModel, HandleBBBSaveRequest and WorldServer's level load, wrapped in a function; the
// Read* functions are the read sequences of the replaced GameMessages::Handle* functions.
#include "BitStreamUtils.h"
#include "dCommonVars.h"
#include "dServer.h"
#include "eBlueprintSaveResponseType.h"
#include "Entity.h"
#include "Game.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include "NiPoint3.h"
#include "ServiceType.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace LegacyGameMessages {
inline void SendStartArrangingWithItem(
Entity* entity,
const SystemAddress& sysAddr,
bool bFirstTime,
const LWOOBJID& buildAreaID,
NiPoint3 buildStartPOS,
int sourceBAG,
const LWOOBJID& sourceID,
LOT sourceLOT,
int sourceTYPE,
const LWOOBJID& targetID,
LOT targetLOT,
NiPoint3 targetPOS,
int targetTYPE
) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::START_ARRANGING_WITH_ITEM);
bitStream.Write(bFirstTime);
bitStream.Write(buildAreaID != LWOOBJID_EMPTY);
if (buildAreaID != LWOOBJID_EMPTY) bitStream.Write(buildAreaID);
bitStream.Write(buildStartPOS);
bitStream.Write(sourceBAG);
bitStream.Write(sourceID);
bitStream.Write(sourceLOT);
bitStream.Write(sourceTYPE);
bitStream.Write(targetID);
bitStream.Write(targetLOT);
bitStream.Write(targetPOS);
bitStream.Write(targetTYPE);
SEND_PACKET;
}
inline void SendFinishArrangingWithItem(Entity* entity, const LWOOBJID& buildArea) {
CBITSTREAM;
CMSGHEADER;
bool bFirstTime = true;
const LWOOBJID& buildAreaID = buildArea;
int newSourceBAG = 0;
const LWOOBJID& newSourceID = LWOOBJID_EMPTY;
LOT newSourceLOT = LOT_NULL;
int newSourceTYPE = 0;
const LWOOBJID& newTargetID = LWOOBJID_EMPTY;
LOT newTargetLOT = LOT_NULL;
int newTargetTYPE = 0;
NiPoint3 newTargetPOS = NiPoint3();
int oldItemBAG = 0;
const LWOOBJID& oldItemID = LWOOBJID_EMPTY;
LOT oldItemLOT = LOT_NULL;
int oldItemTYPE = 0;
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::FINISH_ARRANGING_WITH_ITEM);
bitStream.Write(buildAreaID != LWOOBJID_EMPTY);
if (buildAreaID != LWOOBJID_EMPTY) bitStream.Write(buildAreaID);
bitStream.Write(newSourceBAG);
bitStream.Write(newSourceID);
bitStream.Write(newSourceLOT);
bitStream.Write(newSourceTYPE);
bitStream.Write(newTargetID);
bitStream.Write(newTargetLOT);
bitStream.Write(newTargetTYPE);
bitStream.Write(newTargetPOS);
bitStream.Write(oldItemBAG);
bitStream.Write(oldItemID);
bitStream.Write(oldItemLOT);
bitStream.Write(oldItemTYPE);
SystemAddress sysAddr = entity->GetSystemAddress();
SEND_PACKET;
}
inline void SendModularBuildEnd(Entity* entity) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::MODULAR_BUILD_END);
SystemAddress sysAddr = entity->GetSystemAddress();
SEND_PACKET;
}
inline void SendSetBuildModeConfirmed(LWOOBJID objectId, const SystemAddress& sysAddr, bool start, bool warnVisitors, bool modePaused, int32_t modeValue, LWOOBJID playerId, NiPoint3 startPos) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::SET_BUILD_MODE_CONFIRMED);
bitStream.Write(start);
bitStream.Write(warnVisitors);
bitStream.Write(modePaused);
bitStream.Write1();
bitStream.Write(modeValue);
bitStream.Write(playerId);
bitStream.Write1();
bitStream.Write(startPos);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendBlueprintLoadItemResponse(const SystemAddress& sysAddr, bool success, LWOOBJID oldItemId, LWOOBJID newItemId) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_LOAD_RESPONSE_ITEMID);
bitStream.Write<uint8_t>(success);
bitStream.Write<LWOOBJID>(oldItemId);
bitStream.Write<LWOOBJID>(newItemId);
SEND_PACKET;
}
// GameMessages::HandleUnUseModel, when unknown was set
inline void WriteUnUseModelSaveResponse(const SystemAddress& sysAddr) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::PlacementFailed); // Sending a non-zero error code here prevents the client from deleting its in progress build for some reason?
bitStream.Write<uint32_t>(0);
SEND_PACKET;
}
// GameMessages::HandleBBBSaveRequest's response (models: blueprint ID and the sd0 chunks written one after another)
inline void WriteBBBSaveResponse(const SystemAddress& sysAddr, LWOOBJID localId, const std::vector<std::pair<LWOOBJID, std::vector<std::string>>>& splitLxfmls) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write(localId);
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(splitLxfmls.size());
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
const auto blueprintID = splitLxfmls[i].first;
// Write the ID and data to the response packet
bitStream.Write(blueprintID);
const auto& newSd0 = splitLxfmls[i].second;
uint32_t newSd0Size{};
for (const auto& chunk : newSd0) newSd0Size += chunk.size();
bitStream.Write(newSd0Size);
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
}
SEND_PACKET;
}
// WorldServer's level load: the property's brick built models (blueprint ID and LXFML)
inline void WriteLevelLoadSaveResponse(const SystemAddress& sysAddr, const std::vector<std::pair<LWOOBJID, std::string>>& bbbModels) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(bbbModels.size());
for (auto& bbbModel : bbbModels) {
size_t lxfmlSize = bbbModel.second.size();
// write data
LWOOBJID blueprintID = bbbModel.first;
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(lxfmlSize);
bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(bbbModel.second.c_str()), lxfmlSize);
}
SEND_PACKET;
}
// GameMessages::HandleStartBuildingWithItem
struct LegacyStartBuildingWithItem { bool firstTime{}; bool success{}; int32_t sourceBag{}; LWOOBJID sourceId{}; LOT sourceLot{}; int32_t sourceType{}; LWOOBJID targetId{}; LOT targetLot{}; NiPoint3 targetPosition{}; int32_t targetType{}; };
inline LegacyStartBuildingWithItem ReadStartBuildingWithItem(RakNet::BitStream& inStream) {
bool firstTime{};
bool success{};
int32_t sourceBag{};
LWOOBJID sourceId{};
LOT sourceLot{};
int32_t sourceType{};
LWOOBJID targetId{};
LOT targetLot{};
NiPoint3 targetPosition{};
int32_t targetType{};
inStream.Read(firstTime);
inStream.Read(success);
inStream.Read(sourceBag);
inStream.Read(sourceId);
inStream.Read(sourceLot);
inStream.Read(sourceType);
inStream.Read(targetId);
inStream.Read(targetLot);
inStream.Read(targetPosition);
inStream.Read(targetType);
return { firstTime, success, sourceBag, sourceId, sourceLot, sourceType, targetId, targetLot, targetPosition, targetType };
}
// GameMessages::HandleSetBuildMode
struct LegacySetBuildMode { bool start{}; int32_t distanceType{}; bool modePaused{}; int modeValue{}; LWOOBJID playerId{}; NiPoint3 startPosition; };
inline LegacySetBuildMode ReadSetBuildMode(RakNet::BitStream& inStream) {
bool start{};
int32_t distanceType = -1;
bool modePaused{};
int modeValue = 1;
LWOOBJID playerId{};
NiPoint3 startPosition = NiPoint3Constant::ZERO;
inStream.Read(start);
if (inStream.ReadBit())
inStream.Read(distanceType);
inStream.Read(modePaused);
if (inStream.ReadBit())
inStream.Read(modeValue);
inStream.Read(playerId);
if (inStream.ReadBit())
inStream.Read(startPosition);
return { start, distanceType, modePaused, modeValue, playerId, startPosition };
}
// GameMessages::HandleBuildModeSet (reads only the first field)
inline bool ReadBuildModeSet(RakNet::BitStream& inStream) {
bool bStart = false;
inStream.Read(bStart);
return bStart;
}
// GameMessages::HandleDoneArrangingWithItem
struct LegacyDoneArranging { int newSourceBAG{}; LWOOBJID newSourceID{}; LOT newSourceLOT{}; int newSourceTYPE{}; LWOOBJID newTargetID{}; LOT newTargetLOT{}; int newTargetTYPE{}; NiPoint3 newTargetPOS; int oldItemBAG{}; LWOOBJID oldItemID{}; LOT oldItemLOT{}; int oldItemTYPE{}; };
inline LegacyDoneArranging ReadDoneArrangingWithItem(RakNet::BitStream& inStream) {
int newSourceBAG = 0;
LWOOBJID newSourceID = 0;
LOT newSourceLOT = 0;
int newSourceTYPE = 0;
LWOOBJID newTargetID = 0;
LOT newTargetLOT = 0;
int newTargetTYPE = 0;
NiPoint3 newTargetPOS;
int oldItemBAG = 0;
LWOOBJID oldItemID = 0;
LOT oldItemLOT = 0;
int oldItemTYPE = 0;
inStream.Read(newSourceBAG);
inStream.Read(newSourceID);
inStream.Read(newSourceLOT);
inStream.Read(newSourceTYPE);
inStream.Read(newTargetID);
inStream.Read(newTargetLOT);
inStream.Read(newTargetTYPE);
inStream.Read(newTargetPOS);
inStream.Read(oldItemBAG);
inStream.Read(oldItemID);
inStream.Read(oldItemLOT);
inStream.Read(oldItemTYPE);
return { newSourceBAG, newSourceID, newSourceLOT, newSourceTYPE, newTargetID, newTargetLOT, newTargetTYPE, newTargetPOS, oldItemBAG, oldItemID, oldItemLOT, oldItemTYPE };
}
// GameMessages::HandleModularBuildFinish (the parts are only read for 3 to 7 parts)
inline std::vector<uint32_t> ReadModularBuildFinish(RakNet::BitStream& inStream) {
uint8_t count; // 3 for rockets, 7 for cars
inStream.Read(count);
std::vector<uint32_t> modList;
if (count >= 3 && count < 8) {
for (uint32_t k = 0; k < count; k++) {
uint32_t mod;
inStream.Read(mod);
modList.push_back(mod);
}
}
return modList;
}
// GameMessages::HandleModularBuildMoveAndEquip
inline LOT ReadModularBuildMoveAndEquip(RakNet::BitStream& inStream) {
LOT templateID;
inStream.Read(templateID);
return templateID;
}
// GameMessages::HandleModularBuildConvertModel and HandleBBBLoadItemRequest
inline LWOOBJID ReadObjectId(RakNet::BitStream& inStream) {
LWOOBJID modelID;
inStream.Read(modelID);
return modelID;
}
// GameMessages::HandleUnUseModel (reads only the first two fields)
inline std::pair<bool, LWOOBJID> ReadUnUseModel(RakNet::BitStream& inStream) {
bool unknown{};
LWOOBJID objIdToAddToInventory{};
inStream.Read(unknown);
inStream.Read(objIdToAddToInventory);
return { unknown, objIdToAddToInventory };
}
// GameMessages::HandleBBBSaveRequest
struct LegacyBBBSaveRequest { LWOOBJID localId{}; std::string sd0; uint32_t timeTaken{}; };
inline LegacyBBBSaveRequest ReadBBBSaveRequest(RakNet::BitStream& inStream) {
LWOOBJID localId;
inStream.Read(localId);
uint32_t sd0Size;
inStream.Read(sd0Size);
std::unique_ptr<char[]> sd0Data;
sd0Data.reset(new char[sd0Size]);
inStream.ReadAlignedBytes(reinterpret_cast<unsigned char*>(sd0Data.get()), sd0Size);
uint32_t timeTaken;
inStream.Read(timeTaken);
return { localId, std::string(sd0Data.get(), sd0Size), timeTaken };
}
}
#endif // BUILDINGMESSAGESLEGACY_H