refactor: building game messages and blueprint packets as structs

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 23:21:34 -05:00
parent 1e4df609b0
commit db5af641cd
14 changed files with 1807 additions and 858 deletions

View File

@@ -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<unsigned char>(0x73); //s
bitStream.Write<unsigned char>(0x64); //d
bitStream.Write<unsigned char>(0x30); //0
bitStream.Write<unsigned char>(0x01); //1
bitStream.Write<unsigned char>(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<InventoryComponent>();
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>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::PlacementFailed); // Sending a non-zero error code here prevents the client from deleting its in progress build for some reason?
bitStream.Write<uint32_t>(0);
SEND_PACKET;
}
}
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<InventoryComponent>();
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<uint8_t>(success);
bitStream.Write<LWOOBJID>(oldItemId);
bitStream.Write<LWOOBJID>(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<char[]> 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<unsigned char*>(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<uint32_t>(splitLxfmls.size());
std::vector<LWOOBJID> blueprintIDs;
std::vector<LWOOBJID> 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<const uint8_t*>(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<const unsigned char*>(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<LWOOBJID>(u"blueprintid", blueprintIDs[i]);
info.settings.Insert<int>(u"componentWhitelist", 1);
info.settings.Insert<int>(u"modelType", 2);
info.settings.Insert<bool>(u"propertyObjectID", true);
info.settings.Insert<LWOOBJID>(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<InventoryComponent*>(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<LevelProgressionComponent>();
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<InventoryComponent*>(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<LOT> modList;
auto& oldPartList = character->GetVar<std::string>(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<MissionComponent>();
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<LWOOBJID>(eRacingTaskParam::MODULAR_BUILDING));
}
}
ScriptComponent* script = static_cast<ScriptComponent*>(entity->GetComponent(eReplicaComponentType::SCRIPT));
entity->GetScript()->OnModularBuildExit(entity, character, count >= 3, modList);
// Move remaining temp models back to models
std::vector<Item*> 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<InventoryComponent*>(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<Item*> 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<InventoryComponent*>(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();