Files
DarkflameServer/dGame/dUtilities/PlayerReports.cpp
Aaron Kimbrell e213a7aeff 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>
2026-09-28 22:30:43 -05:00

124 lines
4.9 KiB
C++

#include "PlayerReports.h"
#include <ctime>
#include "BitStream.h"
#include "Database.h"
#include "DashboardNotify.h"
#include "Entity.h"
#include "EntityManager.h"
#include "Character.h"
#include "User.h"
#include "Game.h"
#include "dServer.h"
#include "dZoneManager.h"
#include "Logger.h"
#include "GeneralUtils.h"
#include "PetComponent.h"
#include "PropertyManagementComponent.h"
#include "ePlayerReportKind.h"
#include "magic_enum.hpp"
namespace {
constexpr uint32_t MAX_BODY = 2000;
// Who sent it and where they are
IModeration::PlayerReport NewReport(Entity* reporter, ePlayerReportKind kind, const std::string& body) {
IModeration::PlayerReport report;
report.createdAt = static_cast<int64_t>(std::time(nullptr));
report.kind = std::string(magic_enum::enum_name(kind));
report.body = body.substr(0, MAX_BODY);
if (const auto* character = reporter ? reporter->GetCharacter() : nullptr) {
report.reporterId = character->GetID();
if (const auto* user = character->GetParentUser()) report.reporterAccountId = user->GetAccountID();
}
report.zoneId = Game::server->GetZoneID();
report.instanceId = Game::server->GetInstanceID();
report.cloneId = Game::zoneManager->GetZoneID().GetCloneID();
return report;
}
// The player a report is about, when the world knows
void SetTarget(IModeration::PlayerReport& report, LWOOBJID characterId) {
const auto info = characterId ? Database::Get()->GetCharacterInfo(characterId) : std::nullopt;
if (!info) return;
report.targetCharacterId = info->id;
report.targetAccountId = info->accountId;
}
PlayerReports::RateLimit g_RateLimit;
void Save(const IModeration::PlayerReport& report) {
// By account (a player can't get more by switching characters), or by character when the account is unknown
const uint64_t reporter = report.reporterAccountId != 0 ? report.reporterAccountId : static_cast<uint64_t>(report.reporterId) | (1ULL << 63);
if (!g_RateLimit.Allow(reporter, report.createdAt)) {
LOG("Dropped a player report (%s) from account %u / character %llu: over the report limit", report.kind.c_str(), report.reporterAccountId, report.reporterId);
return;
}
const auto id = Database::Get()->InsertPlayerReport(report);
LOG("Player report %llu (%s) from %llu about %llu", id, report.kind.c_str(), report.reporterId, report.targetCharacterId);
DashboardNotify::Changed("player_reports");
}
// Description (u32 length + UTF-16) followed by an object ID
bool ReadDescriptionAndObject(RakNet::BitStream& inStream, std::string& description, LWOOBJID& objectId) {
uint32_t length{};
if (!inStream.Read(length) || length > MAX_BODY) return false;
std::u16string text;
text.reserve(length);
for (uint32_t i = 0; i < length; i++) {
char16_t character{};
if (!inStream.Read(character)) return false;
text.push_back(character);
}
description = GeneralUtils::UTF16ToWTF8(text);
return inStream.Read(objectId);
}
}
namespace PlayerReports {
void ReportPlayer(Entity* reporter, LWOOBJID reportedId, const std::string& body) {
auto report = NewReport(reporter, ePlayerReportKind::PLAYER, body);
report.objectId = reportedId;
// The client sends the player's object ID; when they're in this world, their character says which one it is
const auto* reported = Game::entityManager->GetEntity(reportedId);
const auto* character = reported ? reported->GetCharacter() : nullptr;
SetTarget(report, character ? character->GetID() : reportedId);
Save(report);
}
void HandleReportOffensiveModel(RakNet::BitStream& inStream, Entity* reporter) {
std::string description;
LWOOBJID objectId{};
if (!ReadDescriptionAndObject(inStream, description, objectId)) return;
auto report = NewReport(reporter, ePlayerReportKind::MODEL, description);
report.objectId = objectId;
auto* object = Game::entityManager->GetEntity(objectId);
if (object) report.objectLot = object->GetLOT();
// A model placed on this property belongs to the property's owner; a pet to its owner
auto* property = PropertyManagementComponent::Instance();
if (property && property->GetModels().contains(objectId)) {
report.propertyId = property->GetId();
SetTarget(report, property->GetOwnerId());
} else if (auto* pet = object ? object->GetComponent<PetComponent>() : nullptr) {
SetTarget(report, pet->GetOwnerId());
}
Save(report);
}
void HandleReportOffensiveProperty(RakNet::BitStream& inStream, Entity* reporter) {
std::string description;
LWOOBJID plaqueId{};
if (!ReadDescriptionAndObject(inStream, description, plaqueId)) return;
auto report = NewReport(reporter, ePlayerReportKind::PROPERTY, description);
report.objectId = plaqueId;
if (auto* plaque = Game::entityManager->GetEntity(plaqueId)) report.objectLot = plaque->GetLOT();
// The plaque stands on the property this world holds
if (auto* property = PropertyManagementComponent::Instance()) {
report.propertyId = property->GetId();
SetTarget(report, property->GetOwnerId());
}
Save(report);
}
}