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:
Aaron Kimbrell
2026-09-28 12:08:29 -05:00
parent 6b7d95acbd
commit df922e3cd9
15 changed files with 555 additions and 88 deletions

View File

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

View File

@@ -8,6 +8,7 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
"GUID.cpp"
"LiveEvents.cpp"
"Contraband.cpp"
"PropertyBuilders.cpp"
"PropertyRent.cpp"
"PropertyReputation.cpp"
"Loot.cpp"

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

View 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__