#include "player_event_logs.h" #include #include "../platform.h" #include "../rulesys.h" #include "player_event_discord_formatter.h" #include "../repositories/player_event_loot_items_repository.h" #include "../repositories/player_event_merchant_sell_repository.h" #include "../repositories/player_event_merchant_purchase_repository.h" #include "../repositories/player_event_npc_handin_repository.h" #include "../repositories/player_event_npc_handin_entries_repository.h" const uint32 PROCESS_RETENTION_TRUNCATION_TIMER_INTERVAL = 60 * 60 * 1000; // 1 hour // general initialization routine void PlayerEventLogs::Init() { m_process_batch_events_timer.SetTimer(RuleI(Logging, BatchPlayerEventProcessIntervalSeconds) * 1000); m_process_retention_truncation_timer.SetTimer(PROCESS_RETENTION_TRUNCATION_TIMER_INTERVAL); m_database_ping_timer.SetTimer(10 * 1000); // 10 seconds ValidateDatabaseConnection(); // initialize settings array for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { m_settings[i].id = i; m_settings[i].event_name = PlayerEvent::EventName[i]; m_settings[i].event_enabled = 1; m_settings[i].retention_days = 0; m_settings[i].discord_webhook_id = 0; m_settings[i].etl_enabled = false; } SetSettingsDefaults(); // initialize settings from database auto s = PlayerEventLogSettingsRepository::All(*m_database); std::vector db{}; db.reserve(s.size()); for (auto &e: s) { if (e.id >= PlayerEvent::MAX) { continue; } m_settings[e.id] = e; db.emplace_back(e.id); } std::vector settings_to_insert{}; // insert entries that don't exist in database for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { bool is_in_database = std::find(db.begin(), db.end(), i) != db.end(); bool is_deprecated = Strings::Contains(PlayerEvent::EventName[i], "Deprecated"); bool is_implemented = !Strings::Contains(PlayerEvent::EventName[i], "Unimplemented"); // remove when deprecated if (is_deprecated && is_in_database) { LogInfo("[Deprecated] Removing PlayerEvent [{}] ({})", PlayerEvent::EventName[i], i); PlayerEventLogSettingsRepository::DeleteWhere(*m_database, fmt::format("id = {}", i)); } // remove when unimplemented if present if (!is_implemented && is_in_database) { LogInfo("[Unimplemented] Removing PlayerEvent [{}] ({})", PlayerEvent::EventName[i], i); PlayerEventLogSettingsRepository::DeleteWhere(*m_database, fmt::format("id = {}", i)); } bool is_missing_in_database = std::find(db.begin(), db.end(), i) == db.end(); if (is_missing_in_database && is_implemented && !is_deprecated) { LogInfo("[New] PlayerEvent [{}] ({})", PlayerEvent::EventName[i], i); auto c = PlayerEventLogSettingsRepository::NewEntity(); c.id = i; c.event_name = PlayerEvent::EventName[i]; c.event_enabled = m_settings[i].event_enabled; c.retention_days = m_settings[i].retention_days; c.etl_enabled = false; settings_to_insert.emplace_back(c); } } if (!settings_to_insert.empty()) { PlayerEventLogSettingsRepository::ReplaceMany(*m_database, settings_to_insert); } bool processing_in_world = !RuleB(Logging, PlayerEventsQSProcess) && IsWorld(); bool processing_in_qs = RuleB(Logging, PlayerEventsQSProcess) && IsQueryServ(); // on initial boot process truncation if (processing_in_world || processing_in_qs) { LoadEtlIds(); ProcessRetentionTruncation(); } } // set the database object, during initialization PlayerEventLogs *PlayerEventLogs::SetDatabase(Database *db) { m_database = db; return this; } // validates whether the connection is valid or not, used in initialization bool PlayerEventLogs::ValidateDatabaseConnection() { if (!m_database) { LogError("No database connection"); return false; } return true; } // determines if the passed in event is enabled or not // this is used to gate logic or events from firing off // this is used prior to building the events, we don't want to // build the events, send them through the stack in a function call // only to discard them immediately afterwards, very wasteful on resources // the quest api currently does this bool PlayerEventLogs::IsEventEnabled(PlayerEvent::EventType event) { return m_settings[event].event_enabled ? m_settings[event].event_enabled : false; } // this processes any current player events on the queue void PlayerEventLogs::ProcessBatchQueue() { m_batch_queue_lock.lock(); if (m_record_batch_queue.empty()) { m_batch_queue_lock.unlock(); return; } static std::map counter{}; counter.clear(); for (auto const &e: m_record_batch_queue) { counter[e.event_type_id]++; } BenchTimer benchmark; EtlQueues etl_queues{}; for (const auto &[type, count]: counter) { if (count > 0) { switch (type) { case PlayerEvent::TRADE: etl_queues.trade.reserve(count); break; case PlayerEvent::SPEECH: etl_queues.speech.reserve(count); break; case PlayerEvent::LOOT_ITEM: etl_queues.loot_items.reserve(count); break; case PlayerEvent::KILLED_NPC: etl_queues.killed_npc.reserve(count); break; case PlayerEvent::NPC_HANDIN: etl_queues.npc_handin.reserve(count); break; case PlayerEvent::AA_PURCHASE: etl_queues.aa_purchase.reserve(count); break; case PlayerEvent::MERCHANT_SELL: etl_queues.merchant_sell.reserve(count); break; case PlayerEvent::KILLED_RAID_NPC: etl_queues.killed_raid_npc.reserve(count); break; case PlayerEvent::KILLED_NAMED_NPC: etl_queues.killed_named_npc.reserve(count); break; case PlayerEvent::MERCHANT_PURCHASE: etl_queues.merchant_purchase.reserve(count); break; default: break; } } } // Helper to deserialize event data auto Deserialize = [](const std::string &data, auto &out) { if (!Strings::IsValidJson(data)) { return; } // cpp exceptions are terrible, don't ever use them try { std::stringstream ss(data); cereal::JSONInputArchive ar(ss); out.serialize(ar); } catch (const std::exception &e) {} }; // Helper to assign ETL table ID auto AssignEtlId = [&]( PlayerEventLogsRepository::PlayerEventLogs& r, PlayerEvent::EventType type ) { if (m_etl_settings.contains(type)) { r.etl_table_id = m_etl_settings.at(type).next_id++; } }; // Define event processors std::unordered_map> event_processors = { { PlayerEvent::EventType::LOOT_ITEM, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::LootItemEvent in{}; PlayerEventLootItemsRepository::PlayerEventLootItems out{}; Deserialize(r.event_data, in); out.charges = in.charges; out.corpse_name = in.corpse_name; out.item_id = in.item_id; out.item_name = in.item_name; out.augment_1_id = in.augment_1_id; out.augment_2_id = in.augment_2_id; out.augment_3_id = in.augment_3_id; out.augment_4_id = in.augment_4_id; out.augment_5_id = in.augment_5_id; out.augment_6_id = in.augment_6_id; out.npc_id = in.npc_id; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::LOOT_ITEM); etl_queues.loot_items.push_back(out); } }, { PlayerEvent::EventType::MERCHANT_SELL, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::MerchantSellEvent in{}; PlayerEventMerchantSellRepository::PlayerEventMerchantSell out{}; Deserialize(r.event_data, in); out.npc_id = in.npc_id; out.merchant_name = in.merchant_name; out.merchant_type = in.merchant_type; out.item_id = in.item_id; out.item_name = in.item_name; out.charges = in.charges; out.cost = in.cost; out.alternate_currency_id = in.alternate_currency_id; out.player_money_balance = in.player_money_balance; out.player_currency_balance = in.player_currency_balance; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::MERCHANT_SELL); etl_queues.merchant_sell.push_back(out); }}, { PlayerEvent::EventType::MERCHANT_PURCHASE, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::MerchantPurchaseEvent in{}; PlayerEventMerchantPurchaseRepository::PlayerEventMerchantPurchase out{}; Deserialize(r.event_data, in); out.npc_id = in.npc_id; out.merchant_name = in.merchant_name; out.merchant_type = in.merchant_type; out.item_id = in.item_id; out.item_name = in.item_name; out.charges = in.charges; out.cost = in.cost; out.alternate_currency_id = in.alternate_currency_id; out.player_money_balance = in.player_money_balance; out.player_currency_balance = in.player_currency_balance; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::MERCHANT_PURCHASE); etl_queues.merchant_purchase.push_back(out); }}, { PlayerEvent::EventType::NPC_HANDIN, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::HandinEvent in{}; PlayerEventNpcHandinRepository::PlayerEventNpcHandin out{}; Deserialize(r.event_data, in); out.npc_id = in.npc_id; out.npc_name = in.npc_name; out.handin_copper = in.handin_money.copper; out.handin_silver = in.handin_money.silver; out.handin_gold = in.handin_money.gold; out.handin_platinum = in.handin_money.platinum; out.return_copper = in.return_money.copper; out.return_silver = in.return_money.silver; out.return_gold = in.return_money.gold; out.return_platinum = in.return_money.platinum; out.is_quest_handin = in.is_quest_handin; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::NPC_HANDIN); etl_queues.npc_handin.push_back(out); for (const auto &i: in.handin_items) { PlayerEventNpcHandinEntriesRepository::PlayerEventNpcHandinEntries entry{}; entry.player_event_npc_handin_id = r.etl_table_id; entry.item_id = i.item_id; entry.charges = i.charges; entry.type = 1; etl_queues.npc_handin_entries.push_back(entry); } for (const auto &i: in.return_items) { PlayerEventNpcHandinEntriesRepository::PlayerEventNpcHandinEntries entry{}; entry.player_event_npc_handin_id = r.etl_table_id; entry.item_id = i.item_id; entry.charges = i.charges; entry.type = 2; etl_queues.npc_handin_entries.push_back(entry); } }}, { PlayerEvent::EventType::TRADE, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::TradeEvent in{}; PlayerEventTradeRepository::PlayerEventTrade out{}; Deserialize(r.event_data, in); out.char1_id = in.character_1_id; out.char2_id = in.character_2_id; out.char1_copper = in.character_1_give_money.copper; out.char1_silver = in.character_1_give_money.silver; out.char1_gold = in.character_1_give_money.gold; out.char1_platinum = in.character_1_give_money.platinum; out.char2_copper = in.character_2_give_money.copper; out.char2_silver = in.character_2_give_money.silver; out.char2_gold = in.character_2_give_money.gold; out.char2_platinum = in.character_2_give_money.platinum; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::TRADE); etl_queues.trade.push_back(out); for (const auto &i: in.character_1_give_items) { PlayerEventTradeEntriesRepository::PlayerEventTradeEntries entry{}; entry.player_event_trade_id = r.etl_table_id; entry.char_id = in.character_1_id; entry.item_id = i.item_id; entry.charges = i.charges; entry.slot = i.slot; etl_queues.trade_entries.push_back(entry); } for (const auto &i: in.character_2_give_items) { PlayerEventTradeEntriesRepository::PlayerEventTradeEntries entry{}; entry.player_event_trade_id = r.etl_table_id; entry.char_id = in.character_2_id; entry.item_id = i.item_id; entry.charges = i.charges; entry.slot = i.slot; etl_queues.trade_entries.push_back(entry); } }}, { PlayerEvent::EventType::SPEECH, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::PlayerSpeech in{}; PlayerEventSpeechRepository::PlayerEventSpeech out{}; Deserialize(r.event_data, in); out.from_char_id = in.from; out.to_char_id = in.to; out.type = in.type; out.min_status = in.min_status; out.message = in.message; out.guild_id = in.guild_id; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::SPEECH); etl_queues.speech.push_back(out); }}, { PlayerEvent::EventType::KILLED_NPC, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::KilledNPCEvent in{}; PlayerEventKilledNpcRepository::PlayerEventKilledNpc out{}; Deserialize(r.event_data, in); out.npc_id = in.npc_id; out.npc_name = in.npc_name; out.combat_time_seconds = in.combat_time_seconds; out.total_damage_per_second_taken = in.total_damage_per_second_taken; out.total_heal_per_second_taken = in.total_heal_per_second_taken; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::KILLED_NPC); etl_queues.killed_npc.push_back(out); }}, { PlayerEvent::EventType::AA_PURCHASE, [&](PlayerEventLogsRepository::PlayerEventLogs &r) { PlayerEvent::AAPurchasedEvent in{}; PlayerEventAaPurchaseRepository::PlayerEventAaPurchase out{}; Deserialize(r.event_data, in); out.aa_ability_id = in.aa_id; out.cost = in.aa_cost; out.previous_id = in.aa_previous_id; out.next_id = in.aa_next_id; out.created_at = r.created_at; AssignEtlId(r, PlayerEvent::EventType::AA_PURCHASE); etl_queues.aa_purchase.push_back(out); }}, }; // Process the batch queue for (auto &r: m_record_batch_queue) { if (m_settings[r.event_type_id].etl_enabled) { auto it = event_processors.find(static_cast(r.event_type_id)); if (it != event_processors.end()) { it->second(r); // Call the appropriate lambda } else { LogPlayerEventsDetail("Non-Implemented ETL routing [{}]", r.event_type_id); } } } // Helper to flush and clear queues auto flush_queue = [&](auto insert_many, auto &queue) { if (!queue.empty()) { insert_many(*m_database, queue); queue.clear(); } }; // flush many PlayerEventLogsRepository::InsertMany(*m_database, m_record_batch_queue); // flush etl queues flush_queue(PlayerEventLootItemsRepository::InsertMany, etl_queues.loot_items); flush_queue(PlayerEventMerchantSellRepository::InsertMany, etl_queues.merchant_sell); flush_queue(PlayerEventMerchantPurchaseRepository::InsertMany, etl_queues.merchant_purchase); flush_queue(PlayerEventNpcHandinRepository::InsertMany, etl_queues.npc_handin); flush_queue(PlayerEventNpcHandinEntriesRepository::InsertMany, etl_queues.npc_handin_entries); flush_queue(PlayerEventTradeRepository::InsertMany, etl_queues.trade); flush_queue(PlayerEventTradeEntriesRepository::InsertMany, etl_queues.trade_entries); flush_queue(PlayerEventSpeechRepository::InsertMany, etl_queues.speech); flush_queue(PlayerEventKilledNpcRepository::InsertMany, etl_queues.killed_npc); flush_queue(PlayerEventKilledNamedNpcRepository::InsertMany, etl_queues.killed_named_npc); flush_queue(PlayerEventKilledRaidNpcRepository::InsertMany, etl_queues.killed_raid_npc); flush_queue(PlayerEventAaPurchaseRepository::InsertMany, etl_queues.aa_purchase); LogPlayerEvents( "Processing batch player event log queue of [{}] took [{}]", m_record_batch_queue.size(), benchmark.elapsed() ); // empty m_record_batch_queue.clear(); m_batch_queue_lock.unlock(); } // adds a player event to the queue void PlayerEventLogs::AddToQueue(PlayerEventLogsRepository::PlayerEventLogs &log) { m_batch_queue_lock.lock(); m_record_batch_queue.emplace_back(log); m_batch_queue_lock.unlock(); } // fills common event data in the SendEvent function void PlayerEventLogs::FillPlayerEvent( const PlayerEvent::PlayerEvent &p, PlayerEventLogsRepository::PlayerEventLogs &n ) { n.account_id = p.account_id; n.character_id = p.character_id; n.zone_id = p.zone_id; n.instance_id = p.instance_id; n.x = p.x; n.y = p.y; n.z = p.z; n.heading = p.heading; } // builds the dynamic packet used to ship the player event over the wire // supports serializing the struct so it can be rebuilt on the other end std::unique_ptr PlayerEventLogs::BuildPlayerEventPacket(const PlayerEvent::PlayerEventContainer &e) { EQ::Net::DynamicPacket dyn_pack; dyn_pack.PutSerialize(0, e); auto pack_size = sizeof(ServerSendPlayerEvent_Struct) + dyn_pack.Length(); auto pack = std::make_unique(ServerOP_PlayerEvent, static_cast(pack_size)); auto buf = reinterpret_cast(pack->pBuffer); buf->cereal_size = static_cast(dyn_pack.Length()); memcpy(buf->cereal_data, dyn_pack.Data(), dyn_pack.Length()); return pack; } const PlayerEventLogSettingsRepository::PlayerEventLogSettings *PlayerEventLogs::GetSettings() const { return m_settings; } bool PlayerEventLogs::IsEventDiscordEnabled(int32_t event_type_id) { // out of bounds check if (event_type_id >= PlayerEvent::EventType::MAX) { return false; } // make sure webhook id is set if (m_settings[event_type_id].discord_webhook_id == 0) { return false; } // ensure there is a matching webhook to begin with if (!EQEmuLogSys::Instance()->GetDiscordWebhooks()[m_settings[event_type_id].discord_webhook_id].webhook_url.empty()) { return true; } return false; } std::string PlayerEventLogs::GetDiscordWebhookUrlFromEventType(int32_t event_type_id) { // out of bounds check if (event_type_id >= PlayerEvent::EventType::MAX) { return ""; } // make sure webhook id is set if (m_settings[event_type_id].discord_webhook_id == 0) { return ""; } // ensure there is a matching webhook to begin with if (!EQEmuLogSys::Instance()->GetDiscordWebhooks()[m_settings[event_type_id].discord_webhook_id].webhook_url.empty()) { return EQEmuLogSys::Instance()->GetDiscordWebhooks()[m_settings[event_type_id].discord_webhook_id].webhook_url; } return ""; } void PlayerEventLogs::LoadPlayerEventSettingsFromQS( const std::vector &settings ) { for (const auto &e : settings) { if (e.id >= PlayerEvent::MAX || e.id < 0) { continue; } m_settings[e.id] = e; } LogInfo("Applied [{}] player event log settings from QS", settings.size()); } // GM_COMMAND | [x] Implemented Formatter // ZONING | [x] Implemented Formatter // AA_GAIN | [x] Implemented Formatter // AA_PURCHASE | [x] Implemented Formatter // FORAGE_SUCCESS | [x] Implemented Formatter // FORAGE_FAILURE | [x] Implemented Formatter // FISH_SUCCESS | [x] Implemented Formatter // FISH_FAILURE | [x] Implemented Formatter // ITEM_DESTROY | [x] Implemented Formatter // WENT_ONLINE | [x] Implemented Formatter // WENT_OFFLINE | [x] Implemented Formatter // LEVEL_GAIN | [x] Implemented Formatter // LEVEL_LOSS | [x] Implemented Formatter // LOOT_ITEM | [x] Implemented Formatter // MERCHANT_PURCHASE | [x] Implemented Formatter // MERCHANT_SELL | [x] Implemented Formatter // GROUP_JOIN | [] Implemented Formatter // GROUP_LEAVE | [] Implemented Formatter // RAID_JOIN | [] Implemented Formatter // RAID_LEAVE | [] Implemented Formatter // GROUNDSPAWN_PICKUP | [x] Implemented Formatter // NPC_HANDIN | [x] Implemented Formatter // SKILL_UP | [x] Implemented Formatter // TASK_ACCEPT | [x] Implemented Formatter // TASK_UPDATE | [x] Implemented Formatter // TASK_COMPLETE | [x] Implemented Formatter // TRADE | [] Implemented Formatter // GIVE_ITEM | [] Implemented Formatter // SAY | [x] Implemented Formatter // REZ_ACCEPTED | [x] Implemented Formatter // DEATH | [x] Implemented Formatter // COMBINE_FAILURE | [x] Implemented Formatter // COMBINE_SUCCESS | [x] Implemented Formatter // DROPPED_ITEM | [x] Implemented Formatter // SPLIT_MONEY | [x] Implemented Formatter // DZ_JOIN | [] Implemented Formatter // DZ_LEAVE | [] Implemented Formatter // TRADER_PURCHASE | [x] Implemented Formatter // TRADER_SELL | [x] Implemented Formatter // BANDOLIER_CREATE | [] Implemented Formatter // BANDOLIER_SWAP | [] Implemented Formatter // DISCOVER_ITEM | [X] Implemented Formatter std::string PlayerEventLogs::GetDiscordPayloadFromEvent(const PlayerEvent::PlayerEventContainer &e) { std::string payload; switch (e.player_event_log.event_type_id) { case PlayerEvent::AA_GAIN: { PlayerEvent::AAGainedEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatAAGainedEvent(e, n); break; } case PlayerEvent::AA_PURCHASE: { PlayerEvent::AAPurchasedEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatAAPurchasedEvent(e, n); break; } case PlayerEvent::COMBINE_FAILURE: case PlayerEvent::COMBINE_SUCCESS: { PlayerEvent::CombineEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatCombineEvent(e, n); break; } case PlayerEvent::DEATH: { PlayerEvent::DeathEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatDeathEvent(e, n); break; } case PlayerEvent::DISCOVER_ITEM: { PlayerEvent::DiscoverItemEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatDiscoverItemEvent(e, n); break; } case PlayerEvent::DROPPED_ITEM: { PlayerEvent::DroppedItemEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatDroppedItemEvent(e, n); break; } case PlayerEvent::FISH_FAILURE: case PlayerEvent::FORAGE_FAILURE: case PlayerEvent::WENT_ONLINE: case PlayerEvent::WENT_OFFLINE: { payload = PlayerEventDiscordFormatter::FormatWithNodata(e); break; } case PlayerEvent::FISH_SUCCESS: { PlayerEvent::FishSuccessEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatFishSuccessEvent(e, n); break; } case PlayerEvent::FORAGE_SUCCESS: { PlayerEvent::ForageSuccessEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatForageSuccessEvent(e, n); break; } case PlayerEvent::ITEM_DESTROY: { PlayerEvent::DestroyItemEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatDestroyItemEvent(e, n); break; } case PlayerEvent::LEVEL_GAIN: { PlayerEvent::LevelGainedEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatLevelGainedEvent(e, n); break; } case PlayerEvent::LEVEL_LOSS: { PlayerEvent::LevelLostEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatLevelLostEvent(e, n); break; } case PlayerEvent::LOOT_ITEM: { PlayerEvent::LootItemEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatLootItemEvent(e, n); break; } case PlayerEvent::GROUNDSPAWN_PICKUP: { PlayerEvent::GroundSpawnPickupEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatGroundSpawnPickupEvent(e, n); break; } case PlayerEvent::NPC_HANDIN: { PlayerEvent::HandinEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatNPCHandinEvent(e, n); break; } case PlayerEvent::SAY: { PlayerEvent::SayEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatEventSay(e, n); break; } case PlayerEvent::GM_COMMAND: { PlayerEvent::GMCommandEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatGMCommand(e, n); break; } case PlayerEvent::SKILL_UP: { PlayerEvent::SkillUpEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatSkillUpEvent(e, n); break; } case PlayerEvent::SPLIT_MONEY: { PlayerEvent::SplitMoneyEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatSplitMoneyEvent(e, n); break; } case PlayerEvent::TASK_ACCEPT: { PlayerEvent::TaskAcceptEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatTaskAcceptEvent(e, n); break; } case PlayerEvent::TASK_COMPLETE: { PlayerEvent::TaskCompleteEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatTaskCompleteEvent(e, n); break; } case PlayerEvent::TASK_UPDATE: { PlayerEvent::TaskUpdateEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatTaskUpdateEvent(e, n); break; } case PlayerEvent::TRADE: { PlayerEvent::TradeEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatTradeEvent(e, n); break; } case PlayerEvent::TRADER_PURCHASE: { PlayerEvent::TraderPurchaseEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatTraderPurchaseEvent(e, n); break; } case PlayerEvent::TRADER_SELL: { PlayerEvent::TraderSellEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatTraderSellEvent(e, n); break; } case PlayerEvent::REZ_ACCEPTED: { PlayerEvent::ResurrectAcceptEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatResurrectAcceptEvent(e, n); break; } case PlayerEvent::MERCHANT_PURCHASE: { PlayerEvent::MerchantPurchaseEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatMerchantPurchaseEvent(e, n); break; } case PlayerEvent::MERCHANT_SELL: { PlayerEvent::MerchantSellEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatMerchantSellEvent(e, n); break; } case PlayerEvent::ZONING: { PlayerEvent::ZoningEvent n{}; std::stringstream ss; { ss << e.player_event_log.event_data; cereal::JSONInputArchive ar(ss); n.serialize(ar); } payload = PlayerEventDiscordFormatter::FormatZoningEvent(e, n); break; } default: { LogPlayerEventsDetail( "Player event [{}] ({}) Discord formatter not implemented", e.player_event_log.event_type_name, e.player_event_log.event_type_id ); } } return payload; } // general process function, used in world or QS depending on rule Logging:PlayerEventsQSProcess void PlayerEventLogs::Process() { if (m_database_ping_timer.Check()) { m_database->ping(); } if (m_process_batch_events_timer.Check() || m_record_batch_queue.size() >= RuleI(Logging, BatchPlayerEventProcessChunkSize)) { ProcessBatchQueue(); } if (m_process_retention_truncation_timer.Check()) { ProcessRetentionTruncation(); } } void PlayerEventLogs::ProcessRetentionTruncation() { LogPlayerEventsDetail("Running truncation"); // Map of repository-specific deletion functions std::unordered_map> repository_deleters = { { PlayerEvent::LOOT_ITEM, [&](const std::string &condition) { return PlayerEventLootItemsRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::MERCHANT_SELL, [&](const std::string &condition) { return PlayerEventMerchantSellRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::MERCHANT_PURCHASE, [&](const std::string &condition) { return PlayerEventMerchantPurchaseRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::NPC_HANDIN, [&](const std::string &condition) { uint32 deleted_count = PlayerEventNpcHandinRepository::DeleteWhere(*m_database, condition); deleted_count += PlayerEventNpcHandinEntriesRepository::DeleteWhere(*m_database, condition); return deleted_count; }}, { PlayerEvent::TRADE, [&](const std::string &condition) { uint32 deleted_count = PlayerEventTradeRepository::DeleteWhere(*m_database, condition); deleted_count += PlayerEventTradeEntriesRepository::DeleteWhere(*m_database, condition); return deleted_count; }}, { PlayerEvent::SPEECH, [&](const std::string &condition) { return PlayerEventSpeechRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::KILLED_NPC, [&](const std::string &condition) { return PlayerEventKilledNpcRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::KILLED_NAMED_NPC, [&](const std::string &condition) { return PlayerEventKilledNamedNpcRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::KILLED_RAID_NPC, [&](const std::string &condition) { return PlayerEventKilledRaidNpcRepository::DeleteWhere(*m_database, condition); }}, { PlayerEvent::AA_PURCHASE, [&](const std::string &condition) { return PlayerEventAaPurchaseRepository::DeleteWhere(*m_database, condition); }} }; // Group event types by retention interval std::unordered_map> retention_groups; for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { if (m_settings[i].retention_days > 0) { retention_groups[m_settings[i].retention_days].push_back(i); } } for (const auto &[retention_days, event_types]: retention_groups) { std::string condition = fmt::format( "created_at < (NOW() - INTERVAL {} DAY)", retention_days ); // Handle ETL deletions for each event type in the group uint32 total_deleted_count = 0; for (int event_type_id: event_types) { if (m_settings[event_type_id].etl_enabled) { auto it = repository_deleters.find(static_cast(m_settings[event_type_id].id)); if (it != repository_deleters.end()) { total_deleted_count += it->second(condition); } else { LogError("Non-Implemented ETL Event Type [{}]", static_cast(m_settings[event_type_id].id)); } } } if (total_deleted_count > 0) { LogInfo( "Truncated [{}] ETL events older than [{}] days", total_deleted_count, retention_days ); } // Batch deletion for player_event_logs std::string event_type_ids = fmt::format( "({})", fmt::join(event_types, ", ") ); uint32 deleted_count = PlayerEventLogsRepository::DeleteWhere( *m_database, fmt::format( "event_type_id IN {} AND {}", event_type_ids, condition ) ); if (deleted_count > 0) { LogInfo( "Truncated [{}] events of types [{}] older than [{}] days", deleted_count, event_type_ids, retention_days ); } } } void PlayerEventLogs::ReloadSettings() { for (auto &e: PlayerEventLogSettingsRepository::All(*m_database)) { if (e.id >= PlayerEvent::MAX || e.id < 0) { continue; } m_settings[e.id] = e; } } const int32_t RETENTION_DAYS_DEFAULT = 7; void PlayerEventLogs::SetSettingsDefaults() { m_settings[PlayerEvent::GM_COMMAND].event_enabled = 1; m_settings[PlayerEvent::ZONING].event_enabled = 1; m_settings[PlayerEvent::AA_GAIN].event_enabled = 1; m_settings[PlayerEvent::AA_PURCHASE].event_enabled = 1; m_settings[PlayerEvent::FORAGE_SUCCESS].event_enabled = 0; m_settings[PlayerEvent::FORAGE_FAILURE].event_enabled = 0; m_settings[PlayerEvent::FISH_SUCCESS].event_enabled = 0; m_settings[PlayerEvent::FISH_FAILURE].event_enabled = 0; m_settings[PlayerEvent::ITEM_DESTROY].event_enabled = 1; m_settings[PlayerEvent::WENT_ONLINE].event_enabled = 0; m_settings[PlayerEvent::WENT_OFFLINE].event_enabled = 0; m_settings[PlayerEvent::LEVEL_GAIN].event_enabled = 1; m_settings[PlayerEvent::LEVEL_LOSS].event_enabled = 1; m_settings[PlayerEvent::LOOT_ITEM].event_enabled = 1; m_settings[PlayerEvent::MERCHANT_PURCHASE].event_enabled = 1; m_settings[PlayerEvent::MERCHANT_SELL].event_enabled = 1; m_settings[PlayerEvent::GROUP_JOIN].event_enabled = 0; m_settings[PlayerEvent::GROUP_LEAVE].event_enabled = 0; m_settings[PlayerEvent::RAID_JOIN].event_enabled = 0; m_settings[PlayerEvent::RAID_LEAVE].event_enabled = 0; m_settings[PlayerEvent::GROUNDSPAWN_PICKUP].event_enabled = 1; m_settings[PlayerEvent::NPC_HANDIN].event_enabled = 1; m_settings[PlayerEvent::SKILL_UP].event_enabled = 0; m_settings[PlayerEvent::TASK_ACCEPT].event_enabled = 1; m_settings[PlayerEvent::TASK_UPDATE].event_enabled = 1; m_settings[PlayerEvent::TASK_COMPLETE].event_enabled = 1; m_settings[PlayerEvent::TRADE].event_enabled = 1; m_settings[PlayerEvent::GIVE_ITEM].event_enabled = 1; m_settings[PlayerEvent::SAY].event_enabled = 0; m_settings[PlayerEvent::REZ_ACCEPTED].event_enabled = 1; m_settings[PlayerEvent::DEATH].event_enabled = 1; m_settings[PlayerEvent::COMBINE_FAILURE].event_enabled = 1; m_settings[PlayerEvent::COMBINE_SUCCESS].event_enabled = 1; m_settings[PlayerEvent::DROPPED_ITEM].event_enabled = 1; m_settings[PlayerEvent::SPLIT_MONEY].event_enabled = 1; m_settings[PlayerEvent::DZ_JOIN].event_enabled = 1; m_settings[PlayerEvent::DZ_LEAVE].event_enabled = 1; m_settings[PlayerEvent::TRADER_PURCHASE].event_enabled = 1; m_settings[PlayerEvent::TRADER_SELL].event_enabled = 1; m_settings[PlayerEvent::BANDOLIER_CREATE].event_enabled = 0; m_settings[PlayerEvent::BANDOLIER_SWAP].event_enabled = 0; m_settings[PlayerEvent::DISCOVER_ITEM].event_enabled = 1; m_settings[PlayerEvent::POSSIBLE_HACK].event_enabled = 1; m_settings[PlayerEvent::KILLED_NPC].event_enabled = 0; m_settings[PlayerEvent::KILLED_NAMED_NPC].event_enabled = 1; m_settings[PlayerEvent::KILLED_RAID_NPC].event_enabled = 1; m_settings[PlayerEvent::ITEM_CREATION].event_enabled = 1; m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM].event_enabled = 1; m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].event_enabled = 1; m_settings[PlayerEvent::PARCEL_SEND].event_enabled = 1; m_settings[PlayerEvent::PARCEL_RETRIEVE].event_enabled = 1; m_settings[PlayerEvent::PARCEL_DELETE].event_enabled = 1; m_settings[PlayerEvent::BARTER_TRANSACTION].event_enabled = 1; m_settings[PlayerEvent::EVOLVE_ITEM].event_enabled = 1; m_settings[PlayerEvent::SPEECH].event_enabled = 0; for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { m_settings[i].retention_days = RETENTION_DAYS_DEFAULT; } } void PlayerEventLogs::LoadEtlIds() { auto e = [&](auto p) -> bool { for (PlayerEventLogSettingsRepository::PlayerEventLogSettings const &c: m_settings) { if (c.id == p) { return c.etl_enabled ? true : false; } } return false; }; m_etl_settings.clear(); m_etl_settings = { { PlayerEvent::LOOT_ITEM, { .enabled = e(PlayerEvent::LOOT_ITEM), .table_name = "player_event_loot_items", .next_id = static_cast(m_database->GetNextTableId(PlayerEventLootItemsRepository::TableName())) } }, { PlayerEvent::MERCHANT_SELL, { .enabled = e(PlayerEvent::MERCHANT_SELL), .table_name = "player_event_merchant_sell", .next_id = static_cast(m_database->GetNextTableId(PlayerEventMerchantSellRepository::TableName())) } }, { PlayerEvent::MERCHANT_PURCHASE, { .enabled = e(PlayerEvent::MERCHANT_PURCHASE), .table_name = "player_event_merchant_purchase", .next_id = static_cast(m_database->GetNextTableId(PlayerEventMerchantPurchaseRepository::TableName())) } }, { PlayerEvent::NPC_HANDIN, { .enabled = e(PlayerEvent::NPC_HANDIN), .table_name = "player_event_npc_handin", .next_id = static_cast(m_database->GetNextTableId(PlayerEventNpcHandinRepository::TableName())) } }, { PlayerEvent::TRADE, { .enabled = e(PlayerEvent::TRADE), .table_name = "player_event_trade", .next_id = static_cast(m_database->GetNextTableId(PlayerEventTradeRepository::TableName())) } }, { PlayerEvent::SPEECH, { .enabled = e(PlayerEvent::SPEECH), .table_name = "player_event_speech", .next_id = static_cast(m_database->GetNextTableId(PlayerEventSpeechRepository::TableName())) } }, { PlayerEvent::KILLED_NPC, { .enabled = e(PlayerEvent::KILLED_NPC), .table_name = "player_event_killed_npc", .next_id = static_cast(m_database->GetNextTableId(PlayerEventKilledNpcRepository::TableName())) } }, { PlayerEvent::KILLED_NAMED_NPC, { .enabled = e(PlayerEvent::KILLED_NAMED_NPC), .table_name = "player_event_killed_named_npc", .next_id = static_cast(m_database->GetNextTableId(PlayerEventKilledNamedNpcRepository::TableName())) } }, { PlayerEvent::KILLED_RAID_NPC, { .enabled = e(PlayerEvent::KILLED_RAID_NPC), .table_name = "player_event_killed_raid_npc", .next_id = static_cast(m_database->GetNextTableId(PlayerEventKilledRaidNpcRepository::TableName())) } }, { PlayerEvent::AA_PURCHASE, { .enabled = e(PlayerEvent::AA_PURCHASE), .table_name = "player_event_aa_purchase", .next_id = static_cast(m_database->GetNextTableId(PlayerEventAaPurchaseRepository::TableName())) } } }; for (auto &e: m_etl_settings) { LogPlayerEventsDetail( "ETL Settings [{}] Enabled [{}] Table [{}] NextId [{}]", PlayerEvent::EventName[e.first], e.second.enabled, e.second.table_name, e.second.next_id ); } } bool PlayerEventLogs::LoadDatabaseConnection() { const auto c = EQEmuConfig::get(); LogInfo( "Connecting to MySQL for PlayerEvents [{}]@[{}]:[{}]", c->DatabaseUsername.c_str(), c->DatabaseHost.c_str(), c->DatabasePort ); if (!player_event_database.Connect( c->DatabaseHost.c_str(), c->DatabaseUsername.c_str(), c->DatabasePassword.c_str(), c->DatabaseDB.c_str(), c->DatabasePort )) { LogError("Cannot continue without a database connection for player events."); return false; } SetDatabase(&player_event_database); return true; }