feat: web dashboard and playground work

The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 17:52:10 -05:00
parent 854d02f509
commit e213a7aeff
681 changed files with 88310 additions and 1321 deletions

View File

@@ -17,6 +17,7 @@
#include "eGameMasterLevel.h"
#include "eGameActivity.h"
#include "User.h"
#include "EconomyLedger.h"
#include "Database.h"
#include "CDRewardCodesTable.h"
#include "Mail.h"
@@ -585,9 +586,9 @@ void CharacterComponent::TrackQuickBuildComplete() {
}
void CharacterComponent::TrackRaceCompleted(bool won) {
m_RacesFinished++;
UpdatePlayerStatistic(RacesFinished);
if (won)
m_FirstPlaceRaceFinishes++;
UpdatePlayerStatistic(FirstPlaceRaceFinishes);
}
void CharacterComponent::TrackPositionUpdate(const NiPoint3& newPosition) {
@@ -615,7 +616,9 @@ void CharacterComponent::HandleZoneStatisticsUpdate(LWOMAPID zoneID, const std::
}
}
void CharacterComponent::UpdatePlayerStatistic(StatisticID updateID, uint64_t updateValue) {
void CharacterComponent::UpdatePlayerStatistic(StatisticID updateID, uint64_t updateValue, bool fromClient) {
if (!fromClient) EconomyLedger::RecordStat(m_Parent, updateID, updateValue);
switch (updateID) {
case CurrencyCollected:
m_CurrencyCollected += updateValue;
@@ -945,3 +948,8 @@ void CharacterComponent::LoadVisitedLevelsXml(const tinyxml2::XMLElement& vl) {
}
// </vl>
}
void CharacterComponent::SetUScore(int64_t uscore, uint32_t source) {
if (const auto* character = m_Parent->GetCharacter()) EconomyLedger::RecordUScore(character->GetID(), uscore - m_Uscore, source);
m_Uscore = uscore;
}

View File

@@ -12,6 +12,7 @@
#include <array>
#include <set>
#include "Loot.h"
#include "StatisticID.h"
enum class eGameActivity : uint32_t;
@@ -34,39 +35,6 @@ struct ZoneStatistics {
bool operator==(const ZoneStatistics& rhs) const = default;
};
/**
* The IDs of each of the possible statistics
*/
enum StatisticID {
CurrencyCollected = 1,
BricksCollected,
SmashablesSmashed,
QuickBuildsCompleted,
EnemiesSmashed,
RocketsUsed,
MissionsCompleted,
PetsTamed,
ImaginationPowerUpsCollected,
LifePowerUpsCollected,
ArmorPowerUpsCollected,
MetersTraveled,
TimesSmashed,
TotalDamageTaken,
TotalDamageHealed,
TotalArmorRepaired,
TotalImaginationRestored,
TotalImaginationUsed,
DistanceDriven,
TimeAirborneInCar,
RacingImaginationPowerUpsCollected,
RacingImaginationCratesSmashed,
RacingCarBoostsActivated,
RacingTimesWrecked,
RacingSmashablesSmashed,
RacesFinished,
FirstPlaceRaceFinishes,
};
/**
* Represents a character, including their rockets and stats
*/
@@ -117,8 +85,9 @@ public:
/**
* Sets the universe score for this entity
* @param uscore the universe score to set
* @param source why it changed, for the economy reports (see EconomyLedger for sources beyond eLootSourceType)
*/
void SetUScore(int64_t uscore) { m_Uscore = uscore; }
void SetUScore(int64_t uscore, uint32_t source);
/**
* Gets the current activity that the character is partaking in, see ScriptedActivityComponent for more details
@@ -267,8 +236,9 @@ public:
* Allows one to generically update a statistic
* @param updateID the 1-indexed ID of the statistic in the order of definition below
* @param updateValue the value to update the statistic with
* @param fromClient whether the client reported it; those are not trusted for the dashboard's daily totals
*/
void UpdatePlayerStatistic(StatisticID updateID, uint64_t updateValue = 1);
void UpdatePlayerStatistic(StatisticID updateID, uint64_t updateValue = 1, bool fromClient = false);
/**
* Add a venture vision effect to the player minimap.

View File

@@ -1,4 +1,5 @@
#include "DestroyableComponent.h"
#include "EconomyLedger.h"
#include "BitStream.h"
#include "Logger.h"
#include "Game.h"
@@ -716,6 +717,11 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
auto* owner = Game::entityManager->GetEntity(source);
// Where players die and what killed them (the killer itself, not whoever owns it; 0 when it is already gone)
if (m_Parent->IsPlayer()) {
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::PLAYER_DEATHS, owner ? owner->GetLOT() : 0, m_Parent->GetPosition(), 1, m_Parent);
}
if (owner != nullptr) {
owner = owner->GetOwner(); // If the owner is overwritten, we collect that here
@@ -729,6 +735,12 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
inventoryComponent->TriggerPassiveAbility(PassiveAbilityTrigger::EnemySmashed, m_Parent);
}
if (isEnemy && owner->IsPlayer()) {
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::ENEMY_KILLS, m_Parent->GetLOT(), m_Parent->GetPosition(), 1, owner);
} else if (owner->IsPlayer() && !m_Parent->IsPlayer()) {
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::SMASHABLES_SMASHED, m_Parent->GetLOT(), m_Parent->GetPosition(), 1, owner);
}
auto* missions = owner->GetComponent<MissionComponent>();
if (missions != nullptr) {
@@ -785,6 +797,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
lootMsg.item = LOT_NULL;
GameMessages::DeliverLocally(lootMsg);
character->SetCoins(coinsTotal, eLootSourceType::DELETION);
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::PLAYER_COIN_DROPS, 0, m_Parent->GetPosition(), static_cast<int64_t>(coinsToLose), m_Parent);
}
}
@@ -970,7 +983,7 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
auto uscoreToLose = static_cast<uint64_t>(uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100.0f));
LOG("Player %llu has lost %llu uscore!", m_Parent->GetObjectID(), uscoreToLose);
character->SetUScore(uscore - uscoreToLose);
character->SetUScore(uscore - uscoreToLose, EconomyLedger::HARDCORE_DEATH_SOURCE);
GameMessages::SendModifyLEGOScore(m_Parent, m_Parent->GetSystemAddress(), -uscoreToLose, eLootSourceType::MISSION);
@@ -1013,6 +1026,7 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
//lose all coins:
chars->SetCoins(coins, eLootSourceType::NONE);
if (coinsToDrop > 0) EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::PLAYER_COIN_DROPS, 0, m_Parent->GetPosition(), static_cast<int64_t>(coinsToDrop), m_Parent);
//drop all coins:
constexpr auto MAX_TO_DROP_PER_GM = 100'000;
@@ -1054,7 +1068,7 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
int uscore = maxHealth * Game::entityManager->GetHardcoreUscoreEnemiesMultiplier() * uscoreReduction;
LOG("Rewarding player %llu with %i uscore for killing enemy %i", player->GetObjectID(), uscore, lot);
playerStats->SetUScore(playerStats->GetUScore() + uscore);
playerStats->SetUScore(playerStats->GetUScore() + uscore, EconomyLedger::HARDCORE_KILL_SOURCE);
GameMessages::SendModifyLEGOScore(player, player->GetSystemAddress(), uscore, eLootSourceType::MISSION);
Game::entityManager->SerializeEntity(m_Parent);

View File

@@ -1,10 +1,12 @@
#include "InventoryComponent.h"
#include "EconomyLedger.h"
#include <sstream>
#include "Entity.h"
#include "Item.h"
#include "Game.h"
#include "dServer.h"
#include "Logger.h"
#include "CDClientManager.h"
#include "ObjectIDManager.h"
@@ -309,6 +311,58 @@ void InventoryComponent::AddItem(
}
}
ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, const uint32_t count, eLootSourceType lootSourceType, const LwoNameValue& config, const LWOOBJID subKey, const bool bound, const ReceiveItemOptions& options) {
if (count == 0 || !Inventory::IsValidItem(lot)) return {};
const auto inventoryType = options.inventory != INVALID ? options.inventory : Inventory::FindInventoryTypeForLot(lot);
auto* inventory = GetInventory(inventoryType);
if (!inventory) return {};
const bool unique = !config.values.empty() || subKey != LWOOBJID_EMPTY || bound || IsUniqueLot(lot);
const bool keepId = id != LWOOBJID_EMPTY && FindItemById(id) == nullptr && (unique || inventory->FindItemByLot(lot) == nullptr);
int32_t slot = -1;
if (keepId) slot = options.preferredSlot != -1 && inventory->IsSlotEmpty(options.preferredSlot) ? options.preferredSlot : inventory->FindEmptySlot();
if (slot != -1) {
auto* item = new Item(id, lot, inventory, slot, count, bound || Inventory::FindItemComponent(lot).isBOP, config, LWOOBJID_EMPTY, subKey, lootSourceType);
GameMessages::SendAddItemToInventoryClientSync(m_Parent, m_Parent->GetSystemAddress(), item, id, options.showFlyingLoot && !options.equip, static_cast<int>(count), subKey, lootSourceType);
EconomyLedger::RecordItems(m_Parent, lot, count, static_cast<uint32_t>(lootSourceType));
if (options.equip) {
item->Equip();
Game::entityManager->SerializeEntity(m_Parent);
}
auto* missions = m_Parent->GetComponent<MissionComponent>();
if (missions && !IsTransferInventory(inventoryType)) missions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", count, IsTransferInventory(options.sourceInventory));
return { id, false };
}
// Merge (or add a new stack) the normal way, then see which stack took the items
std::map<LWOOBJID, uint32_t> before;
for (const auto& [itemId, item] : inventory->GetItems()) {
if (item->GetLot() == lot) before[itemId] = item->GetCount();
}
AddItem(lot, count, lootSourceType, inventoryType, config, LWOOBJID_EMPTY, options.showFlyingLoot, options.equip, subKey, options.sourceInventory, 0, bound, options.preferredSlot);
LWOOBJID receivedBy = LWOOBJID_EMPTY;
int64_t largestGain = 0;
for (const auto& [itemId, item] : inventory->GetItems()) {
if (item->GetLot() != lot) continue;
const auto previous = before.find(itemId);
const int64_t gain = static_cast<int64_t>(item->GetCount()) - (previous == before.end() ? 0 : previous->second);
if (gain > largestGain) {
largestGain = gain;
receivedBy = itemId;
}
}
return { receivedBy, before.contains(receivedBy) };
}
bool InventoryComponent::IsUniqueLot(const LOT lot) {
const auto& info = Inventory::FindItemComponent(lot);
const bool isBrick = Inventory::FindInventoryTypeForLot(lot) == eInventoryType::BRICKS || (info.stackSize == 0 && info.itemType == 1);
return !isBrick && info.stackSize <= 1;
}
bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInventoryType inventoryType, const bool ignoreBound, const bool silent) {
if (count == 0) {
LOG("Attempted to remove 0 of item (%i) from the inventory!", lot);
@@ -356,8 +410,28 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in
const auto subkey = item->GetSubKey();
// Moving between a player's own inventories neither creates nor destroys anything, except for the vendor's
// buyback inventory: selling takes the item out of the game until it is bought back
EconomyLedger::ScopedItemTransfer moving;
const auto originType = origin->GetType();
uint32_t moved = 0;
// Each move gives the items a new object id in the other inventory (as upstream, and as live did), so the ledger
// records it for item traces: the old id, the stack that took the items and whether that stack was already there
auto* character = m_Parent->GetCharacter();
const auto zone = Game::server ? Game::server->GetZoneID() : 0;
const auto recordMove = [&](const LWOOBJID oldId, const ReceivedItem& received, const uint32_t moveCount) {
if (!character || received.id == LWOOBJID_EMPTY || received.id == oldId) return;
EconomyLedger::RecordTransfer({ .method = IEconomyLedger::eTransferMethod::INVENTORY_MOVE, .itemId = oldId, .newItemId = received.id,
.lot = lot, .count = moveCount, .fromCharacter = character->GetID(), .toCharacter = character->GetID(), .zone = zone,
.merged = received.merged });
};
const ReceiveItemOptions moveOptions{ .inventory = inventory, .showFlyingLoot = showFlyingLot, .preferredSlot = preferredSlot, .sourceInventory = originType };
// Items move the way upstream moves them (a new object in the other inventory); only the ledger bookkeeping is added
if (subkey == LWOOBJID_EMPTY && item->GetConfig().values.empty() && (!item->GetBound() || (item->GetBound() && item->GetInfo().isBOP))) {
auto left = std::min<uint32_t>(count, origin->GetLotCount(lot));
moved = left;
while (left > 0) {
if (item == nullptr) {
@@ -372,9 +446,13 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in
left -= delta;
AddItem(lot, delta, eLootSourceType::NONE, inventory, {}, LWOOBJID_EMPTY, showFlyingLot, isModMoveAndEquip, LWOOBJID_EMPTY, origin->GetType(), 0, false, preferredSlot);
auto options = moveOptions;
options.equip = isModMoveAndEquip;
const auto oldId = item->GetId();
const auto received = ReceiveItem(LWOOBJID_EMPTY, lot, delta, eLootSourceType::NONE, {}, LWOOBJID_EMPTY, false, options);
item->SetCount(item->GetCount() - delta, false, false);
recordMove(oldId, received, delta);
isModMoveAndEquip = false;
}
@@ -382,10 +460,21 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in
const auto config = item->GetConfig();
const auto delta = std::min<uint32_t>(item->GetCount(), count);
moved = delta;
AddItem(lot, delta, eLootSourceType::NONE, inventory, config, LWOOBJID_EMPTY, showFlyingLot, isModMoveAndEquip, subkey, origin->GetType(), 0, item->GetBound(), preferredSlot);
auto options = moveOptions;
options.equip = isModMoveAndEquip;
const auto oldId = item->GetId();
const auto received = ReceiveItem(LWOOBJID_EMPTY, lot, delta, eLootSourceType::NONE, config, subkey, item->GetBound(), options);
item->SetCount(item->GetCount() - delta, false, false);
recordMove(oldId, received, delta);
}
if (inventory == eInventoryType::VENDOR_BUYBACK && originType != eInventoryType::VENDOR_BUYBACK) {
EconomyLedger::RecordItemsUnsuppressed(m_Parent, lot, -static_cast<int64_t>(moved), static_cast<uint32_t>(eLootSourceType::VENDOR));
} else if (originType == eInventoryType::VENDOR_BUYBACK && inventory != eInventoryType::VENDOR_BUYBACK) {
EconomyLedger::RecordItemsUnsuppressed(m_Parent, lot, moved, static_cast<uint32_t>(eLootSourceType::VENDOR));
}
auto* missionComponent = m_Parent->GetComponent<MissionComponent>();

View File

@@ -40,6 +40,21 @@ namespace GameMessages {
* of different types, each type representing a different group of items, see `eInventoryType` for a list of
* inventories.
*/
// Where and how ReceiveItem puts items; the defaults suit items arriving from someone else
struct ReceiveItemOptions {
eInventoryType inventory = INVALID; // INVALID: the item's usual inventory
bool showFlyingLoot = true;
bool equip = false;
int32_t preferredSlot = -1;
eInventoryType sourceInventory = INVALID; // set when moving between the player's own inventories
};
// Which item or stack ReceiveItem put the items in
struct ReceivedItem {
LWOOBJID id = LWOOBJID_EMPTY; // LWOOBJID_EMPTY if nothing could be added
bool merged = false; // joined a stack that was already there (and has a history of its own)
};
class InventoryComponent final : public Component {
public:
struct Group {
@@ -145,6 +160,19 @@ public:
int32_t preferredSlot = -1
);
/**
* Receive items that already exist elsewhere (another player's inventory, the mail), keeping them traceable:
* - a unique item (does not stack, or carries config/subkey) keeps its object id
* - stackable items merge into this inventory's stack of the same LOT if there is one, taking that stack's id;
* with nothing to merge into, a whole stack keeps its id and a partial one gets a new id
* @param id the items' current object id, or LWOOBJID_EMPTY for part of a stack
* @return the object id of the item or stack that received them, and whether that stack was already there
*/
ReceivedItem ReceiveItem(LWOOBJID id, LOT lot, uint32_t count, eLootSourceType lootSourceType, const LwoNameValue& config = {}, LWOOBJID subKey = LWOOBJID_EMPTY, bool bound = false, const ReceiveItemOptions& options = {});
// Whether items of this LOT are one of a kind rather than stacking
static bool IsUniqueLot(LOT lot);
/**
* Removes a LOT from the inventory
* @param lot the lot to remove

View File

@@ -5,6 +5,7 @@
#include "Logger.h"
#include "BehaviorStates.h"
#include "PropertyBehaviorActions.h"
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "InventoryComponent.h"
@@ -43,7 +44,7 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults&
if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation);
m_Parent->SetVelocity(NiPoint3Constant::ZERO);
m_Speed = 3.0f;
m_Speed = PropertyBehaviorActions::DEFAULT_SPEED;
m_NumListeningInteract = 0;
m_NumActiveAttack = 0;

View File

@@ -12,6 +12,7 @@
#include "Action.h"
#include "PropertyBehavior.h"
#include "PropertyBehaviorActions.h"
#include "StripUiPosition.h"
class AddMessage;
@@ -209,7 +210,7 @@ private:
LWOOBJID m_userModelID;
// The speed at which this model moves
float m_Speed{ 3.0f };
float m_Speed{ PropertyBehaviorActions::DEFAULT_SPEED };
// Whether or not to restart at the end of the frame.
bool m_RestartAtEndOfFrame{ false };

View File

@@ -1,4 +1,5 @@
#include "PetComponent.h"
#include "DashboardNotify.h"
#include "GameMessages.h"
#include "BrickDatabase.h"
#include "CDClientDatabase.h"
@@ -566,7 +567,8 @@ void PetComponent::RequestSetPetName(std::u16string name) {
if (autoRejectNames && owner && owner->GetCharacter() && owner->GetCharacter()->GetParentUser()->GetIsMuted()) {
m_ModerationStatus = 2; // Approved
std::string forcedName = "Pet";
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus) });
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus), m_Owner });
DashboardNotify::Changed("pet_names", m_DatabaseId);
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
} else {
@@ -574,7 +576,7 @@ void PetComponent::RequestSetPetName(std::u16string name) {
m_Name = "";
//Save our pet's new name to the db:
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name), m_Owner);
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
@@ -606,14 +608,15 @@ void PetComponent::RequestSetPetName(std::u16string name) {
m_Name = "";
std::string forcedName = "Pet";
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus) });
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus), m_Tamer });
DashboardNotify::Changed("pet_names", m_DatabaseId);
LOG("AccountID: %i is muted, forcing use of predefined pet name", tamer->GetCharacter()->GetParentUser()->GetAccountID());
} else {
m_ModerationStatus = 1; // Pending
m_Name = "";
//Save our pet's new name to the db:
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name), m_Tamer);
}
Game::entityManager->SerializeEntity(m_Parent);
@@ -1046,7 +1049,7 @@ PetComponent::~PetComponent() {
m_Owner = LWOOBJID_EMPTY;
}
void PetComponent::SetPetNameForModeration(const std::string& petName) {
void PetComponent::SetPetNameForModeration(const std::string& petName, const LWOOBJID owner) {
int approved = 1; //default, in mod
//Make sure that the name isn't already auto-approved:
@@ -1055,7 +1058,8 @@ void PetComponent::SetPetNameForModeration(const std::string& petName) {
}
//Save to db:
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ petName, approved });
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ petName, approved, owner });
DashboardNotify::Changed("pet_names", m_DatabaseId);
}
void PetComponent::LoadPetNameFromModeration() {
@@ -1071,3 +1075,26 @@ void PetComponent::LoadPetNameFromModeration() {
void PetComponent::SetPreconditions(const std::string& preconditions) {
m_Preconditions = std::make_optional<PreconditionExpression>(preconditions);
}
void PetComponent::ApplyNameModeration(const bool approved) {
auto* owner = GetOwner();
if (approved) {
LoadPetNameFromModeration();
} else {
// Rejected names are removed; the pet goes back to having no name until a new one is chosen
m_ModerationStatus = 0;
m_Name = "";
}
if (owner) {
if (auto* inventoryComponent = owner->GetComponent<InventoryComponent>()) {
auto data = inventoryComponent->GetDatabasePet(m_DatabaseId);
data.name = m_Name;
data.moderationState = m_ModerationStatus;
inventoryComponent->SetDatabasePet(m_DatabaseId, data);
}
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
}
GameMessages::SendPetNameChanged(m_Parent->GetObjectID(), m_ModerationStatus, GeneralUtils::UTF8ToUTF16(m_Name), GeneralUtils::UTF8ToUTF16(m_OwnerName), UNASSIGNED_SYSTEM_ADDRESS);
Game::entityManager->SerializeEntity(m_Parent);
}

View File

@@ -118,6 +118,9 @@ public:
*/
LWOOBJID GetDatabaseId() const;
// A moderator approved or rejected this pet's name (from the dashboard); updates it live for everyone
void ApplyNameModeration(bool approved);
/**
* Returns the ID of the object that the pet is currently interacting with, could be a treasure chest or a switch
* @return the ID of the object that the pet is currently interacting with
@@ -176,8 +179,9 @@ public:
/**
* Sets the name of the pet to be moderated
* @param petName the name of the pet to set
* @param owner the character that owns the pet, saved with the name
*/
void SetPetNameForModeration(const std::string& petName);
void SetPetNameForModeration(const std::string& petName, const LWOOBJID owner);
/**
* Loads the pet name up for moderation along with the moderation status from the database and sets them for this

View File

@@ -1,4 +1,5 @@
#include "PropertyManagementComponent.h"
#include "DashboardNotify.h"
#include <sstream>
@@ -147,6 +148,7 @@ void PropertyManagementComponent::SetPrivacyOption(PropertyPrivacyOption value)
UpdateApprovedStatus(false, "Your property is empty. Please place a model to have a public property.");
} else {
Database::Get()->UpdatePropertyModerationInfo(info);
DashboardNotify::Changed("properties", propertyId);
}
}
@@ -165,6 +167,7 @@ void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::s
Database::Get()->UpdateLastSave(info);
Database::Get()->UpdatePropertyDetails(info);
DashboardNotify::Changed("properties", propertyId);
OnQueryPropertyData(GetOwner(), UNASSIGNED_SYSTEM_ADDRESS);
}
@@ -220,6 +223,7 @@ bool PropertyManagementComponent::Claim(const LWOOBJID playerId) {
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);
@@ -574,6 +578,7 @@ void PropertyManagementComponent::UpdateApprovedStatus(const bool value, const s
info.rejectionReason = rejectionReason;
Database::Get()->UpdatePropertyModerationInfo(info);
DashboardNotify::Changed("properties", propertyId);
}
void PropertyManagementComponent::Load() {
@@ -718,6 +723,7 @@ void PropertyManagementComponent::Save() {
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) {
@@ -812,3 +818,10 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess
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);
}

View File

@@ -1,30 +1,11 @@
#pragma once
#include "ePropertyPrivacyOption.h"
#include <chrono>
#include "Entity.h"
#include "Component.h"
#include "eReplicaComponentType.h"
/**
* Information regarding which players may visit this property
*/
enum class PropertyPrivacyOption {
/**
* Default, only you can visit your property
*/
Private = 0,
/**
* Your friends can visit your property
*/
Friends = 1,
/**
* Requires Mythran approval, everyone can visit your property
*/
Public = 2
};
/**
* Main component that handles interactions with a property, generally the plaques you see on properties.
*/
@@ -137,6 +118,9 @@ public:
*/
void UpdateApprovedStatus(bool value, const std::string& rejectionReason = "");
// A moderator decided on this property from the dashboard (already saved): keep this world's copy in step
void ApplyModeration(bool approved, const std::string& reason);
/**
* Loads all the models on this property from the database
*/

View File

@@ -24,6 +24,7 @@
#include "CppScripts.h"
#include "StringifiedEnum.h"
#include "Amf3.h"
#include "EconomyLedger.h"
QuickBuildComponent::QuickBuildComponent(Entity* const entity, const int32_t componentID) : Component{ entity, componentID } {
std::u16string checkPreconditions = entity->GetVar<std::u16string>(u"CheckPrecondition");
@@ -415,6 +416,7 @@ void QuickBuildComponent::CompleteQuickBuild(Entity* const user) {
if (characterComponent != nullptr) {
characterComponent->SetCurrentActivity(eGameActivity::NONE);
characterComponent->TrackQuickBuildComplete();
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::QUICKBUILDS_COMPLETED, m_Parent->GetLOT(), m_Parent->GetPosition(), 1, user);
} else {
LOG("Some user tried to finish the rebuild but they didn't have a character somehow.");
return;