From 0c2727ad3c0e5d764a0ed1c5341b73de41fbd59e Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 23:21:34 -0500 Subject: [PATCH] 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 --- dGame/dComponents/BuildBorderComponent.cpp | 37 +- dGame/dGameMessages/BuildingMessages.cpp | 731 +++++++++++++++++ dGame/dGameMessages/BuildingMessages.h | 210 +++++ dGame/dGameMessages/CMakeLists.txt | 1 + dGame/dGameMessages/GameMessageHandler.cpp | 53 +- dGame/dGameMessages/GameMessages.cpp | 740 ------------------ dGame/dGameMessages/GameMessages.h | 49 -- dNet/ClientPackets.cpp | 43 + dNet/ClientPackets.h | 34 +- dWorldServer/WorldServer.cpp | 17 +- .../BuildingMessagesTests.cpp | 388 +++++++++ .../dGameMessagesTests/CMakeLists.txt | 1 + .../dGameMessagesTests/GameMessageTests.cpp | 7 +- .../Legacy/BuildingMessagesLegacy.h | 354 +++++++++ 14 files changed, 1807 insertions(+), 858 deletions(-) create mode 100644 dGame/dGameMessages/BuildingMessages.cpp create mode 100644 dGame/dGameMessages/BuildingMessages.h create mode 100644 tests/dGameTests/dGameMessagesTests/BuildingMessagesTests.cpp create mode 100644 tests/dGameTests/dGameMessagesTests/Legacy/BuildingMessagesLegacy.h diff --git a/dGame/dComponents/BuildBorderComponent.cpp b/dGame/dComponents/BuildBorderComponent.cpp index 7209ec9a3..8af18a2b5 100644 --- a/dGame/dComponents/BuildBorderComponent.cpp +++ b/dGame/dComponents/BuildBorderComponent.cpp @@ -2,6 +2,7 @@ #include "EntityManager.h" #include "GameMessages.h" +#include "BuildingMessages.h" #include "Entity.h" #include "Game.h" #include "Logger.h" @@ -44,23 +45,27 @@ void BuildBorderComponent::OnUse(Entity* originator) { LOG("Starting with %llu", buildArea); if (PropertyManagementComponent::Instance() != nullptr) { - GameMessages::SendStartArrangingWithItem( - originator, - originator->GetSystemAddress(), - true, - buildArea, - originator->GetPosition(), - 0, - thinkingHat->GetId(), - thinkingHat->GetLot(), - 4, - 0, - -1, - NiPoint3Constant::ZERO, - 0 - ); + GameMessages::StartArrangingWithItem arranging; + arranging.target = originator->GetObjectID(); + arranging.firstTime = true; + arranging.buildAreaID = buildArea; + arranging.buildStartPos = originator->GetPosition(); + arranging.sourceBag = 0; + arranging.sourceID = thinkingHat->GetId(); + arranging.sourceLot = thinkingHat->GetLot(); + arranging.sourceType = 4; + arranging.targetID = 0; + arranging.targetLot = -1; + arranging.targetPos = NiPoint3Constant::ZERO; + arranging.targetType = 0; + arranging.SendToClient(originator->GetSystemAddress()); } else { - GameMessages::SendStartArrangingWithItem(originator, originator->GetSystemAddress(), true, buildArea, originator->GetPosition()); + GameMessages::StartArrangingWithItem arranging; + arranging.target = originator->GetObjectID(); + arranging.firstTime = true; + arranging.buildAreaID = buildArea; + arranging.buildStartPos = originator->GetPosition(); + arranging.SendToClient(originator->GetSystemAddress()); } InventoryComponent* inv = m_Parent->GetComponent(); diff --git a/dGame/dGameMessages/BuildingMessages.cpp b/dGame/dGameMessages/BuildingMessages.cpp new file mode 100644 index 000000000..e8a2e249f --- /dev/null +++ b/dGame/dGameMessages/BuildingMessages.cpp @@ -0,0 +1,731 @@ +#include "BuildingMessages.h" + +#include "BitStreamUtils.h" +#include "Character.h" +#include "ClientPackets.h" +#include "CppScripts.h" +#include "DashboardNotify.h" +#include "Database.h" +#include "dConfig.h" +#include "dServer.h" +#include "dZoneManager.h" +#include "eBlueprintSaveResponseType.h" +#include "eInventoryType.h" +#include "eLootSourceType.h" +#include "eMissionTaskType.h" +#include "eRacingTaskParam.h" +#include "eReplicaComponentType.h" +#include "Entity.h" +#include "EntityInfo.h" +#include "EntityManager.h" +#include "Game.h" +#include "GeneralUtils.h" +#include "InventoryComponent.h" +#include "Item.h" +#include "Logger.h" +#include "Lxfml.h" +#include "MissionComponent.h" +#include "ObjectIDManager.h" +#include "PropertyManagementComponent.h" +#include "Sd0.h" +#include "ScriptComponent.h" +#include "User.h" +#include "UserManager.h" + +#include +#include + +namespace { + void WriteBuildModeFields(RakNet::BitStream& bitStream, bool start, int32_t distanceType, bool modePaused, int32_t modeValue, LWOOBJID playerId, const NiPoint3& startPos) { + bitStream.Write(start); + BitStreamUtils::WriteOptional(bitStream, distanceType, -1); + bitStream.Write(modePaused); + BitStreamUtils::WriteOptional(bitStream, modeValue, 1); + bitStream.Write(playerId); + BitStreamUtils::WriteOptional(bitStream, startPos, NiPoint3Constant::ZERO); + } + + bool ReadBuildModeFields(RakNet::BitStream& bitStream, bool& start, int32_t& distanceType, bool& modePaused, int32_t& modeValue, LWOOBJID& playerId, NiPoint3& startPos) { + VALIDATE_READ(bitStream.Read(start)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, distanceType, -1)); + VALIDATE_READ(bitStream.Read(modePaused)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, modeValue, 1)); + VALIDATE_READ(bitStream.Read(playerId)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, startPos, NiPoint3Constant::ZERO)); + return true; + } +} + +namespace GameMessages { + void StartBuildingWithItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(firstTime); + bitStream.Write(success); + bitStream.Write(sourceBag); + bitStream.Write(sourceId); + bitStream.Write(sourceLot); + bitStream.Write(sourceType); + bitStream.Write(targetId); + bitStream.Write(targetLot); + bitStream.Write(targetPos); + bitStream.Write(targetType); + } + + bool StartBuildingWithItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(firstTime)); + VALIDATE_READ(bitStream.Read(success)); + VALIDATE_READ(bitStream.Read(sourceBag)); + VALIDATE_READ(bitStream.Read(sourceId)); + VALIDATE_READ(bitStream.Read(sourceLot)); + VALIDATE_READ(bitStream.Read(sourceType)); + VALIDATE_READ(bitStream.Read(targetId)); + VALIDATE_READ(bitStream.Read(targetLot)); + VALIDATE_READ(bitStream.Read(targetPos)); + VALIDATE_READ(bitStream.Read(targetType)); + return true; + } + + void StartBuildingWithItem::Handle(Entity& entity, const SystemAddress& sysAddr) { + if (!entity.HasComponent(eReplicaComponentType::PROPERTY_MANAGEMENT)) { + return; + } + + auto arrangedSourceType = sourceType; + if (arrangedSourceType == 1) { + arrangedSourceType = 4; + } + + LOG("Handling start building with item (%i): (%d) (%d) (%i) (%llu) (%i) (%i) (%llu) (%i) (%i)", entity.GetLOT(), firstTime, success, sourceBag, sourceId, sourceLot, arrangedSourceType, targetId, targetLot, targetType); + + auto* user = UserManager::Instance()->GetUser(sysAddr); + + auto* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); + + StartArrangingWithItem arranging; + arranging.target = player->GetObjectID(); + arranging.firstTime = firstTime; + arranging.buildAreaID = entity.GetObjectID(); + arranging.buildStartPos = player->GetPosition(); + arranging.sourceBag = sourceBag; + arranging.sourceID = sourceId; + arranging.sourceLot = sourceLot; + arranging.sourceType = arrangedSourceType; + arranging.targetID = targetId; + arranging.targetLot = targetLot; + arranging.targetPos = targetPos; + arranging.targetType = targetType; + arranging.SendToClient(sysAddr); + } + + void StartArrangingWithItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(firstTime); + BitStreamUtils::WriteOptional(bitStream, buildAreaID, LWOOBJID_EMPTY); + 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); + } + + bool StartArrangingWithItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(firstTime)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, buildAreaID, LWOOBJID_EMPTY)); + VALIDATE_READ(bitStream.Read(buildStartPos)); + VALIDATE_READ(bitStream.Read(sourceBag)); + VALIDATE_READ(bitStream.Read(sourceID)); + VALIDATE_READ(bitStream.Read(sourceLot)); + VALIDATE_READ(bitStream.Read(sourceType)); + VALIDATE_READ(bitStream.Read(targetID)); + VALIDATE_READ(bitStream.Read(targetLot)); + VALIDATE_READ(bitStream.Read(targetPos)); + VALIDATE_READ(bitStream.Read(targetType)); + return true; + } + + void FinishArrangingWithItem::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, buildAreaID, LWOOBJID_EMPTY); + 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); + } + + bool FinishArrangingWithItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, buildAreaID, LWOOBJID_EMPTY)); + VALIDATE_READ(bitStream.Read(newSourceBag)); + VALIDATE_READ(bitStream.Read(newSourceID)); + VALIDATE_READ(bitStream.Read(newSourceLot)); + VALIDATE_READ(bitStream.Read(newSourceType)); + VALIDATE_READ(bitStream.Read(newTargetID)); + VALIDATE_READ(bitStream.Read(newTargetLot)); + VALIDATE_READ(bitStream.Read(newTargetType)); + VALIDATE_READ(bitStream.Read(newTargetPos)); + VALIDATE_READ(bitStream.Read(oldItemBag)); + VALIDATE_READ(bitStream.Read(oldItemID)); + VALIDATE_READ(bitStream.Read(oldItemLot)); + VALIDATE_READ(bitStream.Read(oldItemType)); + return true; + } + + void DoneArrangingWithItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(newSourceBag); + bitStream.Write(newSourceID); + bitStream.Write(newSourceLot); + bitStream.Write(newSourceType); + bitStream.Write(newTargetID); + bitStream.Write(newTargetLot); + bitStream.Write(newTargetType); + bitStream.Write(newTargetPosition); + bitStream.Write(oldItemBag); + bitStream.Write(oldItemID); + bitStream.Write(oldItemLot); + bitStream.Write(oldItemType); + } + + bool DoneArrangingWithItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(newSourceBag)); + VALIDATE_READ(bitStream.Read(newSourceID)); + VALIDATE_READ(bitStream.Read(newSourceLot)); + VALIDATE_READ(bitStream.Read(newSourceType)); + VALIDATE_READ(bitStream.Read(newTargetID)); + VALIDATE_READ(bitStream.Read(newTargetLot)); + VALIDATE_READ(bitStream.Read(newTargetType)); + VALIDATE_READ(bitStream.Read(newTargetPosition)); + VALIDATE_READ(bitStream.Read(oldItemBag)); + VALIDATE_READ(bitStream.Read(oldItemID)); + VALIDATE_READ(bitStream.Read(oldItemLot)); + VALIDATE_READ(bitStream.Read(oldItemType)); + return true; + } + + void DoneArrangingWithItem::Handle(Entity& entity, const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); + if (!character) return; + InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); + if (!inv) return; + + if (PropertyManagementComponent::Instance() != nullptr) { + const auto& buildAreas = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::BUILD_BORDER); + + const auto& entities = Game::entityManager->GetEntitiesInGroup("PropertyPlaque"); + + Entity* buildArea; + + if (!buildAreas.empty()) { + buildArea = buildAreas[0]; + } else if (!entities.empty()) { + buildArea = entities[0]; + + LOG("Using PropertyPlaque"); + } else { + LOG("No build area found"); + + return; + } + + LOG("Build area found: %llu", buildArea->GetObjectID()); + + StartArrangingWithItem arranging; + arranging.target = character->GetObjectID(); + arranging.firstTime = false; + arranging.buildAreaID = buildArea->GetObjectID(); + arranging.buildStartPos = character->GetPosition(); + arranging.sourceBag = newSourceBag; + arranging.sourceID = newSourceID; + arranging.sourceLot = newSourceLot; + arranging.sourceType = newSourceType; + arranging.targetID = newTargetID; + arranging.targetLot = newTargetLot; + arranging.targetPos = newTargetPosition; + arranging.targetType = newTargetType; + arranging.SendToClient(character->GetSystemAddress()); + } + + LOG("Done Arranging"); + + auto* inventory = inv->GetInventory(TEMP_MODELS); + + std::vector items; + + for (const auto& pair : inventory->GetItems()) { + items.push_back(pair.second); + } + + for (auto* item : items) { + inv->MoveItemToInventory(item, eInventoryType::MODELS, item->GetCount(), false, false); + } + } + + void ModularBuildFinish::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(modules.size()); + for (const auto module : modules) bitStream.Write(module); + } + + bool ModularBuildFinish::Deserialize(RakNet::BitStream& bitStream) { + uint8_t count{}; // 3 for rockets, 7 for cars + VALIDATE_READ(bitStream.Read(count)); + modules.resize(count); + for (auto& module : modules) VALIDATE_READ(bitStream.Read(module)); + return true; + } + + void ModularBuildFinish::Handle(Entity& entity, const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); + if (!character) return; + InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); + if (!inv) return; + + const auto count = modules.size(); // 3 for rockets, 7 for cars + + // Ends the modular build on the client + const auto finishBuilding = [&]() { + FinishArrangingWithItem finish; + finish.target = character->GetObjectID(); + finish.buildAreaID = entity.GetObjectID(); + finish.SendToClient(character->GetSystemAddress()); + + ModularBuildEnd end; // i dont know if this does anything but DLUv2 did it + end.target = character->GetObjectID(); + end.SendToClient(character->GetSystemAddress()); + }; + + auto* temp = inv->GetInventory(TEMP_MODELS); + std::vector modList; + auto& oldPartList = character->GetVar(u"currentModifiedBuild"); + bool everyPieceSwapped = !oldPartList.empty(); // If the player didn't put a build in initially, then they should not get this achievement. + if (count >= 3 && count < 8) { + std::u16string modulesString; + + for (uint32_t k = 0; k < count; k++) { + const auto mod = modules[k]; + modList.push_back(mod); + auto modToStr = GeneralUtils::to_u16string(static_cast(mod)); + modulesString += u"1:" + (modToStr); + if (k + 1 != count) modulesString += u"+"; + + bool hasItem = false; + if (temp->GetLotCount(mod) > 0) { + hasItem = inv->RemoveItem(mod, 1, TEMP_MODELS); + } else { + hasItem = inv->RemoveItem(mod, 1, eInventoryType::ALL); + } + + if (!hasItem) { + LOG("Player (%llu) attempted to finish a modular build without having all the required parts.", character->GetObjectID()); + finishBuilding(); // kick them from modular build + return; + } + + // Doing this check for 1 singular mission that needs to know when you've swapped every part out during a car modular build. + // since all 8129's are the same, skip checking that + if (mod != 8129) { + if (oldPartList.find(GeneralUtils::UTF16ToWTF8(modToStr)) != std::string::npos) everyPieceSwapped = false; + + } + } + + LOG("Build finished"); + finishBuilding(); // kick them from modular build + + LwoNameValue config; + config.Insert(u"assemblyPartLOTs", modulesString); + + LWOOBJID newID = ObjectIDManager::GetPersistentID(); + + if (count == 3) { + inv->AddItem(6416, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newID); + } else if (count == 7) { + inv->AddItem(8092, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newID); + } + + auto* pCharacter = character->GetCharacter(); + Database::Get()->InsertUgcBuild(GeneralUtils::UTF16ToWTF8(modulesString), newID, pCharacter ? std::optional(character->GetCharacter()->GetID()) : std::nullopt); + + auto* missionComponent = character->GetComponent(); + + if (entity.GetLOT() != 9980 || Game::server->GetZoneID() != 1200) { + if (missionComponent != nullptr) { + missionComponent->Progress(eMissionTaskType::SCRIPT, entity.GetLOT(), entity.GetObjectID()); + if (count >= 7 && everyPieceSwapped) missionComponent->Progress(eMissionTaskType::RACING, LWOOBJID_EMPTY, static_cast(eRacingTaskParam::MODULAR_BUILDING)); + } + } + + entity.GetScript()->OnModularBuildExit(&entity, character, count >= 3, modList); + + // Move remaining temp models back to models + std::vector items; + + for (const auto& pair : temp->GetItems()) { + items.push_back(pair.second); + } + + for (auto* item : items) { + inv->MoveItemToInventory(item, eInventoryType::MODELS, item->GetCount(), false); + } + } + } + + void ModularBuildMoveAndEquip::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(templateID); + } + + bool ModularBuildMoveAndEquip::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(templateID)); + return true; + } + + void ModularBuildMoveAndEquip::Handle(Entity& entity, const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); + if (!character) return; + + LOG("Build and move"); + + InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); + if (!inv) return; + + auto* item = inv->FindItemByLot(templateID, TEMP_MODELS); + + if (item == nullptr) { + return; + } + + inv->MoveItemToInventory(item, eInventoryType::MODELS, 1, false, true); + } + + void ModularBuildConvertModel::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(modelID); + } + + bool ModularBuildConvertModel::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(modelID)); + return true; + } + + void ModularBuildConvertModel::Handle(Entity& entity, const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); + if (!character) return; + InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); + if (!inv) return; + + auto* item = inv->FindItemById(modelID); + + if (item == nullptr) { + return; + } + + item->Disassemble(TEMP_MODELS); + + Database::Get()->DeleteUgcBuild(item->GetSubKey()); + + item->SetCount(item->GetCount() - 1, false, false, true, eLootSourceType::QUICKBUILD); + } + + void SetBuildMode::Serialize(RakNet::BitStream& bitStream) const { + WriteBuildModeFields(bitStream, start, distanceType, modePaused, modeValue, playerId, startPos); + } + + bool SetBuildMode::Deserialize(RakNet::BitStream& bitStream) { + return ReadBuildModeFields(bitStream, start, distanceType, modePaused, modeValue, playerId, startPos); + } + + void SetBuildMode::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* player = Game::entityManager->GetEntity(playerId); + + auto confirmedStartPos = startPos; + if (confirmedStartPos == NiPoint3Constant::ZERO) { + confirmedStartPos = player->GetPosition(); + } + + player->GetCharacter()->SetBuildMode(start); + + LOG("Sending build mode confirm (%i): (%d) (%i) (%d) (%i) (%llu)", entity.GetLOT(), start, distanceType, modePaused, modeValue, playerId); + + SetBuildModeConfirmed confirmed; + confirmed.target = entity.GetObjectID(); + confirmed.start = start; + confirmed.warnVisitors = false; + confirmed.modePaused = modePaused; + confirmed.modeValue = modeValue; + confirmed.playerId = playerId; + confirmed.startPos = confirmedStartPos; + confirmed.Send(UNASSIGNED_SYSTEM_ADDRESS); + } + + void SetBuildModeConfirmed::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(start); + bitStream.Write(warnVisitors); + bitStream.Write(modePaused); + bitStream.Write1(); // modeValue's flag, always set + bitStream.Write(modeValue); + bitStream.Write(playerId); + bitStream.Write1(); // startPos's flag, always set + bitStream.Write(startPos); + } + + bool SetBuildModeConfirmed::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(start)); + VALIDATE_READ(bitStream.Read(warnVisitors)); + VALIDATE_READ(bitStream.Read(modePaused)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, modeValue, 1)); + VALIDATE_READ(bitStream.Read(playerId)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, startPos, NiPoint3Constant::ZERO)); + return true; + } + + void BuildModeSet::Serialize(RakNet::BitStream& bitStream) const { + WriteBuildModeFields(bitStream, start, distanceType, modePaused, modeValue, playerId, startPos); + } + + bool BuildModeSet::Deserialize(RakNet::BitStream& bitStream) { + return ReadBuildModeFields(bitStream, start, distanceType, modePaused, modeValue, playerId, startPos); + } + + void BuildModeSet::Handle(Entity& entity, const SystemAddress& sysAddr) { + LOG("Set build mode to (%d) for (%llu)", start, entity.GetObjectID()); + + if (entity.GetCharacter()) { + entity.GetCharacter()->SetBuildMode(start); + } + } + + void UnUseBBBModel::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bHasWorldTransform); + bitStream.Write(modelID); + BitStreamUtils::WriteOptional(bitStream, worldPos, NiPoint3Constant::ZERO); + BitStreamUtils::WriteOptional(bitStream, worldRot, QuatUtils::IDENTITY); + } + + bool UnUseBBBModel::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bHasWorldTransform)); + VALIDATE_READ(bitStream.Read(modelID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, worldPos, NiPoint3Constant::ZERO)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, worldRot, QuatUtils::IDENTITY)); + return true; + } + + void UnUseBBBModel::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) { + auto* inventory = inventoryComponent->GetInventory(eInventoryType::MODELS_IN_BBB); + auto* item = inventory->FindItemById(modelID); + if (item) { + inventoryComponent->MoveItemToInventory(item, eInventoryType::MODELS, 1); + } else { + LOG("item id %llu not found in MODELS_IN_BBB inventory, likely because it does not exist", modelID); + } + } + + if (bHasWorldTransform) { + ClientPackets::BlueprintSaveResponse response; + response.localId = LWOOBJID_EMPTY; //always zero so that a check on the client passes + response.reasonCode = eBlueprintSaveResponseType::PlacementFailed; // Sending a non-zero error code here prevents the client from deleting its in progress build for some reason? + response.Send(sysAddr); + } + } + + void BBBLoadItemRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(itemID); + } + + bool BBBLoadItemRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(itemID)); + return true; + } + + void BBBLoadItemRequest::Handle(Entity& entity, const SystemAddress& sysAddr) { + const LWOOBJID previousItemID = itemID; + + LOG("Load item request for: %lld", previousItemID); + LWOOBJID newId = previousItemID; + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) { + auto* inventory = inventoryComponent->GetInventory(eInventoryType::MODELS); + auto* itemToMove = inventory->FindItemById(previousItemID); + + if (itemToMove) { + LOT previousLot = itemToMove->GetLot(); + inventoryComponent->MoveItemToInventory(itemToMove, eInventoryType::MODELS_IN_BBB, 1, false); + + auto* destinationInventory = inventoryComponent->GetInventory(eInventoryType::MODELS_IN_BBB); + if (destinationInventory) { + auto* movedItem = destinationInventory->FindItemByLot(previousLot); + if (movedItem) newId = movedItem->GetId(); + } + } else { + LOG("item id %llu not found in MODELS inventory, likely because it does not exist", previousItemID); + } + } + + // Second argument always true (successful) for now + ClientPackets::BlueprintLoadItemResponse response; + response.success = true; + response.itemId = previousItemID; + response.destItemId = newId; + response.Send(sysAddr); + } + + void BBBSaveRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(localID); + bitStream.Write(lxfmlDataCompressed.size()); + bitStream.WriteAlignedBytes(reinterpret_cast(lxfmlDataCompressed.data()), lxfmlDataCompressed.size()); + bitStream.Write(timeTakenInMs); + } + + bool BBBSaveRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(localID)); + uint32_t sd0Size{}; + VALIDATE_READ(bitStream.Read(sd0Size)); + // For the sake of letting players make models as big as they want, the only limit is the message itself. + bitStream.AlignReadToByteBoundary(); + if (static_cast(sd0Size) * 8 > bitStream.GetNumberOfUnreadBits()) return false; + lxfmlDataCompressed.resize(sd0Size); + if (sd0Size != 0) VALIDATE_READ(bitStream.ReadAlignedBytes(reinterpret_cast(lxfmlDataCompressed.data()), sd0Size)); + VALIDATE_READ(bitStream.Read(timeTakenInMs)); + return true; + } + + void BBBSaveRequest::Handle(Entity& entity, const SystemAddress& sysAddr) { + /* + On DLU we had agreed that bricks wouldn't be taken anyway, but if your server decides otherwise, feel free to + comment this back out and add the needed code to get the bricks used from lxfml and take them from the inventory. + + Note, in the live client it'll still display the bricks going out as they're being used, but on relog/world change, + they reappear as we didn't take them. + + TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch? + */ + + //Now, the cave of dragons: + + //We need to get a new ID for our model first: + if (!entity.GetCharacter() || !entity.GetCharacter()->GetParentUser()) return; + + //We need to get the propertyID: (stolen from Wincent's propertyManagementComp) + const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); + + const auto zoneId = worldId.GetMapID(); + const auto cloneId = worldId.GetCloneID(); + + auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); + LWOOBJID propertyId = LWOOBJID_EMPTY; + if (propertyInfo) propertyId = propertyInfo->id; + + // Save the binary data to the Sd0 buffer + std::istringstream sd0DataStream(lxfmlDataCompressed); + Sd0 sd0(sd0DataStream); + + // Uncompress the data, split, and nornmalize the model + const auto asStr = sd0.GetAsStringUncompressed(); + + if (Game::config->GetValue("save_lxfmls") == "1") { + // save using localId to avoid conflicts + std::ofstream outFile("debug_lxfml_uncompressed_" + std::to_string(localID) + ".lxfml"); + outFile << asStr; + outFile.close(); + } + + auto splitLxfmls = Lxfml::Split(asStr); + LOG_DEBUG("Split into %zu models", splitLxfmls.size()); + + ClientPackets::BlueprintSaveResponse response; + response.localId = localID; + response.reasonCode = eBlueprintSaveResponseType::EverythingWorked; + + std::vector blueprintIDs; + std::vector modelIDs; + + for (size_t i = 0; i < splitLxfmls.size(); ++i) { + const auto [newID, blueprintID] = ObjectIDManager::GetNewModelIDs(); + + blueprintIDs.push_back(blueprintID); + modelIDs.push_back(newID); + + // Save each model to the database + sd0.FromData(reinterpret_cast(splitLxfmls[i].lxfml.data()), splitLxfmls[i].lxfml.size()); + auto sd0AsStream = sd0.GetAsStream(); + Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity.GetCharacter()->GetParentUser()->GetAccountID(), entity.GetCharacter()->GetID()); + + // Insert the new property model + IPropertyContents::Model model; + model.id = newID; + model.ugcId = blueprintID; + model.position = splitLxfmls[i].center; + model.rotation = QuatUtils::IDENTITY; + model.lot = 14; + Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name"); + DashboardNotify::Changed("properties", propertyId); + + /* + Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream. + (or you uncomment the lxfml decomp stuff above) + */ + + // Send off to UGC for processing, if enabled: + // if (Game::config->GetValue("ugc_remote") == "1") { + // std::string ugcIP = Game::config->GetValue("ugc_ip"); + // int ugcPort = std::stoi(Game::config->GetValue("ugc_port")); + + // httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^ + + // //Send out a request: + // std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml"; + // cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml"); + + // //When the "put" above returns, it means that the UGC HTTP server is done processing our model & + // //the nif, hkx and checksum files are ready to be downloaded from cache. + // } + + // Write the ID and data to the response packet + auto& responseModel = response.models.emplace_back(); + responseModel.blueprintId = blueprintID; + for (const auto& chunk : sd0.GetAsVector()) responseModel.data.append(reinterpret_cast(chunk.data()), chunk.size()); + } + + response.Send(sysAddr); + + // Create entities for each model + for (size_t i = 0; i < splitLxfmls.size(); ++i) { + EntityInfo info; + info.lot = 14; + info.pos = splitLxfmls[i].center; + info.rot = QuatUtils::IDENTITY; + info.spawner = nullptr; + info.spawnerID = entity.GetObjectID(); + info.spawnerNodeID = 0; + + info.settings.Insert(u"blueprintid", blueprintIDs[i]); + info.settings.Insert(u"componentWhitelist", 1); + info.settings.Insert(u"modelType", 2); + info.settings.Insert(u"propertyObjectID", true); + info.settings.Insert(u"userModelID", modelIDs[i]); + Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); + if (newEntity) { + Game::entityManager->ConstructEntity(newEntity); + + //Make sure the propMgmt doesn't delete our model after the server dies + //Trying to do this after the entity is constructed. Shouldn't really change anything but + //there was an issue with builds not appearing since it was placed above ConstructEntity. + PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), modelIDs[i]); + } + } + } +} diff --git a/dGame/dGameMessages/BuildingMessages.h b/dGame/dGameMessages/BuildingMessages.h new file mode 100644 index 000000000..49e259414 --- /dev/null +++ b/dGame/dGameMessages/BuildingMessages.h @@ -0,0 +1,210 @@ +#ifndef BUILDINGMESSAGES_H +#define BUILDINGMESSAGES_H + +#include "GameMessages.h" + +#include "NiQuaternion.h" + +#include +#include + +// Game messages for building: build mode, arranging models on a property or in a modular build area, modular builds +// (rockets and cars) and brick by brick building. +// Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. +namespace GameMessages { + // Client -> server. The player picked something to build with. + struct StartBuildingWithItem : public NetGameMsg { + StartBuildingWithItem() : NetGameMsg(MessageType::Game::START_BUILDING_WITH_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool firstTime{ true }; + bool success{}; + int32_t sourceBag{}; + LWOOBJID sourceId{}; + LOT sourceLot{}; + int32_t sourceType{}; + LWOOBJID targetId{}; + LOT targetLot{}; + NiPoint3 targetPos{}; + int32_t targetType{}; + }; + + // Server -> client. Sent to one client only. + struct StartArrangingWithItem : public NetGameMsg { + StartArrangingWithItem() : NetGameMsg(MessageType::Game::START_ARRANGING_WITH_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool firstTime{ true }; + LWOOBJID buildAreaID{ LWOOBJID_EMPTY }; // optional + NiPoint3 buildStartPos{ NiPoint3Constant::ZERO }; + int32_t sourceBag{}; + LWOOBJID sourceID{ LWOOBJID_EMPTY }; + LOT sourceLot{}; + int32_t sourceType{ 8 }; + LWOOBJID targetID{}; + LOT targetLot{}; + NiPoint3 targetPos{ NiPoint3Constant::ZERO }; + int32_t targetType{}; + }; + + // Server -> client. Sent to one client only; ends arranging (and a modular build). + struct FinishArrangingWithItem : public NetGameMsg { + FinishArrangingWithItem() : NetGameMsg(MessageType::Game::FINISH_ARRANGING_WITH_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + LWOOBJID buildAreaID{ LWOOBJID_EMPTY }; // optional + int32_t newSourceBag{}; + LWOOBJID newSourceID{ LWOOBJID_EMPTY }; + LOT newSourceLot{ LOT_NULL }; + int32_t newSourceType{}; + LWOOBJID newTargetID{ LWOOBJID_EMPTY }; + LOT newTargetLot{ LOT_NULL }; + int32_t newTargetType{}; + NiPoint3 newTargetPos{}; + int32_t oldItemBag{}; + LWOOBJID oldItemID{ LWOOBJID_EMPTY }; + LOT oldItemLot{ LOT_NULL }; + int32_t oldItemType{}; + }; + + // Client -> server. + struct DoneArrangingWithItem : public NetGameMsg { + DoneArrangingWithItem() : NetGameMsg(MessageType::Game::DONE_ARRANGING_WITH_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + int32_t newSourceBag{}; + LWOOBJID newSourceID{}; + LOT newSourceLot{}; + int32_t newSourceType{}; + LWOOBJID newTargetID{}; + LOT newTargetLot{}; + int32_t newTargetType{}; + NiPoint3 newTargetPosition{}; + int32_t oldItemBag{}; + LWOOBJID oldItemID{}; + LOT oldItemLot{}; + int32_t oldItemType{}; + }; + + // Server -> client. No payload. Sent to one client only. + struct ModularBuildEnd : public NetGameMsg { + ModularBuildEnd() : NetGameMsg(MessageType::Game::MODULAR_BUILD_END) {} + }; + + // Client -> server. The finished modular build: 3 parts for a rocket, 7 for a car. + struct ModularBuildFinish : public NetGameMsg { + ModularBuildFinish() : NetGameMsg(MessageType::Game::MODULAR_BUILD_FINISH) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::vector modules{}; // u8 count, then the part LOTs + }; + + // Client -> server. + struct ModularBuildMoveAndEquip : public NetGameMsg { + ModularBuildMoveAndEquip() : NetGameMsg(MessageType::Game::MODULAR_BUILD_MOVE_AND_EQUIP) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LOT templateID{}; + }; + + // Client -> server. Takes a modular build apart again. + struct ModularBuildConvertModel : public NetGameMsg { + ModularBuildConvertModel() : NetGameMsg(MessageType::Game::MODULAR_BUILD_CONVERT_MODEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID modelID{}; + }; + + // Client -> server. + struct SetBuildMode : public NetGameMsg { + SetBuildMode() : NetGameMsg(MessageType::Game::SET_BUILD_MODE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool start{}; + int32_t distanceType{ -1 }; // optional + bool modePaused{}; + int32_t modeValue{ 1 }; // optional + LWOOBJID playerId{}; + NiPoint3 startPos{ NiPoint3Constant::ZERO }; // optional + }; + + // Server -> client. modeValue and startPos are optional in the client, but DLU always writes their flag set. + struct SetBuildModeConfirmed : public NetGameMsg { + SetBuildModeConfirmed() : NetGameMsg(MessageType::Game::SET_BUILD_MODE_CONFIRMED) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool start{}; + bool warnVisitors{ true }; + bool modePaused{}; + int32_t modeValue{ 1 }; + LWOOBJID playerId{}; + NiPoint3 startPos{ NiPoint3Constant::ZERO }; + }; + + // Client -> server. + struct BuildModeSet : public NetGameMsg { + BuildModeSet() : NetGameMsg(MessageType::Game::BUILD_MODE_SET) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool start{}; + int32_t distanceType{ -1 }; // optional + bool modePaused{}; + int32_t modeValue{ 1 }; // optional + LWOOBJID playerId{}; + NiPoint3 startPos{ NiPoint3Constant::ZERO }; // optional + }; + + // Client -> server. The player put a model back from brick by brick building. + struct UnUseBBBModel : public NetGameMsg { + UnUseBBBModel() : NetGameMsg(MessageType::Game::UN_USE_BBB_MODEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bHasWorldTransform{}; + LWOOBJID modelID{}; + NiPoint3 worldPos{ NiPoint3Constant::ZERO }; // optional + NiQuaternion worldRot{ QuatUtils::IDENTITY }; // optional + }; + + // Client -> server. The player opened a model in brick by brick building. + struct BBBLoadItemRequest : public NetGameMsg { + BBBLoadItemRequest() : NetGameMsg(MessageType::Game::BBB_LOAD_ITEM_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID itemID{}; + }; + + // Client -> server. A brick by brick model: its sd0 compressed LXFML. + struct BBBSaveRequest : public NetGameMsg { + BBBSaveRequest() : NetGameMsg(MessageType::Game::BBB_SAVE_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID localID{}; + std::string lxfmlDataCompressed{}; // u32 byte count, then the bytes + uint32_t timeTakenInMs{}; + }; +}; + +#endif // BUILDINGMESSAGES_H diff --git a/dGame/dGameMessages/CMakeLists.txt b/dGame/dGameMessages/CMakeLists.txt index 823ab7ed4..1e7e49c1c 100644 --- a/dGame/dGameMessages/CMakeLists.txt +++ b/dGame/dGameMessages/CMakeLists.txt @@ -1,5 +1,6 @@ set(DGAME_DGAMEMESSAGES_SOURCES "ActivityMessages.cpp" + "BuildingMessages.cpp" "GameMessageDecoder.cpp" "GameMessageHandler.cpp" "GameMessages.cpp" diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 15282c2b1..3ea3c64e2 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -32,6 +32,7 @@ #include "EchoStartSkill.h" #include "EchoSyncSkill.h" #include "ActivityMessages.h" +#include "BuildingMessages.h" #include "RacingMessages.h" #include "MissionMessages.h" #include "PetMessages.h" @@ -103,6 +104,18 @@ namespace { { REPORT_OFFENSIVE_MODEL, []() { return std::make_unique(); } }, { REPORT_OFFENSIVE_PROPERTY, []() { return std::make_unique(); } }, { GET_HOT_PROPERTY_DATA, []() { return std::make_unique(); } }, + + // Building + { START_BUILDING_WITH_ITEM, []() { return std::make_unique(); } }, + { DONE_ARRANGING_WITH_ITEM, []() { return std::make_unique(); } }, + { MODULAR_BUILD_FINISH, []() { return std::make_unique(); } }, + { MODULAR_BUILD_MOVE_AND_EQUIP, []() { return std::make_unique(); } }, + { MODULAR_BUILD_CONVERT_MODEL, []() { return std::make_unique(); } }, + { SET_BUILD_MODE, []() { return std::make_unique(); } }, + { BUILD_MODE_SET, []() { return std::make_unique(); } }, + { UN_USE_BBB_MODEL, []() { return std::make_unique(); } }, + { BBB_LOAD_ITEM_REQUEST, []() { return std::make_unique(); } }, + { BBB_SAVE_REQUEST, []() { return std::make_unique(); } }, }; }; @@ -153,10 +166,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System switch (messageID) { - case MessageType::Game::UN_USE_BBB_MODEL: { - GameMessages::HandleUnUseModel(inStream, entity, sysAddr); - break; - } case MessageType::Game::PLAY_EMOTE: { GameMessages::HandlePlayEmote(inStream, entity); break; @@ -453,10 +462,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleMoveItemBetweenInventoryTypes(inStream, entity, sysAddr); break; - case MessageType::Game::MODULAR_BUILD_FINISH: - GameMessages::HandleModularBuildFinish(inStream, entity, sysAddr); - break; - case MessageType::Game::PUSH_EQUIPPED_ITEMS_STATE: GameMessages::HandlePushEquippedItemsState(inStream, entity); break; @@ -477,22 +482,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleBuybackFromVendor(inStream, entity, sysAddr); break; - case MessageType::Game::MODULAR_BUILD_MOVE_AND_EQUIP: - GameMessages::HandleModularBuildMoveAndEquip(inStream, entity, sysAddr); - break; - - case MessageType::Game::DONE_ARRANGING_WITH_ITEM: - GameMessages::HandleDoneArrangingWithItem(inStream, entity, sysAddr); - break; - - case MessageType::Game::MODULAR_BUILD_CONVERT_MODEL: - GameMessages::HandleModularBuildConvertModel(inStream, entity, sysAddr); - break; - - case MessageType::Game::BUILD_MODE_SET: - GameMessages::HandleBuildModeSet(inStream, entity); - break; - case MessageType::Game::REBUILD_CANCEL: GameMessages::HandleQuickBuildCancel(inStream, entity); break; @@ -539,22 +528,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleChoiceBoxRespond(inStream, entity, sysAddr); break; - case MessageType::Game::START_BUILDING_WITH_ITEM: - GameMessages::HandleStartBuildingWithItem(inStream, entity, sysAddr); - break; - - case MessageType::Game::SET_BUILD_MODE: - GameMessages::HandleSetBuildMode(inStream, entity, sysAddr); - break; - - case MessageType::Game::BBB_LOAD_ITEM_REQUEST: - GameMessages::HandleBBBLoadItemRequest(inStream, entity, sysAddr); - break; - - case MessageType::Game::BBB_SAVE_REQUEST: - GameMessages::HandleBBBSaveRequest(inStream, entity, sysAddr); - break; - // Racing: most racing messages are registered in g_MessageHandlers case MessageType::Game::ACKNOWLEDGE_POSSESSION: GameMessages::HandleAcknowledgePossession(inStream, entity, sysAddr); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 427a14aad..90feea390 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -265,43 +265,6 @@ void GameMessages::SendKnockback(const LWOOBJID& objectID, const LWOOBJID& caste SEND_PACKET_BROADCAST; } -void GameMessages::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; -} - void GameMessages::SendPlayerSetCameraCyclingMode(const LWOOBJID& objectID, const SystemAddress& sysAddr, bool bAllowCyclingWhileDeadOnly, eCyclingMode cyclingMode) { CBITSTREAM; @@ -1077,59 +1040,6 @@ void GameMessages::SendRemoveSkill(Entity* entity, TSkillID skillID) { SEND_PACKET; } -void GameMessages::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; -} - -void GameMessages::SendModularBuildEnd(Entity* entity) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::MODULAR_BUILD_END); - - SystemAddress sysAddr = entity->GetSystemAddress(); - SEND_PACKET; -} - void GameMessages::SendVendorOpenWindow(Entity* entity, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -1726,30 +1636,6 @@ void GameMessages::SendSetName(LWOOBJID objectID, std::u16string name, const Sys SEND_PACKET; } -void GameMessages::SendBBBSaveResponse(const LWOOBJID& objectId, const LWOOBJID& localID, unsigned char* buffer, uint32_t bufferSize, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::BBB_SAVE_RESPONSE); - - bitStream.Write(localID); - - //Write a fake sd0 header: - bitStream.Write(0x73); //s - bitStream.Write(0x64); //d - bitStream.Write(0x30); //0 - bitStream.Write(0x01); //1 - bitStream.Write(0xFF); //end magic - - bitStream.Write(bufferSize); - - for (size_t i = 0; i < bufferSize; ++i) - bitStream.Write(buffer[i]); - - SEND_PACKET; -} - // Property void GameMessages::SendLockNodeRotation(Entity* entity, std::string nodeName) { @@ -1767,180 +1653,6 @@ void GameMessages::SendLockNodeRotation(Entity* entity, std::string nodeName) { SEND_PACKET_BROADCAST; } -void GameMessages::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; -} - -void GameMessages::HandleUnUseModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool unknown{}; - LWOOBJID objIdToAddToInventory{}; - inStream.Read(unknown); - inStream.Read(objIdToAddToInventory); - auto* inventoryComponent = entity->GetComponent(); - if (inventoryComponent) { - auto* inventory = inventoryComponent->GetInventory(eInventoryType::MODELS_IN_BBB); - auto* item = inventory->FindItemById(objIdToAddToInventory); - if (item) { - inventoryComponent->MoveItemToInventory(item, eInventoryType::MODELS, 1); - } else { - LOG("item id %llu not found in MODELS_IN_BBB inventory, likely because it does not exist", objIdToAddToInventory); - } - } - - if (unknown) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); - bitStream.Write(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(0); - SEND_PACKET; - } -} - -void GameMessages::HandleSetBuildMode(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - 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); - - auto* player = Game::entityManager->GetEntity(playerId); - - if (startPosition == NiPoint3Constant::ZERO) { - startPosition = player->GetPosition(); - } - - player->GetCharacter()->SetBuildMode(start); - - LOG("Sending build mode confirm (%i): (%d) (%i) (%d) (%i) (%llu)", entity->GetLOT(), start, distanceType, modePaused, modeValue, playerId); - - SendSetBuildModeConfirmed(entity->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS, start, false, modePaused, modeValue, playerId, startPosition); -} - -void GameMessages::HandleStartBuildingWithItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - if (!entity->HasComponent(eReplicaComponentType::PROPERTY_MANAGEMENT)) { - return; - } - - 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); - - if (sourceType == 1) { - sourceType = 4; - } - - LOG("Handling start building with item (%i): (%d) (%d) (%i) (%llu) (%i) (%i) (%llu) (%i) (%i)", entity->GetLOT(), firstTime, success, sourceBag, sourceId, sourceLot, sourceType, targetId, targetLot, targetType); - - auto* user = UserManager::Instance()->GetUser(sysAddr); - - auto* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - - SendStartArrangingWithItem( - player, - sysAddr, - firstTime, - entity->GetObjectID(), - player->GetPosition(), - sourceBag, - sourceId, - sourceLot, - sourceType, - targetId, - targetLot, - targetPosition, - targetType - ); -} - -void GameMessages::HandleBBBLoadItemRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LWOOBJID previousItemID = LWOOBJID_EMPTY; - inStream.Read(previousItemID); - - LOG("Load item request for: %lld", previousItemID); - LWOOBJID newId = previousItemID; - auto* inventoryComponent = entity->GetComponent(); - if (inventoryComponent) { - auto* inventory = inventoryComponent->GetInventory(eInventoryType::MODELS); - auto* itemToMove = inventory->FindItemById(previousItemID); - - if (itemToMove) { - LOT previousLot = itemToMove->GetLot(); - inventoryComponent->MoveItemToInventory(itemToMove, eInventoryType::MODELS_IN_BBB, 1, false); - - auto* destinationInventory = inventoryComponent->GetInventory(eInventoryType::MODELS_IN_BBB); - if (destinationInventory) { - auto* movedItem = destinationInventory->FindItemByLot(previousLot); - if (movedItem) newId = movedItem->GetId(); - } - } else { - LOG("item id %llu not found in MODELS inventory, likely because it does not exist", previousItemID); - } - } - - // Second argument always true (successful) for now - SendBlueprintLoadItemResponse(sysAddr, true, previousItemID, newId); -} - -void GameMessages::SendBlueprintLoadItemResponse(const SystemAddress& sysAddr, bool success, LWOOBJID oldItemId, LWOOBJID newItemId) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_LOAD_RESPONSE_ITEMID); - bitStream.Write(success); - bitStream.Write(oldItemId); - bitStream.Write(newItemId); - SEND_PACKET; -} - void GameMessages::SendSmash(Entity* entity, float force, float ghostOpacity, LWOOBJID killerID, bool ignoreObjectVisibility) { CBITSTREAM; CMSGHEADER; @@ -1972,188 +1684,6 @@ void GameMessages::SendUnSmash(Entity* entity, LWOOBJID builderID, float duratio SEND_PACKET_BROADCAST; } -void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - /* - ___ ___ - /\ /\___ _ __ ___ / __\ ___ / \_ __ __ _ __ _ ___ _ __ ___ - / /_/ / _ \ '__/ _ \ /__\/// _ \ / /\ / '__/ _` |/ _` |/ _ \| '_ \/ __| - / __ / __/ | | __/ / \/ \ __/ / /_//| | | (_| | (_| | (_) | | | \__ \ - \/ /_/ \___|_| \___| \_____/\___| /___,' |_| \__,_|\__, |\___/|_| |_|___/ - |___/ - ___ _ - / __\ _____ ____ _ _ __ ___ / \ - /__\/// _ \ \ /\ / / _` | '__/ _ \/ / - / \/ \ __/\ V V / (_| | | | __/\_/ - \_____/\___| \_/\_/ \__,_|_| \___\/ - <>=======() - (/\___ /|\\ ()==========<>_ - \_/ | \\ //|\ ______/ \) - \_| \\ // | \_/ - \|\/|\_ // /\/ - (oo)\ \_// / - //_/\_\/ / | - @@/ |=\ \ | - \_=\_ \ | - \==\ \|\_ snd - __(\===\( )\ - (((~) __(_/ | - (((~) \ / - ______/ / - '------' - */ - LWOOBJID localId; - - inStream.Read(localId); - - uint32_t sd0Size; - inStream.Read(sd0Size); - - // For the sake of letting players make models as big as they want, only reject if we cant allocate the required memory. - std::unique_ptr sd0Data; - try { - sd0Data.reset(new char[sd0Size]); - } catch (std::exception& e) { - LOG("Failed to allocate sd0 of size %u", sd0Size); - return; - } - - inStream.ReadAlignedBytes(reinterpret_cast(sd0Data.get()), sd0Size); - - uint32_t timeTaken; - inStream.Read(timeTaken); - - /* - On DLU we had agreed that bricks wouldn't be taken anyway, but if your server decides otherwise, feel free to - comment this back out and add the needed code to get the bricks used from lxfml and take them from the inventory. - - Note, in the live client it'll still display the bricks going out as they're being used, but on relog/world change, - they reappear as we didn't take them. - - TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch? - */ - - //Now, the cave of dragons: - - //We need to get a new ID for our model first: - if (!entity || !entity->GetCharacter() || !entity->GetCharacter()->GetParentUser()) return; - - //We need to get the propertyID: (stolen from Wincent's propertyManagementComp) - const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); - - const auto zoneId = worldId.GetMapID(); - const auto cloneId = worldId.GetCloneID(); - - auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); - LWOOBJID propertyId = LWOOBJID_EMPTY; - if (propertyInfo) propertyId = propertyInfo->id; - - // Save the binary data to the Sd0 buffer - std::string str(sd0Data.get(), sd0Size); - std::istringstream sd0DataStream(str); - Sd0 sd0(sd0DataStream); - - // Uncompress the data, split, and nornmalize the model - const auto asStr = sd0.GetAsStringUncompressed(); - - if (Game::config->GetValue("save_lxfmls") == "1") { - // save using localId to avoid conflicts - std::ofstream outFile("debug_lxfml_uncompressed_" + std::to_string(localId) + ".lxfml"); - outFile << asStr; - outFile.close(); - } - - auto splitLxfmls = Lxfml::Split(asStr); - LOG_DEBUG("Split into %zu models", splitLxfmls.size()); - - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); - bitStream.Write(localId); - bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); - bitStream.Write(splitLxfmls.size()); - - std::vector blueprintIDs; - std::vector modelIDs; - - for (size_t i = 0; i < splitLxfmls.size(); ++i) { - const auto [newID, blueprintID] = ObjectIDManager::GetNewModelIDs(); - - blueprintIDs.push_back(blueprintID); - modelIDs.push_back(newID); - - // Save each model to the database - sd0.FromData(reinterpret_cast(splitLxfmls[i].lxfml.data()), splitLxfmls[i].lxfml.size()); - auto sd0AsStream = sd0.GetAsStream(); - Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); - - // Insert the new property model - IPropertyContents::Model model; - model.id = newID; - model.ugcId = blueprintID; - model.position = splitLxfmls[i].center; - model.rotation = QuatUtils::IDENTITY; - model.lot = 14; - Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name"); - DashboardNotify::Changed("properties", propertyId); - - /* - Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream. - (or you uncomment the lxfml decomp stuff above) - */ - - // Send off to UGC for processing, if enabled: - // if (Game::config->GetValue("ugc_remote") == "1") { - // std::string ugcIP = Game::config->GetValue("ugc_ip"); - // int ugcPort = std::stoi(Game::config->GetValue("ugc_port")); - - // httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^ - - // //Send out a request: - // std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml"; - // cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml"); - - // //When the "put" above returns, it means that the UGC HTTP server is done processing our model & - // //the nif, hkx and checksum files are ready to be downloaded from cache. - // } - - // Write the ID and data to the response packet - bitStream.Write(blueprintID); - - const auto& newSd0 = sd0.GetAsVector(); - uint32_t newSd0Size{}; - for (const auto& chunk : newSd0) newSd0Size += chunk.size(); - bitStream.Write(newSd0Size); - for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast(chunk.data()), chunk.size()); - } - - SEND_PACKET; - - // Create entities for each model - for (size_t i = 0; i < splitLxfmls.size(); ++i) { - EntityInfo info; - info.lot = 14; - info.pos = splitLxfmls[i].center; - info.rot = QuatUtils::IDENTITY; - info.spawner = nullptr; - info.spawnerID = entity->GetObjectID(); - info.spawnerNodeID = 0; - - info.settings.Insert(u"blueprintid", blueprintIDs[i]); - info.settings.Insert(u"componentWhitelist", 1); - info.settings.Insert(u"modelType", 2); - info.settings.Insert(u"propertyObjectID", true); - info.settings.Insert(u"userModelID", modelIDs[i]); - Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); - if (newEntity) { - Game::entityManager->ConstructEntity(newEntity); - - //Make sure the propMgmt doesn't delete our model after the server dies - //Trying to do this after the entity is constructed. Shouldn't really change anything but - //there was an issue with builds not appearing since it was placed above ConstructEntity. - PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), modelIDs[i]); - } - } -} - void GameMessages::HandleSetConsumableItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { LOT lot; @@ -3666,31 +3196,6 @@ void GameMessages::HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity) } } -void GameMessages::HandleModularBuildConvertModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LWOOBJID modelID; - - inStream.Read(modelID); - - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!character) return; - InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - auto* item = inv->FindItemById(modelID); - - if (item == nullptr) { - return; - } - - item->Disassemble(TEMP_MODELS); - - Database::Get()->DeleteUgcBuild(item->GetSubKey()); - - item->SetCount(item->GetCount() - 1, false, false, true, eLootSourceType::QUICKBUILD); -} - void GameMessages::HandleNotifyServerLevelProcessingComplete(RakNet::BitStream& inStream, Entity* entity) { auto* levelComp = entity->GetComponent(); if (!levelComp) return; @@ -3998,251 +3503,6 @@ void GameMessages::HandleMoveItemBetweenInventoryTypes(RakNet::BitStream& inStre Game::entityManager->SerializeEntity(entity); } -void GameMessages::HandleBuildModeSet(RakNet::BitStream& inStream, Entity* entity) { - bool bStart = false; - - inStream.Read(bStart); - // there's more here but we don't need it (for now?) - - LOG("Set build mode to (%d) for (%llu)", bStart, entity->GetObjectID()); - - if (entity->GetCharacter()) { - entity->GetCharacter()->SetBuildMode(bStart); - } -} - -void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!character) return; - InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - - uint8_t count; // 3 for rockets, 7 for cars - - inStream.Read(count); - - auto* temp = inv->GetInventory(TEMP_MODELS); - std::vector modList; - auto& oldPartList = character->GetVar(u"currentModifiedBuild"); - bool everyPieceSwapped = !oldPartList.empty(); // If the player didn't put a build in initially, then they should not get this achievement. - if (count >= 3 && count < 8) { - std::u16string modules; - - for (uint32_t k = 0; k < count; k++) { - uint32_t mod; - inStream.Read(mod); - modList.push_back(mod); - auto modToStr = GeneralUtils::to_u16string(mod); - modules += u"1:" + (modToStr); - if (k + 1 != count) modules += u"+"; - - bool hasItem = false; - if (temp->GetLotCount(mod) > 0) { - hasItem = inv->RemoveItem(mod, 1, TEMP_MODELS); - } else { - hasItem = inv->RemoveItem(mod, 1, eInventoryType::ALL); - } - - if (!hasItem) { - LOG("Player (%llu) attempted to finish a modular build without having all the required parts.", character->GetObjectID()); - GameMessages::SendFinishArrangingWithItem(character, entity->GetObjectID()); // kick them from modular build - GameMessages::SendModularBuildEnd(character); // i dont know if this does anything but DLUv2 did it - return; - } - - // Doing this check for 1 singular mission that needs to know when you've swapped every part out during a car modular build. - // since all 8129's are the same, skip checking that - if (mod != 8129) { - if (oldPartList.find(GeneralUtils::UTF16ToWTF8(modToStr)) != std::string::npos) everyPieceSwapped = false; - - } - } - - LOG("Build finished"); - GameMessages::SendFinishArrangingWithItem(character, entity->GetObjectID()); // kick them from modular build - GameMessages::SendModularBuildEnd(character); // i dont know if this does anything but DLUv2 did it - - //inv->UnequipItem(inv->GetItemStackByLOT(6086, eInventoryType::ITEMS)); // take off the thinking cap - //Game::entityManager->SerializeEntity(entity); - - LwoNameValue config; - config.Insert(u"assemblyPartLOTs", modules); - - LWOOBJID newID = ObjectIDManager::GetPersistentID(); - - if (count == 3) { - inv->AddItem(6416, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newID); - } else if (count == 7) { - inv->AddItem(8092, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newID); - } - - auto* pCharacter = character->GetCharacter(); - Database::Get()->InsertUgcBuild(GeneralUtils::UTF16ToWTF8(modules), newID, pCharacter ? std::optional(character->GetCharacter()->GetID()) : std::nullopt); - - auto* missionComponent = character->GetComponent(); - - if (entity->GetLOT() != 9980 || Game::server->GetZoneID() != 1200) { - if (missionComponent != nullptr) { - missionComponent->Progress(eMissionTaskType::SCRIPT, entity->GetLOT(), entity->GetObjectID()); - if (count >= 7 && everyPieceSwapped) missionComponent->Progress(eMissionTaskType::RACING, LWOOBJID_EMPTY, static_cast(eRacingTaskParam::MODULAR_BUILDING)); - } - } - - ScriptComponent* script = static_cast(entity->GetComponent(eReplicaComponentType::SCRIPT)); - - entity->GetScript()->OnModularBuildExit(entity, character, count >= 3, modList); - - // Move remaining temp models back to models - std::vector items; - - for (const auto& pair : temp->GetItems()) { - items.push_back(pair.second); - } - - for (auto* item : items) { - inv->MoveItemToInventory(item, eInventoryType::MODELS, item->GetCount(), false); - } - } -} - -void GameMessages::HandleDoneArrangingWithItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!character) return; - InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - /** - [int] - newSourceBAG - [LWOOBJID] - newSourceID - [LOT] - newSourceLOT - [int] - newSourceTYPE - [LWOOBJID] - newTargetID - [LOT] - newTargetLOT - [int] - newTargetTYPE - [NiPoint3] - newTtargetPOS - [int] - oldItemBAG - [LWOOBJID] - oldItemID - [LOT] - oldItemLOT - [int] - oldItemTYPE - */ - 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); - - /* - LOG("GameMessages", - "\nnewSourceBAG: %d\nnewSourceID: %llu\nnewSourceLOT: %d\nnewSourceTYPE: %d\nnewTargetID: %llu\nnewTargetLOT: %d\nnewTargetTYPE: %d\nnewTargetPOS: %f, %f, %f\noldItemBAG: %d\noldItemID: %llu\noldItemLOT: %d\noldItemTYPE: %d", - newSourceBAG, newSourceID, newSourceLOT, newSourceTYPE, newTargetID, newTargetLOT, newTargetTYPE, newTargetPOS.x, newTargetPOS.y, newTargetPOS.z, oldItemBAG, oldItemID, oldItemLOT, oldItemTYPE - ); - */ - - if (PropertyManagementComponent::Instance() != nullptr) { - const auto& buildAreas = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::BUILD_BORDER); - - const auto& entities = Game::entityManager->GetEntitiesInGroup("PropertyPlaque"); - - Entity* buildArea; - - if (!buildAreas.empty()) { - buildArea = buildAreas[0]; - } else if (!entities.empty()) { - buildArea = entities[0]; - - LOG("Using PropertyPlaque"); - } else { - LOG("No build area found"); - - return; - } - - LOG("Build area found: %llu", buildArea->GetObjectID()); - - GameMessages::SendStartArrangingWithItem( - character, - character->GetSystemAddress(), - false, - buildArea->GetObjectID(), - character->GetPosition(), - newSourceBAG, - newSourceID, - newSourceLOT, - newSourceTYPE, - newTargetID, - newTargetLOT, - newTargetPOS, - newTargetTYPE - ); - } - - LOG("Done Arranging"); - - //GenericInventory* models = inv->GetGenericInventory(MODELS); - //GenericInventory* tempModels = inv->GetGenericInventory(TEMP_MODELS); - - auto* inventory = inv->GetInventory(TEMP_MODELS); - - std::vector items; - - for (const auto& pair : inventory->GetItems()) { - items.push_back(pair.second); - } - - for (auto* item : items) { - inv->MoveItemToInventory(item, eInventoryType::MODELS, item->GetCount(), false, false); - } -} - -void GameMessages::HandleModularBuildMoveAndEquip(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* character = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!character) return; - - LOG("Build and move"); - - LOT templateID; - - inStream.Read(templateID); - - InventoryComponent* inv = static_cast(character->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - auto* item = inv->FindItemByLot(templateID, TEMP_MODELS); - - if (item == nullptr) { - return; - } - - inv->MoveItemToInventory(item, eInventoryType::MODELS, 1, false, true); -} - void GameMessages::HandleResurrect(RakNet::BitStream& inStream, Entity* entity) { bool immediate = inStream.ReadBit(); diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 82c40c215..22d52fb95 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -132,21 +132,6 @@ namespace GameMessages { void SendPlayerAllowedRespawn(LWOOBJID entityID, bool doNotPromptRespawn, const SystemAddress& systemAddress); void SendInvalidZoneTransferList(Entity* entity, const SystemAddress& sysAddr, const std::u16string& feedbackURL, const std::u16string& invalidMapTransferList, bool feedbackOnExit, bool feedbackOnInvalidTransfer); void SendKnockback(const LWOOBJID& objectID, const LWOOBJID& caster, const LWOOBJID& originator, int knockBackTimeMS, const NiPoint3& vector); - void SendStartArrangingWithItem( - Entity* entity, - const SystemAddress& sysAddr, - bool bFirstTime = true, - const LWOOBJID& buildAreaID = LWOOBJID_EMPTY, - NiPoint3 buildStartPOS = NiPoint3Constant::ZERO, - int sourceBAG = 0, - const LWOOBJID& sourceID = LWOOBJID_EMPTY, - LOT sourceLOT = 0, - int sourceTYPE = 8, - const LWOOBJID& targetID = 0, - LOT targetLOT = 0, - NiPoint3 targetPOS = NiPoint3Constant::ZERO, - int targetTYPE = 0 - ); void SendPlayerSetCameraCyclingMode(const LWOOBJID& objectID, const SystemAddress& sysAddr, bool bAllowCyclingWhileDeadOnly = true, eCyclingMode cyclingMode = eCyclingMode::ALLOW_CYCLE_TEAMMATES); @@ -206,9 +191,6 @@ namespace GameMessages { void SendAddSkill(Entity* entity, TSkillID skillID, BehaviorSlot slotID); void SendRemoveSkill(Entity* entity, TSkillID skillID); - void SendFinishArrangingWithItem(Entity* entity, const LWOOBJID& buildAreaID); - void SendModularBuildEnd(Entity* entity); - void SendVendorOpenWindow(Entity* entity, const SystemAddress& sysAddr); void SendVendorStatusUpdate(Entity* entity, const SystemAddress& sysAddr, bool bUpdateOnly = false); void SendVendorTransactionResult(Entity* entity, const SystemAddress& sysAddr, eVendorTransactionResult result); @@ -221,7 +203,6 @@ namespace GameMessages { void SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response); void SendMatchUpdate(Entity* entity, const SystemAddress& sysAddr, std::string data, eMatchUpdate type); - void HandleUnUseModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void SendStartCelebrationEffect(Entity* entity, const SystemAddress& sysAddr, int celebrationID); // https://lcdruniverse.org/lu_packets/lu_packets/world/gm/client/struct.SetResurrectRestoreValues.html @@ -268,9 +249,6 @@ namespace GameMessages { void SendNotifyClientFailedPrecondition(LWOOBJID objectId, const SystemAddress& sysAddr, const std::u16string& failedReason, int preconditionID); - // The success or failure response sent back to the client will preserve the same value for localID. - void SendBBBSaveResponse(const LWOOBJID& objectId, const LWOOBJID& localID, unsigned char* buffer, uint32_t bufferSize, const SystemAddress& sysAddr); - void SendAddBuff(LWOOBJID& objectID, const LWOOBJID& casterID, uint32_t buffID, uint32_t msDuration, bool addImmunity = false, bool cancelOnDamaged = false, bool cancelOnDeath = true, bool cancelOnLogout = false, bool cancelOnRemoveBuff = true, bool cancelOnUi = false, @@ -278,30 +256,8 @@ namespace GameMessages { void SendSetName(LWOOBJID objectID, std::u16string name, const SystemAddress& sysAddr); - // Property messages - - /** - * @brief Send an updated item id to the client when they load a blueprint in brick build mode - * - * @param sysAddr SystemAddress to respond to - * @param oldItemId The item ID that was requested to be loaded - * @param newItemId The new item ID of the loaded item - * - */ - void SendBlueprintLoadItemResponse(const SystemAddress& sysAddr, bool success, LWOOBJID oldItemId, LWOOBJID newItemId); - void SendLockNodeRotation(Entity* entity, std::string nodeName); - void SendSetBuildModeConfirmed(LWOOBJID objectId, const SystemAddress& sysAddr, bool start, bool warnVisitors, bool modePaused, int32_t modeValue, LWOOBJID playerId, NiPoint3 startPos = NiPoint3Constant::ZERO); - - void HandleSetBuildMode(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleStartBuildingWithItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleBBBLoadItemRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleSetConsumableItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void SendPlayCinematic(LWOOBJID objectId, std::u16string pathName, const SystemAddress& sysAddr, @@ -501,7 +457,6 @@ namespace GameMessages { void HandleRequestPlatformResync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* entity); void HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity); - void HandleModularBuildConvertModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleNotifyServerLevelProcessingComplete(RakNet::BitStream& inStream, Entity* entity); void HandlePickupCurrency(RakNet::BitStream& inStream, Entity* entity); void HandleRequestDie(RakNet::BitStream& inStream, Entity* entity); @@ -510,10 +465,6 @@ namespace GameMessages { void HandleRemoveItemFromInventory(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleMoveItemInInventory(RakNet::BitStream& inStream, Entity* entity); void HandleMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleBuildModeSet(RakNet::BitStream& inStream, Entity* entity); - void HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleDoneArrangingWithItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleModularBuildMoveAndEquip(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandlePickupItem(RakNet::BitStream& inStream, Entity* entity); void HandleResurrect(RakNet::BitStream& inStream, Entity* entity); void HandleModifyPlayerZoneStatistic(RakNet::BitStream& inStream, Entity* entity); diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index d83746a11..322ac2162 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -440,3 +440,46 @@ namespace ClientPackets { return true; } } + +void ClientPackets::BlueprintSaveResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(localId); + bitStream.Write(reasonCode); + bitStream.Write(models.size()); + for (const auto& model : models) { + bitStream.Write(model.blueprintId); + bitStream.Write(model.data.size()); + bitStream.WriteAlignedBytes(reinterpret_cast(model.data.data()), model.data.size()); + } +} + +bool ClientPackets::BlueprintSaveResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(localId)); + VALIDATE_READ(bitStream.Read(reasonCode)); + uint32_t count{}; + VALIDATE_READ(bitStream.Read(count)); + models.clear(); + for (uint32_t i = 0; i < count; i++) { + auto& model = models.emplace_back(); + VALIDATE_READ(bitStream.Read(model.blueprintId)); + uint32_t size{}; + VALIDATE_READ(bitStream.Read(size)); + bitStream.AlignReadToByteBoundary(); + if (static_cast(size) * 8 > bitStream.GetNumberOfUnreadBits()) return false; + model.data.resize(size); + if (size != 0) VALIDATE_READ(bitStream.ReadAlignedBytes(reinterpret_cast(model.data.data()), size)); + } + return true; +} + +void ClientPackets::BlueprintLoadItemResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(success); + bitStream.Write(itemId); + bitStream.Write(destItemId); +} + +bool ClientPackets::BlueprintLoadItemResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(success)); + VALIDATE_READ(bitStream.Read(itemId)); + VALIDATE_READ(bitStream.Read(destItemId)); + return true; +} diff --git a/dNet/ClientPackets.h b/dNet/ClientPackets.h index 7ee271968..cc07fb32e 100644 --- a/dNet/ClientPackets.h +++ b/dNet/ClientPackets.h @@ -14,10 +14,11 @@ #include #include "BitStreamUtils.h" -#include "MessageType/Client.h" -#include "Stamps.h" #include "dCommonVars.h" +#include "eBlueprintSaveResponseType.h" +#include "MessageType/Client.h" #include "NiPoint3.h" +#include "Stamps.h" enum class eCharacterCreationResponse : uint8_t; enum class eGameMasterLevel : uint8_t; @@ -231,6 +232,35 @@ namespace ClientPackets { void Serialize(RakNet::BitStream& bitStream) const override; bool Deserialize(RakNet::BitStream& bitStream) override; }; + + // Server -> client. The answer to a BBBSaveRequest (and, with PlacementFailed and no models, to UnUseBBBModel); + // also sent on world load to give the client the property's brick built models. Each model is the blueprint's + // ID and its data, written as a u32 byte count followed by the bytes. + struct BlueprintSaveResponse : public LUBitStream { + BlueprintSaveResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + struct Model { + LWOOBJID blueprintId{}; + std::string data{}; + }; + + LWOOBJID localId{}; + eBlueprintSaveResponseType reasonCode{}; + std::vector models{}; // u32 count, then the models + }; + + // Server -> client. The answer to BBBLoadItemRequest: the model item's ID once it moved to the BBB inventory. + struct BlueprintLoadItemResponse : public LUBitStream { + BlueprintLoadItemResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::BLUEPRINT_LOAD_RESPONSE_ITEMID) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + uint8_t success{}; + LWOOBJID itemId{}; + LWOOBJID destItemId{}; + }; }; #endif // CLIENTPACKETS_H diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index c759407ba..4260bbf32 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1091,11 +1091,9 @@ void LoadPlayer(const SystemAddress& sysAddr) { goto noBBB; } - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); - bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes - bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); - bitStream.Write(bbbModels.size()); + ClientPackets::BlueprintSaveResponse response; + response.localId = LWOOBJID_EMPTY; //always zero so that a check on the client passes + response.reasonCode = eBlueprintSaveResponseType::EverythingWorked; for (auto& bbbModel : bbbModels) { LOG("Getting lxfml ugcID: %llu", bbbModel.id); @@ -1104,12 +1102,11 @@ void LoadPlayer(const SystemAddress& sysAddr) { bbbModel.lxfmlData.seekg(0); // write data - LWOOBJID blueprintID = bbbModel.id; - bitStream.Write(blueprintID); - bitStream.Write(lxfmlSize); - bitStream.WriteAlignedBytes(reinterpret_cast(bbbModel.lxfmlData.str().c_str()), lxfmlSize); + auto& model = response.models.emplace_back(); + model.blueprintId = bbbModel.id; + model.data = bbbModel.lxfmlData.str().substr(0, lxfmlSize); } - SEND_PACKET; + response.Send(sysAddr); } noBBB: diff --git a/tests/dGameTests/dGameMessagesTests/BuildingMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/BuildingMessagesTests.cpp new file mode 100644 index 000000000..384c565cb --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/BuildingMessagesTests.cpp @@ -0,0 +1,388 @@ +#include "BuildingMessages.h" +#include "ClientPackets.h" +#include "GameDependencies.h" +#include "GameMessageTestUtils.h" +#include "Legacy/BuildingMessagesLegacy.h" + +#include +#include +#include +#include +#include + +#include + +using namespace GameMessageTestUtils; + +namespace { + const std::vector g_Ints = { 0, 1, -1, 8, std::numeric_limits::max(), std::numeric_limits::min() }; + const std::vector 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 + std::pair ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function& read) { + RakNet::BitStream wire; + msg.Serialize(wire); + RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false); + auto result = read(legacyStream); + return { result, static_cast(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& 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> chunkSets = { {}, { "sd0" }, { std::string("\x73\x64\x30\x01\xff", 5), std::string(1000, '\x7f'), "" } }; + std::vector>> saved; + ClientPackets::BlueprintSaveResponse save; + save.localId = id; + save.reasonCode = eBlueprintSaveResponseType::EverythingWorked; + std::vector> 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(i), chunkSets[i]); + auto& model = save.models.emplace_back(); + model.blueprintId = id + static_cast(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(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(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(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(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(move, LegacyGameMessages::ReadModularBuildMoveAndEquip).first, value); + EXPECT_EQ(RoundTrip(move).templateID, value); + ExpectTruncatedFails(move); + } + + GameMessages::ModularBuildConvertModel convert; + convert.modelID = id; + EXPECT_EQ(ReadWithLegacy(convert, LegacyGameMessages::ReadObjectId).first, id); + EXPECT_EQ(RoundTrip(convert).modelID, id); + ExpectTruncatedFails(convert); + + GameMessages::BBBLoadItemRequest load; + load.itemID = id; + EXPECT_EQ(ReadWithLegacy(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>(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(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 modules : { std::vector{ 1, 2, 3 }, std::vector{ 8129, 8129, -1, 4, 5, 6, 7 } }) { + GameMessages::ModularBuildFinish finish; + finish.modules = modules; + const auto [legacy, bits] = ReadWithLegacy>(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(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)); +} diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index 61ca78cfe..cf20079c8 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -1,5 +1,6 @@ SET(DGAMEMESSAGES_TESTS "ActivityMessagesTests.cpp" + "BuildingMessagesTests.cpp" "GameMessageTests.cpp" "GameMsgSplitTests.cpp" "MissionMessagesTests.cpp" diff --git a/tests/dGameTests/dGameMessagesTests/GameMessageTests.cpp b/tests/dGameTests/dGameMessagesTests/GameMessageTests.cpp index e75dc5c42..6946c631f 100644 --- a/tests/dGameTests/dGameMessagesTests/GameMessageTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/GameMessageTests.cpp @@ -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(Game::server)->GetMostRecentBitStream(); ASSERT_NE(bitStream, nullptr); ASSERT_EQ(bitStream->GetNumberOfUnreadBits(), 200); diff --git a/tests/dGameTests/dGameMessagesTests/Legacy/BuildingMessagesLegacy.h b/tests/dGameTests/dGameMessagesTests/Legacy/BuildingMessagesLegacy.h new file mode 100644 index 000000000..d3b754ab6 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/Legacy/BuildingMessagesLegacy.h @@ -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 +#include +#include +#include + +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(success); + bitStream.Write(oldItemId); + bitStream.Write(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_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(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>>& splitLxfmls) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); + bitStream.Write(localId); + bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); + bitStream.Write(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(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>& bbbModels) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); + bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes + bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); + bitStream.Write(bbbModels.size()); + for (auto& bbbModel : bbbModels) { + size_t lxfmlSize = bbbModel.second.size(); + + // write data + LWOOBJID blueprintID = bbbModel.first; + bitStream.Write(blueprintID); + bitStream.Write(lxfmlSize); + bitStream.WriteAlignedBytes(reinterpret_cast(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 ReadModularBuildFinish(RakNet::BitStream& inStream) { + uint8_t count; // 3 for rockets, 7 for cars + + inStream.Read(count); + + std::vector 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 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 sd0Data; + sd0Data.reset(new char[sd0Size]); + + inStream.ReadAlignedBytes(reinterpret_cast(sd0Data.get()), sd0Size); + + uint32_t timeTaken; + inStream.Read(timeTaken); + return { localId, std::string(sd0Data.get(), sd0Size), timeTaken }; + } +} + +#endif // BUILDINGMESSAGESLEGACY_H