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:
Aaron Kimbrell
2026-09-27 04:21:48 -05:00
parent 9256b31b23
commit 6ce261c58c
19 changed files with 976 additions and 451 deletions

View File

@@ -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.");

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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>(); } },

View File

@@ -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);

View File

@@ -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{};

View File

@@ -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 {

View File

@@ -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

View 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;
}

View 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

View File

@@ -1,4 +1,5 @@
set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
"BrickByBrick.cpp"
"ChatServerLink.cpp"
"CheatDetection.cpp"
"DashboardActions.cpp"

View File

@@ -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

View 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 }));
}

View File

@@ -11,6 +11,7 @@ set(DGAMETEST_SOURCES
"CollisionFilterTests.cpp"
"ItemSetPassiveAbilityTests.cpp"
"PowerUpStatisticTests.cpp"
"BrickByBrickTests.cpp"
)
add_subdirectory(dComponentsTests)

View File

@@ -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);
}

View File

@@ -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);
}