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