From 7534f48de9a3413971c072d9f38ea9e82f396a51 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 10:23:35 -0500 Subject: [PATCH] feat(database): look up players' creations and where they are IUgcLookup: search ugc and ugc_modular_build by id, creator, property, model name or LOT, and find where a creation is placed or mailed. The SQL is shared by MySQL and SQLite (UgcLookupSql.h). Co-Authored-By: Claude Opus 5.5 --- dDatabase/GameDatabase/GameDatabase.h | 3 +- dDatabase/GameDatabase/ITables/IUgcLookup.h | 86 +++++++++++++++++++ dDatabase/GameDatabase/ITables/UgcLookupSql.h | 80 +++++++++++++++++ dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 6 ++ .../GameDatabase/MySQL/Tables/CMakeLists.txt | 1 + .../GameDatabase/MySQL/Tables/UgcLookup.cpp | 77 +++++++++++++++++ .../GameDatabase/SQLite/SQLiteDatabase.h | 6 ++ .../GameDatabase/SQLite/Tables/CMakeLists.txt | 1 + .../GameDatabase/SQLite/Tables/UgcLookup.cpp | 77 +++++++++++++++++ .../GameDatabase/TestSQL/TestSQLDatabase.h | 6 ++ 10 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 dDatabase/GameDatabase/ITables/IUgcLookup.h create mode 100644 dDatabase/GameDatabase/ITables/UgcLookupSql.h create mode 100644 dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp diff --git a/dDatabase/GameDatabase/GameDatabase.h b/dDatabase/GameDatabase/GameDatabase.h index a5f623ba2..7dd9ccdae 100644 --- a/dDatabase/GameDatabase/GameDatabase.h +++ b/dDatabase/GameDatabase/GameDatabase.h @@ -53,6 +53,7 @@ #include "IBbbAutosave.h" #include "IServerTraffic.h" #include "IApiKeys.h" +#include "IUgcLookup.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) @@ -66,7 +67,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 IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys { + 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 IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup { public: virtual ~GameDatabase() = default; // TODO: These should be made private. diff --git a/dDatabase/GameDatabase/ITables/IUgcLookup.h b/dDatabase/GameDatabase/ITables/IUgcLookup.h new file mode 100644 index 000000000..7b3801259 --- /dev/null +++ b/dDatabase/GameDatabase/ITables/IUgcLookup.h @@ -0,0 +1,86 @@ +#ifndef IUGCLOOKUP_H +#define IUGCLOOKUP_H + +#include +#include +#include +#include + +#include "dCommonVars.h" +#include "IUgc.h" + +/** + * Finding players' creations for the dashboard: brick built models (ugc) and cars and rockets (ugc_modular_build), + * who made them, what the UGC server made of them and where they are (placed on a property, attached to a mail). + */ +class IUgcLookup { +public: + enum class eUgcKind : uint8_t { + MODEL = 0, // ugc: a brick built model, its id is the blueprint id + MODULAR = 1, // ugc_modular_build: a car or rocket, its id is the item's subkey + }; + + struct UgcEntry { + eUgcKind kind{}; + LWOOBJID id{}; + LWOOBJID characterId{}; // the creator + std::string characterName; // empty when the character is gone + uint32_t accountId{}; + std::string accountName; + IUgc::eProcessState state{}; + std::string error; // why the UGC server failed, if it did + std::string detail; // models: the file name it was uploaded as; modular builds: the modules + }; + + // What SearchUgc matches. A number (when set) is matched against ids; text against names + struct UgcSearch { + enum class eField : uint8_t { + ANY, // any of the below + ID, // the ugc / blueprint id, the placed model's object id, the property, creator character or account id + OWNER, // the creator's character or account name (or id) + PROPERTY, // the name (or id) of a property it is placed on + MODEL, // the name or description given to it on a property, or its upload file name + LOT, // the LOT it is placed as, or one of a modular build's modules + }; + eField field = eField::ANY; + std::string text; // matched as a part of names; empty for none + std::optional number; // matched exactly against ids + }; + + // Up to `limit` of each kind, the newest first + virtual std::vector SearchUgc(const UgcSearch& search, const uint32_t limit) = 0; + + // The entries (of either kind) with these ids + virtual std::vector GetUgcEntries(const std::vector& ids) = 0; + + // A creation placed on a property (properties_contents) + struct UgcPlacement { + LWOOBJID ugcId{}; + LWOOBJID modelId{}; // the placed object's id + LOT lot{}; + LWOOBJID propertyId{}; + std::string propertyName; + LWOOBJID ownerId{}; // the property owner's character + std::string ownerName; + uint32_t zoneId{}; + std::string modelName; + std::string modelDescription; + }; + + virtual std::vector GetUgcPlacements(const std::vector& ugcIds) = 0; + + // A mail with an attachment that may be a creation: its subkey is one of `subkeys` (modular builds), or it is a + // brick built model item (`modelItemLot`, whose blueprint is in the attachment's config) + struct UgcMail { + uint64_t id{}; + LWOOBJID receiverId{}; + std::string receiverName; + LOT lot{}; + LWOOBJID subkey{}; + std::string config; + }; + + virtual std::vector GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) = 0; +}; + +#endif //!IUGCLOOKUP_H diff --git a/dDatabase/GameDatabase/ITables/UgcLookupSql.h b/dDatabase/GameDatabase/ITables/UgcLookupSql.h new file mode 100644 index 000000000..0ffa132a6 --- /dev/null +++ b/dDatabase/GameDatabase/ITables/UgcLookupSql.h @@ -0,0 +1,80 @@ +#ifndef UGCLOOKUPSQL_H +#define UGCLOOKUPSQL_H + +#include +#include + +#include "IUgcLookup.h" + +/** + * The SQL IUgcLookup runs, the same on MySQL and SQLite. Ids are numbers, so they are written into the query; the + * search text is bound (TEXT_BINDS times, always, so the statement's parameters don't change with the search). + */ +namespace UgcLookupSql { + constexpr int TEXT_BINDS = 6; + + inline std::string IdList(const std::vector& ids) { + std::string list; + for (const auto id : ids) list += (list.empty() ? "" : ",") + std::to_string(id); + return list; + } + + // The columns every entry query selects, for models (ugc AS u) or modular builds (ugc_modular_build AS b) + inline std::string Select(bool modular) { + return modular + ? "SELECT b.ugc_id AS id, b.character_id, c.name AS character_name, COALESCE(c.account_id, 0) AS account_id, a.name AS account_name, " + "b.is_optimized, b.process_error, b.ldf_config AS detail FROM ugc_modular_build AS b " + "LEFT JOIN charinfo AS c ON c.id = b.character_id LEFT JOIN accounts AS a ON a.id = c.account_id " + : "SELECT u.id, u.character_id, c.name AS character_name, u.account_id, a.name AS account_name, " + "u.is_optimized, u.process_error, u.filename AS detail FROM ugc AS u " + "LEFT JOIN charinfo AS c ON c.id = u.character_id LEFT JOIN accounts AS a ON a.id = u.account_id "; + } + + // WHERE for a search; binds the search's %text% TEXT_BINDS times + inline std::string Where(const IUgcLookup::UgcSearch& search, bool modular) { + using eField = IUgcLookup::UgcSearch::eField; + const auto field = search.field; + const bool any = field == eField::ANY; + const bool num = search.number.has_value(); + const bool txt = !search.text.empty(); + const std::string n = num ? std::to_string(*search.number) : "NULL"; + const std::string id = modular ? "b.ugc_id" : "u.id"; + const std::string character = modular ? "b.character_id" : "u.character_id"; + const std::string account = modular ? "c.account_id" : "u.account_id"; + const std::string detail = modular ? "b.ldf_config" : "u.filename"; + const auto on = [](bool value) { return std::string(value ? "1=1" : "0=1"); }; + const std::string placed = "SELECT 1 FROM properties_contents AS pc WHERE pc.ugc_id = " + id; + + std::string where = "WHERE (0=1"; + if (num && (any || field == eField::ID)) { + where += " OR " + id + " = " + n + " OR " + character + " = " + n + " OR " + account + " = " + n + + " OR EXISTS (" + placed + " AND (pc.id = " + n + " OR pc.property_id = " + n + "))"; + } + if (num && field == eField::OWNER) where += " OR " + character + " = " + n + " OR " + account + " = " + n; + if (num && (any || field == eField::LOT)) { + if (modular) where += " OR b.ldf_config LIKE '%:" + n + "+%' OR b.ldf_config LIKE '%:" + n + "'"; + else where += " OR EXISTS (" + placed + " AND pc.lot = " + n + ")"; + } + // The bound text: owner (2), property (1), model (3) + where += " OR (" + on(txt && (any || field == eField::OWNER)) + " AND (c.name LIKE ? OR a.name LIKE ?))"; + where += " OR (" + on((txt && any) || field == eField::PROPERTY) + " AND EXISTS (SELECT 1 FROM properties_contents AS pc " + "JOIN properties AS p ON p.id = pc.property_id WHERE pc.ugc_id = " + id + " AND ((" + on(txt) + " AND p.name LIKE ?) OR p.id = " + n + ")))"; + where += " OR (" + on(txt && (any || field == eField::MODEL)) + " AND (" + detail + " LIKE ? OR EXISTS (" + placed + + " AND (pc.model_name LIKE ? OR pc.model_description LIKE ?))))"; + return where + ") "; + } + + inline std::string Placements(const std::vector& ugcIds) { + return "SELECT pc.ugc_id, pc.id, pc.lot, pc.property_id, p.name AS property_name, p.owner_id, c.name AS owner_name, p.zone_id, " + "pc.model_name, pc.model_description FROM properties_contents AS pc JOIN properties AS p ON p.id = pc.property_id " + "LEFT JOIN charinfo AS c ON c.id = p.owner_id WHERE pc.ugc_id IN (" + IdList(ugcIds) + ") ORDER BY pc.id;"; + } + + inline std::string Mail(const std::vector& subkeys, LOT modelItemLot) { + return "SELECT id, receiver_id, receiver_name, attachment_lot, attachment_subkey, attachment_config FROM mail WHERE attachment_count > 0 AND " + "(attachment_lot = " + std::to_string(modelItemLot) + (subkeys.empty() ? "" : " OR attachment_subkey IN (" + IdList(subkeys) + ")") + + ") ORDER BY id DESC LIMIT 500;"; + } +} + +#endif //!UGCLOOKUPSQL_H diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index e93dd5e17..072130bcf 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -220,6 +220,12 @@ public: std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override; uint32_t PruneTrafficMinutes(int64_t beforeTime) override; + // IUgcLookup + std::vector SearchUgc(const UgcSearch& search, const uint32_t limit) override; + std::vector GetUgcEntries(const std::vector& ids) override; + std::vector GetUgcPlacements(const std::vector& ugcIds) override; + std::vector GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) override; + // IApiKeys uint64_t InsertApiKey(const ApiKey& key) override; std::optional GetApiKey(uint64_t id) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt index 1d950500c..dd89bf7cf 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt @@ -47,6 +47,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES "Servers.cpp" "Ugc.cpp" "UgcModularBuild.cpp" + "UgcLookup.cpp" PARENT_SCOPE ) diff --git a/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp b/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp new file mode 100644 index 000000000..77393bfe7 --- /dev/null +++ b/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp @@ -0,0 +1,77 @@ +#include "MySQLDatabase.h" + +#include "UgcLookupSql.h" + +namespace { + IUgcLookup::UgcEntry ReadEntry(PreparedStmtResultSet& result, IUgcLookup::eUgcKind kind) { + IUgcLookup::UgcEntry entry; + entry.kind = kind; + entry.id = result->getInt64("id"); + entry.characterId = result->getInt64("character_id"); + entry.characterName = std::string(result->getString("character_name").c_str()); + entry.accountId = static_cast(result->getInt64("account_id")); + entry.accountName = std::string(result->getString("account_name").c_str()); + entry.state = static_cast(result->getInt("is_optimized")); + entry.error = std::string(result->getString("process_error").c_str()); + entry.detail = std::string(result->getString("detail").c_str()); + return entry; + } +} + +std::vector MySQLDatabase::SearchUgc(const UgcSearch& search, const uint32_t limit) { + std::vector entries; + const std::string pattern = "%" + search.text + "%"; + for (const bool modular : { false, true }) { + const auto order = modular ? "ORDER BY b.ugc_id DESC LIMIT ?;" : "ORDER BY u.id DESC LIMIT ?;"; + auto result = ExecuteSelect(UgcLookupSql::Select(modular) + UgcLookupSql::Where(search, modular) + order, + pattern, pattern, pattern, pattern, pattern, pattern, limit); + while (result->next()) entries.push_back(ReadEntry(result, modular ? eUgcKind::MODULAR : eUgcKind::MODEL)); + } + return entries; +} + +std::vector MySQLDatabase::GetUgcEntries(const std::vector& ids) { + std::vector entries; + if (ids.empty()) return entries; + const auto list = UgcLookupSql::IdList(ids); + for (const bool modular : { false, true }) { + auto result = ExecuteSelect(UgcLookupSql::Select(modular) + (modular ? "WHERE b.ugc_id IN (" : "WHERE u.id IN (") + list + ");"); + while (result->next()) entries.push_back(ReadEntry(result, modular ? eUgcKind::MODULAR : eUgcKind::MODEL)); + } + return entries; +} + +std::vector MySQLDatabase::GetUgcPlacements(const std::vector& ugcIds) { + std::vector placements; + if (ugcIds.empty()) return placements; + auto result = ExecuteSelect(UgcLookupSql::Placements(ugcIds)); + while (result->next()) { + auto& placement = placements.emplace_back(); + placement.ugcId = result->getInt64("ugc_id"); + placement.modelId = result->getInt64("id"); + placement.lot = result->getInt("lot"); + placement.propertyId = result->getInt64("property_id"); + placement.propertyName = std::string(result->getString("property_name").c_str()); + placement.ownerId = result->getInt64("owner_id"); + placement.ownerName = std::string(result->getString("owner_name").c_str()); + placement.zoneId = static_cast(result->getInt("zone_id")); + placement.modelName = std::string(result->getString("model_name").c_str()); + placement.modelDescription = std::string(result->getString("model_description").c_str()); + } + return placements; +} + +std::vector MySQLDatabase::GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) { + std::vector mail; + auto result = ExecuteSelect(UgcLookupSql::Mail(subkeys, modelItemLot)); + while (result->next()) { + auto& entry = mail.emplace_back(); + entry.id = static_cast(result->getInt64("id")); + entry.receiverId = result->getInt64("receiver_id"); + entry.receiverName = std::string(result->getString("receiver_name").c_str()); + entry.lot = result->getInt("attachment_lot"); + entry.subkey = result->getInt64("attachment_subkey"); + entry.config = std::string(result->getString("attachment_config").c_str()); + } + return mail; +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 59a035878..bd2904e0b 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -204,6 +204,12 @@ public: std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override; uint32_t PruneTrafficMinutes(int64_t beforeTime) override; + // IUgcLookup + std::vector SearchUgc(const UgcSearch& search, const uint32_t limit) override; + std::vector GetUgcEntries(const std::vector& ids) override; + std::vector GetUgcPlacements(const std::vector& ugcIds) override; + std::vector GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) override; + // IApiKeys uint64_t InsertApiKey(const ApiKey& key) override; std::optional GetApiKey(uint64_t id) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt index 345f5ff89..f0f23ab5a 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt @@ -47,6 +47,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES "Servers.cpp" "Ugc.cpp" "UgcModularBuild.cpp" + "UgcLookup.cpp" PARENT_SCOPE ) diff --git a/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp b/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp new file mode 100644 index 000000000..324ad60fb --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp @@ -0,0 +1,77 @@ +#include "SQLiteDatabase.h" + +#include "UgcLookupSql.h" + +namespace { + IUgcLookup::UgcEntry ReadEntry(CppSQLite3Query& result, IUgcLookup::eUgcKind kind) { + IUgcLookup::UgcEntry entry; + entry.kind = kind; + entry.id = result.getInt64Field("id"); + entry.characterId = result.getInt64Field("character_id"); + entry.characterName = result.getStringField("character_name", ""); + entry.accountId = static_cast(result.getInt64Field("account_id")); + entry.accountName = result.getStringField("account_name", ""); + entry.state = static_cast(result.getIntField("is_optimized")); + entry.error = result.getStringField("process_error", ""); + entry.detail = result.getStringField("detail", ""); + return entry; + } +} + +std::vector SQLiteDatabase::SearchUgc(const UgcSearch& search, const uint32_t limit) { + std::vector entries; + const std::string pattern = "%" + search.text + "%"; + for (const bool modular : { false, true }) { + const auto order = modular ? "ORDER BY b.ugc_id DESC LIMIT ?;" : "ORDER BY u.id DESC LIMIT ?;"; + auto [_, result] = ExecuteSelect(UgcLookupSql::Select(modular) + UgcLookupSql::Where(search, modular) + order, + pattern, pattern, pattern, pattern, pattern, pattern, limit); + for (; !result.eof(); result.nextRow()) entries.push_back(ReadEntry(result, modular ? eUgcKind::MODULAR : eUgcKind::MODEL)); + } + return entries; +} + +std::vector SQLiteDatabase::GetUgcEntries(const std::vector& ids) { + std::vector entries; + if (ids.empty()) return entries; + const auto list = UgcLookupSql::IdList(ids); + for (const bool modular : { false, true }) { + auto [_, result] = ExecuteSelect(UgcLookupSql::Select(modular) + (modular ? "WHERE b.ugc_id IN (" : "WHERE u.id IN (") + list + ");"); + for (; !result.eof(); result.nextRow()) entries.push_back(ReadEntry(result, modular ? eUgcKind::MODULAR : eUgcKind::MODEL)); + } + return entries; +} + +std::vector SQLiteDatabase::GetUgcPlacements(const std::vector& ugcIds) { + std::vector placements; + if (ugcIds.empty()) return placements; + auto [_, result] = ExecuteSelect(UgcLookupSql::Placements(ugcIds)); + for (; !result.eof(); result.nextRow()) { + auto& placement = placements.emplace_back(); + placement.ugcId = result.getInt64Field("ugc_id"); + placement.modelId = result.getInt64Field("id"); + placement.lot = result.getIntField("lot"); + placement.propertyId = result.getInt64Field("property_id"); + placement.propertyName = result.getStringField("property_name", ""); + placement.ownerId = result.getInt64Field("owner_id"); + placement.ownerName = result.getStringField("owner_name", ""); + placement.zoneId = static_cast(result.getIntField("zone_id")); + placement.modelName = result.getStringField("model_name", ""); + placement.modelDescription = result.getStringField("model_description", ""); + } + return placements; +} + +std::vector SQLiteDatabase::GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) { + std::vector mail; + auto [_, result] = ExecuteSelect(UgcLookupSql::Mail(subkeys, modelItemLot)); + for (; !result.eof(); result.nextRow()) { + auto& entry = mail.emplace_back(); + entry.id = static_cast(result.getInt64Field("id")); + entry.receiverId = result.getInt64Field("receiver_id"); + entry.receiverName = result.getStringField("receiver_name", ""); + entry.lot = result.getIntField("attachment_lot"); + entry.subkey = result.getInt64Field("attachment_subkey"); + entry.config = result.getStringField("attachment_config", ""); + } + return mail; +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index acb937309..40ee2ee9c 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -267,6 +267,12 @@ class TestSQLDatabase : public GameDatabase { void DeleteBbbAutosave(const LWOOBJID characterId) override { m_BbbAutosaves.erase(characterId); } std::map m_BbbAutosaves; + // IUgcLookup + std::vector SearchUgc(const UgcSearch& search, const uint32_t limit) override { return {}; } + std::vector GetUgcEntries(const std::vector& ids) override { return {}; } + std::vector GetUgcPlacements(const std::vector& ugcIds) override { return {}; } + std::vector GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) override { return {}; } + // IPropertyReputation VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override { return {}; } int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override { return 0; }