mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-19 03:02:49 +00:00
feat: contraband list with flagging and optional removal
Staff list contraband items on a new dashboard page (item search, reason, flag or flag and remove; contraband_manage to edit, reports_view to see). World servers check every inventory when a character loads and every item a player receives: each find is an economy flag of the new kind Contraband, shown with the other flags and in the character's related data. Items marked for removal are taken away, with a character snapshot kept first so they can be given back, an audit entry and a mail or chat message telling the player why. Staff are skipped unless contraband_ignore_staff is off. Worlds reload the list when it changes (RELOAD_CONTRABAND, added at the end of ePlayerAction). Fixes #1563 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include "InventoryComponent.h"
|
||||
#include "Contraband.h"
|
||||
#include "EconomyLedger.h"
|
||||
|
||||
#include <sstream>
|
||||
@@ -204,6 +205,8 @@ void InventoryComponent::AddItem(
|
||||
inventoryType = Inventory::FindInventoryTypeForLot(lot);
|
||||
}
|
||||
|
||||
Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, inventorySourceType);
|
||||
|
||||
auto* missions = static_cast<MissionComponent*>(this->m_Parent->GetComponent(eReplicaComponentType::MISSION));
|
||||
|
||||
auto* inventory = GetInventory(inventoryType);
|
||||
@@ -336,6 +339,7 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c
|
||||
addItem.showFlyingLoot = options.showFlyingLoot && !options.equip;
|
||||
addItem.SendToClient(m_Parent->GetSystemAddress());
|
||||
EconomyLedger::RecordItems(m_Parent, lot, count, static_cast<uint32_t>(lootSourceType));
|
||||
Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, options.sourceInventory);
|
||||
if (options.equip) {
|
||||
item->Equip();
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
|
||||
@@ -6,6 +6,7 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
|
||||
"DashboardNotify.cpp"
|
||||
"GUID.cpp"
|
||||
"LiveEvents.cpp"
|
||||
"Contraband.cpp"
|
||||
"Loot.cpp"
|
||||
"Mail.cpp"
|
||||
"MessageInspector.cpp"
|
||||
|
||||
199
dGame/dUtilities/Contraband.cpp
Normal file
199
dGame/dUtilities/Contraband.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
#include "Contraband.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
|
||||
#include "magic_enum.hpp"
|
||||
|
||||
#include "Character.h"
|
||||
#include "ChatPackets.h"
|
||||
#include "Database.h"
|
||||
#include "dConfig.h"
|
||||
#include "dServer.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "EconomyLedger.h"
|
||||
#include "Entity.h"
|
||||
#include "Game.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Inventory.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "Item.h"
|
||||
#include "Logger.h"
|
||||
#include "Mail.h"
|
||||
#include "User.h"
|
||||
#include "ZCompression.h"
|
||||
|
||||
namespace {
|
||||
std::optional<Contraband::List> g_List;
|
||||
|
||||
constexpr const char* ACTOR = "World server";
|
||||
|
||||
bool IgnoreStaff() {
|
||||
return !Game::config || Game::config->GetValue("contraband_ignore_staff") != "0";
|
||||
}
|
||||
|
||||
bool NotifyPlayers() {
|
||||
return !Game::config || Game::config->GetValue("contraband_notify_players") != "0";
|
||||
}
|
||||
|
||||
eGameMasterLevel AccountLevel(const Entity* player) {
|
||||
const auto* character = player ? player->GetCharacter() : nullptr;
|
||||
const auto* user = character ? character->GetParentUser() : nullptr;
|
||||
return user ? user->GetMaxGMLevel() : eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
|
||||
std::string Compress(const std::string& data) {
|
||||
std::string out(ZCompression::GetMaxCompressedLength(static_cast<uint32_t>(data.size())), '\0');
|
||||
const auto size = ZCompression::Compress(reinterpret_cast<const uint8_t*>(data.data()), static_cast<uint32_t>(data.size()),
|
||||
reinterpret_cast<uint8_t*>(out.data()), static_cast<uint32_t>(out.size()));
|
||||
out.resize(size > 0 ? static_cast<size_t>(size) : 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Where() {
|
||||
if (!Game::server) return "";
|
||||
return " (zone " + std::to_string(Game::server->GetZoneID()) + " instance " + std::to_string(Game::server->GetInstanceID()) + ")";
|
||||
}
|
||||
|
||||
// Every item a player has (not the proxy items of sets), across every inventory
|
||||
std::vector<Contraband::HeldItem> HeldItems(InventoryComponent& inventory) {
|
||||
std::vector<Contraband::HeldItem> items;
|
||||
for (const auto& [type, inv] : inventory.GetInventories()) {
|
||||
if (!inv) continue;
|
||||
for (const auto& [id, item] : inv->GetItems()) {
|
||||
if (!item || item->GetParent() != LWOOBJID_EMPTY) continue;
|
||||
items.push_back({ id, item->GetLot(), item->GetCount(), type });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// Remove every item of a LOT (all inventories); how many were removed
|
||||
uint32_t RemoveLot(InventoryComponent& inventory, LOT lot, bool silent) {
|
||||
uint32_t removed = 0;
|
||||
for (const auto& [type, inv] : inventory.GetInventories()) {
|
||||
if (!inv) continue;
|
||||
std::vector<Item*> items;
|
||||
for (const auto& [id, item] : inv->GetItems()) if (item && item->GetLot() == lot && item->GetParent() == LWOOBJID_EMPTY) items.push_back(item);
|
||||
for (auto* item : items) {
|
||||
removed += item->GetCount();
|
||||
item->SetCount(0, silent, false, false, eLootSourceType::MODERATION);
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Contraband {
|
||||
std::vector<Finding> Find(const std::vector<HeldItem>& items, const List& list) {
|
||||
std::vector<Finding> found;
|
||||
if (list.empty()) return found;
|
||||
for (const auto& item : items) {
|
||||
const auto it = list.find(item.lot);
|
||||
if (it != list.end() && item.count > 0) found.push_back({ item, it->second });
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bool Applies(eGameMasterLevel accountLevel, bool ignoreStaff) {
|
||||
return !ignoreStaff || accountLevel <= eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
|
||||
bool CountsAsAdded(eLootSourceType source, eInventoryType sourceInventory) {
|
||||
return sourceInventory == eInventoryType::INVALID && source != eLootSourceType::RELOCATE && source != eLootSourceType::INVENTORY;
|
||||
}
|
||||
|
||||
uint32_t Reload() {
|
||||
List list;
|
||||
for (const auto& item : Database::Get()->GetContrabandItems()) list[item.lot] = { item.reason, item.action };
|
||||
LOG("Loaded %zu contraband item(s)", list.size());
|
||||
g_List = std::move(list);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const List& Get() {
|
||||
if (!g_List) Reload();
|
||||
return *g_List;
|
||||
}
|
||||
|
||||
void CheckOnLoad(Entity* player) {
|
||||
auto* character = player ? player->GetCharacter() : nullptr;
|
||||
auto* inventory = player ? player->GetComponent<InventoryComponent>() : nullptr;
|
||||
if (!character || !inventory || Get().empty() || !Applies(AccountLevel(player), IgnoreStaff())) return;
|
||||
|
||||
const auto findings = Find(HeldItems(*inventory), Get());
|
||||
if (findings.empty()) return;
|
||||
|
||||
const auto characterId = character->GetID();
|
||||
const auto accountId = character->GetParentUser() ? character->GetParentUser()->GetAccountID() : 0;
|
||||
bool snapshotTaken = false;
|
||||
std::vector<std::string> removedNames;
|
||||
for (const auto& finding : findings) {
|
||||
const bool remove = finding.entry.action == IContraband::eContrabandAction::REMOVE;
|
||||
const auto where = std::string(magic_enum::enum_name(finding.item.inventory));
|
||||
Database::Get()->InsertEconomyFlag({ 0, IDashboardAdmin::eFlagKind::CONTRABAND, characterId, finding.item.lot, finding.item.id,
|
||||
finding.item.count, remove ? 1 : 0, (remove ? "Removed at login from " : "Found at login in ") + where + ": " + finding.entry.reason });
|
||||
if (!remove) continue;
|
||||
|
||||
// Keep the character as it was, so staff can give the items back from the character page
|
||||
if (!snapshotTaken) {
|
||||
const auto& xml = character->GetXMLData();
|
||||
Database::Get()->InsertCharacterSnapshot({ 0, characterId, static_cast<int64_t>(std::time(nullptr)), "before contraband removal", ACTOR,
|
||||
static_cast<uint32_t>(xml.size()), "", Compress(xml) });
|
||||
snapshotTaken = true;
|
||||
}
|
||||
auto* item = inventory->FindItemById(finding.item.id);
|
||||
if (!item) continue;
|
||||
// Silent: the character hasn't been sent to the client yet
|
||||
item->SetCount(0, true, false, false, eLootSourceType::MODERATION);
|
||||
removedNames.push_back(std::to_string(finding.item.count) + "x " + std::to_string(finding.item.lot));
|
||||
Database::Get()->InsertAuditLog(0, ACTOR, "contraband_removed", character->GetName() + ": removed " + std::to_string(finding.item.count) +
|
||||
" of item " + std::to_string(finding.item.lot) + " (id " + std::to_string(finding.item.id) + ", " + where + ") at login" + Where() + ": " + finding.entry.reason,
|
||||
accountId, characterId);
|
||||
}
|
||||
LOG("Character %llu:%s has %zu contraband item stack(s), %zu removed", characterId, character->GetName().c_str(), findings.size(), removedNames.size());
|
||||
|
||||
if (!removedNames.empty() && NotifyPlayers()) {
|
||||
std::string body = "Items that are not allowed on this server were removed from your character:";
|
||||
for (const auto& finding : findings) {
|
||||
if (finding.entry.action != IContraband::eContrabandAction::REMOVE) continue;
|
||||
body += "\n- " + std::to_string(finding.item.count) + " x item " + std::to_string(finding.item.lot) + (finding.entry.reason.empty() ? "" : ": " + finding.entry.reason);
|
||||
}
|
||||
body += "\nIf you think this is a mistake, contact the server's staff.";
|
||||
Mail::SendMail(player, "Items removed", body, LOT_NULL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void OnItemAdded(Entity* owner, LOT lot, uint32_t count, eLootSourceType source, eInventoryType sourceInventory) {
|
||||
if (!owner || !owner->IsPlayer() || count == 0) return;
|
||||
const auto& list = Get();
|
||||
const auto it = list.find(lot);
|
||||
if (it == list.end() || !CountsAsAdded(source, sourceInventory) || !Applies(AccountLevel(owner), IgnoreStaff())) return;
|
||||
|
||||
auto* character = owner->GetCharacter();
|
||||
if (!character) return;
|
||||
const auto entry = it->second;
|
||||
const bool remove = entry.action == IContraband::eContrabandAction::REMOVE;
|
||||
const auto sourceName = std::string(magic_enum::enum_name(source));
|
||||
// One flag per character, item and day for items that arrive (the load check flags each item on its own)
|
||||
Database::Get()->InsertEconomyFlag({ EconomyLedger::Today(), IDashboardAdmin::eFlagKind::CONTRABAND, character->GetID(), lot, 0,
|
||||
count, remove ? 1 : 0, std::string(remove ? "Removed when received" : "Received") + " (" + sourceName + ")" + Where() + ": " + entry.reason });
|
||||
if (!remove) return;
|
||||
|
||||
// Take it away once the add (and whatever trade, mail or loot code called it) has finished
|
||||
owner->AddCallbackTimer(0.0f, [owner, lot, entry]() {
|
||||
auto* inventory = owner->GetComponent<InventoryComponent>();
|
||||
auto* character = owner->GetCharacter();
|
||||
if (!inventory || !character) return;
|
||||
const auto removed = RemoveLot(*inventory, lot, false);
|
||||
if (removed == 0) return;
|
||||
const auto accountId = character->GetParentUser() ? character->GetParentUser()->GetAccountID() : 0;
|
||||
Database::Get()->InsertAuditLog(0, ACTOR, "contraband_removed", character->GetName() + ": removed " + std::to_string(removed) + " of item " +
|
||||
std::to_string(lot) + " right after receiving it" + Where() + ": " + entry.reason, accountId, character->GetID());
|
||||
if (NotifyPlayers()) {
|
||||
ChatPackets::SendSystemMessage(owner->GetSystemAddress(), "An item you received (" + std::to_string(lot) + ") is not allowed on this server and was removed" +
|
||||
(entry.reason.empty() ? "." : ": " + entry.reason));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
77
dGame/dUtilities/Contraband.h
Normal file
77
dGame/dUtilities/Contraband.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef __CONTRABAND__H__
|
||||
#define __CONTRABAND__H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "eInventoryType.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "IContraband.h"
|
||||
|
||||
class Entity;
|
||||
enum class eGameMasterLevel : uint8_t;
|
||||
|
||||
/**
|
||||
* Contraband (issue #1563): items staff don't want players to have, listed on the dashboard's Contraband page
|
||||
* (IContraband). This world loads the list when it is first needed and again when the dashboard changes it
|
||||
* (ePlayerAction::RELOAD_CONTRABAND).
|
||||
*
|
||||
* - When a character loads (before it is sent to the client), every inventory (vault included) is checked. Each
|
||||
* listed item is flagged (an economy flag of kind CONTRABAND, once per item). Items whose entry says so are removed
|
||||
* too: a snapshot of the character is kept first (so the dashboard's "Give back lost items" can return them), the
|
||||
* removal is audited and the player gets a mail saying why.
|
||||
* - When a listed item is added (loot, trade, mail, vendors, ...), the character is flagged for that item for the
|
||||
* day, and an item to remove is removed right after it arrives, with a chat message saying why.
|
||||
*
|
||||
* Staff accounts (GM level above civilian) are not checked unless contraband_ignore_staff is 0.
|
||||
*/
|
||||
namespace Contraband {
|
||||
struct Entry {
|
||||
std::string reason;
|
||||
IContraband::eContrabandAction action{};
|
||||
};
|
||||
|
||||
using List = std::map<LOT, Entry>;
|
||||
|
||||
struct HeldItem {
|
||||
LWOOBJID id{};
|
||||
LOT lot{};
|
||||
uint32_t count{};
|
||||
eInventoryType inventory{};
|
||||
};
|
||||
|
||||
struct Finding {
|
||||
HeldItem item;
|
||||
Entry entry;
|
||||
};
|
||||
|
||||
// ---- Pure rules, unit tested ----
|
||||
|
||||
// The held items that are on the list, in the order given
|
||||
std::vector<Finding> Find(const std::vector<HeldItem>& items, const List& list);
|
||||
|
||||
// Whether an account at this GM level is checked
|
||||
bool Applies(eGameMasterLevel accountLevel, bool ignoreStaff);
|
||||
|
||||
// Whether an added item should be checked: moves between a player's own inventories are not new items
|
||||
bool CountsAsAdded(eLootSourceType source, eInventoryType sourceInventory);
|
||||
|
||||
// ---- This world ----
|
||||
|
||||
// Load the list again from the database. Returns 1 (one world reloaded).
|
||||
uint32_t Reload();
|
||||
|
||||
// The list as loaded (loads it the first time)
|
||||
const List& Get();
|
||||
|
||||
// Check a player's inventories when their character loads, before it is sent to the client
|
||||
void CheckOnLoad(Entity* player);
|
||||
|
||||
// A listed item was added to a player's inventory (called for every add; cheap when the LOT isn't listed)
|
||||
void OnItemAdded(Entity* owner, LOT lot, uint32_t count, eLootSourceType source, eInventoryType sourceInventory);
|
||||
}
|
||||
|
||||
#endif //!__CONTRABAND__H__
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "dChatFilter.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "Mission.h"
|
||||
#include "Contraband.h"
|
||||
#include "LiveEvents.h"
|
||||
|
||||
namespace {
|
||||
@@ -293,6 +294,8 @@ uint32_t DashboardActions::Apply(const PlayerActionRequest& request) {
|
||||
return ChangeMission(request.action, request.characterId, static_cast<uint32_t>(request.targetId), request.approved, request.text);
|
||||
case ePlayerAction::RELOAD_LIVE_OPS:
|
||||
return LiveEvents::Reload();
|
||||
case ePlayerAction::RELOAD_CONTRABAND:
|
||||
return Contraband::Reload();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user