#include "PropertyManagementComponent.h" #include "PropertyRent.h" #include "ChatPackets.h" #include "DashboardNotify.h" #include #include "MissionComponent.h" #include "EntityManager.h" #include "PropertyMessages.h" #include "CDClientManager.h" #include "CDPropertyTemplateTable.h" #include "UserManager.h" #include "GameMessages.h" #include "Character.h" #include "CDClientDatabase.h" #include "dZoneManager.h" #include "Game.h" #include "Item.h" #include "Database.h" #include "ObjectIDManager.h" #include "RocketLaunchpadControlComponent.h" #include "PropertyEntranceComponent.h" #include "InventoryComponent.h" #include "eMissionTaskType.h" #include "eObjectBits.h" #include "CharacterComponent.h" #include "PlayerManager.h" #include "ModelComponent.h" #include #include "CppScripts.h" #include #include "dConfig.h" #include "BrickByBrick.h" #include "eLootSourceType.h" #include "eKillType.h" #include "PropertyBuilders.h" #include "BuildingMessages.h" PropertyManagementComponent* PropertyManagementComponent::instance = nullptr; PropertyManagementComponent::PropertyManagementComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { this->owner = LWOOBJID_EMPTY; this->templateId = 0; this->propertyId = LWOOBJID_EMPTY; this->models = {}; this->propertyName = ""; this->propertyDescription = ""; this->privacyOption = PropertyPrivacyOption::Private; this->originalPrivacyOption = PropertyPrivacyOption::Private; instance = this; const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); const auto zoneId = worldId.GetMapID(); const auto cloneId = worldId.GetCloneID(); auto query = CDClientDatabase::CreatePreppedStmt("SELECT id FROM PropertyTemplate WHERE mapID = ?;"); query.bind(1, static_cast(zoneId)); auto result = query.execQuery(); if (result.eof() || result.fieldIsNull("id")) { return; } templateId = result.getIntField("id"); auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); if (propertyInfo) { this->propertyId = propertyInfo->id; this->owner = propertyInfo->ownerId; GeneralUtils::SetBit(this->owner, eObjectBits::CHARACTER); this->clone_Id = propertyInfo->cloneId; this->propertyName = propertyInfo->name; this->propertyDescription = propertyInfo->description; this->privacyOption = static_cast(propertyInfo->privacyOption); this->rejectionReason = propertyInfo->rejectionReason; this->moderatorRequested = propertyInfo->modApproved == 0 && rejectionReason == "" && privacyOption == PropertyPrivacyOption::Public; this->LastUpdatedTime = propertyInfo->lastUpdatedTime; this->claimedTime = propertyInfo->claimedTime; this->reputation = propertyInfo->reputation; // Rent that went unpaid while the owner was away makes the property private if (privacyOption != PropertyPrivacyOption::Private && PropertyRent::IsOverdue(propertyId, zoneId)) { privacyOption = PropertyPrivacyOption::Private; Database::Get()->SetPropertyPrivacy(propertyId, static_cast(privacyOption)); LOG("Property %llu is private: its rent is unpaid", propertyId); } Load(); } // Turning property_bff_build on or off changes who can build right away static bool configHandlerAdded = false; if (!configHandlerAdded && Game::config) { Game::config->AddConfigHandler([]() { if (instance) instance->UpdateBuildRights(); }); configHandlerAdded = true; } } PropertyManagementComponent::~PropertyManagementComponent() { if (instance == this) instance = nullptr; } bool PropertyManagementComponent::IsBestFriend(const LWOOBJID player) const { if (owner == LWOOBJID_EMPTY || player == owner) return false; const auto cached = bestFriends.find(player); if (cached != bestFriends.end()) return cached->second; const auto status = Database::Get()->GetBestFriendStatus(player, owner); const bool isBestFriend = status && status->bestFriendStatus == 3; bestFriends[player] = isBestFriend; return isBestFriend; } bool PropertyManagementComponent::CanBuild(const Entity& player) const { // Handed off to a new instance: building happens there if (frozen) return false; const bool bestFriendsBuild = PropertyBuilders::BestFriendsBuild(); PropertyBuilders::Player state{ .id = player.GetObjectID(), .isBuilding = builders.contains(player.GetObjectID()) }; // Only a best friend needs the lookup if (bestFriendsBuild && state.id != owner) state.isBestFriend = IsBestFriend(state.id); return PropertyBuilders::CanBuild(state, owner, builders.contains(owner), bestFriendsBuild); } void PropertyManagementComponent::OnPlayerLoaded(Entity& player) { IsBestFriend(player.GetObjectID()); } void PropertyManagementComponent::OnPlayerRemoved(Entity& player) { const auto id = player.GetObjectID(); const bool wasBuilding = EndBuilding(player); sentBuildRights.erase(id); bestFriends.erase(id); // The owner leaving build mode stops best friends who aren't building from joining if (wasBuilding) UpdateBuildRights(); } void PropertyManagementComponent::UpdateBuildRights() { for (auto* player : PlayerManager::GetAllPlayers()) { if (!player) continue; const auto id = player->GetObjectID(); const auto sent = sentBuildRights.find(id); // Never sent the property's data: the client has no rights to take away const bool couldBuild = sent != sentBuildRights.end() && sent->second; const bool canBuild = CanBuild(*player); if (couldBuild == canBuild) continue; if (!canBuild && builders.contains(id)) { // Out of build mode, as when they leave it themselves GameMessages::SetBuildModeConfirmed confirmed; confirmed.target = id; confirmed.start = false; confirmed.warnVisitors = true; confirmed.playerId = id; confirmed.startPos = player->GetPosition(); confirmed.Send(player->GetSystemAddress()); if (player->GetCharacter()) player->GetCharacter()->SetBuildMode(false); EndBuilding(*player); Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditEnd(); ChatPackets::SendSystemMessage(player->GetSystemAddress(), u"You can't build on this property any more."); } OnQueryPropertyData(player, player->GetSystemAddress()); } } LWOOBJID PropertyManagementComponent::GetOwnerId() const { return owner; } Entity* PropertyManagementComponent::GetOwner() const { return Game::entityManager->GetEntity(owner); } void PropertyManagementComponent::SetOwner(Entity* value) { SetOwnerId(value->GetObjectID()); } std::vector PropertyManagementComponent::GetPaths() const { const auto zoneId = Game::zoneManager->GetZone()->GetWorldID(); auto query = CDClientDatabase::CreatePreppedStmt( "SELECT path FROM PropertyTemplate WHERE mapID = ?;"); query.bind(1, static_cast(zoneId)); auto result = query.execQuery(); std::vector paths{}; if (result.eof()) { return paths; } std::vector points; for (const auto& str : GeneralUtils::SplitString(result.getStringField("path"), ' ')) { const auto value = GeneralUtils::TryParse(str); if (value) points.push_back(value.value()); } for (auto i = 0u; i + 2 < points.size(); i += 3) { paths.emplace_back(points[i], points[i + 1], points[i + 2]); } return paths; } PropertyPrivacyOption PropertyManagementComponent::GetPrivacyOption() const { return privacyOption; } void PropertyManagementComponent::SetPrivacyOption(PropertyPrivacyOption value) { if (owner == LWOOBJID_EMPTY) return; if (value == static_cast(3)) // Client sends 3 for private for some reason, but expects 0 in return? { value = PropertyPrivacyOption::Private; } // Unpaid rent keeps the property private, like live if (value != PropertyPrivacyOption::Private && PropertyRent::IsOverdue(propertyId, Game::zoneManager->GetZoneID().GetMapID())) { value = PropertyPrivacyOption::Private; auto* ownerEntity = GetOwner(); if (ownerEntity) ChatPackets::SendSystemMessage(ownerEntity->GetSystemAddress(), u"Your property's rent is unpaid, so it stays private until the rent is paid."); } if (value == PropertyPrivacyOption::Public && privacyOption != PropertyPrivacyOption::Public) { rejectionReason = ""; moderatorRequested = true; } privacyOption = value; IProperty::Info info; info.id = propertyId; info.privacyOption = static_cast(privacyOption); info.rejectionReason = rejectionReason; info.modApproved = 0; if (models.empty() && Game::config->GetValue("auto_reject_empty_properties") == "1") { UpdateApprovedStatus(false, "Your property is empty. Please place a model to have a public property."); } else { Database::Get()->UpdatePropertyModerationInfo(info); DashboardNotify::Changed("properties", propertyId); } } void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::string description) { if (owner == LWOOBJID_EMPTY) return; propertyName = name; propertyDescription = description; IProperty::Info info; info.id = propertyId; info.name = propertyName; info.description = propertyDescription; info.lastUpdatedTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); Database::Get()->UpdateLastSave(info); Database::Get()->UpdatePropertyDetails(info); DashboardNotify::Changed("properties", propertyId); OnQueryPropertyData(nullptr, UNASSIGNED_SYSTEM_ADDRESS); } bool PropertyManagementComponent::Claim(const LWOOBJID playerId) { if (owner != LWOOBJID_EMPTY || frozen) { return false; } auto* entity = Game::entityManager->GetEntity(playerId); auto character = entity->GetCharacter(); if (!character) return false; auto* zone = Game::zoneManager->GetZone(); const auto& worldId = zone->GetZoneID(); const auto propertyZoneId = worldId.GetMapID(); const auto propertyCloneId = worldId.GetCloneID(); const auto playerCloneId = character->GetPropertyCloneID(); // If we are not on our clone do not allow us to claim the property if (propertyCloneId != playerCloneId) return false; std::string name = zone->GetZoneName(); std::string description = ""; auto prop_path = zone->GetPath(m_Parent->GetVarAsString(u"propertyName")); if (prop_path) { if (!prop_path->property.displayName.empty()) name = prop_path->property.displayName; description = prop_path->property.displayDesc; } SetOwnerId(playerId); // Due to legacy IDs being random propertyId = ObjectIDManager::GetPersistentID(); const uint32_t maxTries = 100; uint32_t tries = 0; while (Database::Get()->GetPropertyInfo(propertyId) && tries < maxTries) { tries++; LOG("Found a duplicate property %llu, getting a new propertyId", propertyId); propertyId = ObjectIDManager::GetPersistentID(); } IProperty::Info info; info.id = propertyId; info.ownerId = character->GetID(); info.cloneId = playerCloneId; info.name = name; info.description = description; Database::Get()->InsertNewProperty(info, templateId, worldId); DashboardNotify::Changed("properties", propertyId); auto* zoneControlObject = Game::zoneManager->GetZoneControlObject(); if (zoneControlObject) zoneControlObject->GetScript()->OnZonePropertyRented(zoneControlObject, entity); return true; } void PropertyManagementComponent::OnStartBuilding(Entity& builder) { const bool first = builders.empty(); if (!builders.insert(builder.GetObjectID()).second) return; // The owner starting lets their best friends join if (builder.GetObjectID() == owner) UpdateBuildRights(); auto inventoryComponent = builder.GetComponent(); // Push equipped items if (inventoryComponent) inventoryComponent->PushEquippedItems(); // Someone else is already building: the property is already private and paused if (!first) return; const auto players = PlayerManager::GetAllPlayers(); LWOMAPID zoneId = 1100; const auto entrance = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::PROPERTY_ENTRANCE); originalPrivacyOption = privacyOption; SetPrivacyOption(PropertyPrivacyOption::Private); // Cant visit player which is building if (!entrance.empty()) { auto* rocketPad = entrance[0]->GetComponent(); if (rocketPad != nullptr) { zoneId = rocketPad->GetDefaultZone(); } } // Everyone who can't build here leaves for (auto* player : players) { if (!player || CanBuild(*player)) continue; auto* characterComponent = player->GetComponent(); if (characterComponent) characterComponent->SendToZone(zoneId); } for (auto modelID : models | std::views::keys) { auto* model = Game::entityManager->GetEntity(modelID); if (model) { auto* modelComponent = model->GetComponent(); if (modelComponent) modelComponent->Pause(); Game::entityManager->SerializeEntity(model); GameMessages::ResetModelToDefaults reset; reset.target = modelID; model->HandleMsg(reset); } } for (auto* const entity : Game::entityManager->GetEntitiesInGroup("SpawnedPropertyEnemies")) { if (entity) entity->Smash(); } } void PropertyManagementComponent::OnFinishBuilding(const Entity& builder) { // A best friend who left can't come back until the owner builds; the owner leaving stops the others joining if (EndBuilding(builder)) UpdateBuildRights(); } bool PropertyManagementComponent::EndBuilding(const Entity& builder) { if (builders.erase(builder.GetObjectID()) == 0) return false; // Others are still building: keep the property private and paused if (!builders.empty()) { Save(); return true; } UpdateApprovedStatus(false); SetPrivacyOption(originalPrivacyOption); Save(); for (auto modelID : models | std::views::keys) { auto* model = Game::entityManager->GetEntity(modelID); if (model) { auto* modelComponent = model->GetComponent(); if (modelComponent) modelComponent->Resume(); Game::entityManager->SerializeEntity(model); GameMessages::ResetModelToDefaults reset; reset.target = modelID; model->HandleMsg(reset); } } for (auto* const entity : Game::entityManager->GetEntitiesInGroup("SpawnedPropertyEnemies")) { if (entity) entity->Smash(); } return true; } Entity* PropertyManagementComponent::SpawnModel(const LOT lot, const LWOOBJID modelId, const NiPoint3& position, const NiQuaternion& rotation, const LwoNameValue& config, const LWOOBJID placer) { 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(u"userModelID", modelId); node->config.Insert(u"modelType", 2); node->config.Insert(u"propertyObjectID", true); node->config.Insert(u"componentWhitelist", 1); if (lot != BrickByBrick::MODEL_OBJECT_LOT) node->config.Insert(u"modelBehaviors", 0); SpawnerInfo info{}; 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(); // 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); if (!spawner) return nullptr; auto* model = spawner->Spawn(); if (!model) return nullptr; // Placed while someone is building: stays still until the last builder finishes (OnFinishBuilding resumes every // model) auto* modelComponent = model->GetComponent(); if (modelComponent) modelComponent->Pause(); models.insert_or_assign(model->GetObjectID(), spawnerId); placedBy.insert_or_assign(modelId, placer); return model; } LWOOBJID PropertyManagementComponent::PlaceModelFromItem(const Entity& builder, 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(u"blueprintid", blueprintId); const auto behaviors = config.find(u"userModelBehaviors"); if (behaviors != config.end() && behaviors->second) modelConfig.Insert(u"userModelBehaviors", behaviors->second->GetValueAsString()); model = SpawnModel(BrickByBrick::MODEL_OBJECT_LOT, modelId, position, rotation, modelConfig, builder.GetObjectID()); } 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, {}, builder.GetObjectID()); } 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 builder finishes editing Save(); return modelId; } void PropertyManagementComponent::SendModelsOnProperty() const { GameMessages::GetModelsOnProperty msg; msg.models = { models.begin(), models.end() }; // Each player's own character, as PropertyContentsFromClient answers: the owner may not be here to target for (const auto* player : PlayerManager::GetAllPlayers()) { if (!player) continue; msg.target = player->GetObjectID(); msg.Send(player->GetSystemAddress()); } } void PropertyManagementComponent::UpdateModelPosition(Entity& builder, const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) { // Handed off to a new instance (live update): nothing changes here any more if (frozen) return; LOG("Placing model <%f, %f, %f>", position.x, position.y, position.z); auto* entity = &builder; auto* inventoryComponent = entity->GetComponent(); 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(builder, *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 = BrickByBrick::PLACE_MODEL_PLACED; msg.rotation = originalRotation; msg.Send(entity->GetSystemAddress()); } SendModelsOnProperty(); Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPlaced(entity); // Progress place model missions auto missionComponent = entity->GetComponent(); if (missionComponent != nullptr) missionComponent->Progress(eMissionTaskType::PLACE_MODEL, 0); } void PropertyManagementComponent::DeleteModel(Entity& builder, const LWOOBJID id, const int deleteReason) { if (frozen) return; LOG("Delete model: (%llu) (%i)", id, deleteReason); auto* model = Game::entityManager->GetEntity(id); if (model == nullptr) { LOG("Failed to find model entity"); return; } const auto index = models.find(id); if (index == models.end()) { LOG("Failed to find model"); return; } const auto modelId = index->second; // The model goes back to whoever placed it const auto placer = GetPlacedBy(modelId); auto* placerEntity = placer == builder.GetObjectID() ? &builder : PlayerManager::GetPlayer(placer); const auto placerName = [&]() -> std::string { if (placerEntity && placerEntity->GetCharacter()) return placerEntity->GetCharacter()->GetName(); const auto info = Database::Get()->GetCharacterInfo(placer); return info ? info->name : "another player"; }; switch (PropertyBuilders::PlanModelReturn(builder.GetObjectID(), placer, placerEntity != nullptr, deleteReason)) { case PropertyBuilders::eModelReturn::PLACER_AWAY: LOG("%llu picked up model %llu placed by %llu, who isn't here; leaving it on the property", builder.GetObjectID(), modelId, placer); ChatPackets::SendSystemMessage(builder.GetSystemAddress(), "This model is " + placerName() + "'s. It stays on the property until they are here to get it back."); return; case PropertyBuilders::eModelReturn::NOT_THEIRS: ChatPackets::SendSystemMessage(builder.GetSystemAddress(), "This model is " + placerName() + "'s. Only they can take it apart."); return; default: break; } auto& receiver = *placerEntity; const bool toBuilder = &receiver == &builder; auto* inventoryComponent = receiver.GetComponent(); if (inventoryComponent == nullptr) return; const auto removal = BrickByBrick::PlanModelRemoval(deleteReason); // 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(u"blueprintid"), modelId, model->GetVar(u"userModelBehaviors")); subKey = modelId; } // Only the player picking it up carries it const auto received = inventoryComponent->ReceiveItem(LWOOBJID_EMPTY, itemLot, 1, eLootSourceType::PROPERTY, config, subKey, false, { .inventory = eInventoryType::MODELS, .showFlyingLoot = false, .equip = toBuilder && removal.equip }); if (received.id == LWOOBJID_EMPTY) { LOG("Could not give model %llu back to %llu, leaving it on the property", modelId, receiver.GetObjectID()); if (!toBuilder) ChatPackets::SendSystemMessage(builder.GetSystemAddress(), "This model is " + placerName() + "'s, and they have no room for it. It stays on the property."); return; } models.erase(index); placedBy.erase(modelId); Game::entityManager->DestructEntity(model); 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 && toBuilder) { auto* item = inventoryComponent->FindItemById(received.id); GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg; msg.target = builder.GetObjectID(); msg.invItem = received.id; msg.itemsTotal = item ? item->GetCount() : 1; msg.Send(builder.GetSystemAddress()); } if (!toBuilder) { const auto builderName = builder.GetCharacter() ? builder.GetCharacter()->GetName() : "Another player"; ChatPackets::SendSystemMessage(builder.GetSystemAddress(), "This model is " + placerName() + "'s, so it went back to them."); ChatPackets::SendSystemMessage(receiver.GetSystemAddress(), builderName + " picked up one of your models. It is back in your models."); } SendModelsOnProperty(); { GameMessages::PlaceModelResponse msg; msg.target = builder.GetObjectID(); msg.response = BrickByBrick::PLACE_MODEL_REMOVED; msg.Send(builder.GetSystemAddress()); } switch (static_cast(deleteReason)) { case BrickByBrick::eDeleteReason::PICKING_MODEL_UP: Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPickedUp(&builder); break; case BrickByBrick::eDeleteReason::RETURNING_MODEL_TO_INVENTORY: Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelRemoved(&builder); break; default: break; } } LWOOBJID PropertyManagementComponent::GetPlacedBy(const LWOOBJID modelId) const { const auto placer = placedBy.find(modelId); return PropertyBuilders::Placer(placer != placedBy.end() ? placer->second : LWOOBJID_EMPTY, owner); } void PropertyManagementComponent::UpdateApprovedStatus(const bool value, const std::string& rejectionReason) { if (owner == LWOOBJID_EMPTY) return; IProperty::Info info; info.id = propertyId; info.modApproved = value; info.privacyOption = static_cast(privacyOption); info.rejectionReason = rejectionReason; Database::Get()->UpdatePropertyModerationInfo(info); DashboardNotify::Changed("properties", propertyId); } void PropertyManagementComponent::Load() { if (propertyId == LWOOBJID_EMPTY) { return; } auto propertyModels = Database::Get()->GetPropertyModels(propertyId); for (const auto& databaseModel : propertyModels) { auto* node = new SpawnerNode(); node->position = databaseModel.position; node->rotation = databaseModel.rotation; SpawnerInfo info{}; info.templateID = databaseModel.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 = databaseModel.id; LwoNameValue& settings = node->config; //BBB property models need to have extra stuff set for them: if (databaseModel.lot == 14) { LWOOBJID blueprintID = databaseModel.ugcId; settings.Insert(u"blueprintid", blueprintID); settings.Insert(u"componentWhitelist", 1); settings.Insert(u"modelType", 2); settings.Insert(u"propertyObjectID", true); settings.Insert(u"userModelID", databaseModel.id); } else { settings.Insert(u"modelType", 2); settings.Insert(u"userModelID", databaseModel.id); settings.Insert(u"modelBehaviors", 0); settings.Insert(u"propertyObjectID", true); settings.Insert(u"componentWhitelist", 1); } std::ostringstream userModelBehavior; bool firstAdded = false; for (auto behavior : databaseModel.behaviors) { if (behavior < 0) { LOG("Invalid behavior ID: %d, removing behavior reference from model", behavior); behavior = 0; } if (firstAdded) userModelBehavior << ","; userModelBehavior << behavior; firstAdded = true; } settings.Insert(u"userModelBehaviors", userModelBehavior.str()); const auto spawnerId = Game::zoneManager->MakeSpawner(info); auto* spawner = Game::zoneManager->GetSpawner(spawnerId); auto* model = spawner->Spawn(); models.insert_or_assign(model->GetObjectID(), spawnerId); if (databaseModel.placedBy != LWOOBJID_EMPTY) placedBy.insert_or_assign(databaseModel.id, databaseModel.placedBy); } } void PropertyManagementComponent::Save() { // Handed off to a new instance (live update): what it saves is what counts if (frozen) return; // From the property's own ids: it saves whoever is here (the owner may not be) if (propertyId == LWOOBJID_EMPTY || owner == LWOOBJID_EMPTY) { return; } auto present = Database::Get()->GetPropertyModels(propertyId); std::vector modelIds; for (const auto& pair : models) { const auto id = pair.second; modelIds.push_back(id); auto* entity = Game::entityManager->GetEntity(pair.first); if (entity == nullptr) { continue; } auto* modelComponent = entity->GetComponent(); if (!modelComponent) continue; const auto modelBehaviors = modelComponent->GetBehaviorsForSave(); // save the behaviors of the model, as the model's placer's const auto placer = GetPlacedBy(id); for (const auto& [behaviorId, behaviorStr] : modelBehaviors) { if (behaviorStr.empty() || behaviorId == -1 || behaviorId == 0) continue; IBehaviors::Info info{ .behaviorId = behaviorId, .characterId = placer, .behaviorInfo = behaviorStr }; Database::Get()->AddBehavior(info); } // Always save the original position so we can move the model freely const auto& position = modelComponent->GetOriginalPosition(); const auto& rotation = modelComponent->GetOriginalRotation(); if (std::find(present.begin(), present.end(), id) == present.end()) { IPropertyContents::Model model; model.id = id; model.lot = entity->GetLOT(); model.position = position; model.rotation = rotation; // 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(u"blueprintid") : 0; for (auto i = 0; i < model.behaviors.size(); i++) { model.behaviors[i] = modelBehaviors[i].first; } model.placedBy = placer; Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_" + std::to_string(model.lot) + "_name"); } else { Database::Get()->UpdateModel(id, position, rotation, modelBehaviors); } } for (auto model : present) { if (std::find(modelIds.begin(), modelIds.end(), model.id) != modelIds.end()) { continue; } Database::Get()->RemoveModel(model.id); } IProperty::Info info; info.id = propertyId; info.lastUpdatedTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); Database::Get()->UpdateLastSave(info); DashboardNotify::Changed("properties", propertyId); } void PropertyManagementComponent::FreezeForHandOff() { if (frozen) return; // Everyone still building stops (their models are kept as they are now), and nobody can start again std::vector building(builders.begin(), builders.end()); for (const auto id : building) { auto* builder = Game::entityManager->GetEntity(id); if (builder) EndBuilding(*builder); else builders.erase(id); } Save(); frozen = true; // Takes the build rights the clients were given away (CanBuild is false for everyone now) UpdateBuildRights(); LOG("Property %llu (clone %u) saved and handed off to a new instance", propertyId, clone_Id); } void PropertyManagementComponent::Unfreeze() { if (!frozen) return; frozen = false; UpdateBuildRights(); LOG("Property %llu (clone %u) is no longer handed off", propertyId, clone_Id); } void PropertyManagementComponent::AddModel(LWOOBJID modelId, LWOOBJID spawnerId) { models[modelId] = spawnerId; } PropertyManagementComponent* PropertyManagementComponent::Instance() { return instance; } void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const SystemAddress& sysAddr, LWOOBJID author) { if (author == LWOOBJID_EMPTY) { author = m_Parent->GetObjectID(); } const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); const auto zoneId = worldId.GetMapID(); const auto cloneId = worldId.GetCloneID(); LOG("Getting property info for %d", zoneId); const auto propertyTemplate = CDClientManager::GetTable()->GetByMapID(zoneId); const auto isClaimed = GetOwnerId() != LWOOBJID_EMPTY; LWOOBJID ownerId = GetOwnerId(); std::string ownerName; auto charInfo = Database::Get()->GetCharacterInfo(ownerId); if (charInfo) ownerName = charInfo->name; std::string name = ""; std::string description = ""; uint64_t claimed = 0; char privacy = 0; if (isClaimed) { name = propertyName; description = propertyDescription; claimed = claimedTime; privacy = static_cast(this->privacyOption); if (moderatorRequested) { auto moderationInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); if (moderationInfo) { if (moderationInfo->rejectionReason != "") { moderatorRequested = false; rejectionReason = moderationInfo->rejectionReason; } else if (moderationInfo->rejectionReason == "" && moderationInfo->modApproved == 1) { moderatorRequested = false; rejectionReason = ""; } else { moderatorRequested = true; rejectionReason = ""; } } } } GameMessages::DownloadPropertyData message; message.target = author; message.propertyId = 0; message.templateId = static_cast(propertyTemplate.id); message.mapId = static_cast(propertyTemplate.mapID); message.vendorMapId = static_cast(propertyTemplate.vendorMapID); message.cloneId = clone_Id; message.name = GeneralUtils::UTF8ToUTF16(name); message.description = GeneralUtils::UTF8ToUTF16(description); message.ownerName = GeneralUtils::UTF8ToUTF16(ownerName); message.ownerId = ownerId; message.propertyType = 0; message.zoneCode = 0; message.rent = 0; message.rentalPeriod = 1; message.expirationDate = LastUpdatedTime; message.rentAmount = 1; message.reputation = reputation; message.spawnName = GeneralUtils::ASCIIToUTF16(propertyTemplate.spawnName); message.rentDuration = 0; message.votes = 1; message.durationType = 1; message.renew = static_cast(privacy); message.ownerAccountID = 0; if (rejectionReason != "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_REJECTED; else if (moderatorRequested == true && rejectionReason == "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_APPROVED; else message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_PENDING; message.lastLogoutTime = 0; message.dayOfMonthPlaqueWasBought = 1; message.repAchievementReq = 1; message.zonePosition = { 548.0f, 406.0f, 178.0f }; message.maxBuildHeight = 128.0f; message.rentalDate = claimed; message.accessType = static_cast(privacy); message.pathPositions = GetPaths(); LOG("(%llu) sending property data (%d)", author, true); // Each player gets their own: it tells builders they own the property if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) { for (auto* player : PlayerManager::GetAllPlayers()) { if (player) SendPropertyData(*player, message); } } else { auto* player = PlayerManager::GetPlayer(sysAddr); if (player) SendPropertyData(*player, message); } // send rejection here? } void PropertyManagementComponent::SendPropertyData(const Entity& player, const GameMessages::DownloadPropertyData& message) { const bool canBuild = CanBuild(player); sentBuildRights[player.GetObjectID()] = canBuild; // The client lets only the property's owner build (ownerId == its character); a best friend who can build is told // they own it, while ownerName stays the owner's auto forPlayer = message; if (canBuild) forPlayer.ownerId = player.GetObjectID(); forPlayer.Send(player.GetSystemAddress()); } void PropertyManagementComponent::OnUse(Entity* originator) { OnQueryPropertyData(originator, UNASSIGNED_SYSTEM_ADDRESS); GameMessages::OpenPropertyManagement msg; msg.target = PropertyManagementComponent::Instance()->GetParent()->GetObjectID(); msg.Send(originator->GetSystemAddress()); } void PropertyManagementComponent::SetOwnerId(const LWOOBJID value) { if (owner != value) bestFriends.clear(); owner = value; } const std::map& PropertyManagementComponent::GetModels() const { return models; } void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMessage) const { for (const auto& modelID : models | std::views::keys) { auto* const model = Game::entityManager->GetEntity(modelID); if (!model) continue; auto* const modelComponent = model->GetComponent(); if (!modelComponent) continue; modelComponent->OnChatMessageReceived(sMessage); } } void PropertyManagementComponent::ApplyModeration(const bool approved, const std::string& reason) { rejectionReason = approved ? "" : reason; // The dashboard makes rejected properties private; don't let a later save here publish it again if (!approved) privacyOption = PropertyPrivacyOption::Private; OnQueryPropertyData(nullptr, UNASSIGNED_SYSTEM_ADDRESS); }