mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Converts the property messages to NetGameMsgs in PropertyMessages.{h,cpp}.
Sent: OpenPropertyVendor, OpenPropertyManagement, DownloadPropertyData
(replaces PropertyDataMessage, now with the client's PropertyData field
names), PropertyRentalResponse, PropertyEntranceBegin,
PropertySelectQuery (replaces PropertySelectQueryProperty with the
client's PropertyInfo), GetModelsOnProperty, PlaceModelResponse and
HandleUGCEquipPre/PostDeleteBasedOnEditMode. Received, registered in
GameMessageHandler's map: SetPropertyAccess,
UpdatePropertyOrModelForFilterCheck, QueryPropertyData,
PropertyEditorBegin/End, PropertyContentsFromClient,
ZonePropertyModelEquipped/Rotated, PlacePropertyModel,
UpdateModelFromClient, DeleteModelFromClient, ControlBehaviors,
PropertyEntranceSync, EnterProperty1, UpdatePropertyPerformanceCost,
ReportOffensiveModel/Property and GetHotPropertyData. The news screen's
NewsSendHotPropertiesInfoToClient moves here with the top properties
lookup; the dashboard's player reports now take the decoded text.
Handlers keep their logic and hand the work to PropertyManagementComponent,
PropertyVendorComponent, PropertyEntranceComponent and
MultiZoneEntranceComponent as before. Messages whose payload DLU ignored
(PropertyEditorBegin, PropertyContentsFromClient, ZonePropertyModel*)
now read it with the client's layout. The never-called
SendZonePropertyModelEquipped (which wrote no default flags) is gone.
No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions, PropertyDataMessage
and PropertySelectQueryProperty over an input grid (to one client and
broadcast), received messages compared with the old handlers' read
sequences with every truncated payload rejected, hand computed golden
bytes, round trips and a mutation check. Layouts confirmed against the
1.10.64 client in Ghidra. Known wire bug kept as is: PlaceModelResponse
writes response where the client reads a rotation quaternion
(PlaceModelResponse::Deserialize 0x00dc0170).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
933 lines
27 KiB
C++
933 lines
27 KiB
C++
#include "PropertyManagementComponent.h"
|
|
#include "DashboardNotify.h"
|
|
|
|
#include <sstream>
|
|
|
|
#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 <vector>
|
|
#include "CppScripts.h"
|
|
#include <ranges>
|
|
#include "dConfig.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<int32_t>(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<PropertyPrivacyOption>(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();
|
|
}
|
|
}
|
|
|
|
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<NiPoint3> 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<int>(zoneId));
|
|
|
|
auto result = query.execQuery();
|
|
|
|
std::vector<NiPoint3> paths{};
|
|
|
|
if (result.eof()) {
|
|
return paths;
|
|
}
|
|
|
|
std::vector<float> points;
|
|
|
|
for (const auto& str : GeneralUtils::SplitString(result.getStringField("path"), ' ')) {
|
|
const auto value = GeneralUtils::TryParse<float>(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<PropertyPrivacyOption>(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<uint32_t>(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::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
|
|
|
Database::Get()->UpdateLastSave(info);
|
|
Database::Get()->UpdatePropertyDetails(info);
|
|
DashboardNotify::Changed("properties", propertyId);
|
|
|
|
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);
|
|
DashboardNotify::Changed("properties", propertyId);
|
|
|
|
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<RocketLaunchpadControlComponent>();
|
|
|
|
if (rocketPad != nullptr) {
|
|
zoneId = rocketPad->GetDefaultZone();
|
|
}
|
|
}
|
|
|
|
for (auto* player : players) {
|
|
if (player == ownerEntity) continue;
|
|
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
if (characterComponent) characterComponent->SendToZone(zoneId);
|
|
}
|
|
auto inventoryComponent = ownerEntity->GetComponent<InventoryComponent>();
|
|
|
|
// 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<ModelComponent>();
|
|
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;
|
|
|
|
UpdateApprovedStatus(false);
|
|
|
|
SetPrivacyOption(originalPrivacyOption);
|
|
|
|
Save();
|
|
|
|
for (auto modelID : models | std::views::keys) {
|
|
auto* model = Game::entityManager->GetEntity(modelID);
|
|
if (model) {
|
|
auto* modelComponent = model->GetComponent<ModelComponent>();
|
|
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<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);
|
|
|
|
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.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);
|
|
|
|
auto* model = spawner->Spawn();
|
|
auto* modelComponent = model->GetComponent<ModelComponent>();
|
|
if (modelComponent) modelComponent->Pause();
|
|
|
|
models.insert_or_assign(model->GetObjectID(), spawnerId);
|
|
|
|
{
|
|
GameMessages::PlaceModelResponse msg;
|
|
msg.target = entity->GetObjectID();
|
|
msg.position = position;
|
|
msg.propertyPlaqueID = m_Parent->GetObjectID();
|
|
msg.response = 14;
|
|
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);
|
|
}
|
|
|
|
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPlaced(entity);
|
|
|
|
// Progress place model missions
|
|
auto missionComponent = entity->GetComponent<MissionComponent>();
|
|
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<InventoryComponent>();
|
|
|
|
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;
|
|
|
|
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
|
|
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());
|
|
}
|
|
|
|
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<uint32_t>(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<LWOOBJID>(u"blueprintid", blueprintID);
|
|
settings.Insert<int>(u"componentWhitelist", 1);
|
|
settings.Insert<int>(u"modelType", 2);
|
|
settings.Insert<bool>(u"propertyObjectID", true);
|
|
settings.Insert<LWOOBJID>(u"userModelID", databaseModel.id);
|
|
} else {
|
|
settings.Insert<int>(u"modelType", 2);
|
|
settings.Insert<LWOOBJID>(u"userModelID", databaseModel.id);
|
|
settings.Insert<LWOOBJID>(u"modelBehaviors", 0);
|
|
settings.Insert<bool>(u"propertyObjectID", true);
|
|
settings.Insert<int>(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<std::string>(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);
|
|
}
|
|
}
|
|
|
|
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<LWOOBJID> 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<ModelComponent>();
|
|
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::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
|
Database::Get()->UpdateLastSave(info);
|
|
DashboardNotify::Changed("properties", propertyId);
|
|
}
|
|
|
|
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<CDPropertyTemplateTable>()->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<char>(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<int32_t>(propertyTemplate.id);
|
|
message.mapId = static_cast<uint16_t>(propertyTemplate.mapID);
|
|
message.vendorMapId = static_cast<uint16_t>(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<uint8_t>(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<uint8_t>(privacy);
|
|
message.pathPositions = GetPaths();
|
|
|
|
LOG("(%llu) sending property data (%d)", author, true);
|
|
message.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
// send rejection here?
|
|
}
|
|
|
|
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) {
|
|
owner = value;
|
|
}
|
|
|
|
const std::map<LWOOBJID, LWOOBJID>& 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<ModelComponent>();
|
|
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(GetOwner(), UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|