mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(property): best friends of the owner can build on the property
property_bff_build (off by default, as live): while the owner is in build mode, their best friends can join it and place, move and pick up models, build brick by brick and edit behaviors, each from their own inventory. Only the owner starts build mode; a best friend still in it when the owner leaves keeps building until they leave it. The client lets only the property's owner edit, so each player now gets their own DownloadPropertyData, with their own id as the owner while they can build, sent again whenever that changes. SetBuildModeConfirmed and GetModelsOnProperty go to each player instead of everyone. The first builder makes the property private and pauses the models; the last one restores them. Saving no longer needs the owner in the world. A model taken off the property goes to whoever placed it; if they aren't here it stays placed. Privacy and the property's name stay owner only (neither was checked before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,7 @@ namespace {
|
||||
c.Add(Bool(WORLD, "allow_nameplate_off", "Players can hide their nameplate", "Staff always can; this lets players (GM 0) turn off the name above their head too.", false));
|
||||
c.Add(Bool(WORLD, "allow_players_to_skip_cinematics", "Players can skip cinematics", "Most cutscenes get a skip option.", false));
|
||||
c.Add(Bool(WORLD, "auto_reject_empty_properties", "Reject empty properties", "Properties made public without models are rejected automatically.", false));
|
||||
c.Add(Bool(WORLD, "property_bff_build", "Best friends build on properties", "Best friends of a property's owner can join the owner's build mode there: place, move and pick up models, build brick by brick and edit behaviors. Only the owner starts build mode. Off (as live): only the owner builds.", false));
|
||||
c.Add(Unused(Bool(WORLD, "disable_drops", "Turn off loot drops", "Not read by this version.", false)));
|
||||
|
||||
c.AddSection("Hardcore mode", "Players lose items, coins and U-score when they die, and earn extra U-score from enemies.");
|
||||
|
||||
@@ -167,6 +167,9 @@ Entity::~Entity() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ends their building on the property; the owner leaving can stop their best friends building
|
||||
if (auto* property = PropertyManagementComponent::Instance()) property->OnPlayerRemoved(*this);
|
||||
|
||||
auto* zoneControl = Game::entityManager->GetZoneControlEntity();
|
||||
if (zoneControl) {
|
||||
zoneControl->GetScript()->OnPlayerExit(zoneControl, this);
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
#include "BrickByBrick.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "eKillType.h"
|
||||
#include "PropertyBuilders.h"
|
||||
#include "BuildingMessages.h"
|
||||
|
||||
PropertyManagementComponent* PropertyManagementComponent::instance = nullptr;
|
||||
|
||||
@@ -91,6 +93,77 @@ PropertyManagementComponent::PropertyManagementComponent(Entity* parent, const i
|
||||
|
||||
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 {
|
||||
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 {
|
||||
@@ -102,7 +175,7 @@ Entity* PropertyManagementComponent::GetOwner() const {
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::SetOwner(Entity* value) {
|
||||
owner = value->GetObjectID();
|
||||
SetOwnerId(value->GetObjectID());
|
||||
}
|
||||
|
||||
std::vector<NiPoint3> PropertyManagementComponent::GetPaths() const {
|
||||
@@ -190,7 +263,7 @@ void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::s
|
||||
Database::Get()->UpdatePropertyDetails(info);
|
||||
DashboardNotify::Changed("properties", propertyId);
|
||||
|
||||
OnQueryPropertyData(GetOwner(), UNASSIGNED_SYSTEM_ADDRESS);
|
||||
OnQueryPropertyData(nullptr, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
}
|
||||
|
||||
bool PropertyManagementComponent::Claim(const LWOOBJID playerId) {
|
||||
@@ -251,10 +324,20 @@ bool PropertyManagementComponent::Claim(const LWOOBJID playerId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::OnStartBuilding() {
|
||||
auto* ownerEntity = GetOwner();
|
||||
void PropertyManagementComponent::OnStartBuilding(Entity& builder) {
|
||||
const bool first = builders.empty();
|
||||
if (!builders.insert(builder.GetObjectID()).second) return;
|
||||
|
||||
if (ownerEntity == nullptr) return;
|
||||
// The owner starting lets their best friends join
|
||||
if (builder.GetObjectID() == owner) UpdateBuildRights();
|
||||
|
||||
auto inventoryComponent = builder.GetComponent<InventoryComponent>();
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -274,16 +357,13 @@ void PropertyManagementComponent::OnStartBuilding() {
|
||||
}
|
||||
}
|
||||
|
||||
// Everyone who can't build here leaves
|
||||
for (auto* player : players) {
|
||||
if (player == ownerEntity) continue;
|
||||
if (!player || CanBuild(*player)) 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);
|
||||
@@ -302,13 +382,22 @@ void PropertyManagementComponent::OnStartBuilding() {
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::OnFinishBuilding() {
|
||||
auto* ownerEntity = GetOwner();
|
||||
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();
|
||||
}
|
||||
|
||||
if (ownerEntity == nullptr) return;
|
||||
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();
|
||||
@@ -328,9 +417,10 @@ void PropertyManagementComponent::OnFinishBuilding() {
|
||||
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) {
|
||||
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;
|
||||
@@ -361,15 +451,17 @@ Entity* PropertyManagementComponent::SpawnModel(const LOT lot, const LWOOBJID mo
|
||||
auto* model = spawner->Spawn();
|
||||
if (!model) return nullptr;
|
||||
|
||||
// Placed while the owner is editing: stays still until they finish (OnFinishBuilding resumes every model)
|
||||
// Placed while someone is building: stays still until the last builder finishes (OnFinishBuilding resumes every
|
||||
// model)
|
||||
auto* modelComponent = model->GetComponent<ModelComponent>();
|
||||
if (modelComponent) modelComponent->Pause();
|
||||
|
||||
models.insert_or_assign(model->GetObjectID(), spawnerId);
|
||||
placedBy.insert_or_assign(modelId, placer);
|
||||
return model;
|
||||
}
|
||||
|
||||
LWOOBJID PropertyManagementComponent::PlaceModelFromItem(Item& item, const NiPoint3& position, const NiQuaternion& rotation) {
|
||||
LWOOBJID PropertyManagementComponent::PlaceModelFromItem(const Entity& builder, Item& item, const NiPoint3& position, const NiQuaternion& rotation) {
|
||||
LWOOBJID modelId = LWOOBJID_EMPTY;
|
||||
Entity* model = nullptr;
|
||||
|
||||
@@ -389,35 +481,38 @@ LWOOBJID PropertyManagementComponent::PlaceModelFromItem(Item& item, const NiPoi
|
||||
modelConfig.Insert<LWOOBJID>(u"blueprintid", blueprintId);
|
||||
const auto behaviors = config.find(u"userModelBehaviors");
|
||||
if (behaviors != config.end() && behaviors->second) modelConfig.Insert<std::string>(u"userModelBehaviors", behaviors->second->GetValueAsString());
|
||||
model = SpawnModel(BrickByBrick::MODEL_OBJECT_LOT, modelId, position, rotation, modelConfig);
|
||||
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, {});
|
||||
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 owner finishes editing
|
||||
// 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.target = owner;
|
||||
msg.models = { models.begin(), models.end() };
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
// 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(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) {
|
||||
void PropertyManagementComponent::UpdateModelPosition(Entity& builder, 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* entity = &builder;
|
||||
|
||||
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
||||
if (inventoryComponent == nullptr) return;
|
||||
@@ -433,7 +528,7 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N
|
||||
rotation = { rotation.w, rotation.z, rotation.y, rotation.x };
|
||||
}
|
||||
|
||||
const auto modelId = PlaceModelFromItem(*item, position, rotation);
|
||||
const auto modelId = PlaceModelFromItem(builder, *item, position, rotation);
|
||||
if (modelId == LWOOBJID_EMPTY) return;
|
||||
|
||||
// As a live server answered a placed model
|
||||
@@ -464,15 +559,9 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N
|
||||
if (missionComponent != nullptr) missionComponent->Progress(eMissionTaskType::PLACE_MODEL, 0);
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int deleteReason) {
|
||||
void PropertyManagementComponent::DeleteModel(Entity& builder, 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");
|
||||
@@ -486,6 +575,32 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet
|
||||
}
|
||||
|
||||
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<InventoryComponent>();
|
||||
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
|
||||
@@ -498,14 +613,17 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet
|
||||
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 = removal.equip });
|
||||
{ .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, entity->GetObjectID());
|
||||
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) {
|
||||
@@ -517,36 +635,47 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet
|
||||
// Straight to the database, as for placing
|
||||
Save();
|
||||
|
||||
if (removal.notifyPostDelete) {
|
||||
if (removal.notifyPostDelete && toBuilder) {
|
||||
auto* item = inventoryComponent->FindItemById(received.id);
|
||||
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.target = builder.GetObjectID();
|
||||
msg.invItem = received.id;
|
||||
msg.itemsTotal = item ? item->GetCount() : 1;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
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 = entity->GetObjectID();
|
||||
msg.target = builder.GetObjectID();
|
||||
msg.response = BrickByBrick::PLACE_MODEL_REMOVED;
|
||||
msg.Send(entity->GetSystemAddress());
|
||||
msg.Send(builder.GetSystemAddress());
|
||||
}
|
||||
|
||||
switch (static_cast<BrickByBrick::eDeleteReason>(deleteReason)) {
|
||||
case BrickByBrick::eDeleteReason::PICKING_MODEL_UP:
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPickedUp(entity);
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPickedUp(&builder);
|
||||
break;
|
||||
case BrickByBrick::eDeleteReason::RETURNING_MODEL_TO_INVENTORY:
|
||||
Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelRemoved(entity);
|
||||
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;
|
||||
|
||||
@@ -627,20 +756,16 @@ void PropertyManagementComponent::Load() {
|
||||
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() {
|
||||
if (propertyId == LWOOBJID_EMPTY) {
|
||||
// From the property's own ids: it saves whoever is here (the owner may not be)
|
||||
if (propertyId == LWOOBJID_EMPTY || owner == 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;
|
||||
@@ -659,12 +784,13 @@ void PropertyManagementComponent::Save() {
|
||||
if (!modelComponent) continue;
|
||||
const auto modelBehaviors = modelComponent->GetBehaviorsForSave();
|
||||
|
||||
// save the behaviors of the model
|
||||
// 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 = character->GetID(),
|
||||
.characterId = placer,
|
||||
.behaviorInfo = behaviorStr
|
||||
};
|
||||
Database::Get()->AddBehavior(info);
|
||||
@@ -685,6 +811,7 @@ void PropertyManagementComponent::Save() {
|
||||
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 {
|
||||
@@ -795,10 +922,29 @@ void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const
|
||||
message.pathPositions = GetPaths();
|
||||
|
||||
LOG("(%llu) sending property data (%d)", author, true);
|
||||
message.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
// 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;
|
||||
@@ -807,6 +953,7 @@ void PropertyManagementComponent::OnUse(Entity* originator) {
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::SetOwnerId(const LWOOBJID value) {
|
||||
if (owner != value) bestFriends.clear();
|
||||
owner = value;
|
||||
}
|
||||
|
||||
@@ -829,5 +976,5 @@ void PropertyManagementComponent::ApplyModeration(const bool approved, const std
|
||||
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);
|
||||
OnQueryPropertyData(nullptr, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "ePropertyPrivacyOption.h"
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include "Entity.h"
|
||||
#include "Component.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
@@ -9,6 +11,10 @@
|
||||
|
||||
class Item;
|
||||
|
||||
namespace GameMessages {
|
||||
struct DownloadPropertyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main component that handles interactions with a property, generally the plaques you see on properties.
|
||||
*/
|
||||
@@ -16,8 +22,24 @@ class PropertyManagementComponent final : public Component {
|
||||
public:
|
||||
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::PROPERTY_MANAGEMENT;
|
||||
PropertyManagementComponent(Entity* parent, const int32_t componentID);
|
||||
~PropertyManagementComponent() override;
|
||||
static PropertyManagementComponent* Instance();
|
||||
|
||||
/**
|
||||
* Whether the player can build on this property: its owner, or with property_bff_build a best friend of the owner
|
||||
* while the owner is building (and, once in build mode, until they leave it)
|
||||
*/
|
||||
bool CanBuild(const Entity& player) const;
|
||||
|
||||
// A player finished loading into this world: looks up whether they are a best friend of the owner
|
||||
void OnPlayerLoaded(Entity& player);
|
||||
|
||||
// A player left this world: ends their building and updates who else can build
|
||||
void OnPlayerRemoved(Entity& player);
|
||||
|
||||
// Tells every player whose right to build changed (CanBuild) and ends building for those who lost it
|
||||
void UpdateBuildRights();
|
||||
|
||||
/**
|
||||
* Event handler for when an entity requests information about this property, will send back whether it's owned, etc.
|
||||
* @param originator the entity that triggered the event
|
||||
@@ -90,44 +112,51 @@ public:
|
||||
bool Claim(LWOOBJID playerId);
|
||||
|
||||
/**
|
||||
* Event triggered when the owner of the property starts building, will kick other entities out
|
||||
* Event triggered when a player who can build starts building. The first one to start (the owner) makes the property
|
||||
* private, pauses the models and sends away the players who can't build.
|
||||
*/
|
||||
void OnStartBuilding();
|
||||
void OnStartBuilding(Entity& builder);
|
||||
|
||||
/**
|
||||
* Event triggered when the owner of the property finished building, will re-apply this property for moderation
|
||||
* request.
|
||||
* Event triggered when a builder finished building. When the last one finishes the property is re-applied for
|
||||
* moderation, gets its privacy back and the models run again.
|
||||
*/
|
||||
void OnFinishBuilding();
|
||||
void OnFinishBuilding(const Entity& builder);
|
||||
|
||||
/**
|
||||
* Updates the position of a model on the property
|
||||
* @param id the ID of the model to reposition
|
||||
* Places a model from the builder's inventory on the property
|
||||
* @param builder the player placing the model
|
||||
* @param id the ID of the model item to place
|
||||
* @param position the position to place the model on
|
||||
* @param rotation the rotation to place the model on
|
||||
*/
|
||||
void UpdateModelPosition(LWOOBJID id, NiPoint3 position, NiQuaternion rotation);
|
||||
void UpdateModelPosition(Entity& builder, LWOOBJID id, NiPoint3 position, NiQuaternion rotation);
|
||||
|
||||
/**
|
||||
* Deletes a model for a property
|
||||
* Takes a model off the property, into the inventory of the player who placed it
|
||||
* @param builder the player taking the model off
|
||||
* @param id the ID of the model to delete
|
||||
* @param deleteReason the reason of the deletion, e.g. picked up or destroyed (in case of UGC)
|
||||
*/
|
||||
void DeleteModel(LWOOBJID id, int deleteReason);
|
||||
void DeleteModel(Entity& builder, LWOOBJID id, int deleteReason);
|
||||
|
||||
/**
|
||||
* Spawns a model on the property and records it in the property's models (not yet saved)
|
||||
* @param lot the model object's LOT (14 for a brick built model)
|
||||
* @param modelId the model's id in properties_contents (its UGID)
|
||||
* @param config extra config for the model object
|
||||
* @param placedBy the player who placed it
|
||||
*/
|
||||
Entity* SpawnModel(LOT lot, LWOOBJID modelId, const NiPoint3& position, const NiQuaternion& rotation, const LwoNameValue& config);
|
||||
Entity* SpawnModel(LOT lot, LWOOBJID modelId, const NiPoint3& position, const NiQuaternion& rotation, const LwoNameValue& config, LWOOBJID placedBy);
|
||||
|
||||
/**
|
||||
* Places a model item on the property, uses the item up and saves the property
|
||||
* Places a model item of the builder's on the property, uses the item up and saves the property
|
||||
* @return the placed model's id, or LWOOBJID_EMPTY if it could not be placed
|
||||
*/
|
||||
LWOOBJID PlaceModelFromItem(Item& item, const NiPoint3& position, const NiQuaternion& rotation);
|
||||
LWOOBJID PlaceModelFromItem(const Entity& builder, Item& item, const NiPoint3& position, const NiQuaternion& rotation);
|
||||
|
||||
// The player who placed a model (by its id in properties_contents); the owner for models placed before this was kept
|
||||
LWOOBJID GetPlacedBy(LWOOBJID modelId) const;
|
||||
|
||||
// GetModelsOnProperty with every model on the property
|
||||
void SendModelsOnProperty() const;
|
||||
@@ -175,6 +204,16 @@ public:
|
||||
|
||||
void OnChatMessageReceived(const std::string& sMessage) const;
|
||||
private:
|
||||
// Sends the property's data to one player; a player who can build is told they own it, which is the client's only
|
||||
// check before editing (the owner's name stays the owner's)
|
||||
void SendPropertyData(const Entity& player, const GameMessages::DownloadPropertyData& message);
|
||||
|
||||
// Whether the player is a best friend of the owner (looked up the first time)
|
||||
bool IsBestFriend(LWOOBJID player) const;
|
||||
|
||||
// OnFinishBuilding without updating who can build; false if they weren't building
|
||||
bool EndBuilding(const Entity& builder);
|
||||
|
||||
/**
|
||||
* This
|
||||
*/
|
||||
@@ -249,4 +288,17 @@ private:
|
||||
* The privacy setting before it was changed, saved to set back after a player finishes building
|
||||
*/
|
||||
PropertyPrivacyOption originalPrivacyOption = PropertyPrivacyOption::Private;
|
||||
|
||||
// Whether each player on the property is a best friend of the owner, looked up once per player (again when the
|
||||
// owner changes)
|
||||
mutable std::map<LWOOBJID, bool> bestFriends;
|
||||
|
||||
// Whether each player on the property was last told they can build (see SendPropertyData)
|
||||
std::map<LWOOBJID, bool> sentBuildRights;
|
||||
|
||||
// Players building right now
|
||||
std::set<LWOOBJID> builders;
|
||||
|
||||
// Who placed each model, by its id in properties_contents (LWOOBJID_EMPTY: the owner)
|
||||
std::map<LWOOBJID, LWOOBJID> placedBy;
|
||||
};
|
||||
|
||||
@@ -435,6 +435,7 @@ namespace GameMessages {
|
||||
|
||||
void SetBuildMode::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* player = Game::entityManager->GetEntity(playerId);
|
||||
if (!player || !player->GetCharacter()) return;
|
||||
|
||||
auto confirmedStartPos = startPos;
|
||||
if (confirmedStartPos == NiPoint3Constant::ZERO) {
|
||||
@@ -453,7 +454,8 @@ namespace GameMessages {
|
||||
confirmed.modeValue = modeValue;
|
||||
confirmed.playerId = playerId;
|
||||
confirmed.startPos = confirmedStartPos;
|
||||
confirmed.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
// Only the builder: another player building on the same property must not be put in (or out of) build mode
|
||||
confirmed.Send(sysAddr);
|
||||
}
|
||||
|
||||
void SetBuildModeConfirmed::Serialize(RakNet::BitStream& bitStream) const {
|
||||
|
||||
@@ -345,9 +345,12 @@ namespace GameMessages {
|
||||
void SetPropertyAccess::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
LOG("Set privacy option to: %i", accessType);
|
||||
|
||||
if (PropertyManagementComponent::Instance() == nullptr) return;
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
// Only the owner decides who visits (a best friend who builds sees the owner's plaque too)
|
||||
const auto* sender = PlayerManager::GetPlayer(sysAddr);
|
||||
if (!property || !sender || property->GetOwnerId() != sender->GetObjectID()) return;
|
||||
|
||||
PropertyManagementComponent::Instance()->SetPrivacyOption(static_cast<PropertyPrivacyOption>(accessType));
|
||||
property->SetPrivacyOption(static_cast<PropertyPrivacyOption>(accessType));
|
||||
}
|
||||
|
||||
void UpdatePropertyOrModelForFilterCheck::Serialize(RakNet::BitStream& bitStream) const {
|
||||
@@ -370,7 +373,12 @@ namespace GameMessages {
|
||||
}
|
||||
|
||||
void UpdatePropertyOrModelForFilterCheck::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
PropertyManagementComponent::Instance()->UpdatePropertyDetails(GeneralUtils::UTF16ToWTF8(newName), GeneralUtils::UTF16ToWTF8(newDescription));
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
// Only the owner names the property
|
||||
const auto* sender = PlayerManager::GetPlayer(sysAddr);
|
||||
if (!property || !sender || property->GetOwnerId() != sender->GetObjectID()) return;
|
||||
|
||||
property->UpdatePropertyDetails(GeneralUtils::UTF16ToWTF8(newName), GeneralUtils::UTF16ToWTF8(newDescription));
|
||||
}
|
||||
|
||||
void QueryPropertyData::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
@@ -406,16 +414,19 @@ namespace GameMessages {
|
||||
|
||||
void PropertyEditorBegin::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
if (!property || property->GetOwnerId() != entity.GetObjectID()) return;
|
||||
property->OnStartBuilding();
|
||||
auto* builder = PlayerManager::GetPlayer(sysAddr);
|
||||
if (!property || !builder || !property->CanBuild(*builder)) return;
|
||||
property->OnStartBuilding(*builder);
|
||||
|
||||
Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditBegin();
|
||||
}
|
||||
|
||||
void PropertyEditorEnd::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
if (!property || property->GetOwnerId() != entity.GetObjectID()) return;
|
||||
property->OnFinishBuilding();
|
||||
auto* builder = PlayerManager::GetPlayer(sysAddr);
|
||||
// A builder who lost the right to build was already taken out of build mode (UpdateBuildRights)
|
||||
if (!property || !builder || !property->CanBuild(*builder)) return;
|
||||
property->OnFinishBuilding(*builder);
|
||||
|
||||
Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditEnd();
|
||||
}
|
||||
@@ -503,9 +514,10 @@ namespace GameMessages {
|
||||
|
||||
void UpdateModelFromClient::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
// Only the owner edits their property; the model comes from the owner's inventory
|
||||
if (!property || property->GetOwnerId() != entity.GetObjectID()) return;
|
||||
property->UpdateModelPosition(modelID, position, rotation);
|
||||
// Only players who can build edit the property; the model comes from the builder's inventory
|
||||
auto* builder = PlayerManager::GetPlayer(sysAddr);
|
||||
if (!property || !builder || !property->CanBuild(*builder)) return;
|
||||
property->UpdateModelPosition(*builder, modelID, position, rotation);
|
||||
}
|
||||
|
||||
void DeleteModelFromClient::Serialize(RakNet::BitStream& bitStream) const {
|
||||
@@ -521,8 +533,9 @@ namespace GameMessages {
|
||||
|
||||
void DeleteModelFromClient::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
if (!property || property->GetOwnerId() != entity.GetObjectID()) return;
|
||||
property->DeleteModel(modelID, reason);
|
||||
auto* builder = PlayerManager::GetPlayer(sysAddr);
|
||||
if (!property || !builder || !property->CanBuild(*builder)) return;
|
||||
property->DeleteModel(*builder, modelID, reason);
|
||||
}
|
||||
|
||||
void PropertyEntranceSync::Serialize(RakNet::BitStream& bitStream) const {
|
||||
@@ -683,10 +696,12 @@ namespace GameMessages {
|
||||
}
|
||||
|
||||
void ControlBehaviors::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* const owner = PropertyManagementComponent::Instance()->GetOwner();
|
||||
if (!owner) return;
|
||||
auto* const property = PropertyManagementComponent::Instance();
|
||||
// The model's behaviors are edited by a player who can build here; the answers go to them
|
||||
auto* const builder = PlayerManager::GetPlayer(sysAddr);
|
||||
if (!property || !builder || !property->CanBuild(*builder)) return;
|
||||
|
||||
::ControlBehaviors::Instance().ProcessCommand(&entity, *args, command, owner);
|
||||
::ControlBehaviors::Instance().ProcessCommand(&entity, *args, command, builder);
|
||||
}
|
||||
|
||||
void GetHotPropertyData::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "RacingControlComponent.h"
|
||||
#include "WorldMigration.h"
|
||||
#include "BrickByBrick.h"
|
||||
#include "PropertyManagementComponent.h"
|
||||
#include "dConfig.h"
|
||||
#include "dZoneManager.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
@@ -123,6 +124,9 @@ namespace GameMessages {
|
||||
// models that were open
|
||||
BrickByBrick::OnPlayerLoaded(*entity);
|
||||
|
||||
// On a property: whether they are a best friend of the owner, who may build (and the owner arriving lets them)
|
||||
if (auto* property = PropertyManagementComponent::Instance()) property->OnPlayerLoaded(*entity);
|
||||
|
||||
LOG("Player %s (%llu) loaded.", entity->GetCharacter()->GetName().c_str(), entity->GetObjectID());
|
||||
|
||||
// After we've done our thing, tell the client they're ready
|
||||
|
||||
@@ -249,9 +249,9 @@ void BrickByBrick::Save(Entity& player, const LWOOBJID localId, const std::strin
|
||||
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
// Brick by brick building happens on the player's own property; the build becomes models placed there
|
||||
if (!inventory || !property || property->GetOwnerId() != player.GetObjectID()) {
|
||||
LOG("Player %llu saved a brick by brick model but is not on their own property", player.GetObjectID());
|
||||
// Brick by brick building happens on a property the player can build on; the build becomes models placed there
|
||||
if (!inventory || !property || !property->CanBuild(player)) {
|
||||
LOG("Player %llu saved a brick by brick model but can't build on this property", player.GetObjectID());
|
||||
response.reasonCode = eBlueprintSaveResponseType::PlacementFailed;
|
||||
response.Send(player.GetSystemAddress());
|
||||
return;
|
||||
@@ -285,7 +285,7 @@ void BrickByBrick::Save(Entity& player, const LWOOBJID localId, const std::strin
|
||||
for (const auto& model : models) {
|
||||
LwoNameValue config;
|
||||
config.Insert<LWOOBJID>(u"blueprintid", model.blueprintId);
|
||||
property->SpawnModel(MODEL_OBJECT_LOT, model.modelId, model.center, QuatUtils::IDENTITY, config);
|
||||
property->SpawnModel(MODEL_OBJECT_LOT, model.modelId, model.center, QuatUtils::IDENTITY, config, player.GetObjectID());
|
||||
}
|
||||
property->Save();
|
||||
DashboardNotify::Changed("properties", property->GetId());
|
||||
@@ -315,8 +315,8 @@ void BrickByBrick::ReturnModel(Entity& player, const LWOOBJID itemId, const bool
|
||||
auto* item = inventory->FindItemById(itemId);
|
||||
if (item && item->GetInventory()->GetType() == eInventoryType::MODELS_IN_BBB) {
|
||||
auto* property = PropertyManagementComponent::Instance();
|
||||
if (hasWorldTransform && property && property->GetOwnerId() == player.GetObjectID()) {
|
||||
property->PlaceModelFromItem(*item, position, rotation);
|
||||
if (hasWorldTransform && property && property->CanBuild(player)) {
|
||||
property->PlaceModelFromItem(player, *item, position, rotation);
|
||||
property->SendModelsOnProperty();
|
||||
} else {
|
||||
MoveKeepingId(*inventory, item, eInventoryType::MODELS);
|
||||
|
||||
@@ -8,6 +8,7 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
|
||||
"GUID.cpp"
|
||||
"LiveEvents.cpp"
|
||||
"Contraband.cpp"
|
||||
"PropertyBuilders.cpp"
|
||||
"PropertyRent.cpp"
|
||||
"PropertyReputation.cpp"
|
||||
"Loot.cpp"
|
||||
|
||||
28
dGame/dUtilities/PropertyBuilders.cpp
Normal file
28
dGame/dUtilities/PropertyBuilders.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "PropertyBuilders.h"
|
||||
|
||||
#include "BrickByBrick.h"
|
||||
#include "dConfig.h"
|
||||
#include "Game.h"
|
||||
|
||||
bool PropertyBuilders::BestFriendsBuild() {
|
||||
return Game::config && Game::config->GetValue("property_bff_build") == "1";
|
||||
}
|
||||
|
||||
bool PropertyBuilders::CanBuild(const Player& player, const LWOOBJID owner, const bool ownerBuilding, const bool bestFriendsBuild) {
|
||||
if (owner == LWOOBJID_EMPTY || player.id == LWOOBJID_EMPTY) return false;
|
||||
if (player.id == owner) return true;
|
||||
if (!bestFriendsBuild || !player.isBestFriend) return false;
|
||||
// Joining needs the owner building; staying doesn't
|
||||
return ownerBuilding || player.isBuilding;
|
||||
}
|
||||
|
||||
LWOOBJID PropertyBuilders::Placer(const LWOOBJID placedBy, const LWOOBJID owner) {
|
||||
return placedBy == LWOOBJID_EMPTY ? owner : placedBy;
|
||||
}
|
||||
|
||||
PropertyBuilders::eModelReturn PropertyBuilders::PlanModelReturn(const LWOOBJID picker, const LWOOBJID placer, const bool placerHere, const int32_t deleteReason) {
|
||||
if (picker == placer) return eModelReturn::PICKER;
|
||||
// Taking a model apart opens it from the picker's own inventory (BBBLoadItemRequest), where it wouldn't be
|
||||
if (static_cast<BrickByBrick::eDeleteReason>(deleteReason) == BrickByBrick::eDeleteReason::BREAKING_MODEL_APART) return eModelReturn::NOT_THEIRS;
|
||||
return placerHere ? eModelReturn::PLACER : eModelReturn::PLACER_AWAY;
|
||||
}
|
||||
44
dGame/dUtilities/PropertyBuilders.h
Normal file
44
dGame/dUtilities/PropertyBuilders.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef __PROPERTYBUILDERS__H__
|
||||
#define __PROPERTYBUILDERS__H__
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
/**
|
||||
* Who builds on a property (docs/PropertyBuilding.md): the owner, and with property_bff_build the owner's best friends
|
||||
* while the owner is building. The rules here have no game state (unit tested); PropertyManagementComponent applies
|
||||
* them.
|
||||
*/
|
||||
namespace PropertyBuilders {
|
||||
// worldconfig.ini's property_bff_build
|
||||
bool BestFriendsBuild();
|
||||
|
||||
// Where a player stands on the property
|
||||
struct Player {
|
||||
LWOOBJID id{};
|
||||
bool isBestFriend{}; // of the owner
|
||||
bool isBuilding{}; // in build mode on the property
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a player can build: the owner always. A best friend of the owner (with property_bff_build) while the owner
|
||||
* is building, and after the owner stopped until they leave build mode themselves.
|
||||
*/
|
||||
bool CanBuild(const Player& player, LWOOBJID owner, bool ownerBuilding, bool bestFriendsBuild);
|
||||
|
||||
// Who placed a model, from its properties_contents.placed_by (0, NULL in the database: the owner)
|
||||
LWOOBJID Placer(LWOOBJID placedBy, LWOOBJID owner);
|
||||
|
||||
enum class eModelReturn {
|
||||
PICKER, // the player taking it off placed it: into their inventory
|
||||
PLACER, // someone else placed it and is in this world: into the placer's inventory
|
||||
PLACER_AWAY, // someone else placed it and isn't in this world: it stays on the property
|
||||
NOT_THEIRS, // someone else placed it and the picker wants to take it apart: it stays on the property
|
||||
};
|
||||
|
||||
// Where a model taken off the property goes (DeleteModelFromClient's reason, see BrickByBrick::eDeleteReason)
|
||||
eModelReturn PlanModelReturn(LWOOBJID picker, LWOOBJID placer, bool placerHere, int32_t deleteReason);
|
||||
}
|
||||
|
||||
#endif //!__PROPERTYBUILDERS__H__
|
||||
81
docs/PropertyBuilding.md
Normal file
81
docs/PropertyBuilding.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Property building
|
||||
|
||||
Who can build on a property, and what happens to the models they place. The rules are in
|
||||
`dGame/dUtilities/PropertyBuilders.h` (unit tested in `tests/dGameTests/PropertyBuildersTests.cpp`);
|
||||
`PropertyManagementComponent` applies them.
|
||||
|
||||
## Setting (worldconfig.ini)
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `property_bff_build` | `0` | `1`: best friends of the owner can join the owner's build mode on the property. `0` (as live): only the owner builds. |
|
||||
|
||||
It is read when used; a config reload applies it to players already on the property.
|
||||
|
||||
## Who can build
|
||||
|
||||
`PropertyManagementComponent::CanBuild(player)`:
|
||||
|
||||
- The owner: always. Only the owner starts build mode (`PropertyEditorBegin`).
|
||||
- A best friend of the owner (`friends.best_friend = 3`), with `property_bff_build=1`:
|
||||
- while the owner is in build mode: can enter build mode and build;
|
||||
- after the owner left build mode (or the world): keeps building while still in build mode; once they leave build
|
||||
mode they can't enter it again until the owner enters it again.
|
||||
- Anyone else, and anyone on an unclaimed property: never.
|
||||
|
||||
Best friend status is looked up from the database once per player (when they load or are first checked) and again
|
||||
when the property's owner changes.
|
||||
|
||||
Building means: entering build mode (`PropertyEditorBegin`/`End`), placing, moving and picking up models
|
||||
(`UpdateModelFromClient`, `DeleteModelFromClient`), saving and placing brick by brick models (`BBBSaveRequest`,
|
||||
`UnUseBBBModel` with a world position) and editing behaviors (`ControlBehaviors`). Each uses the builder's own
|
||||
inventory, missions and client; `ControlBehaviors` answers the player who sent it.
|
||||
|
||||
Owner only, whatever the setting: privacy (`SetPropertyAccess`) and the property's name and description
|
||||
(`UpdatePropertyOrModelForFilterCheck`).
|
||||
|
||||
## Property data per player
|
||||
|
||||
The client allows editing only when `DownloadPropertyData.ownerId` is its own character (without it the client shows
|
||||
`PRECONDITION_OWN_PROPERTY`). Every `DownloadPropertyData` is sent to each player on their own address: a player who
|
||||
can build and is not the owner gets their own character id as `ownerId`; everyone else gets the owner's.
|
||||
`ownerName` is always the owner's. The plaque then shows a builder the owner's management screen; privacy and name
|
||||
changes from it are refused.
|
||||
|
||||
The rights each player was last sent are kept. When they change (the owner enters or leaves build mode, a best friend
|
||||
leaves build mode, a builder leaves the world, the setting changes, the property is claimed) each player whose
|
||||
rights changed gets `DownloadPropertyData` again. A player in build mode who lost the right (the setting turned off)
|
||||
is sent `SetBuildModeConfirmed` with `start=false`, their building ends as if they had left build mode, and they get a
|
||||
chat message.
|
||||
|
||||
`SetBuildModeConfirmed` goes only to the player who asked. `GetModelsOnProperty` goes to each player, targeting their
|
||||
own character.
|
||||
|
||||
## Building together
|
||||
|
||||
- The first builder to enter build mode (the owner) makes the property private (keeping the previous privacy),
|
||||
pauses and resets the models, smashes spawned property enemies and sends every player who can't build to the
|
||||
property's launch zone. Best friends who can build stay.
|
||||
- Later builders only push their equipped items.
|
||||
- Each builder leaving build mode saves the property. The last one sets the property back to pending moderation,
|
||||
restores the previous privacy, resumes the models and smashes spawned property enemies.
|
||||
- A builder leaving the world while in build mode counts as leaving build mode.
|
||||
|
||||
Saving uses the property's id and owner id; it does not need the owner to be in the world.
|
||||
|
||||
## Who placed a model
|
||||
|
||||
`properties_contents.placed_by` (MySQL migration 93, SQLite migration 76) is the character who placed the model:
|
||||
written when a model is placed from an inventory or saved from brick by brick building. `NULL` (models placed before
|
||||
the column, dashboard imports) means the owner. A model's behaviors are saved under its placer's character.
|
||||
|
||||
A model taken off the property (`DeleteModelFromClient`) goes to its placer:
|
||||
|
||||
| Taken off by | Placer in the world | Result |
|
||||
|---|---|---|
|
||||
| The placer | - | Into the placer's MODELS, as before (carried when picked up). |
|
||||
| Another builder | Yes | Into the placer's MODELS, not carried. Both get a chat message. The builder gets `PlaceModelResponse` 16 (removed). |
|
||||
| Another builder | No | Stays placed; the builder gets a chat message. |
|
||||
| Another builder, taking it apart | - | Stays placed; only the placer takes their model apart. |
|
||||
|
||||
If the placer's inventory can't take it, the model stays placed. Nothing is deleted or mailed.
|
||||
@@ -81,6 +81,11 @@ cdclient_mismatch_message=We detected that your client is out of date. Please up
|
||||
# Auto reject properties which contain no models | must be 1 in order to auto reject.
|
||||
auto_reject_empty_properties=0
|
||||
|
||||
# 1: best friends of a property's owner can join the owner's build mode on the property (place, move and pick up
|
||||
# models, brick by brick building, behaviors). Only the owner starts build mode; best friends in it stay until they
|
||||
# leave it. 0 (as live): only the owner builds.
|
||||
property_bff_build=0
|
||||
|
||||
# comma delimited list of items to not drop in hardcore mode
|
||||
hardcore_excluded_item_drops=6086,7044
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ set(DGAMETEST_SOURCES
|
||||
"ItemSetPassiveAbilityTests.cpp"
|
||||
"PowerUpStatisticTests.cpp"
|
||||
"BrickByBrickTests.cpp"
|
||||
"PropertyBuildersTests.cpp"
|
||||
"ModularBuildTableTests.cpp"
|
||||
"UgcManifestTests.cpp"
|
||||
"SceneGhostingTests.cpp"
|
||||
|
||||
83
tests/dGameTests/PropertyBuildersTests.cpp
Normal file
83
tests/dGameTests/PropertyBuildersTests.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "PropertyBuilders.h"
|
||||
#include "BrickByBrick.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// Who builds on a property and where models taken off it go (docs/PropertyBuilding.md).
|
||||
namespace {
|
||||
constexpr LWOOBJID OWNER = 1152921510000000001LL;
|
||||
constexpr LWOOBJID FRIEND = 1152921510000000002LL;
|
||||
constexpr LWOOBJID VISITOR = 1152921510000000003LL;
|
||||
|
||||
int32_t Reason(const BrickByBrick::eDeleteReason reason) { return static_cast<int32_t>(reason); }
|
||||
|
||||
bool CanBuild(const LWOOBJID id, const bool isBestFriend, const bool isBuilding, const bool ownerBuilding, const bool bestFriendsBuild, const LWOOBJID owner = OWNER) {
|
||||
return PropertyBuilders::CanBuild({ .id = id, .isBestFriend = isBestFriend, .isBuilding = isBuilding }, owner, ownerBuilding, bestFriendsBuild);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, OnlyTheOwnerBuildsByDefault) {
|
||||
EXPECT_TRUE(CanBuild(OWNER, false, false, false, false));
|
||||
EXPECT_FALSE(CanBuild(FRIEND, true, false, true, false));
|
||||
EXPECT_FALSE(CanBuild(FRIEND, true, true, true, false));
|
||||
EXPECT_FALSE(CanBuild(VISITOR, false, false, true, false));
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, NobodyBuildsOnAnUnclaimedProperty) {
|
||||
EXPECT_FALSE(CanBuild(FRIEND, true, true, true, true, LWOOBJID_EMPTY));
|
||||
EXPECT_FALSE(CanBuild(LWOOBJID_EMPTY, false, false, false, true, LWOOBJID_EMPTY));
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, TheOwnerStartsBuilding) {
|
||||
// A best friend can't start: the owner isn't building
|
||||
EXPECT_FALSE(CanBuild(FRIEND, true, false, false, true));
|
||||
// The owner always can
|
||||
EXPECT_TRUE(CanBuild(OWNER, false, false, false, true));
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, BestFriendsJoinWhileTheOwnerBuilds) {
|
||||
EXPECT_TRUE(CanBuild(FRIEND, true, false, true, true));
|
||||
EXPECT_TRUE(CanBuild(FRIEND, true, true, true, true));
|
||||
// Not a best friend: never
|
||||
EXPECT_FALSE(CanBuild(VISITOR, false, false, true, true));
|
||||
EXPECT_FALSE(CanBuild(VISITOR, false, true, true, true));
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, BestFriendsStayAfterTheOwnerStops) {
|
||||
// Still in build mode: keeps building
|
||||
EXPECT_TRUE(CanBuild(FRIEND, true, true, false, true));
|
||||
// Left build mode: can't come back until the owner builds again
|
||||
EXPECT_FALSE(CanBuild(FRIEND, true, false, false, true));
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, ModelsWithNoPlacerAreTheOwners) {
|
||||
EXPECT_EQ(PropertyBuilders::Placer(LWOOBJID_EMPTY, OWNER), OWNER);
|
||||
EXPECT_EQ(PropertyBuilders::Placer(FRIEND, OWNER), FRIEND);
|
||||
EXPECT_EQ(PropertyBuilders::Placer(OWNER, OWNER), OWNER);
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, APlayersOwnModelGoesToThem) {
|
||||
using enum BrickByBrick::eDeleteReason;
|
||||
using enum PropertyBuilders::eModelReturn;
|
||||
for (const auto reason : { PICKING_MODEL_UP, RETURNING_MODEL_TO_INVENTORY, BREAKING_MODEL_APART }) {
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(FRIEND, FRIEND, true, Reason(reason)), PICKER);
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(OWNER, OWNER, true, Reason(reason)), PICKER);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, SomeoneElsesModelGoesBackToThem) {
|
||||
using enum BrickByBrick::eDeleteReason;
|
||||
using enum PropertyBuilders::eModelReturn;
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(FRIEND, OWNER, true, Reason(PICKING_MODEL_UP)), PLACER);
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(OWNER, FRIEND, true, Reason(RETURNING_MODEL_TO_INVENTORY)), PLACER);
|
||||
// Not in this world: it stays placed
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(OWNER, FRIEND, false, Reason(PICKING_MODEL_UP)), PLACER_AWAY);
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(FRIEND, OWNER, false, Reason(RETURNING_MODEL_TO_INVENTORY)), PLACER_AWAY);
|
||||
}
|
||||
|
||||
TEST(PropertyBuildersTests, OnlyThePlacerTakesTheirModelApart) {
|
||||
using enum BrickByBrick::eDeleteReason;
|
||||
using enum PropertyBuilders::eModelReturn;
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(FRIEND, OWNER, true, Reason(BREAKING_MODEL_APART)), NOT_THEIRS);
|
||||
EXPECT_EQ(PropertyBuilders::PlanModelReturn(OWNER, FRIEND, false, Reason(BREAKING_MODEL_APART)), NOT_THEIRS);
|
||||
}
|
||||
Reference in New Issue
Block a user