#include "PropertyManagementComponent.h" #include #include "MissionComponent.h" #include "EntityManager.h" #include "PropertyDataMessage.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 "PositionUpdate.h" #include "GeneralUtils.h" #include "User.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; Load(); // Cache owner's account ID for same-account reputation exclusion if (this->owner != LWOOBJID_EMPTY) { auto ownerCharId = this->owner; GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER); auto charInfo = Database::Get()->GetCharacterInfo(ownerCharId); if (charInfo) { m_OwnerAccountId = charInfo->accountId; } } // Load reputation config auto configFloat = [](const std::string& key, float def) { const auto& val = Game::config->GetValue(key); return val.empty() ? def : std::stof(val); }; auto configUint = [](const std::string& key, uint32_t def) { const auto& val = Game::config->GetValue(key); return val.empty() ? def : static_cast(std::stoul(val)); }; m_RepInterval = configFloat("property_rep_interval", 60.0f); m_RepDailyCap = configUint("property_rep_daily_cap", 50); m_RepPerTick = configUint("property_rep_per_tick", 1); m_RepMultiplier = configFloat("property_rep_multiplier", 1.0f); m_RepVelocityThreshold = configFloat("property_rep_velocity_threshold", 0.5f); m_RepSaveInterval = configFloat("property_rep_save_interval", 300.0f); m_RepDecayRate = configFloat("property_rep_decay_rate", 0.0f); m_RepDecayInterval = configFloat("property_rep_decay_interval", 86400.0f); m_RepDecayMinimum = configUint("property_rep_decay_minimum", 0); // Load daily reputation contributions and subscribe to position updates m_CurrentDate = GeneralUtils::GetCurrentUTCDate(); LoadDailyContributions(); Entity::OnPlayerPositionUpdate += [this](Entity* player, const PositionUpdate& update) { OnPlayerPositionUpdateHandler(player, update); }; } } LWOOBJID PropertyManagementComponent::GetOwnerId() const { return owner; } Entity* PropertyManagementComponent::GetOwner() const { return Game::entityManager->GetEntity(owner); } void PropertyManagementComponent::SetOwner(Entity* value) { owner = 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; std::istringstream stream(result.getStringField("path")); std::string token; while (std::getline(stream, token, ' ')) { try { auto value = std::stof(token); points.push_back(value); } catch (std::invalid_argument& exception) { LOG("Failed to parse value (%s): (%s)!", token.c_str(), exception.what()); } } for (auto i = 0u; i < 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; } 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); } } 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); OnQueryPropertyData(GetOwner(), UNASSIGNED_SYSTEM_ADDRESS); } bool PropertyManagementComponent::Claim(const LWOOBJID playerId) { if (owner != LWOOBJID_EMPTY) { 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); auto* zoneControlObject = Game::zoneManager->GetZoneControlObject(); if (zoneControlObject) zoneControlObject->GetScript()->OnZonePropertyRented(zoneControlObject, entity); return true; } void PropertyManagementComponent::OnStartBuilding() { auto* ownerEntity = GetOwner(); if (ownerEntity == nullptr) 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(); } } for (auto* player : players) { if (player == ownerEntity) continue; auto* characterComponent = player->GetComponent(); if (characterComponent) characterComponent->SendToZone(zoneId); } auto inventoryComponent = ownerEntity->GetComponent(); // Push equipped items if (inventoryComponent) inventoryComponent->PushEquippedItems(); 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() { auto* ownerEntity = GetOwner(); if (ownerEntity == nullptr) return; SetPrivacyOption(originalPrivacyOption); UpdateApprovedStatus(false); 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(); } } 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(); 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; for (auto* setting : item->GetConfig()) { info.settings.push_back(setting->Copy()); } Entity* newEntity = Game::entityManager->CreateEntity(info); if (newEntity != nullptr) { Game::entityManager->ConstructEntity(newEntity); auto* modelComponent = newEntity->GetComponent(); 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); auto* node = new SpawnerNode(); node->position = position; node->rotation = rotation; SpawnerInfo info{}; info.templateID = modelLOT; 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); const auto spawnerId = Game::zoneManager->MakeSpawner(info); auto* spawner = Game::zoneManager->GetSpawner(spawnerId); info.nodes[0]->config.push_back(new LDFData(u"modelBehaviors", 0)); info.nodes[0]->config.push_back(new LDFData(u"userModelID", info.spawnerID)); info.nodes[0]->config.push_back(new LDFData(u"modelType", 2)); info.nodes[0]->config.push_back(new LDFData(u"propertyObjectID", true)); info.nodes[0]->config.push_back(new LDFData(u"componentWhitelist", 1)); auto* model = spawner->Spawn(); auto* modelComponent = model->GetComponent(); if (modelComponent) modelComponent->Pause(); models.insert_or_assign(model->GetObjectID(), spawnerId); GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), position, m_Parent->GetObjectID(), 14, originalRotation); GameMessages::SendUGCEquipPreCreateBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), 0, spawnerId); GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); 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(const LWOOBJID id, const int deleteReason) { LOG("Delete model: (%llu) (%i)", id, deleteReason); auto* entity = GetOwner(); if (entity == nullptr) { return; } auto* inventoryComponent = entity->GetComponent(); 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::SendUGCEquipPostDeleteBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), LWOOBJID_EMPTY, 0); // Need this to pop the user out of their current state GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), entity->GetPosition(), m_Parent->GetObjectID(), 14, entity->GetRotation()); return; } const auto index = models.find(id); if (index == models.end()) { LOG("Failed to find model"); return; } const auto spawnerId = index->second; auto* spawner = Game::zoneManager->GetSpawner(spawnerId); models.erase(id); if (spawner == nullptr) { LOG("Failed to find spawner"); } Game::entityManager->DestructEntity(model); LOG("Deleting model LOT %i", model->GetLOT()); if (model->GetLOT() == 14) { //add it to the inv std::vector settings; //fill our settings with BBB gurbage LDFBaseData* ldfBlueprintID = new LDFData(u"blueprintid", model->GetVar(u"blueprintid")); LDFBaseData* userModelDesc = new LDFData(u"userModelDesc", u"A cool model you made!"); LDFBaseData* userModelHasBhvr = new LDFData(u"userModelHasBhvr", false); LDFBaseData* userModelID = new LDFData(u"userModelID", model->GetVar(u"userModelID")); LDFBaseData* userModelMod = new LDFData(u"userModelMod", false); LDFBaseData* userModelName = new LDFData(u"userModelName", u"My Cool Model"); LDFBaseData* propertyObjectID = new LDFData(u"userModelOpt", true); LDFBaseData* modelType = new LDFData(u"userModelPhysicsType", 2); settings.push_back(ldfBlueprintID); settings.push_back(userModelDesc); settings.push_back(userModelHasBhvr); settings.push_back(userModelID); settings.push_back(userModelMod); settings.push_back(userModelName); settings.push_back(propertyObjectID); settings.push_back(modelType); inventoryComponent->AddItem(6662, 1, eLootSourceType::DELETION, eInventoryType::MODELS_IN_BBB, settings, 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::SendUGCEquipPostDeleteBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), item->GetId(), item->GetCount()); } GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 16, QuatUtils::IDENTITY); 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::SendUGCEquipPostDeleteBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), item->GetId(), item->GetCount()); 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"); } } GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 16, QuatUtils::IDENTITY); if (spawner != nullptr) { Game::zoneManager->RemoveSpawner(spawner->m_Info.spawnerID); } else { model->Smash(LWOOBJID_EMPTY, eKillType::SILENT); } } 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); } 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; std::vector settings; //BBB property models need to have extra stuff set for them: if (databaseModel.lot == 14) { LWOOBJID blueprintID = databaseModel.ugcId; settings.push_back(new LDFData(u"blueprintid", blueprintID)); settings.push_back(new LDFData(u"componentWhitelist", 1)); settings.push_back(new LDFData(u"modelType", 2)); settings.push_back(new LDFData(u"propertyObjectID", true)); settings.push_back(new LDFData(u"userModelID", databaseModel.id)); } else { settings.push_back(new LDFData(u"modelType", 2)); settings.push_back(new LDFData(u"userModelID", databaseModel.id)); settings.push_back(new LDFData(u"modelBehaviors", 0)); settings.push_back(new LDFData(u"propertyObjectID", true)); settings.push_back(new LDFData(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.push_back(new LDFData(u"userModelBehaviors", userModelBehavior.str())); node->config = settings; const auto spawnerId = Game::zoneManager->MakeSpawner(info); auto* spawner = Game::zoneManager->GetSpawner(spawnerId); auto* model = spawner->Spawn(); models.insert_or_assign(model->GetObjectID(), spawnerId); } } void PropertyManagementComponent::Save() { if (propertyId == LWOOBJID_EMPTY) { return; } const auto* const owner = GetOwner(); if (!owner) return; const auto* const character = owner->GetCharacter(); if (!character) 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 for (const auto& [behaviorId, behaviorStr] : modelBehaviors) { if (behaviorStr.empty() || behaviorId == -1 || behaviorId == 0) continue; IBehaviors::Info info{ .behaviorId = behaviorId, .characterId = character->GetID(), .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; model.ugcId = 0; for (auto i = 0; i < model.behaviors.size(); i++) { model.behaviors[i] = modelBehaviors[i].first; } 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); } 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); GameMessages::PropertyDataMessage message = GameMessages::PropertyDataMessage(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->rejectionReason != "") { moderatorRequested = false; rejectionReason = moderationInfo->rejectionReason; } else if (moderationInfo->rejectionReason == "" && moderationInfo->modApproved == 1) { moderatorRequested = false; rejectionReason = ""; } else { moderatorRequested = true; rejectionReason = ""; } } } message.moderatorRequested = moderatorRequested; message.reputation = reputation; message.LastUpdatedTime = LastUpdatedTime; message.OwnerId = ownerId; message.OwnerName = ownerName; message.Name = name; message.Description = description; message.ClaimedTime = claimed; message.PrivacyOption = privacy; message.cloneId = clone_Id; message.rejectionReason = rejectionReason; message.Paths = GetPaths(); SendDownloadPropertyData(author, message, UNASSIGNED_SYSTEM_ADDRESS); // send rejection here? } void PropertyManagementComponent::OnUse(Entity* originator) { OnQueryPropertyData(originator, UNASSIGNED_SYSTEM_ADDRESS); GameMessages::SendOpenPropertyManagment(m_Parent->GetObjectID(), originator->GetSystemAddress()); } void PropertyManagementComponent::SetOwnerId(const LWOOBJID value) { 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); } } PropertyManagementComponent::~PropertyManagementComponent() { SaveReputation(); } void PropertyManagementComponent::Update(float deltaTime) { // Check for day rollover const auto currentDate = GeneralUtils::GetCurrentUTCDate(); if (currentDate != m_CurrentDate) { m_CurrentDate = currentDate; m_PlayerActivity.clear(); } // Periodic reputation save m_ReputationSaveTimer += deltaTime; if (m_ReputationSaveTimer >= m_RepSaveInterval && m_ReputationDirty) { SaveReputation(); m_ReputationSaveTimer = 0.0f; } // Property reputation decay if (m_RepDecayRate > 0.0f && owner != LWOOBJID_EMPTY) { m_DecayTimer += deltaTime; if (m_DecayTimer >= m_RepDecayInterval) { m_DecayTimer = 0.0f; if (reputation > m_RepDecayMinimum) { const auto loss = static_cast(m_RepDecayRate); reputation = (reputation > m_RepDecayMinimum + loss) ? reputation - loss : m_RepDecayMinimum; m_ReputationDirty = true; } } } } void PropertyManagementComponent::OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update) { if (owner == LWOOBJID_EMPTY) return; if (propertyId == LWOOBJID_EMPTY) return; if (m_RepInterval <= 0.0f) return; // Check same-account exclusion (covers owner + owner's alts) auto* character = player->GetCharacter(); if (!character) return; auto* parentUser = character->GetParentUser(); if (!parentUser) return; if (parentUser->GetAccountID() == m_OwnerAccountId) return; // Check velocity threshold (player must be active/moving) if (update.velocity.SquaredLength() < m_RepVelocityThreshold * m_RepVelocityThreshold) return; const auto playerId = player->GetObjectID(); auto& info = m_PlayerActivity[playerId]; // Check daily cap if (info.dailyContribution >= m_RepDailyCap) return; // Compute delta time since last position update for this player const auto now = std::chrono::steady_clock::now(); if (info.hasLastUpdate) { const auto dt = std::chrono::duration(now - info.lastUpdate).count(); // Cap delta to avoid spikes from reconnects etc. const auto clampedDt = std::min(dt, 1.0f); info.activeTime += clampedDt; } info.lastUpdate = now; info.hasLastUpdate = true; // Check if we've accumulated enough active time for a reputation tick if (info.activeTime >= m_RepInterval) { info.activeTime -= m_RepInterval; const auto repGain = static_cast(m_RepPerTick * m_RepMultiplier); if (repGain == 0) return; // Clamp to daily cap const auto actualGain = std::min(repGain, m_RepDailyCap - info.dailyContribution); if (actualGain == 0) return; // Grant property reputation reputation += actualGain; info.dailyContribution += actualGain; m_ReputationDirty = true; // Grant character reputation to property owner auto* ownerEntity = Game::entityManager->GetEntity(owner); if (ownerEntity) { auto* charComp = ownerEntity->GetComponent(); if (charComp) { charComp->SetReputation(charComp->GetReputation() + actualGain); } } else { // Owner is offline, update DB directly auto ownerCharId = owner; GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER); const auto currentRep = Database::Get()->GetCharacterReputation(ownerCharId); Database::Get()->SetCharacterReputation(ownerCharId, currentRep + actualGain); } } } void PropertyManagementComponent::SaveReputation() { if (!m_ReputationDirty) return; if (propertyId == LWOOBJID_EMPTY) return; Database::Get()->UpdatePropertyReputation(propertyId, reputation); for (const auto& [playerId, info] : m_PlayerActivity) { if (info.dailyContribution > 0) { Database::Get()->UpdatePropertyReputationContribution(propertyId, playerId, m_CurrentDate, info.dailyContribution); } } m_ReputationDirty = false; } void PropertyManagementComponent::LoadDailyContributions() { if (propertyId == LWOOBJID_EMPTY) return; const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, m_CurrentDate); for (const auto& contrib : contributions) { m_PlayerActivity[contrib.playerId].dailyContribution = contrib.reputationGained; } }