mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix: brick by brick and model placement work the way the client expects
Mapped from the 1.10.64 client and live captures (docs/BuildWorkflow.md). Model placement (PropertyManagementComponent): - A brick built model placed from the inventory spawned at the world origin with no rotation and without PlaceModelResponse/PreCreate, and was saved to properties_contents with ugc_id 0; it is now placed where the client put it, keeps its UGID and blueprint and is saved with them. - Picking up, putting away and taking apart a brick built model gave it to MODELS_IN_BBB and then deleted it; every way off the property now puts it in MODELS (carried when picked up), as live, with its blueprint config. Taking a premade model apart no longer deletes it either. - Placing and removing a model saves the property at once, so a crash or a disconnect before PropertyEditorEnd does not lose it. - DoneArrangingWithItem only answers when something new is picked (not when leaving), with the subject as build area; SetBuildModeConfirmed's warnVisitors matches live. Brick by brick (BrickByBrick): - BBBLoadItemRequest moves the model to MODELS_IN_BBB keeping its id and fails cleanly when the player has no such model. - MoveInventoryBatch moves bricks between BRICKS and BRICKS_IN_BBB (it was not handled, so the client and server disagreed until a relog). - BBBSaveRequest uses up the opened models, places the new ones through the property, returns the bricks (or uses them with bbb_consume_bricks=1), clears the autosave and sends RequeryPropertyModels. Every save makes new ugc rows (is_optimized 0, so the UGC server processes them). - Quick save: SetBBBAutosave is stored per character (bbb_autosave). - UnUseBBBModel puts a model back on the property where it was when it came from there, otherwise back in MODELS. - Leaving brick mode without a save, a disconnect or a crash: the autosave is rebuilt into models (RebuildBBBAutosaveMsg) or the opened models go back to MODELS. MODELS_IN_BBB is saved with the character now and loads into MODELS, BRICKS_IN_BBB into BRICKS. Fixes #1632 Fixes #159 Fixes #1565 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,7 @@ namespace {
|
||||
c.Add(Format(Text(SHARED, "dump_folder", "Crash dump folder", "Where crash logs go. Empty turns them off.", "", true), eFormat::PATH));
|
||||
c.Add(Bool(WORLD, "generate_dump", "Crash dumps from world servers", "Write a dump when a world server crashes (needs the crash dump folder).", false, true));
|
||||
c.Add(Bool(WORLD, "save_lxfmls", "Save model files", "Save players' models (LXFML) to disk before they are split, for debugging.", false));
|
||||
c.Add(Bool(WORLD, "bbb_consume_bricks", "Brick building uses bricks", "Saving a brick by brick model uses up the bricks in it, as live did. Off: the bricks go back to the backpack.", false));
|
||||
c.Add(Bool(SHARED, "dont_generate_dcf", "Don't build the chat filter file", "Skip compiling the chat word list to a file.", false, true));
|
||||
|
||||
c.AddSection("Chat web API", "A small HTTP API on the chat server, on localhost only.");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "InventoryComponent.h"
|
||||
#include "BrickByBrick.h"
|
||||
#include "Contraband.h"
|
||||
#include "EconomyLedger.h"
|
||||
|
||||
@@ -663,6 +664,9 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) {
|
||||
|
||||
bag->QueryAttribute("t", &type);
|
||||
|
||||
// A build in progress does not survive a reload: what was in the BBB inventories is back in MODELS and BRICKS
|
||||
const auto savedType = type;
|
||||
type = BrickByBrick::InventoryToLoadInto(type);
|
||||
auto* inventory = GetInventory(static_cast<eInventoryType>(type));
|
||||
|
||||
if (inventory == nullptr) {
|
||||
@@ -696,6 +700,12 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) {
|
||||
itemElement->QueryAttribute("parent", &parent);
|
||||
// End custom xml
|
||||
|
||||
if (savedType != type && !inventory->IsSlotEmpty(slot)) {
|
||||
const auto freeSlot = inventory->FindEmptySlot();
|
||||
if (freeSlot == -1) inventory->SetSize(inventory->GetSize() + 1);
|
||||
slot = freeSlot != -1 ? freeSlot : inventory->FindEmptySlot();
|
||||
}
|
||||
|
||||
auto* item = new Item(id, lot, inventory, slot, count, bound, {}, parent, subKey);
|
||||
|
||||
item->LoadConfigXml(*itemElement);
|
||||
@@ -740,7 +750,9 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) {
|
||||
for (const auto& pair : this->m_Inventories) {
|
||||
auto* inventory = pair.second;
|
||||
|
||||
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, MODELS_IN_BBB, ITEM_SETS };
|
||||
// MODELS_IN_BBB is saved so a model open in brick by brick building survives a disconnect or a crash; it loads
|
||||
// back into MODELS (BrickByBrick::InventoryToLoadInto)
|
||||
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, ITEM_SETS };
|
||||
if (std::ranges::find(EXCLUDED_INVENTORIES, inventory->GetType()) != EXCLUDED_INVENTORIES.end()) {
|
||||
continue;
|
||||
}
|
||||
@@ -1438,7 +1450,7 @@ BehaviorSlot InventoryComponent::FindBehaviorSlot(const std::string& equipLocati
|
||||
|
||||
|
||||
bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) {
|
||||
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
|
||||
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB || type == BRICKS_IN_BBB;
|
||||
}
|
||||
|
||||
uint32_t InventoryComponent::FindSkill(const LOT lot) {
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
#include "CppScripts.h"
|
||||
#include <ranges>
|
||||
#include "dConfig.h"
|
||||
#include "BrickByBrick.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "eKillType.h"
|
||||
|
||||
PropertyManagementComponent* PropertyManagementComponent::instance = nullptr;
|
||||
|
||||
@@ -327,129 +330,132 @@ void PropertyManagementComponent::OnFinishBuilding() {
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) {
|
||||
LOG("Placing model <%f, %f, %f>", position.x, position.y, position.z);
|
||||
|
||||
auto* entity = GetOwner();
|
||||
|
||||
if (entity == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
||||
|
||||
if (inventoryComponent == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* item = inventoryComponent->FindItemById(id);
|
||||
|
||||
if (item == nullptr) {
|
||||
LOG("Failed to find item with id %d", id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NiQuaternion originalRotation = rotation;
|
||||
|
||||
const auto modelLOT = item->GetLot();
|
||||
|
||||
if (rotation != QuatUtils::IDENTITY) {
|
||||
rotation = { rotation.w, rotation.z, rotation.y, rotation.x };
|
||||
}
|
||||
|
||||
if (item->GetLot() == 6662) {
|
||||
LWOOBJID spawnerID = item->GetSubKey();
|
||||
|
||||
EntityInfo info;
|
||||
info.lot = 14;
|
||||
info.pos = {};
|
||||
info.rot = {};
|
||||
info.spawner = nullptr;
|
||||
info.spawnerID = spawnerID;
|
||||
info.spawnerNodeID = 0;
|
||||
info.settings = item->GetConfig();
|
||||
|
||||
Entity* newEntity = Game::entityManager->CreateEntity(info);
|
||||
if (newEntity != nullptr) {
|
||||
Game::entityManager->ConstructEntity(newEntity);
|
||||
auto* modelComponent = newEntity->GetComponent<ModelComponent>();
|
||||
if (modelComponent) modelComponent->Pause();
|
||||
|
||||
// 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(), spawnerID);
|
||||
}
|
||||
|
||||
item->SetCount(item->GetCount() - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
item->SetCount(item->GetCount() - 1);
|
||||
|
||||
Entity* PropertyManagementComponent::SpawnModel(const LOT lot, const LWOOBJID modelId, const NiPoint3& position, const NiQuaternion& rotation, const LwoNameValue& config) {
|
||||
auto* node = new SpawnerNode();
|
||||
|
||||
node->position = position;
|
||||
node->rotation = rotation;
|
||||
node->config = config;
|
||||
// Every placed model: a model object whose UGID is its property model id (GetModelsOnProperty pairs the two)
|
||||
node->config.Insert<LWOOBJID>(u"userModelID", modelId);
|
||||
node->config.Insert<int>(u"modelType", 2);
|
||||
node->config.Insert<bool>(u"propertyObjectID", true);
|
||||
node->config.Insert<int>(u"componentWhitelist", 1);
|
||||
if (lot != BrickByBrick::MODEL_OBJECT_LOT) node->config.Insert<LWOOBJID>(u"modelBehaviors", 0);
|
||||
|
||||
SpawnerInfo info{};
|
||||
|
||||
info.templateID = modelLOT;
|
||||
info.templateID = lot;
|
||||
info.nodes = { node };
|
||||
info.templateScale = 1.0f;
|
||||
info.activeOnLoad = true;
|
||||
info.amountMaintained = 1;
|
||||
info.respawnTime = 10;
|
||||
|
||||
info.emulated = true;
|
||||
info.emulator = Game::entityManager->GetZoneControlEntity()->GetObjectID();
|
||||
|
||||
info.spawnerID = ObjectIDManager::GetPersistentID();
|
||||
GeneralUtils::SetBit(info.spawnerID, eObjectBits::CLIENT);
|
||||
// The spawner's id is the model's id in properties_contents
|
||||
info.spawnerID = modelId;
|
||||
|
||||
const auto spawnerId = Game::zoneManager->MakeSpawner(info);
|
||||
|
||||
auto* spawner = Game::zoneManager->GetSpawner(spawnerId);
|
||||
|
||||
info.nodes[0]->config.Insert<LWOOBJID>(u"modelBehaviors", 0);
|
||||
info.nodes[0]->config.Insert<LWOOBJID>(u"userModelID", info.spawnerID);
|
||||
info.nodes[0]->config.Insert<int>(u"modelType", 2);
|
||||
info.nodes[0]->config.Insert<bool>(u"propertyObjectID", true);
|
||||
info.nodes[0]->config.Insert<int>(u"componentWhitelist", 1);
|
||||
if (!spawner) return nullptr;
|
||||
|
||||
auto* model = spawner->Spawn();
|
||||
if (!model) return nullptr;
|
||||
|
||||
// Placed while the owner is editing: stays still until they finish (OnFinishBuilding resumes every model)
|
||||
auto* modelComponent = model->GetComponent<ModelComponent>();
|
||||
if (modelComponent) modelComponent->Pause();
|
||||
|
||||
models.insert_or_assign(model->GetObjectID(), spawnerId);
|
||||
return model;
|
||||
}
|
||||
|
||||
LWOOBJID PropertyManagementComponent::PlaceModelFromItem(Item& item, const NiPoint3& position, const NiQuaternion& rotation) {
|
||||
LWOOBJID modelId = LWOOBJID_EMPTY;
|
||||
Entity* model = nullptr;
|
||||
|
||||
if (item.GetLot() == BrickByBrick::MODEL_ITEM_LOT) {
|
||||
// A brick built model keeps its UGID (the item's subkey and userModelID) and blueprint wherever it goes
|
||||
const auto& config = item.GetConfig();
|
||||
modelId = item.GetSubKey();
|
||||
if (modelId == LWOOBJID_EMPTY) modelId = BrickByBrick::ConfigObjectId(config, u"userModelID");
|
||||
if (modelId == LWOOBJID_EMPTY) modelId = ObjectIDManager::GetNewModelIDs().modelID;
|
||||
const auto blueprintId = BrickByBrick::ConfigObjectId(config, u"blueprintid");
|
||||
if (blueprintId == LWOOBJID_EMPTY) {
|
||||
LOG("Model item %llu has no blueprint, not placing it", item.GetId());
|
||||
return LWOOBJID_EMPTY;
|
||||
}
|
||||
|
||||
LwoNameValue modelConfig;
|
||||
modelConfig.Insert<LWOOBJID>(u"blueprintid", blueprintId);
|
||||
const auto behaviors = config.find(u"userModelBehaviors");
|
||||
if (behaviors != config.end() && behaviors->second) modelConfig.Insert<std::string>(u"userModelBehaviors", behaviors->second->GetValueAsString());
|
||||
model = SpawnModel(BrickByBrick::MODEL_OBJECT_LOT, modelId, position, rotation, modelConfig);
|
||||
} else {
|
||||
// A premade model gets a new UGID each time it is placed (live: the placed model's id, then a new item id
|
||||
// when it is picked up)
|
||||
modelId = ObjectIDManager::GetPersistentID();
|
||||
GeneralUtils::SetBit(modelId, eObjectBits::CLIENT);
|
||||
model = SpawnModel(item.GetLot(), modelId, position, rotation, {});
|
||||
}
|
||||
|
||||
if (!model) return LWOOBJID_EMPTY;
|
||||
|
||||
item.SetCount(item.GetCount() - 1, false, false, false, eLootSourceType::PROPERTY);
|
||||
// Straight to the database: the model must not be lost if the server stops before the owner finishes editing
|
||||
Save();
|
||||
return modelId;
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::SendModelsOnProperty() const {
|
||||
GameMessages::GetModelsOnProperty msg;
|
||||
msg.target = owner;
|
||||
msg.models = { models.begin(), models.end() };
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) {
|
||||
LOG("Placing model <%f, %f, %f>", position.x, position.y, position.z);
|
||||
|
||||
auto* entity = GetOwner();
|
||||
if (entity == nullptr) return;
|
||||
|
||||
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
||||
if (inventoryComponent == nullptr) return;
|
||||
|
||||
auto* item = inventoryComponent->FindItemById(id);
|
||||
if (item == nullptr) {
|
||||
LOG("Failed to find item with id %llu", id);
|
||||
return;
|
||||
}
|
||||
|
||||
const NiQuaternion originalRotation = rotation;
|
||||
if (rotation != QuatUtils::IDENTITY) {
|
||||
rotation = { rotation.w, rotation.z, rotation.y, rotation.x };
|
||||
}
|
||||
|
||||
const auto modelId = PlaceModelFromItem(*item, position, rotation);
|
||||
if (modelId == LWOOBJID_EMPTY) return;
|
||||
|
||||
// As a live server answered a placed model
|
||||
{
|
||||
GameMessages::HandleUGCEquipPreCreateBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.modelCount = 0;
|
||||
msg.modelID = modelId;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
{
|
||||
GameMessages::PlaceModelResponse msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.position = position;
|
||||
msg.propertyPlaqueID = m_Parent->GetObjectID();
|
||||
msg.response = 14;
|
||||
msg.response = BrickByBrick::PLACE_MODEL_PLACED;
|
||||
msg.rotation = originalRotation;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
{
|
||||
GameMessages::HandleUGCEquipPreCreateBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.modelCount = 0;
|
||||
msg.modelID = spawnerId;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
{
|
||||
const auto& propertyModels = GetModels();
|
||||
GameMessages::GetModelsOnProperty msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.models = { propertyModels.begin(), propertyModels.end() };
|
||||
LOG("Sending property models to (%llu) (%d)", msg.target, true);
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
}
|
||||
SendModelsOnProperty();
|
||||
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPlaced(entity);
|
||||
|
||||
@@ -462,205 +468,83 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet
|
||||
LOG("Delete model: (%llu) (%i)", id, deleteReason);
|
||||
|
||||
auto* entity = GetOwner();
|
||||
|
||||
if (entity == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (entity == nullptr) return;
|
||||
|
||||
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
||||
|
||||
if (inventoryComponent == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (inventoryComponent == nullptr) return;
|
||||
|
||||
auto* model = Game::entityManager->GetEntity(id);
|
||||
|
||||
if (model == nullptr) {
|
||||
LOG("Failed to find model entity");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (model->GetLOT() == 14 && deleteReason == 0) {
|
||||
LOG("User is trying to pick up a BBB model, but this is not implemented, so we return to prevent the user from losing the model");
|
||||
|
||||
{
|
||||
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.invItem = LWOOBJID_EMPTY;
|
||||
msg.itemsTotal = 0;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
// Need this to pop the user out of their current state
|
||||
{
|
||||
GameMessages::PlaceModelResponse msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.position = entity->GetPosition();
|
||||
msg.propertyPlaqueID = m_Parent->GetObjectID();
|
||||
msg.response = 14;
|
||||
msg.rotation = entity->GetRotation();
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = models.find(id);
|
||||
|
||||
if (index == models.end()) {
|
||||
LOG("Failed to find model");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const auto spawnerId = index->second;
|
||||
const auto modelId = index->second;
|
||||
const auto removal = BrickByBrick::PlanModelRemoval(deleteReason);
|
||||
|
||||
auto* spawner = Game::zoneManager->GetSpawner(spawnerId);
|
||||
|
||||
models.erase(id);
|
||||
|
||||
if (spawner == nullptr) {
|
||||
LOG("Failed to find spawner");
|
||||
// Every way off the property puts the model in MODELS; taking it apart then opens it in brick by brick building
|
||||
LOT itemLot = model->GetLOT();
|
||||
LwoNameValue config;
|
||||
LWOOBJID subKey = LWOOBJID_EMPTY;
|
||||
if (model->GetLOT() == BrickByBrick::MODEL_OBJECT_LOT) {
|
||||
itemLot = BrickByBrick::MODEL_ITEM_LOT;
|
||||
config = BrickByBrick::ModelItemConfig(model->GetVar<LWOOBJID>(u"blueprintid"), modelId, model->GetVar<std::string>(u"userModelBehaviors"));
|
||||
subKey = modelId;
|
||||
}
|
||||
|
||||
const auto received = inventoryComponent->ReceiveItem(LWOOBJID_EMPTY, itemLot, 1, eLootSourceType::PROPERTY, config, subKey, false,
|
||||
{ .inventory = eInventoryType::MODELS, .showFlyingLoot = false, .equip = removal.equip });
|
||||
if (received.id == LWOOBJID_EMPTY) {
|
||||
LOG("Could not give model %llu back to %llu, leaving it on the property", modelId, entity->GetObjectID());
|
||||
return;
|
||||
}
|
||||
|
||||
models.erase(index);
|
||||
Game::entityManager->DestructEntity(model);
|
||||
|
||||
LOG("Deleting model LOT %i", model->GetLOT());
|
||||
|
||||
if (model->GetLOT() == 14) {
|
||||
//add it to the inv
|
||||
LwoNameValue actualConfig;
|
||||
|
||||
//fill our settings with BBB gurbage
|
||||
actualConfig.Insert(u"blueprintid", model->GetVar<LWOOBJID>(u"blueprintid"));
|
||||
actualConfig.Insert(u"userModelDesc", u"A cool model you made!");
|
||||
actualConfig.Insert(u"userModelHasBhvr", false);
|
||||
actualConfig.Insert(u"userModelID", model->GetVar<LWOOBJID>(u"userModelID"));
|
||||
actualConfig.Insert(u"userModelMod", false);
|
||||
actualConfig.Insert(u"userModelName", u"My Cool Model");
|
||||
actualConfig.Insert(u"userModelOpt", true);
|
||||
actualConfig.Insert(u"userModelPhysicsType", 2);
|
||||
|
||||
inventoryComponent->AddItem(6662, 1, eLootSourceType::DELETION, eInventoryType::MODELS_IN_BBB, actualConfig, LWOOBJID_EMPTY, false, false, spawnerId);
|
||||
auto* item = inventoryComponent->FindItemBySubKey(spawnerId);
|
||||
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteReason == 0) {
|
||||
//item->Equip();
|
||||
}
|
||||
|
||||
if (deleteReason == 0 || deleteReason == 2) {
|
||||
{
|
||||
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.invItem = item->GetId();
|
||||
msg.itemsTotal = item->GetCount();
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const auto& propertyModels = GetModels();
|
||||
GameMessages::GetModelsOnProperty msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.models = { propertyModels.begin(), propertyModels.end() };
|
||||
LOG("Sending property models to (%llu) (%d)", msg.target, true);
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
}
|
||||
|
||||
{
|
||||
GameMessages::PlaceModelResponse msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.position = NiPoint3Constant::ZERO;
|
||||
msg.propertyPlaqueID = LWOOBJID_EMPTY;
|
||||
msg.response = 16;
|
||||
msg.rotation = QuatUtils::IDENTITY;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
if (spawner != nullptr) {
|
||||
Game::zoneManager->RemoveSpawner(spawner->m_Info.spawnerID);
|
||||
} else {
|
||||
model->Smash(LWOOBJID_EMPTY, eKillType::SILENT);
|
||||
}
|
||||
|
||||
item->SetCount(0, true, false, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
inventoryComponent->AddItem(model->GetLOT(), 1, eLootSourceType::DELETION, INVALID, {}, LWOOBJID_EMPTY, false);
|
||||
|
||||
auto* item = inventoryComponent->FindItemByLot(model->GetLOT());
|
||||
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (deleteReason) {
|
||||
case 0: // Pickup
|
||||
{
|
||||
item->Equip();
|
||||
|
||||
{
|
||||
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.invItem = item->GetId();
|
||||
msg.itemsTotal = item->GetCount();
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPickedUp(entity);
|
||||
|
||||
break;
|
||||
}
|
||||
case 1: // Return to inv
|
||||
{
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelRemoved(entity);
|
||||
|
||||
break;
|
||||
}
|
||||
case 2: // Break apart
|
||||
{
|
||||
item->SetCount(item->GetCount() - 1);
|
||||
|
||||
LOG("DLU currently does not support breaking apart brick by brick models.");
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
LOG("Invalid delete reason");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const auto& propertyModels = GetModels();
|
||||
GameMessages::GetModelsOnProperty msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.models = { propertyModels.begin(), propertyModels.end() };
|
||||
LOG("Sending property models to (%llu) (%d)", msg.target, true);
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
}
|
||||
|
||||
{
|
||||
GameMessages::PlaceModelResponse msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.position = NiPoint3Constant::ZERO;
|
||||
msg.propertyPlaqueID = LWOOBJID_EMPTY;
|
||||
msg.response = 16;
|
||||
msg.rotation = QuatUtils::IDENTITY;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
auto* spawner = Game::zoneManager->GetSpawner(modelId);
|
||||
if (spawner != nullptr) {
|
||||
Game::zoneManager->RemoveSpawner(spawner->m_Info.spawnerID);
|
||||
} else {
|
||||
model->Smash(LWOOBJID_EMPTY, eKillType::SILENT);
|
||||
}
|
||||
|
||||
// Straight to the database, as for placing
|
||||
Save();
|
||||
|
||||
if (removal.notifyPostDelete) {
|
||||
auto* item = inventoryComponent->FindItemById(received.id);
|
||||
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.invItem = received.id;
|
||||
msg.itemsTotal = item ? item->GetCount() : 1;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
SendModelsOnProperty();
|
||||
|
||||
{
|
||||
GameMessages::PlaceModelResponse msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.response = BrickByBrick::PLACE_MODEL_REMOVED;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
}
|
||||
|
||||
switch (static_cast<BrickByBrick::eDeleteReason>(deleteReason)) {
|
||||
case BrickByBrick::eDeleteReason::PICKING_MODEL_UP:
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPickedUp(entity);
|
||||
break;
|
||||
case BrickByBrick::eDeleteReason::RETURNING_MODEL_TO_INVENTORY:
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelRemoved(entity);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::UpdateApprovedStatus(const bool value, const std::string& rejectionReason) {
|
||||
@@ -796,7 +680,8 @@ void PropertyManagementComponent::Save() {
|
||||
model.lot = entity->GetLOT();
|
||||
model.position = position;
|
||||
model.rotation = rotation;
|
||||
model.ugcId = 0;
|
||||
// A brick built model keeps its blueprint (it used to be saved as 0, losing the model on the next load)
|
||||
model.ugcId = model.lot == BrickByBrick::MODEL_OBJECT_LOT ? entity->GetVar<LWOOBJID>(u"blueprintid") : 0;
|
||||
for (auto i = 0; i < model.behaviors.size(); i++) {
|
||||
model.behaviors[i] = modelBehaviors[i].first;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include "Entity.h"
|
||||
#include "Component.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
#include "LDFFormat.h"
|
||||
|
||||
class Item;
|
||||
|
||||
/**
|
||||
* Main component that handles interactions with a property, generally the plaques you see on properties.
|
||||
@@ -112,6 +115,23 @@ public:
|
||||
*/
|
||||
void DeleteModel(LWOOBJID id, int deleteReason);
|
||||
|
||||
/**
|
||||
* Spawns a model on the property and records it in the property's models (not yet saved)
|
||||
* @param lot the model object's LOT (14 for a brick built model)
|
||||
* @param modelId the model's id in properties_contents (its UGID)
|
||||
* @param config extra config for the model object
|
||||
*/
|
||||
Entity* SpawnModel(LOT lot, LWOOBJID modelId, const NiPoint3& position, const NiQuaternion& rotation, const LwoNameValue& config);
|
||||
|
||||
/**
|
||||
* Places a model item on the property, uses the item up and saves the property
|
||||
* @return the placed model's id, or LWOOBJID_EMPTY if it could not be placed
|
||||
*/
|
||||
LWOOBJID PlaceModelFromItem(Item& item, const NiPoint3& position, const NiQuaternion& rotation);
|
||||
|
||||
// GetModelsOnProperty with every model on the property
|
||||
void SendModelsOnProperty() const;
|
||||
|
||||
/**
|
||||
* Updates whether or not this property is approved by a moderator
|
||||
* @param value true if the property should be approved, false otherwise
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "PropertyManagementComponent.h"
|
||||
#include "Sd0.h"
|
||||
#include "ScriptComponent.h"
|
||||
#include "BrickByBrick.h"
|
||||
#include "User.h"
|
||||
#include "UserManager.h"
|
||||
|
||||
@@ -217,31 +218,14 @@ namespace GameMessages {
|
||||
InventoryComponent* inv = static_cast<InventoryComponent*>(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());
|
||||
|
||||
// Picking something new to build or arrange (a brick for a new model, a placed model to take apart) starts
|
||||
// arranging again; the build area is the subject, as for StartBuildingWithItem. Leaving (newSourceType 0) gets
|
||||
// no answer, as on live.
|
||||
if (PropertyManagementComponent::Instance() != nullptr && newSourceType != 0) {
|
||||
StartArrangingWithItem arranging;
|
||||
arranging.target = character->GetObjectID();
|
||||
arranging.firstTime = false;
|
||||
arranging.buildAreaID = buildArea->GetObjectID();
|
||||
arranging.buildAreaID = entity.GetObjectID();
|
||||
arranging.buildStartPos = character->GetPosition();
|
||||
arranging.sourceBag = newSourceBag;
|
||||
arranging.sourceID = newSourceID;
|
||||
@@ -462,7 +446,7 @@ namespace GameMessages {
|
||||
SetBuildModeConfirmed confirmed;
|
||||
confirmed.target = entity.GetObjectID();
|
||||
confirmed.start = start;
|
||||
confirmed.warnVisitors = false;
|
||||
confirmed.warnVisitors = !start; // as a live server answered: false going in, true coming out
|
||||
confirmed.modePaused = modePaused;
|
||||
confirmed.modeValue = modeValue;
|
||||
confirmed.playerId = playerId;
|
||||
@@ -523,23 +507,7 @@ namespace GameMessages {
|
||||
}
|
||||
|
||||
void UnUseBBBModel::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* inventoryComponent = entity.GetComponent<InventoryComponent>();
|
||||
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);
|
||||
}
|
||||
BrickByBrick::ReturnModel(entity, modelID, bHasWorldTransform, worldPos, worldRot);
|
||||
}
|
||||
|
||||
void BBBLoadItemRequest::Serialize(RakNet::BitStream& bitStream) const {
|
||||
@@ -552,34 +520,14 @@ namespace GameMessages {
|
||||
}
|
||||
|
||||
void BBBLoadItemRequest::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
const LWOOBJID previousItemID = itemID;
|
||||
const auto movedId = BrickByBrick::LoadModel(entity, itemID);
|
||||
|
||||
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
|
||||
// A live server answered with the same id: the item keeps it in MODELS_IN_BBB. Without the model the client
|
||||
// shows BBB_ERROR_LOADING_BLUEPRINT and drops the load (0x00b75bb0).
|
||||
ClientPackets::BlueprintLoadItemResponse response;
|
||||
response.success = true;
|
||||
response.itemId = previousItemID;
|
||||
response.destItemId = newId;
|
||||
response.success = movedId != LWOOBJID_EMPTY;
|
||||
response.itemId = itemID;
|
||||
response.destItemId = movedId != LWOOBJID_EMPTY ? movedId : itemID;
|
||||
response.Send(sysAddr);
|
||||
}
|
||||
|
||||
@@ -604,129 +552,53 @@ namespace GameMessages {
|
||||
}
|
||||
|
||||
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.
|
||||
BrickByBrick::Save(entity, localID, lxfmlDataCompressed);
|
||||
}
|
||||
|
||||
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.
|
||||
void ActivateBrickMode::Serialize(RakNet::BitStream& bitStream) const {
|
||||
BitStreamUtils::WriteOptional(bitStream, buildObjectID, LWOOBJID_EMPTY);
|
||||
BitStreamUtils::WriteOptional(bitStream, buildType, 2);
|
||||
bitStream.Write(enterBuildFromWorld);
|
||||
bitStream.Write(enterFlag);
|
||||
}
|
||||
|
||||
TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch?
|
||||
*/
|
||||
bool ActivateBrickMode::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, buildObjectID, LWOOBJID_EMPTY));
|
||||
VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, buildType, 2));
|
||||
VALIDATE_READ(bitStream.Read(enterBuildFromWorld));
|
||||
VALIDATE_READ(bitStream.Read(enterFlag));
|
||||
return true;
|
||||
}
|
||||
|
||||
//Now, the cave of dragons:
|
||||
void ActivateBrickMode::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
if (!enterFlag) BrickByBrick::EndSession(entity);
|
||||
}
|
||||
|
||||
//We need to get a new ID for our model first:
|
||||
if (!entity.GetCharacter() || !entity.GetCharacter()->GetParentUser()) return;
|
||||
void SetBBBAutosave::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write<uint32_t>(lxfmlDataCompressed.size());
|
||||
if (!lxfmlDataCompressed.empty()) bitStream.Write(lxfmlDataCompressed.data(), lxfmlDataCompressed.size());
|
||||
}
|
||||
|
||||
//We need to get the propertyID: (stolen from Wincent's propertyManagementComp)
|
||||
const auto& worldId = Game::zoneManager->GetZone()->GetZoneID();
|
||||
bool SetBBBAutosave::Deserialize(RakNet::BitStream& bitStream) {
|
||||
uint32_t size{};
|
||||
VALIDATE_READ(bitStream.Read(size));
|
||||
if (static_cast<uint64_t>(size) * 8 > bitStream.GetNumberOfUnreadBits()) return false;
|
||||
lxfmlDataCompressed.resize(size);
|
||||
if (size != 0) VALIDATE_READ(bitStream.Read(lxfmlDataCompressed.data(), size));
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto zoneId = worldId.GetMapID();
|
||||
const auto cloneId = worldId.GetCloneID();
|
||||
void SetBBBAutosave::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
BrickByBrick::Autosave(entity, lxfmlDataCompressed);
|
||||
}
|
||||
|
||||
auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId);
|
||||
LWOOBJID propertyId = LWOOBJID_EMPTY;
|
||||
if (propertyInfo) propertyId = propertyInfo->id;
|
||||
void RebuildBBBAutosaveMsg::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(count);
|
||||
}
|
||||
|
||||
// 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<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
|
||||
auto& responseModel = response.models.emplace_back();
|
||||
responseModel.blueprintId = blueprintID;
|
||||
for (const auto& chunk : sd0.GetAsVector()) responseModel.data.append(reinterpret_cast<const char*>(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<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]);
|
||||
}
|
||||
}
|
||||
bool RebuildBBBAutosaveMsg::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(count));
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetModelToBuild::Serialize(RakNet::BitStream& bitStream) const {
|
||||
|
||||
@@ -206,6 +206,42 @@ namespace GameMessages {
|
||||
uint32_t timeTakenInMs{};
|
||||
};
|
||||
|
||||
// Client -> server. The player went into (enterFlag) or came out of brick by brick building.
|
||||
// Layout as GameMessage::ActivateBrickMode::Deserialize (0x00d8ecb0) reads it.
|
||||
struct ActivateBrickMode : public NetGameMsg {
|
||||
ActivateBrickMode() : NetGameMsg(MessageType::Game::ACTIVATE_BRICK_MODE) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
|
||||
|
||||
LWOOBJID buildObjectID{ LWOOBJID_EMPTY }; // optional; the property's build area when entering
|
||||
int32_t buildType{ 2 }; // optional; 0 nowhere, 1 in the world, 2 on a property
|
||||
bool enterBuildFromWorld{ true };
|
||||
bool enterFlag{ true };
|
||||
};
|
||||
|
||||
// Client -> server. The client's quick save of the model being built: sd0 compressed LXFML, or just the sd0 header
|
||||
// when there is nothing to keep. Sent every five minutes, when told the player is AFK (msgInformAFK) and before the
|
||||
// client shuts down. Layout as GameMessage::SetBBBAutosave::Deserialize (0x00f2af60) reads it.
|
||||
struct SetBBBAutosave : public NetGameMsg {
|
||||
SetBBBAutosave() : NetGameMsg(MessageType::Game::SET_BBB_AUTOSAVE) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
|
||||
|
||||
std::string lxfmlDataCompressed{}; // u32 byte count, then the bytes
|
||||
};
|
||||
|
||||
// Server -> client. How many unfinished (autosaved) models the server rebuilt into models; the client shows
|
||||
// BBB_AUTOSAVE_REBUILDING_SINGLE or _MULTIPLE (LWOBBBComponent_Client::msgRebuildBBBAutosaveMsg, 0x00cfabd0).
|
||||
struct RebuildBBBAutosaveMsg : public NetGameMsg {
|
||||
RebuildBBBAutosaveMsg() : NetGameMsg(MessageType::Game::REBUILD_BBB_AUTOSAVE_MSG) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
|
||||
int32_t count{};
|
||||
};
|
||||
|
||||
struct SetModelToBuild : public NetGameMsg {
|
||||
SetModelToBuild() : NetGameMsg(MessageType::Game::SET_MODEL_TO_BUILD) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
|
||||
@@ -143,6 +143,9 @@ namespace {
|
||||
{ UN_USE_BBB_MODEL, []() { return std::make_unique<UnUseBBBModel>(); } },
|
||||
{ BBB_LOAD_ITEM_REQUEST, []() { return std::make_unique<BBBLoadItemRequest>(); } },
|
||||
{ BBB_SAVE_REQUEST, []() { return std::make_unique<BBBSaveRequest>(); } },
|
||||
{ SET_BBB_AUTOSAVE, []() { return std::make_unique<SetBBBAutosave>(); } },
|
||||
{ ACTIVATE_BRICK_MODE, []() { return std::make_unique<ActivateBrickMode>(); } },
|
||||
{ MOVE_INVENTORY_BATCH, []() { return std::make_unique<MoveInventoryBatch>(); } },
|
||||
|
||||
// Vendors and donation vendors
|
||||
{ REQUEST_VENDOR_STATUS_UPDATE, []() { return std::make_unique<RequestVendorStatusUpdate>(); } },
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Inventory.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "Item.h"
|
||||
#include "BrickByBrick.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
@@ -306,6 +307,10 @@ namespace GameMessages {
|
||||
return true;
|
||||
}
|
||||
|
||||
void MoveInventoryBatch::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
BrickByBrick::MoveBricks(entity, *this);
|
||||
}
|
||||
|
||||
void NotifyNotEnoughInvSpace::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(freeSlotsNeeded);
|
||||
BitStreamUtils::WriteOptional(bitStream, inventoryType, eInventoryType::ITEMS);
|
||||
|
||||
@@ -171,6 +171,10 @@ namespace GameMessages {
|
||||
MoveInventoryBatch() : NetGameMsg(MessageType::Game::MOVE_INVENTORY_BATCH) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
// Client -> server: brick by brick moving bricks between BRICKS and BRICKS_IN_BBB. The client has already taken
|
||||
// them out of srcBag (LWOInventoryComponent_Common::msgMoveInventoryBatch, 0x00ce1310) and waits for
|
||||
// AddItemToInventoryClientSync for dstBag.
|
||||
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
|
||||
|
||||
bool bAllowPartial{};
|
||||
bool bOutSuccess{};
|
||||
|
||||
@@ -148,6 +148,12 @@ namespace GameMessages {
|
||||
NiQuaternion rotation{ QuatUtils::IDENTITY }; // optional
|
||||
};
|
||||
|
||||
// Server -> client. No payload. Makes the client ask for the property's models again (PropertyContentsFromClient);
|
||||
// a live server sent it after a brick by brick save and after placing a model.
|
||||
struct RequeryPropertyModels : public NetGameMsg {
|
||||
RequeryPropertyModels() : NetGameMsg(MessageType::Game::REQUERY_PROPERTY_MODELS) {}
|
||||
};
|
||||
|
||||
// Server -> client. Sent with id HANDLE_UGC_POST_CREATE_BASED_ON_EDIT_MODE (1301), which the client names
|
||||
// HandleUGCEquipPreCreateBasedOnEditMode.
|
||||
struct HandleUGCEquipPreCreateBasedOnEditMode : public NetGameMsg {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "LevelProgressionComponent.h"
|
||||
#include "RacingControlComponent.h"
|
||||
#include "WorldMigration.h"
|
||||
#include "BrickByBrick.h"
|
||||
#include "dConfig.h"
|
||||
#include "dZoneManager.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
@@ -118,6 +119,10 @@ namespace GameMessages {
|
||||
// Moved here from another instance: put back what their save doesn't keep (the pet that was out)
|
||||
WorldMigration::OnPlayerLoaded(entity);
|
||||
|
||||
// A brick by brick build that ended without a save (disconnect, crash): rebuild the autosave or give back the
|
||||
// models that were open
|
||||
BrickByBrick::OnPlayerLoaded(*entity);
|
||||
|
||||
LOG("Player %s (%llu) loaded.", entity->GetCharacter()->GetName().c_str(), entity->GetObjectID());
|
||||
|
||||
// After we've done our thing, tell the client they're ready
|
||||
|
||||
389
dGame/dUtilities/BrickByBrick.cpp
Normal file
389
dGame/dUtilities/BrickByBrick.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
#include "BrickByBrick.h"
|
||||
|
||||
#include "BuildingMessages.h"
|
||||
#include "Character.h"
|
||||
#include "ClientPackets.h"
|
||||
#include "DashboardNotify.h"
|
||||
#include "Database.h"
|
||||
#include "dConfig.h"
|
||||
#include "EconomyLedger.h"
|
||||
#include "eBlueprintSaveResponseType.h"
|
||||
#include "eInventoryType.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "Entity.h"
|
||||
#include "Game.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Inventory.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "InventoryMessages.h"
|
||||
#include "Item.h"
|
||||
#include "Logger.h"
|
||||
#include "Lxfml.h"
|
||||
#include "ObjectIDManager.h"
|
||||
#include "PropertyManagementComponent.h"
|
||||
#include "PropertyMessages.h"
|
||||
#include "Sd0.h"
|
||||
#include "User.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
// Every item in one inventory, copied so the inventory can change while going through them
|
||||
std::vector<Item*> ItemsIn(InventoryComponent& inventory, const eInventoryType type) {
|
||||
std::vector<Item*> items;
|
||||
auto* bag = inventory.GetInventory(type);
|
||||
if (!bag) return items;
|
||||
for (auto* item : bag->GetItems() | std::views::values) items.push_back(item);
|
||||
return items;
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> IdsIn(InventoryComponent& inventory, const eInventoryType type) {
|
||||
std::vector<LWOOBJID> ids;
|
||||
for (const auto* item : ItemsIn(inventory, type)) ids.push_back(item->GetId());
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Moves one unique item (a model) to another of the player's inventories keeping its object id, as a live server
|
||||
// did for BBBLoadItemRequest: RemoveItemFromInventory for the old place, AddItemToInventoryClientSync for the new.
|
||||
LWOOBJID MoveKeepingId(InventoryComponent& inventory, Item* item, const eInventoryType to) {
|
||||
if (!item) return LWOOBJID_EMPTY;
|
||||
const auto from = item->GetInventory()->GetType();
|
||||
if (from == to) return item->GetId();
|
||||
|
||||
const auto id = item->GetId();
|
||||
const auto lot = item->GetLot();
|
||||
const auto count = item->GetCount();
|
||||
const auto subKey = item->GetSubKey();
|
||||
const auto bound = item->GetBound();
|
||||
const LwoNameValue config = item->GetConfig();
|
||||
|
||||
EconomyLedger::ScopedItemTransfer moving;
|
||||
if (item->IsEquipped()) item->UnEquip();
|
||||
item->SetCount(0, false, false, false, eLootSourceType::RELOCATE);
|
||||
const auto received = inventory.ReceiveItem(id, lot, count, eLootSourceType::RELOCATE, config, subKey, bound,
|
||||
{ .inventory = to, .showFlyingLoot = false, .sourceInventory = from });
|
||||
return received.id;
|
||||
}
|
||||
|
||||
// Bricks the player took into brick by brick building: used up by the model (as live did) when bbb_consume_bricks
|
||||
// is 1, otherwise back to the backpack (DLU's choice: building does not cost bricks).
|
||||
void SettleBricks(InventoryComponent& inventory, const bool usedByModel) {
|
||||
const bool consume = usedByModel && Game::config && Game::config->GetValue("bbb_consume_bricks") == "1";
|
||||
for (auto* item : ItemsIn(inventory, eInventoryType::BRICKS_IN_BBB)) {
|
||||
if (consume) {
|
||||
item->SetCount(0, false, false, false, eLootSourceType::INVENTORY);
|
||||
} else {
|
||||
inventory.MoveItemToInventory(item, eInventoryType::BRICKS, item->GetCount(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SavedModel {
|
||||
LWOOBJID modelId{};
|
||||
LWOOBJID blueprintId{};
|
||||
NiPoint3 center{};
|
||||
std::string sd0{}; // the model's own sd0 compressed LXFML
|
||||
};
|
||||
|
||||
// Splits a build into its models and stores each as a new ugc row (is_optimized 0: the UGC server makes its mesh
|
||||
// and icon). Every save makes new blueprint ids: the client caches blueprints by id.
|
||||
std::vector<SavedModel> StoreModels(Entity& player, const std::string& sd0Data, const std::string& debugName) {
|
||||
std::vector<SavedModel> saved;
|
||||
auto* character = player.GetCharacter();
|
||||
if (!character || !character->GetParentUser()) return saved;
|
||||
|
||||
std::istringstream sd0Stream(sd0Data);
|
||||
Sd0 sd0(sd0Stream);
|
||||
const auto lxfml = sd0.GetAsStringUncompressed();
|
||||
if (lxfml.empty()) return saved;
|
||||
|
||||
if (Game::config && Game::config->GetValue("save_lxfmls") == "1") {
|
||||
std::ofstream outFile("debug_lxfml_uncompressed_" + debugName + ".lxfml");
|
||||
outFile << lxfml;
|
||||
}
|
||||
|
||||
for (const auto& part : Lxfml::Split(lxfml)) {
|
||||
const auto [modelId, blueprintId] = ObjectIDManager::GetNewModelIDs();
|
||||
Sd0 model = sd0;
|
||||
model.FromData(reinterpret_cast<const uint8_t*>(part.lxfml.data()), part.lxfml.size());
|
||||
auto stream = model.GetAsStream();
|
||||
Database::Get()->InsertNewUgcModel(stream, blueprintId, character->GetParentUser()->GetAccountID(), character->GetID());
|
||||
|
||||
auto& entry = saved.emplace_back();
|
||||
entry.modelId = modelId;
|
||||
entry.blueprintId = blueprintId;
|
||||
entry.center = part.center;
|
||||
for (const auto& chunk : model.GetAsVector()) entry.sd0.append(reinterpret_cast<const char*>(chunk.data()), chunk.size());
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
|
||||
bool BrickByBrick::IsEmptyModel(const std::string_view sd0) {
|
||||
// The client clears its autosave with the bare sd0 header ("sd0" 01 ff) and no chunks
|
||||
return sd0.size() <= 5;
|
||||
}
|
||||
|
||||
BrickByBrick::ModelRemoval BrickByBrick::PlanModelRemoval(const int32_t reason) {
|
||||
switch (static_cast<eDeleteReason>(reason)) {
|
||||
case eDeleteReason::PICKING_MODEL_UP: return { .equip = true, .notifyPostDelete = true };
|
||||
case eDeleteReason::BREAKING_MODEL_APART: return { .equip = false, .notifyPostDelete = true };
|
||||
default: return { .equip = false, .notifyPostDelete = false };
|
||||
}
|
||||
}
|
||||
|
||||
BrickByBrick::Recovery BrickByBrick::PlanRecovery(const std::vector<LWOOBJID>& modelsInBbb, const std::optional<IBbbAutosave::Info>& autosave) {
|
||||
Recovery recovery;
|
||||
recovery.rebuild = autosave.has_value() && !IsEmptyModel(autosave->lxfml);
|
||||
if (recovery.rebuild) recovery.consume = autosave->sourceItems;
|
||||
for (const auto id : modelsInBbb) {
|
||||
if (std::ranges::find(recovery.consume, id) == recovery.consume.end()) recovery.giveBack.push_back(id);
|
||||
}
|
||||
return recovery;
|
||||
}
|
||||
|
||||
LwoNameValue BrickByBrick::ModelItemConfig(const LWOOBJID blueprintId, const LWOOBJID userModelId, const std::string& behaviors) {
|
||||
// The keys a live server gave a brick built model item (AddItemToInventoryClientSync when picking one up)
|
||||
LwoNameValue config;
|
||||
config.Insert<LWOOBJID>(u"blueprintid", blueprintId);
|
||||
config.Insert<LWOOBJID>(u"userModelID", userModelId);
|
||||
config.Insert<std::u16string>(u"userModelName", u"");
|
||||
config.Insert<std::u16string>(u"userModelDesc", u"");
|
||||
config.Insert<bool>(u"userModelHasBhvr", !behaviors.empty() && behaviors.find_first_not_of("0,") != std::string::npos);
|
||||
config.Insert<std::string>(u"userModelBehaviors", behaviors.empty() ? "0,0,0,0,0" : behaviors);
|
||||
config.Insert<std::string>(u"userModelBehaviorSourceIDs", "0,0,0,0,0");
|
||||
config.Insert<bool>(u"userModelOpt", true);
|
||||
config.Insert<int32_t>(u"userModelMod", 1);
|
||||
config.Insert<int32_t>(u"userModelPhysicsType", 2);
|
||||
return config;
|
||||
}
|
||||
|
||||
LWOOBJID BrickByBrick::ConfigObjectId(const LwoNameValue& config, const std::u16string& key) {
|
||||
const auto it = config.find(key);
|
||||
if (it == config.end() || !it->second) return LWOOBJID_EMPTY;
|
||||
return GeneralUtils::TryParse<LWOOBJID>(it->second->GetValueAsString()).value_or(LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
uint32_t BrickByBrick::InventoryToLoadInto(const uint32_t savedType) {
|
||||
if (savedType == eInventoryType::MODELS_IN_BBB) return eInventoryType::MODELS;
|
||||
if (savedType == eInventoryType::BRICKS_IN_BBB) return eInventoryType::BRICKS;
|
||||
return savedType;
|
||||
}
|
||||
|
||||
LWOOBJID BrickByBrick::LoadModel(Entity& player, const LWOOBJID itemId) {
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
if (!inventory) return LWOOBJID_EMPTY;
|
||||
|
||||
auto* item = inventory->FindItemById(itemId);
|
||||
if (!item) {
|
||||
LOG("Player %llu opened model item %llu in brick by brick, but has no such item", player.GetObjectID(), itemId);
|
||||
return LWOOBJID_EMPTY;
|
||||
}
|
||||
|
||||
const auto type = item->GetInventory()->GetType();
|
||||
if (type == eInventoryType::MODELS_IN_BBB) return itemId;
|
||||
if (type != eInventoryType::MODELS) {
|
||||
LOG("Player %llu opened item %llu from inventory %i in brick by brick", player.GetObjectID(), itemId, type);
|
||||
return LWOOBJID_EMPTY;
|
||||
}
|
||||
|
||||
return MoveKeepingId(*inventory, item, eInventoryType::MODELS_IN_BBB);
|
||||
}
|
||||
|
||||
void BrickByBrick::Save(Entity& player, const LWOOBJID localId, const std::string& sd0Data) {
|
||||
ClientPackets::BlueprintSaveResponse response;
|
||||
response.localId = localId;
|
||||
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
// Brick by brick building happens on the player's own property; the build becomes models placed there
|
||||
if (!inventory || !property || property->GetOwnerId() != player.GetObjectID()) {
|
||||
LOG("Player %llu saved a brick by brick model but is not on their own property", player.GetObjectID());
|
||||
response.reasonCode = eBlueprintSaveResponseType::PlacementFailed;
|
||||
response.Send(player.GetSystemAddress());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto models = StoreModels(player, sd0Data, std::to_string(localId));
|
||||
if (models.empty()) {
|
||||
// BBB_COULD_NOT_GENERATE_MODEL; the client keeps the build
|
||||
response.reasonCode = eBlueprintSaveResponseType::ModelGenerationFailed;
|
||||
response.Send(player.GetSystemAddress());
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Split into %zu models", models.size());
|
||||
|
||||
// As live: the bricks are settled, then the response, then the models that were opened are gone
|
||||
SettleBricks(*inventory, true);
|
||||
|
||||
response.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
|
||||
for (const auto& model : models) {
|
||||
auto& responseModel = response.models.emplace_back();
|
||||
responseModel.blueprintId = model.blueprintId;
|
||||
responseModel.data = model.sd0;
|
||||
}
|
||||
response.Send(player.GetSystemAddress());
|
||||
|
||||
for (auto* item : ItemsIn(*inventory, eInventoryType::MODELS_IN_BBB)) {
|
||||
item->SetCount(0, false, false, false, eLootSourceType::INVENTORY);
|
||||
}
|
||||
Database::Get()->DeleteBbbAutosave(player.GetObjectID());
|
||||
|
||||
for (const auto& model : models) {
|
||||
LwoNameValue config;
|
||||
config.Insert<LWOOBJID>(u"blueprintid", model.blueprintId);
|
||||
property->SpawnModel(MODEL_OBJECT_LOT, model.modelId, model.center, QuatUtils::IDENTITY, config);
|
||||
}
|
||||
property->Save();
|
||||
DashboardNotify::Changed("properties", property->GetId());
|
||||
|
||||
GameMessages::RequeryPropertyModels requery;
|
||||
requery.target = player.GetObjectID();
|
||||
requery.SendToClient(player.GetSystemAddress());
|
||||
}
|
||||
|
||||
void BrickByBrick::Autosave(Entity& player, const std::string& sd0) {
|
||||
if (IsEmptyModel(sd0)) {
|
||||
Database::Get()->DeleteBbbAutosave(player.GetObjectID());
|
||||
return;
|
||||
}
|
||||
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
IBbbAutosave::Info info;
|
||||
info.lxfml = sd0;
|
||||
if (inventory) info.sourceItems = IdsIn(*inventory, eInventoryType::MODELS_IN_BBB);
|
||||
info.updatedAt = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
Database::Get()->SetBbbAutosave(player.GetObjectID(), info);
|
||||
}
|
||||
|
||||
void BrickByBrick::ReturnModel(Entity& player, const LWOOBJID itemId, const bool hasWorldTransform, const NiPoint3& position, const NiQuaternion& rotation) {
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
if (inventory) {
|
||||
auto* item = inventory->FindItemById(itemId);
|
||||
if (item && item->GetInventory()->GetType() == eInventoryType::MODELS_IN_BBB) {
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
if (hasWorldTransform && property && property->GetOwnerId() == player.GetObjectID()) {
|
||||
property->PlaceModelFromItem(*item, position, rotation);
|
||||
property->SendModelsOnProperty();
|
||||
} else {
|
||||
MoveKeepingId(*inventory, item, eInventoryType::MODELS);
|
||||
}
|
||||
} else {
|
||||
LOG("Player %llu returned model item %llu, which is not in their brick by brick inventory", player.GetObjectID(), itemId);
|
||||
}
|
||||
}
|
||||
|
||||
// The autosave must not use up a model the player has back
|
||||
auto autosave = Database::Get()->GetBbbAutosave(player.GetObjectID());
|
||||
if (autosave && std::erase(autosave->sourceItems, itemId) != 0) Database::Get()->SetBbbAutosave(player.GetObjectID(), *autosave);
|
||||
|
||||
if (hasWorldTransform) {
|
||||
// The client waits in its saving state after sending this (BBBManager::SendUnUseBBBModel, 0x00b6b210); any
|
||||
// response with the local id it expects (none pending: 0) ends it. A code other than EverythingWorked keeps
|
||||
// the build the player is working on (EverythingWorked would clear it and leave brick mode).
|
||||
ClientPackets::BlueprintSaveResponse response;
|
||||
response.localId = LWOOBJID_EMPTY;
|
||||
response.reasonCode = eBlueprintSaveResponseType::PlacementFailed;
|
||||
response.Send(player.GetSystemAddress());
|
||||
}
|
||||
}
|
||||
|
||||
void BrickByBrick::MoveBricks(Entity& player, const GameMessages::MoveInventoryBatch& move) {
|
||||
const auto isBrickBag = [](const eInventoryType type) { return type == eInventoryType::BRICKS || type == eInventoryType::BRICKS_IN_BBB; };
|
||||
if (!isBrickBag(move.srcBag) || !isBrickBag(move.dstBag) || move.srcBag == move.dstBag || move.moveLOT == LOT_NULL || move.count == 0) {
|
||||
LOG_DEBUG("Ignoring MoveInventoryBatch from %i to %i of %i", move.srcBag, move.dstBag, move.moveLOT);
|
||||
return;
|
||||
}
|
||||
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
if (!inventory) return;
|
||||
auto* source = inventory->GetInventory(move.srcBag);
|
||||
if (!source) return;
|
||||
|
||||
const auto available = source->GetLotCount(move.moveLOT);
|
||||
const auto toMove = std::min(move.count, available);
|
||||
if (toMove == 0 || (!move.bAllowPartial && toMove < move.count)) {
|
||||
LOG("Player %llu moved %u of %i between brick inventories but has %u", player.GetObjectID(), move.count, move.moveLOT, available);
|
||||
return;
|
||||
}
|
||||
|
||||
EconomyLedger::ScopedItemTransfer moving;
|
||||
// The client already took them out of the source bag (0x00ce1310), so that side is silent
|
||||
Item* item = move.startObjectID != LWOOBJID_EMPTY ? inventory->FindItemById(move.startObjectID) : nullptr;
|
||||
uint32_t left = toMove;
|
||||
while (left > 0) {
|
||||
if (!item || item->GetInventory() != source || item->GetLot() != move.moveLOT) item = source->FindItemByLot(move.moveLOT);
|
||||
if (!item) break;
|
||||
const auto delta = std::min(item->GetCount(), left);
|
||||
left -= delta;
|
||||
item->SetCount(item->GetCount() - delta, true, false);
|
||||
item = nullptr;
|
||||
}
|
||||
|
||||
const auto moved = toMove - left;
|
||||
if (moved == 0) return;
|
||||
inventory->ReceiveItem(LWOOBJID_EMPTY, move.moveLOT, moved, eLootSourceType::RELOCATE, {}, LWOOBJID_EMPTY, false,
|
||||
{ .inventory = move.dstBag, .showFlyingLoot = move.showFlyingLoot, .sourceInventory = move.srcBag });
|
||||
}
|
||||
|
||||
void BrickByBrick::EndSession(Entity& player) {
|
||||
RecoverUnfinishedBuild(player);
|
||||
}
|
||||
|
||||
void BrickByBrick::OnPlayerLoaded(Entity& player) {
|
||||
RecoverUnfinishedBuild(player);
|
||||
}
|
||||
|
||||
uint32_t BrickByBrick::RecoverUnfinishedBuild(Entity& player) {
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
if (!inventory) return 0;
|
||||
|
||||
const auto autosave = Database::Get()->GetBbbAutosave(player.GetObjectID());
|
||||
auto recovery = PlanRecovery(IdsIn(*inventory, eInventoryType::MODELS_IN_BBB), autosave);
|
||||
const bool hadBricks = !ItemsIn(*inventory, eInventoryType::BRICKS_IN_BBB).empty();
|
||||
if (!autosave && recovery.giveBack.empty() && !hadBricks) return 0;
|
||||
|
||||
uint32_t rebuilt = 0;
|
||||
if (recovery.rebuild) {
|
||||
for (const auto& model : StoreModels(player, autosave->lxfml, "autosave_" + std::to_string(player.GetObjectID()))) {
|
||||
inventory->ReceiveItem(LWOOBJID_EMPTY, MODEL_ITEM_LOT, 1, eLootSourceType::INVENTORY, ModelItemConfig(model.blueprintId, model.modelId), model.modelId, false,
|
||||
{ .inventory = eInventoryType::MODELS, .showFlyingLoot = false });
|
||||
rebuilt++;
|
||||
}
|
||||
}
|
||||
|
||||
if (rebuilt > 0) {
|
||||
// The rebuilt models replace the ones that were open when the autosave was made (they may already be back in
|
||||
// MODELS after a reload)
|
||||
for (const auto id : recovery.consume) {
|
||||
auto* item = inventory->FindItemById(id);
|
||||
if (item) item->SetCount(0, false, false, false, eLootSourceType::INVENTORY);
|
||||
}
|
||||
} else {
|
||||
for (const auto id : recovery.consume) {
|
||||
if (std::ranges::find(recovery.giveBack, id) == recovery.giveBack.end()) recovery.giveBack.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto id : recovery.giveBack) {
|
||||
auto* item = inventory->FindItemById(id);
|
||||
if (item && item->GetInventory()->GetType() == eInventoryType::MODELS_IN_BBB) MoveKeepingId(*inventory, item, eInventoryType::MODELS);
|
||||
}
|
||||
|
||||
SettleBricks(*inventory, rebuilt > 0);
|
||||
if (autosave) Database::Get()->DeleteBbbAutosave(player.GetObjectID());
|
||||
|
||||
LOG("Recovered the brick by brick build of %llu: %u models rebuilt, %zu models given back", player.GetObjectID(), rebuilt, recovery.giveBack.size());
|
||||
if (rebuilt > 0) {
|
||||
GameMessages::RebuildBBBAutosaveMsg message;
|
||||
message.target = player.GetObjectID();
|
||||
message.count = static_cast<int32_t>(rebuilt);
|
||||
message.SendToClient(player.GetSystemAddress());
|
||||
}
|
||||
return rebuilt;
|
||||
}
|
||||
106
dGame/dUtilities/BrickByBrick.h
Normal file
106
dGame/dUtilities/BrickByBrick.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#ifndef BRICKBYBRICK_H
|
||||
#define BRICKBYBRICK_H
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "IBbbAutosave.h"
|
||||
#include "LDFFormat.h"
|
||||
#include "NiPoint3.h"
|
||||
#include "NiQuaternion.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class Entity;
|
||||
class InventoryComponent;
|
||||
|
||||
namespace GameMessages {
|
||||
struct MoveInventoryBatch;
|
||||
}
|
||||
|
||||
// Brick by brick building and the model items it works on, as the 1.10.64 client expects them (docs/BuildWorkflow.md).
|
||||
//
|
||||
// While a player builds, MODELS_IN_BBB holds the models they opened (their original items) and BRICKS_IN_BBB the bricks
|
||||
// they took out of their backpack. A save turns the build into new models on the property and uses up the originals.
|
||||
// Anything else that ends a build (leaving without saving, a disconnect or a crash) goes through RecoverUnfinishedBuild:
|
||||
// the client's last autosave is rebuilt into models, or the originals go back to MODELS, so a model is never lost.
|
||||
namespace BrickByBrick {
|
||||
// A brick built model as an item (in MODELS, VAULT_MODELS or MODELS_IN_BBB)
|
||||
constexpr LOT MODEL_ITEM_LOT = 6662;
|
||||
// A brick built model placed in the world
|
||||
constexpr LOT MODEL_OBJECT_LOT = 14;
|
||||
|
||||
// DeleteModelFromClient's reason
|
||||
enum class eDeleteReason : int32_t {
|
||||
PICKING_MODEL_UP = 0, // into MODELS, equipped to carry it
|
||||
RETURNING_MODEL_TO_INVENTORY, // into MODELS
|
||||
BREAKING_MODEL_APART, // into MODELS; the client then opens it with BBBLoadItemRequest
|
||||
};
|
||||
|
||||
// PlaceModelResponse's response
|
||||
constexpr int32_t PLACE_MODEL_PLACED = 14;
|
||||
constexpr int32_t PLACE_MODEL_REMOVED = 16;
|
||||
|
||||
// ---- Rules with no game state (unit tested) ----
|
||||
|
||||
// Whether a SetBBBAutosave / BBBSaveRequest payload holds no model: nothing, or only the sd0 header the client sends
|
||||
// to clear its autosave
|
||||
bool IsEmptyModel(std::string_view sd0);
|
||||
|
||||
// What taking a model off the property does with it
|
||||
struct ModelRemoval {
|
||||
bool equip{}; // the player carries it
|
||||
bool notifyPostDelete{}; // HandleUGCEquipPostDeleteBasedOnEditMode tells the client which item it became
|
||||
};
|
||||
ModelRemoval PlanModelRemoval(int32_t reason);
|
||||
|
||||
// What ending a build without a save does
|
||||
struct Recovery {
|
||||
bool rebuild{}; // the autosave is rebuilt into models
|
||||
std::vector<LWOOBJID> consume; // originals the rebuilt models replace (only once a model was rebuilt)
|
||||
std::vector<LWOOBJID> giveBack; // MODELS_IN_BBB items that go back to MODELS
|
||||
};
|
||||
Recovery PlanRecovery(const std::vector<LWOOBJID>& modelsInBbb, const std::optional<IBbbAutosave::Info>& autosave);
|
||||
|
||||
// The config of a brick built model item (6662) and of its placed object (14)
|
||||
LwoNameValue ModelItemConfig(LWOOBJID blueprintId, LWOOBJID userModelId, const std::string& behaviors = "");
|
||||
|
||||
// An object id kept in a config (blueprintid, userModelID), or LWOOBJID_EMPTY
|
||||
LWOOBJID ConfigObjectId(const LwoNameValue& config, const std::u16string& key);
|
||||
|
||||
// ---- Handlers ----
|
||||
|
||||
// BBBLoadItemRequest: moves the model item from MODELS to MODELS_IN_BBB keeping its object id (as live did).
|
||||
// Returns the id in MODELS_IN_BBB, or LWOOBJID_EMPTY when the player has no such model.
|
||||
LWOOBJID LoadModel(Entity& player, LWOOBJID itemId);
|
||||
|
||||
// BBBSaveRequest
|
||||
void Save(Entity& player, LWOOBJID localId, const std::string& sd0);
|
||||
|
||||
// SetBBBAutosave: the client's quick save
|
||||
void Autosave(Entity& player, const std::string& sd0);
|
||||
|
||||
// UnUseBBBModel: a model the player opened goes back where it came from (the import tool's undo, or a model that
|
||||
// could not be loaded). With a world transform it goes back on the property there, otherwise to MODELS.
|
||||
void ReturnModel(Entity& player, LWOOBJID itemId, bool hasWorldTransform, const NiPoint3& position, const NiQuaternion& rotation);
|
||||
|
||||
// MoveInventoryBatch between BRICKS and BRICKS_IN_BBB
|
||||
void MoveBricks(Entity& player, const GameMessages::MoveInventoryBatch& move);
|
||||
|
||||
// ActivateBrickMode leaving brick mode
|
||||
void EndSession(Entity& player);
|
||||
|
||||
// PlayerLoaded
|
||||
void OnPlayerLoaded(Entity& player);
|
||||
|
||||
// Resolves a build that ended without a save; returns how many models were rebuilt from the autosave
|
||||
uint32_t RecoverUnfinishedBuild(Entity& player);
|
||||
|
||||
// Whether an inventory bag saved as a BBB bag loads into the normal one (MODELS_IN_BBB -> MODELS,
|
||||
// BRICKS_IN_BBB -> BRICKS): a build never survives a reload, its items do.
|
||||
uint32_t InventoryToLoadInto(uint32_t savedType);
|
||||
};
|
||||
|
||||
#endif //!BRICKBYBRICK_H
|
||||
@@ -1,4 +1,5 @@
|
||||
set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
|
||||
"BrickByBrick.cpp"
|
||||
"ChatServerLink.cpp"
|
||||
"CheatDetection.cpp"
|
||||
"DashboardActions.cpp"
|
||||
|
||||
@@ -105,3 +105,6 @@ hardcore_coin_keep=
|
||||
|
||||
# save pre-split lxfmls to disk for debugging
|
||||
save_lxfmls=0
|
||||
|
||||
# 1: saving a brick by brick model uses up the bricks in it, as live did. 0: they go back to the backpack.
|
||||
bbb_consume_bricks=0
|
||||
|
||||
121
tests/dGameTests/BrickByBrickTests.cpp
Normal file
121
tests/dGameTests/BrickByBrickTests.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "BrickByBrick.h"
|
||||
#include "BbbAutosaveItems.h"
|
||||
#include "Database.h"
|
||||
#include "Entity.h"
|
||||
#include "GameDependencies.h"
|
||||
#include "eInventoryType.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// The brick by brick workflow's rules (docs/BuildWorkflow.md): what happens to models and bricks at each step.
|
||||
class BrickByBrickTests : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override { SetUpDependencies(); }
|
||||
void TearDown() override { TearDownDependencies(); }
|
||||
};
|
||||
|
||||
namespace {
|
||||
const std::string HEADER_ONLY("sd0\x01\xff", 5);
|
||||
const std::string ONE_CHUNK("sd0\x01\xff\x02\x00\x00\x00\x78\x9c", 11);
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, EmptyModelIsNothingOrTheBareHeader) {
|
||||
EXPECT_TRUE(BrickByBrick::IsEmptyModel(""));
|
||||
EXPECT_TRUE(BrickByBrick::IsEmptyModel(HEADER_ONLY));
|
||||
EXPECT_FALSE(BrickByBrick::IsEmptyModel(ONE_CHUNK));
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, TakingAModelOffThePropertyPutsItInModels) {
|
||||
using enum BrickByBrick::eDeleteReason;
|
||||
const auto pickUp = BrickByBrick::PlanModelRemoval(static_cast<int32_t>(PICKING_MODEL_UP));
|
||||
EXPECT_TRUE(pickUp.equip);
|
||||
EXPECT_TRUE(pickUp.notifyPostDelete);
|
||||
|
||||
const auto putAway = BrickByBrick::PlanModelRemoval(static_cast<int32_t>(RETURNING_MODEL_TO_INVENTORY));
|
||||
EXPECT_FALSE(putAway.equip);
|
||||
EXPECT_FALSE(putAway.notifyPostDelete);
|
||||
|
||||
// Taking a model apart: into MODELS, not carried; the client then opens it with BBBLoadItemRequest
|
||||
const auto apart = BrickByBrick::PlanModelRemoval(static_cast<int32_t>(BREAKING_MODEL_APART));
|
||||
EXPECT_FALSE(apart.equip);
|
||||
EXPECT_TRUE(apart.notifyPostDelete);
|
||||
|
||||
const auto unknown = BrickByBrick::PlanModelRemoval(7);
|
||||
EXPECT_FALSE(unknown.equip);
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, LeavingWithoutAnAutosaveGivesEveryModelBack) {
|
||||
const auto recovery = BrickByBrick::PlanRecovery({ 1, 2 }, std::nullopt);
|
||||
EXPECT_FALSE(recovery.rebuild);
|
||||
EXPECT_TRUE(recovery.consume.empty());
|
||||
EXPECT_EQ(recovery.giveBack, (std::vector<LWOOBJID>{ 1, 2 }));
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, AnAutosaveReplacesTheModelsItWasMadeFrom) {
|
||||
IBbbAutosave::Info autosave{ ONE_CHUNK, { 2, 3 }, 0 };
|
||||
const auto recovery = BrickByBrick::PlanRecovery({ 1, 2 }, autosave);
|
||||
EXPECT_TRUE(recovery.rebuild);
|
||||
// 3 is no longer in the BBB inventory (e.g. back in MODELS after a reload) but still made the autosave
|
||||
EXPECT_EQ(recovery.consume, (std::vector<LWOOBJID>{ 2, 3 }));
|
||||
// 1 was opened after the autosave: it goes back
|
||||
EXPECT_EQ(recovery.giveBack, (std::vector<LWOOBJID>{ 1 }));
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, AnEmptyAutosaveRebuildsNothing) {
|
||||
IBbbAutosave::Info autosave{ HEADER_ONLY, { 1 }, 0 };
|
||||
const auto recovery = BrickByBrick::PlanRecovery({ 1 }, autosave);
|
||||
EXPECT_FALSE(recovery.rebuild);
|
||||
EXPECT_TRUE(recovery.consume.empty());
|
||||
EXPECT_EQ(recovery.giveBack, (std::vector<LWOOBJID>{ 1 }));
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, BuildInventoriesLoadIntoTheNormalOnes) {
|
||||
EXPECT_EQ(BrickByBrick::InventoryToLoadInto(eInventoryType::MODELS_IN_BBB), eInventoryType::MODELS);
|
||||
EXPECT_EQ(BrickByBrick::InventoryToLoadInto(eInventoryType::BRICKS_IN_BBB), eInventoryType::BRICKS);
|
||||
for (const auto type : { eInventoryType::ITEMS, eInventoryType::BRICKS, eInventoryType::MODELS, eInventoryType::VAULT_MODELS, eInventoryType::TEMP_MODELS }) {
|
||||
EXPECT_EQ(BrickByBrick::InventoryToLoadInto(type), type);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, ModelItemConfigKeepsTheModelsIds) {
|
||||
const auto config = BrickByBrick::ModelItemConfig(1152921508340227662, 1152921510759098851, "10447,0,0,0,0");
|
||||
EXPECT_EQ(BrickByBrick::ConfigObjectId(config, u"blueprintid"), 1152921508340227662);
|
||||
EXPECT_EQ(BrickByBrick::ConfigObjectId(config, u"userModelID"), 1152921510759098851);
|
||||
EXPECT_EQ(config.find(u"userModelBehaviors")->second->GetValueAsString(), "10447,0,0,0,0");
|
||||
EXPECT_EQ(config.find(u"userModelHasBhvr")->second->GetValueAsString(), "1");
|
||||
EXPECT_EQ(config.find(u"userModelPhysicsType")->second->GetValueAsString(), "2");
|
||||
|
||||
const auto plain = BrickByBrick::ModelItemConfig(5, 6);
|
||||
EXPECT_EQ(plain.find(u"userModelHasBhvr")->second->GetValueAsString(), "0");
|
||||
EXPECT_EQ(plain.find(u"userModelBehaviors")->second->GetValueAsString(), "0,0,0,0,0");
|
||||
EXPECT_EQ(BrickByBrick::ConfigObjectId(plain, u"missing"), LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, AutosaveItemsListRoundTrips) {
|
||||
const std::vector<LWOOBJID> items = { 1152921510759098815, 2, 3 };
|
||||
EXPECT_EQ(BbbAutosaveItems::Join(items), "1152921510759098815,2,3");
|
||||
EXPECT_EQ(BbbAutosaveItems::Parse(BbbAutosaveItems::Join(items)), items);
|
||||
EXPECT_TRUE(BbbAutosaveItems::Parse("").empty());
|
||||
EXPECT_EQ(BbbAutosaveItems::Parse("4,,x,5"), (std::vector<LWOOBJID>{ 4, 5 }));
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, QuickSaveIsKeptUntilClearedWithTheBareHeader) {
|
||||
Entity player(1152921510000000001, info);
|
||||
BrickByBrick::Autosave(player, ONE_CHUNK);
|
||||
const auto saved = Database::Get()->GetBbbAutosave(player.GetObjectID());
|
||||
ASSERT_TRUE(saved.has_value());
|
||||
EXPECT_EQ(saved->lxfml, ONE_CHUNK);
|
||||
EXPECT_GT(saved->updatedAt, 0);
|
||||
|
||||
BrickByBrick::Autosave(player, HEADER_ONLY);
|
||||
EXPECT_FALSE(Database::Get()->GetBbbAutosave(player.GetObjectID()).has_value());
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, AReturnedModelIsNeverUsedUpByTheAutosave) {
|
||||
Entity player(1152921510000000002, info);
|
||||
Database::Get()->SetBbbAutosave(player.GetObjectID(), { ONE_CHUNK, { 7, 8 }, 1 });
|
||||
BrickByBrick::ReturnModel(player, 7, false, NiPoint3Constant::ZERO, QuatUtils::IDENTITY);
|
||||
const auto saved = Database::Get()->GetBbbAutosave(player.GetObjectID());
|
||||
ASSERT_TRUE(saved.has_value());
|
||||
EXPECT_EQ(saved->sourceItems, (std::vector<LWOOBJID>{ 8 }));
|
||||
}
|
||||
@@ -11,6 +11,7 @@ set(DGAMETEST_SOURCES
|
||||
"CollisionFilterTests.cpp"
|
||||
"ItemSetPassiveAbilityTests.cpp"
|
||||
"PowerUpStatisticTests.cpp"
|
||||
"BrickByBrickTests.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(dComponentsTests)
|
||||
|
||||
@@ -386,3 +386,52 @@ TEST_F(BuildingMessagesTests, GoldenBytes) {
|
||||
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));
|
||||
}
|
||||
|
||||
// Messages added for the brick by brick workflow (docs/BuildWorkflow.md), laid out as the 1.10.64 client reads them.
|
||||
TEST_F(BuildingMessagesTests, BrickModeMessagesRoundTrip) {
|
||||
GameMessages::ActivateBrickMode enter;
|
||||
enter.buildObjectID = 0x1122334455667788;
|
||||
enter.enterBuildFromWorld = false;
|
||||
enter.enterFlag = true;
|
||||
// flag 1 + the build area, flag 0 (build type 2, on a property), 0, 1 (0x00d8ecb0)
|
||||
EXPECT_PACKET_EQ(FromHex("c4 3b b3 2a a2 19 91 08 90", 68), Payload(enter));
|
||||
const auto enterCopy = RoundTrip(enter);
|
||||
EXPECT_EQ(enterCopy.buildObjectID, enter.buildObjectID);
|
||||
EXPECT_EQ(enterCopy.buildType, 2);
|
||||
EXPECT_FALSE(enterCopy.enterBuildFromWorld);
|
||||
EXPECT_TRUE(enterCopy.enterFlag);
|
||||
ExpectTruncatedFails(enter);
|
||||
|
||||
GameMessages::ActivateBrickMode leave;
|
||||
leave.buildType = 1;
|
||||
leave.enterBuildFromWorld = true;
|
||||
leave.enterFlag = false;
|
||||
const auto leaveCopy = RoundTrip(leave);
|
||||
EXPECT_EQ(leaveCopy.buildObjectID, LWOOBJID_EMPTY);
|
||||
EXPECT_EQ(leaveCopy.buildType, 1);
|
||||
EXPECT_FALSE(leaveCopy.enterFlag);
|
||||
|
||||
// The client clears its autosave with the bare sd0 header: u32 size, then the bytes (0x00f2af60)
|
||||
GameMessages::SetBBBAutosave clear;
|
||||
clear.lxfmlDataCompressed = std::string("sd0\x01\xff", 5);
|
||||
EXPECT_PACKET_EQ(FromHex("05 00 00 00 73 64 30 01 ff"), Payload(clear));
|
||||
EXPECT_EQ(RoundTrip(clear).lxfmlDataCompressed, clear.lxfmlDataCompressed);
|
||||
ExpectTruncatedFails(clear);
|
||||
|
||||
GameMessages::SetBBBAutosave empty;
|
||||
EXPECT_PACKET_EQ(FromHex("00 00 00 00"), Payload(empty));
|
||||
EXPECT_TRUE(RoundTrip(empty).lxfmlDataCompressed.empty());
|
||||
|
||||
// A size larger than what is left is dropped rather than read past the end
|
||||
RakNet::BitStream tooLong;
|
||||
tooLong.Write<uint32_t>(100);
|
||||
tooLong.Write<uint8_t>(1);
|
||||
GameMessages::SetBBBAutosave read;
|
||||
EXPECT_FALSE(read.Deserialize(tooLong));
|
||||
|
||||
GameMessages::RebuildBBBAutosaveMsg rebuilt;
|
||||
rebuilt.count = 2;
|
||||
EXPECT_PACKET_EQ(FromHex("02 00 00 00"), Payload(rebuilt));
|
||||
EXPECT_EQ(RoundTrip(rebuilt).count, 2);
|
||||
ExpectTruncatedFails(rebuilt);
|
||||
}
|
||||
|
||||
@@ -619,3 +619,9 @@ TEST_F(PropertyMessagesTests, GoldenBytes) {
|
||||
enter.index = -1;
|
||||
EXPECT_PACKET_EQ(FromHex("ff ff ff ff 80", 33), Payload(enter));
|
||||
}
|
||||
|
||||
// A live server's RequeryPropertyModels (after a brick by brick save) had no payload
|
||||
TEST_F(PropertyMessagesTests, RequeryPropertyModelsHasNoPayload) {
|
||||
GameMessages::RequeryPropertyModels requery;
|
||||
EXPECT_EQ(Payload(requery).bits, 0u);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user