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:
Aaron Kimbrell
2026-09-26 23:20:16 -05:00
parent 1e0e19062d
commit 5f608a8abb
36 changed files with 766 additions and 22 deletions

View File

@@ -47,6 +47,7 @@
#include "ILiveOps.h"
#include "IFeaturedProperties.h"
#include "IMessageCaptures.h"
#include "IContraband.h"
#ifdef _DEBUG
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
@@ -60,7 +61,7 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures {
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.

View File

@@ -0,0 +1,40 @@
#ifndef __ICONTRABAND__H__
#define __ICONTRABAND__H__
#include <cstdint>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* Contraband: items staff don't want players to have (issue #1563), edited on the dashboard. World servers check a
* character's items against the list when it loads and when items are added, and flag (an economy flag of kind
* CONTRABAND) or also remove them.
*/
class IContraband {
public:
enum class eContrabandAction : uint8_t {
FLAG = 0, // only flag the character
REMOVE = 1 // flag the character and remove the item
};
struct ContrabandItem {
LOT lot{};
std::string reason;
eContrabandAction action{};
std::string addedBy;
int64_t addedAt{};
};
// Every listed item, by LOT
virtual std::vector<ContrabandItem> GetContrabandItems() = 0;
// Insert or replace the row of item.lot
virtual void SetContrabandItem(const ContrabandItem& item) = 0;
// Whether there was a row to delete
virtual bool DeleteContrabandItem(LOT lot) = 0;
};
#endif //!__ICONTRABAND__H__

View File

@@ -58,7 +58,10 @@ public:
DUPLICATE = 3, // an object id exists in more than one place (day 0, so each item is flagged once)
// different items share an object id and the login migration will not separate them: the characters holding
// them already migrated, or a copy is in mail (day 0)
ID_COLLISION = 4
ID_COLLISION = 4,
// a character has an item on the contraband list (value: how many, baseline: 1 when it was removed). Found when
// the character loaded (day 0, one flag per item) or when the item was added (that day, item id 0)
CONTRABAND = 5
};
enum class eFlagStatus : uint8_t { OPEN = 0, DISMISSED = 1, ACTIONED = 2 };

View File

@@ -215,6 +215,11 @@ public:
std::vector<ZoneLimit> GetZoneLimits() override;
void SetZoneLimit(const ZoneLimit& limit) override;
void DeleteZoneLimit(uint32_t zoneId) override;
// IContraband
std::vector<ContrabandItem> GetContrabandItems() override;
void SetContrabandItem(const ContrabandItem& item) override;
bool DeleteContrabandItem(LOT lot) override;
// IFeaturedProperties
std::vector<FeaturedSlot> GetFeaturedPropertySlots() override;
void SetFeaturedPropertySlot(const FeaturedSlot& slot) override;

View File

@@ -18,6 +18,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"ServerOperations.cpp"
"LiveOps.cpp"
"FeaturedProperties.cpp"
"Contraband.cpp"
"MessageCaptures.cpp"
"ChatLog.cpp"
"RelatedData.cpp"

View File

@@ -0,0 +1,22 @@
#include "MySQLDatabase.h"
std::vector<IContraband::ContrabandItem> MySQLDatabase::GetContrabandItems() {
std::vector<ContrabandItem> items;
auto result = ExecuteSelect("SELECT * FROM contraband_items ORDER BY lot;");
while (result->next()) {
items.push_back({ result->getInt("lot"), result->getString("reason").c_str(), static_cast<eContrabandAction>(result->getUInt("action")),
result->getString("added_by").c_str(), result->getInt64("added_at") });
}
return items;
}
void MySQLDatabase::SetContrabandItem(const ContrabandItem& item) {
ExecuteInsert(
"INSERT INTO contraband_items (lot, reason, action, added_by, added_at) VALUES (?, ?, ?, ?, ?) "
"ON DUPLICATE KEY UPDATE reason = VALUES(reason), action = VALUES(action), added_by = VALUES(added_by), added_at = VALUES(added_at);",
item.lot, item.reason, static_cast<uint32_t>(item.action), item.addedBy, item.addedAt);
}
bool MySQLDatabase::DeleteContrabandItem(LOT lot) {
return ExecuteUpdate("DELETE FROM contraband_items WHERE lot = ?;", lot) > 0;
}

View File

@@ -199,6 +199,11 @@ public:
std::vector<ZoneLimit> GetZoneLimits() override;
void SetZoneLimit(const ZoneLimit& limit) override;
void DeleteZoneLimit(uint32_t zoneId) override;
// IContraband
std::vector<ContrabandItem> GetContrabandItems() override;
void SetContrabandItem(const ContrabandItem& item) override;
bool DeleteContrabandItem(LOT lot) override;
// IFeaturedProperties
std::vector<FeaturedSlot> GetFeaturedPropertySlots() override;
void SetFeaturedPropertySlot(const FeaturedSlot& slot) override;

View File

@@ -18,6 +18,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
"ServerOperations.cpp"
"LiveOps.cpp"
"FeaturedProperties.cpp"
"Contraband.cpp"
"MessageCaptures.cpp"
"ChatLog.cpp"
"RelatedData.cpp"

View File

@@ -0,0 +1,22 @@
#include "SQLiteDatabase.h"
std::vector<IContraband::ContrabandItem> SQLiteDatabase::GetContrabandItems() {
std::vector<ContrabandItem> items;
auto [_, result] = ExecuteSelect("SELECT * FROM contraband_items ORDER BY lot;");
for (; !result.eof(); result.nextRow()) {
items.push_back({ result.getIntField("lot"), result.getStringField("reason"), static_cast<eContrabandAction>(result.getIntField("action")),
result.getStringField("added_by"), result.getInt64Field("added_at") });
}
return items;
}
void SQLiteDatabase::SetContrabandItem(const ContrabandItem& item) {
ExecuteInsert(
"INSERT INTO contraband_items (lot, reason, action, added_by, added_at) VALUES (?, ?, ?, ?, ?) "
"ON CONFLICT(lot) DO UPDATE SET reason = excluded.reason, action = excluded.action, added_by = excluded.added_by, added_at = excluded.added_at;",
item.lot, item.reason, static_cast<uint32_t>(item.action), item.addedBy, item.addedAt);
}
bool SQLiteDatabase::DeleteContrabandItem(LOT lot) {
return ExecuteUpdate("DELETE FROM contraband_items WHERE lot = ?;", lot) > 0;
}

View File

@@ -245,6 +245,11 @@ class TestSQLDatabase : public GameDatabase {
std::vector<ZoneLimit> GetZoneLimits() override { return {}; }
void SetZoneLimit(const ZoneLimit& limit) override {}
void DeleteZoneLimit(uint32_t zoneId) override {}
// IContraband
std::vector<ContrabandItem> GetContrabandItems() override { return {}; }
void SetContrabandItem(const ContrabandItem& item) override {}
bool DeleteContrabandItem(LOT lot) override { return false; }
// IFeaturedProperties
std::vector<FeaturedSlot> GetFeaturedPropertySlots() override { return {}; }
void SetFeaturedPropertySlot(const FeaturedSlot& slot) override {}