From d9f545a5eca7fd19b01046b14fa78b236157ab0a Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 12 Feb 2023 21:31:01 -0600 Subject: [PATCH] [Logging] Implement Player Event Logging system (#2833) * Plumbing * Batch processing in world * Cleanup * Cleanup * Update player_event_logs.cpp * Add player zoning event * Use generics * Comments * Add events * Add more events * AA_GAIN, AA_PURCHASE, FORAGE_SUCCESS, FORAGE_FAILURE * FISH_SUCCESS, FISH_FAILURE, ITEM_DESTROY * Add charges to ITEM_DESTROY * WENT_ONLINE, WENT_OFFLINE * LEVEL_GAIN, LEVEL_LOSS * LOOT_ITEM * MERCHANT_PURCHASE * MERCHANT_SELL * SKILL_UP * Add events * Add more events * TASK_ACCEPT, TASK_COMPLETE, and TASK_UPDATE * GROUNDSPAWN_PICKUP * SAY * REZ_ACCEPTED * COMBINE_FAILURE and COMBINE_SUCCESS * DROPPED_ITEM * DEATH * SPLIT_MONEY * TRADER_PURCHASE and TRADER_SELL * DISCOVER_ITEM * Convert GM_COMMAND to use new macro * Convert ZONING event to use macro * Revert some code changes * Revert "Revert some code changes" This reverts commit d53682f997e89a053a660761085913245db91e9d. * Add cereal generation support to repositories * TRADE * Formatting * Cleanup * Relocate discord_manager to discord folder * Discord sending plumbing * Rename UCS's Database class to UCSDatabase to be more specific and not collide with base Database class for repository usage * More discord sending plumbing * More discord message formatting work * More discord formatting work * Discord formatting of events * Format WENT_ONLINE, WENT_OFFLINE * Add merchant purchase event * Handle Discord MERCHANT_SELL formatter * Update player_event_discord_formatter.cpp * Tweaks * Implement retention truncation * Put mutex locking on batch queue, put processor on its own thread * Process on initial bootup * Implement optional QS processing, implement keepalive from world to QS * Reload player event settings when logs are reloaded in game * Set settings defaults * Update player_event_logs.cpp * Update player_event_logs.cpp * Set retention days on boot * Update player_event_logs.cpp * Player Handin Event Testing. Testing player handin stuff. * Cleanup. * Finish NPC Handin. * set a reference to the client inside of the trade object as well for plugins to process * Fix for windows _inline * Bump to cpp20 default, ignore excessive warnings on windows * Bump FMT to 6.1.2 for cpp20 compat and swap fmt::join for Strings::Join * Windows compile fixes * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * Create 2022_12_19_player_events_tables.sql * [Formatters] Work on Discord Formatters * Handin money. * Format header * [Formatters] Work on Discord Formatters * Format * Format * [Formatters] More Formatter work, need to test further. * [Formatters] More Work on Formatters. * Add missing #endif * [Formatters] Work on Formatters, fix Bot formatting in ^create help * NPC Handin Discord Formatter * Update player_event_logs.cpp * Discover Item Discord Formatter * Dropped Item Discord Formatter * Split Money Discord Formatter * Trader Discord Formatters * Cleanup. * Trade Event Discord Formatter Groundwork * SAY don't record GM commands * GM_Command don't record #help * Update player_event_logs.cpp * Fill in more event data * Post rebase fixes * Post rebase fix * Discord formatting adjustments * Add event deprecation or unimplemented tag support * Trade events * Add return money and sanity checks. * Update schema * Update ucs.cpp * Update client.cpp * Update 2022_12_19_player_events_tables.sql * Implement archive single line * Replace hackers table and functions with PossibleHack player event * Replace very old eventlog table since the same events are covered by player event logs * Update bot_command.cpp * Record NPC kill events ALL / Named / Raid * Add BatchEventProcessIntervalSeconds rule * Naming * Update CMakeLists.txt * Update database_schema.h * Remove logging function and methods * DB version * Cleanup SendPlayerHandinEvent --------- Co-authored-by: Kinglykrab Co-authored-by: Aeadoin <109764533+Aeadoin@users.noreply.github.com> --- common/CMakeLists.txt | 21 +- common/database.cpp | 38 - common/database.h | 3 - common/database_schema.h | 4 +- common/discord/discord.cpp | 104 +- common/discord/discord.h | 5 + common/{ => discord}/discord_manager.cpp | 14 +- common/{ => discord}/discord_manager.h | 5 +- common/eq_constants.h | 21 +- .../events/player_event_discord_formatter.cpp | 1343 +++++++++++++++++ .../events/player_event_discord_formatter.h | 214 +++ common/events/player_event_logs.cpp | 703 +++++++++ common/events/player_event_logs.h | 85 ++ common/events/player_events.h | 935 ++++++++++++ common/json/json_archive_single_line.h | 1010 +++++++++++++ common/platform.cpp | 27 +- common/platform.h | 2 + .../base/base_eventlog_repository.h | 412 ----- ...se_player_event_log_settings_repository.h} | 164 +- .../base/base_player_event_logs_repository.h | 465 ++++++ .../repositories/character_data_repository.h | 3 +- .../dynamic_zone_members_repository.h | 2 +- .../expedition_lockouts_repository.h | 2 +- common/repositories/expeditions_repository.h | 2 +- ...=> player_event_log_settings_repository.h} | 18 +- ...itory.h => player_event_logs_repository.h} | 18 +- common/ruletypes.h | 2 + common/servertalk.h | 32 +- common/strings.cpp | 14 + common/strings.h | 1 + common/timer.cpp | 1 - common/version.h | 2 +- queryserv/database.cpp | 19 +- queryserv/database.h | 2 +- queryserv/worldserver.cpp | 2 +- ucs/database.h | 1 + ucs/ucs.cpp | 8 +- ucs/worldserver.cpp | 15 +- utils/sql/db_update_manifest.txt | 1 + .../2022_12_19_player_events_tables.sql | 34 + world/cli/test.cpp | 15 +- world/client.cpp | 29 +- world/client.h | 1 + world/dynamic_zone_manager.cpp | 12 +- world/expedition_database.cpp | 4 +- world/main.cpp | 17 + world/queryserv.cpp | 9 +- world/queryserv.h | 3 + world/shared_task_manager.cpp | 6 +- world/shared_task_world_messaging.cpp | 2 +- world/zoneserver.cpp | 27 + zone/aa.cpp | 14 + zone/attack.cpp | 20 +- zone/bot_command.cpp | 73 - zone/bot_command.h | 1 - zone/cheat_manager.cpp | 86 +- zone/client.cpp | 312 +++- zone/client.h | 7 +- zone/client_packet.cpp | 325 ++-- zone/client_process.cpp | 59 +- zone/combat_record.cpp | 55 +- zone/combat_record.h | 16 +- zone/command.cpp | 13 +- zone/command.h | 1 - zone/common.h | 7 +- zone/corpse.cpp | 24 +- zone/embparser_api.cpp | 6 + zone/embperl.h | 2 + zone/exp.cpp | 31 +- zone/expedition.h | 1 + zone/forage.cpp | 23 + zone/gm_commands/logcommand.cpp | 89 -- zone/groups.cpp | 13 + zone/inventory.cpp | 132 +- zone/lua_general.cpp | 6 + zone/lua_parser_events.cpp | 6 +- zone/main.cpp | 4 + zone/mob.h | 4 +- zone/mob_ai.cpp | 4 +- zone/object.cpp | 24 +- zone/questmgr.cpp | 171 ++- zone/questmgr.h | 1 + zone/raids.cpp | 13 + zone/spells.cpp | 46 +- zone/task_client_state.cpp | 41 + zone/task_manager.cpp | 2 +- zone/tradeskills.cpp | 23 + zone/trading.cpp | 284 ++-- zone/worldserver.cpp | 7 +- zone/zonedb.cpp | 28 - zone/zonedb.h | 1 - zone/zoning.cpp | 17 + 92 files changed, 6480 insertions(+), 1391 deletions(-) rename common/{ => discord}/discord_manager.cpp (83%) rename common/{ => discord}/discord_manager.h (64%) create mode 100644 common/events/player_event_discord_formatter.cpp create mode 100644 common/events/player_event_discord_formatter.h create mode 100644 common/events/player_event_logs.cpp create mode 100644 common/events/player_event_logs.h create mode 100644 common/events/player_events.h create mode 100644 common/json/json_archive_single_line.h delete mode 100644 common/repositories/base/base_eventlog_repository.h rename common/repositories/base/{base_hackers_repository.h => base_player_event_log_settings_repository.h} (56%) create mode 100644 common/repositories/base/base_player_event_logs_repository.h rename common/repositories/{eventlog_repository.h => player_event_log_settings_repository.h} (68%) rename common/repositories/{hackers_repository.h => player_event_logs_repository.h} (71%) create mode 100644 utils/sql/git/required/2022_12_19_player_events_tables.sql delete mode 100755 zone/gm_commands/logcommand.cpp diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b9b4fa21c..0f9fe6aa6 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -33,9 +33,11 @@ SET(common_sources eq_stream_proxy.cpp eqtime.cpp event_sub.cpp + events/player_event_logs.cpp + events/player_event_discord_formatter.cpp expedition_lockout_timer.cpp extprofile.cpp - discord_manager.cpp + discord/discord_manager.cpp faction.cpp file.cpp guild_base.cpp @@ -198,7 +200,6 @@ SET(repositories repositories/base/base_dynamic_zones_repository.h repositories/base/base_dynamic_zone_members_repository.h repositories/base/base_dynamic_zone_templates_repository.h - repositories/base/base_eventlog_repository.h repositories/base/base_expeditions_repository.h repositories/base/base_expedition_lockouts_repository.h repositories/base/base_faction_association_repository.h @@ -218,7 +219,6 @@ SET(repositories repositories/base/base_guilds_repository.h repositories/base/base_guild_ranks_repository.h repositories/base/base_guild_relations_repository.h - repositories/base/base_hackers_repository.h repositories/base/base_horses_repository.h repositories/base/base_instance_list_repository.h repositories/base/base_instance_list_player_repository.h @@ -264,6 +264,8 @@ SET(repositories repositories/base/base_pets_equipmentset_repository.h repositories/base/base_pets_equipmentset_entries_repository.h repositories/base/base_player_titlesets_repository.h + repositories/base/base_player_event_log_settings_repository.h + repositories/base/base_player_event_logs_repository.h repositories/base/base_quest_globals_repository.h repositories/base/base_raid_details_repository.h repositories/base/base_raid_members_repository.h @@ -376,7 +378,6 @@ SET(repositories repositories/dynamic_zones_repository.h repositories/dynamic_zone_members_repository.h repositories/dynamic_zone_templates_repository.h - repositories/eventlog_repository.h repositories/expeditions_repository.h repositories/expedition_lockouts_repository.h repositories/faction_association_repository.h @@ -396,7 +397,6 @@ SET(repositories repositories/guilds_repository.h repositories/guild_ranks_repository.h repositories/guild_relations_repository.h - repositories/hackers_repository.h repositories/horses_repository.h repositories/instance_list_repository.h repositories/instance_list_player_repository.h @@ -442,6 +442,8 @@ SET(repositories repositories/pets_equipmentset_repository.h repositories/pets_equipmentset_entries_repository.h repositories/player_titlesets_repository.h + repositories/player_event_log_settings_repository.h + repositories/player_event_logs_repository.h repositories/quest_globals_repository.h repositories/raid_details_repository.h repositories/raid_members_repository.h @@ -507,7 +509,7 @@ SET(common_headers dbcore.h deity.h discord/discord.h - discord_manager.h + discord/discord_manager.h dynamic_zone_base.h emu_constants.h emu_limits.h @@ -530,6 +532,9 @@ SET(common_headers eq_stream_locator.h eq_stream_proxy.h eqtime.h + events/player_event_logs.h + events/player_event_discord_formatter.h + events/player_events.h errmsg.h event_sub.h expedition_lockout_timer.h @@ -608,6 +613,7 @@ SET(common_headers event/event_loop.h event/task.h event/timer.h + json/json_archive_single_line.h json/json.h json/json-forwards.h net/console_server.h @@ -661,8 +667,7 @@ SET(common_headers StackWalker/StackWalker.h util/memory_stream.h util/directory.h - util/uuid.h - ) + util/uuid.h) SOURCE_GROUP(Event FILES event/event_loop.h diff --git a/common/database.cpp b/common/database.cpp index a93cb7669..06b9ca159 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -1281,44 +1281,6 @@ bool Database::MoveCharacterToZone(const char *charname, uint32 zone_id) return results.RowsAffected() != 0; } -bool Database::SetHackerFlag(const char* accountname, const char* charactername, const char* hacked) { - std::string query = StringFormat("INSERT INTO `hackers` (account, name, hacked) values('%s','%s','%s')", accountname, charactername, hacked); - auto results = QueryDatabase(query); - - if (!results.Success()) { - return false; - } - - return results.RowsAffected() != 0; -} - -bool Database::SetMQDetectionFlag(const char* accountname, const char* charactername, const char* hacked, const char* zone) { - //Utilize the "hacker" table, but also give zone information. - std::string query = StringFormat("INSERT INTO hackers(account,name,hacked,zone) values('%s','%s','%s','%s')", accountname, charactername, hacked, zone); - auto results = QueryDatabase(query); - - if (!results.Success()) - { - return false; - } - - return results.RowsAffected() != 0; -} - -bool Database::SetMQDetectionFlag(const char* accountname, const char* charactername, const std::string &hacked, const char* zone) { - //Utilize the "hacker" table, but also give zone information. - auto query = fmt::format("INSERT INTO hackers(account, name, hacked, zone) values('{}', '{}', '{}', '{}')", - accountname, charactername, hacked, zone); - auto results = QueryDatabase(query); - - if (!results.Success()) - { - return false; - } - - return results.RowsAffected() != 0; -} - uint8 Database::GetRaceSkill(uint8 skillid, uint8 in_race) { uint16 race_cap = 0; diff --git a/common/database.h b/common/database.h index 33ec0e474..2f0803720 100644 --- a/common/database.h +++ b/common/database.h @@ -108,9 +108,6 @@ public: bool MoveCharacterToZone(uint32 character_id, uint32 zone_id); bool ReserveName(uint32 account_id, char *name); bool SaveCharacterCreate(uint32 character_id, uint32 account_id, PlayerProfile_Struct *pp); - bool SetHackerFlag(const char *accountname, const char *charactername, const char *hacked); - bool SetMQDetectionFlag(const char *accountname, const char *charactername, const char *hacked, const char *zone); - bool SetMQDetectionFlag(const char *accountname, const char *charactername, const std::string &hacked, const char *zone); bool UpdateName(const char *oldname, const char *newname); bool CopyCharacter( const std::string& source_character_name, diff --git a/common/database_schema.h b/common/database_schema.h index af88774be..7a22ea1a7 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -321,13 +321,11 @@ namespace DatabaseSchema { "discord_webhooks", "dynamic_zone_members", "dynamic_zones", - "eventlog", "expedition_lockouts", "expeditions", "gm_ips", "group_id", "group_leaders", - "hackers", "instance_list", "ip_exemptions", "item_tick", @@ -343,6 +341,8 @@ namespace DatabaseSchema { "respawn_times", "saylink", "server_scheduled_events", + "player_event_log_settings", + "player_event_logs" "shared_task_activity_state", "shared_task_dynamic_zones", "shared_task_members", diff --git a/common/discord/discord.cpp b/common/discord/discord.cpp index 294f51c65..2ba584503 100644 --- a/common/discord/discord.cpp +++ b/common/discord/discord.cpp @@ -1,22 +1,17 @@ +#include +#include #include "discord.h" #include "../http/httplib.h" #include "../json/json.h" #include "../strings.h" #include "../eqemu_logsys.h" +#include "../events/player_event_logs.h" constexpr int MAX_RETRIES = 10; void Discord::SendWebhookMessage(const std::string &message, const std::string &webhook_url) { - // validate - if (webhook_url.empty()) { - LogDiscord("[webhook_url] is empty"); - return; - } - - // validate - if (webhook_url.find("http://") == std::string::npos && webhook_url.find("https://") == std::string::npos) { - LogDiscord("[webhook_url] [{}] does not contain a valid http/s prefix.", webhook_url); + if (!ValidateWebhookUrl(webhook_url)) { return; } @@ -28,7 +23,7 @@ void Discord::SendWebhookMessage(const std::string &message, const std::string & std::string endpoint = Strings::Replace(webhook_url, base_url, ""); // client - httplib::Client cli(base_url.c_str()); + httplib::Client cli(base_url); cli.set_connection_timeout(0, 15000000); // 15 sec cli.set_read_timeout(15, 0); // 15 seconds cli.set_write_timeout(15, 0); // 15 seconds @@ -46,9 +41,9 @@ void Discord::SendWebhookMessage(const std::string &message, const std::string & int retries = 0; int retry_timer = 1000; while (retry) { - if (auto res = cli.Post(endpoint.c_str(), payload.str(), "application/json")) { + if (auto res = cli.Post(endpoint, payload.str(), "application/json")) { if (res->status != 200 && res->status != 204) { - LogError("Code [{}] Error [{}]", res->status, res->body); + LogError("[Discord Client] Code [{}] Error [{}]", res->status, res->body); } if (res->status == 429) { if (!res->body.empty()) { @@ -81,6 +76,74 @@ void Discord::SendWebhookMessage(const std::string &message, const std::string & } } +void Discord::SendPlayerEventMessage( + const PlayerEvent::PlayerEventContainer &e, + const std::string &webhook_url +) +{ + if (!ValidateWebhookUrl(webhook_url)) { + return; + } + + auto s = Strings::Split(webhook_url, '/'); + + // url + std::string base_url = fmt::format("{}//{}", s[0], s[2]); + std::string endpoint = Strings::Replace(webhook_url, base_url, ""); + + // client + httplib::Client cli(base_url); + cli.set_connection_timeout(0, 15000000); // 15 sec + cli.set_read_timeout(15, 0); // 15 seconds + cli.set_write_timeout(15, 0); // 15 seconds + httplib::Headers headers = { + {"Content-Type", "application/json"} + }; + + std::string payload = PlayerEventLogs::GetDiscordPayloadFromEvent(e); + if (payload.empty()) { + return; + } + + bool retry = true; + int retries = 0; + int retry_timer = 1000; + while (retry) { + if (auto res = cli.Post(endpoint, payload, "application/json")) { + if (res->status != 200 && res->status != 204) { + LogError("Code [{}] Error [{}]", res->status, res->body); + } + if (res->status == 429) { + if (!res->body.empty()) { + std::stringstream ss(res->body); + Json::Value response; + + try { + ss >> response; + } + catch (std::exception const &ex) { + LogDiscord("JSON serialization failure [{}] via [{}]", ex.what(), res->body); + } + + retry_timer = std::stoi(response["retry_after"].asString()) + 500; + } + + LogDiscord("Rate limited... retrying message in [{}ms]", retry_timer); + std::this_thread::sleep_for(std::chrono::milliseconds(retry_timer + 500)); + } + if (res->status == 204) { + retry = false; + } + if (retries > MAX_RETRIES) { + LogDiscord("Retries exceeded for player event message"); + retry = false; + } + + retries++; + } + } +} + std::string Discord::FormatDiscordMessage(uint16 category_id, const std::string &message) { if (category_id == Logs::LogCategory::MySQLQuery) { @@ -89,3 +152,20 @@ std::string Discord::FormatDiscordMessage(uint16 category_id, const std::string return message + "\n"; } + +bool Discord::ValidateWebhookUrl(const std::string &webhook_url) +{ + // validate + if (webhook_url.empty()) { + LogDiscord("[webhook_url] is empty"); + return false; + } + + // validate + if (!Strings::Contains(webhook_url, "http://") && !Strings::Contains(webhook_url, "https://")) { + LogDiscord("[webhook_url] [{}] does not contain a valid http/s prefix.", webhook_url); + return false; + } + + return true; +} diff --git a/common/discord/discord.h b/common/discord/discord.h index d4ebc73f1..5cc59202a 100644 --- a/common/discord/discord.h +++ b/common/discord/discord.h @@ -4,11 +4,16 @@ #include #include "../types.h" +#include "../http/httplib.h" +#include "../repositories/player_event_logs_repository.h" +#include "../events/player_events.h" class Discord { public: static void SendWebhookMessage(const std::string& message, const std::string& webhook_url); static std::string FormatDiscordMessage(uint16 category_id, const std::string& message); + static void SendPlayerEventMessage(const PlayerEvent::PlayerEventContainer& e, const std::string &webhook_url); + static bool ValidateWebhookUrl(const std::string &webhook_url); }; diff --git a/common/discord_manager.cpp b/common/discord/discord_manager.cpp similarity index 83% rename from common/discord_manager.cpp rename to common/discord/discord_manager.cpp index aac1dadcc..e8691e763 100644 --- a/common/discord_manager.cpp +++ b/common/discord/discord_manager.cpp @@ -1,7 +1,6 @@ #include "discord_manager.h" -#include "../common/discord/discord.h" -#include "../common/eqemu_logsys.h" -#include "../common/strings.h" +#include "../../common/discord/discord.h" +#include "../events/player_event_logs.h" void DiscordManager::QueueWebhookMessage(uint32 webhook_id, const std::string &message) { @@ -55,7 +54,6 @@ void DiscordManager::ProcessMessageQueue() message = ""; } } - // final flush if (!message.empty()) { Discord::SendWebhookMessage( @@ -67,3 +65,11 @@ void DiscordManager::ProcessMessageQueue() webhook_message_queue.clear(); webhook_queue_lock.unlock(); } + +void DiscordManager::QueuePlayerEventMessage(const PlayerEvent::PlayerEventContainer& e) +{ + auto w = player_event_logs.GetDiscordWebhookUrlFromEventType(e.player_event_log.event_type_id); + if (!w.empty()) { + Discord::SendPlayerEventMessage(e, w); + } +} diff --git a/common/discord_manager.h b/common/discord/discord_manager.h similarity index 64% rename from common/discord_manager.h rename to common/discord/discord_manager.h index cc3573630..f577d994f 100644 --- a/common/discord_manager.h +++ b/common/discord/discord_manager.h @@ -4,12 +4,15 @@ #include #include #include -#include "../common/types.h" +#include "../../common/types.h" +#include "../repositories/player_event_logs_repository.h" +#include "../events/player_events.h" class DiscordManager { public: void QueueWebhookMessage(uint32 webhook_id, const std::string& message); void ProcessMessageQueue(); + void QueuePlayerEventMessage(const PlayerEvent::PlayerEventContainer& e); private: std::mutex webhook_queue_lock{}; std::map> webhook_message_queue{}; diff --git a/common/eq_constants.h b/common/eq_constants.h index 2ab28bdca..ae2cce84b 100644 --- a/common/eq_constants.h +++ b/common/eq_constants.h @@ -1017,14 +1017,13 @@ enum Anonymity : uint8 Roleplaying }; -enum ZoningMessage : int8 -{ - ZoneNoMessage = 0, - ZoneSuccess = 1, - ZoneNotReady = -1, - ZoneValidPC = -2, - ZoneStoryZone = -3, - ZoneNoExpansion = -6, +enum ZoningMessage : int8 { + ZoneNoMessage = 0, + ZoneSuccess = 1, + ZoneNotReady = -1, + ZoneValidPC = -2, + ZoneStoryZone = -3, + ZoneNoExpansion = -6, ZoneNoExperience = -7 }; @@ -1040,4 +1039,10 @@ enum class RecipeCountType : uint8 #define ALT_CURRENCY_ID_RADIANT 4 #define ALT_CURRENCY_ID_EBON 5 +enum ResurrectionActions +{ + Decline, + Accept +}; + #endif /*COMMON_EQ_CONSTANTS_H*/ diff --git a/common/events/player_event_discord_formatter.cpp b/common/events/player_event_discord_formatter.cpp new file mode 100644 index 000000000..791c6c02d --- /dev/null +++ b/common/events/player_event_discord_formatter.cpp @@ -0,0 +1,1343 @@ +#include "player_event_discord_formatter.h" +#include "../repositories/character_data_repository.h" +#include "../json/json_archive_single_line.h" +#include +#include +#include +#include + +std::string PlayerEventDiscordFormatter::GetCurrentTimestamp() +{ + time_t now; + time(&now); + char buf[sizeof "2011-10-08T07:07:09Z"]; + strftime(buf, sizeof buf, "%FT%TZ", gmtime(&now)); + std::string timestamp = buf; + return timestamp; +} + +void PlayerEventDiscordFormatter::BuildDiscordField( + std::vector *f, + const std::string &name, + const std::string &value, + bool is_inline +) +{ + if (value.empty()) { + return; + } + + f->emplace_back( + DiscordField{ + .name = name, + .value = value, + .is_inline = is_inline, + } + ); +} + +void PlayerEventDiscordFormatter::BuildBaseEmbed( + std::vector *e, + const std::vector &f, + const PlayerEvent::PlayerEventContainer c +) +{ + auto d = DiscordEmbed{}; + d.fields = f; + d.author = DiscordAuthor{ + .name = fmt::format( + "[Player Event] {}", + PlayerEvent::EventName[c.player_event_log.event_type_id] + ), + }; + // d.timestamp = GetCurrentTimestamp() + + e->emplace_back(d); +} + +std::string PlayerEventDiscordFormatter::FormatEventSay( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::SayEvent &e +) +{ + std::vector f = {}; + BuildDiscordField(&f, "Message", e.message); + BuildDiscordField(&f, "Target", e.target); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + + auto root = BuildDiscordWebhook(c, embeds); + + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatGMCommand( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::GMCommandEvent &e +) +{ + std::vector f = {}; + BuildDiscordField(&f, "Message", e.message); + if (e.target != "NONE") { + BuildDiscordField(&f, "Target", e.target); + } + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + + auto root = BuildDiscordWebhook(c, embeds); + + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatWithNodata(const PlayerEvent::PlayerEventContainer &c) +{ + std::vector f = {}; + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatMerchantPurchaseEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::MerchantPurchaseEvent &e +) +{ + std::vector f = {}; + BuildDiscordField(&f, "Merchant", fmt::format("{} ({}) NPC ID ({})", e.merchant_name, e.merchant_type, e.npc_id)); + BuildDiscordField(&f, "Item", fmt::format("{} ({}) x({})", e.item_name, e.item_id, e.charges)); + BuildDiscordField( + &f, + "Cost", + fmt::format( + ":moneybag: {}", + Strings::Money((e.cost / 1000), (e.cost / 100) % 10, (e.cost / 10) % 10, e.cost % 10))); + BuildDiscordField( + &f, + "Player Balance", + fmt::format( + ":moneybag: [{}] \n:gem: Currency [{}]", + Strings::Commify(std::to_string(e.player_money_balance)), + e.player_currency_balance + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatMerchantSellEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::MerchantSellEvent &e +) +{ + std::vector f = {}; + BuildDiscordField(&f, "Merchant", fmt::format("{} ({}) NPC ID ({})", e.merchant_name, e.merchant_type, e.npc_id)); + BuildDiscordField(&f, "Item", fmt::format("{} ({}) x({})", e.item_name, e.item_id, e.charges)); + BuildDiscordField( + &f, + "Cost", + fmt::format( + ":moneybag: {}", + Strings::Money((e.cost / 1000), (e.cost / 100) % 10, (e.cost / 10) % 10, e.cost % 10))); + BuildDiscordField( + &f, + "Player Balance", + fmt::format( + ":moneybag: [{}] \n:gem: Currency [{}]", + Strings::Commify(std::to_string(e.player_money_balance)), + e.player_currency_balance + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatZoningEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::ZoningEvent &e +) +{ + std::string instance_id_info; + std::string instance_version_info; + + if (e.to_instance_id > 0 || e.from_instance_id > 0) { + instance_id_info = fmt::format("Instance ID: ({}) :arrow_right: ({})", e.from_instance_id, e.to_instance_id); + } + + if (e.from_instance_version > 0 || e.to_instance_version > 0) { + instance_version_info = fmt::format( + "Instance Version: ({}) :arrow_right: ({})", + e.from_instance_version, + e.to_instance_version + ); + } + + std::vector f = {}; + BuildDiscordField( + &f, + "Zoning Information", + fmt::format( + "Zone: {} ({}) ({}) :arrow_right: {} ({}) ({})\n{}\n{}", + e.from_zone_long_name, + e.from_zone_short_name, + e.from_zone_id, + e.to_zone_long_name, + e.to_zone_short_name, + e.to_zone_id, + instance_id_info, + instance_version_info + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatAAGainedEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::AAGainedEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Points Gained", + fmt::format( + "AA Gained ({})", + e.aa_gained + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatAAPurchasedEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::AAPurchasedEvent &e +) +{ + std::string aa_info; + if (e.aa_previous_id != -1 || e.aa_next_id != -1) { + aa_info = fmt::format("AA Previous ID ({}) \nAA Next ID ({})", e.aa_previous_id, e.aa_next_id); + } + + std::vector f = {}; + BuildDiscordField( + &f, + "AA Purchased", + fmt::format( + "AA ID ({}) \nAA Cost ({}) \n{}", + e.aa_id, e.aa_cost, aa_info + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatForageSuccessEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::ForageSuccessEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Foraged Item", + fmt::format( + "Item ID ({}) \nItem Name ({})", + e.item_id, e.item_name + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatDestroyItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DestroyItemEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Destroyed Item", + fmt::format( + "{} ({}) \nCharges ({}) \nReason ({})", + e.item_name, e.item_id, e.charges, e.reason + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatLevelGainedEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::LevelGainedEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Level Information", + fmt::format( + "From ({}) > ({}) \nLevels Gained ({})", + e.from_level, e.to_level, e.levels_gained + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatLevelLostEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::LevelLostEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Level Information", + fmt::format( + "From ({}) > ({}) \nLevels Lost ({})", + e.from_level, e.to_level, e.levels_lost + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatLootItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::LootItemEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Looted Item", + fmt::format( + "{} ({})\nCharges: {}\nNPC: {} ({})", + e.item_name, + e.item_id, + e.charges, + e.corpse_name, + e.npc_id + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + + +std::string PlayerEventDiscordFormatter::FormatGroundSpawnPickupEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::GroundSpawnPickupEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Picked Up Item", + fmt::format( + "{} ({})", + e.item_name, e.item_id + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatSkillUpEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::SkillUpEvent &e +) +{ + std::string target_info; + if (e.against_who.length()) { + if (e.against_who == c.player_event.character_name) { + target_info = fmt::format("Target: Self"); + } + else { + target_info = fmt::format("Target: {}", e.against_who); + } + } + + std::vector f = {}; + BuildDiscordField( + &f, + "Skill Information", + fmt::format( + "Skill: {} \nLevel: ({}/{}) \n{}", + e.skill_id, e.value, e.max_skill, target_info + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatTaskAcceptEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TaskAcceptEvent &e +) +{ + + std::vector f = {}; + BuildDiscordField( + &f, + "Task Information", + fmt::format( + "{} ({}) \n {} ({})", + e.task_name, e.task_id, e.npc_name, e.npc_id + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatTaskCompleteEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TaskCompleteEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Task Information", + fmt::format( + "{} ({}) \nActivity ID ({}) \nDone ({})", + e.task_name, e.task_id, e.activity_id, e.done_count + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatTaskUpdateEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TaskUpdateEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Task Information", + fmt::format( + "{} ({}) \nActivity ID ({}) \nDone ({})", + e.task_name, e.task_id, e.activity_id, e.done_count + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatResurrectAcceptEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::ResurrectAcceptEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Resurrect Information", + fmt::format( + "From: {} \nSpell: {} ({})", + e.resurrecter_name, e.spell_name, e.spell_id + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatCombineEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::CombineEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Combine Information", + fmt::format( + "{} ({}) \n Made ({})", + e.recipe_name, e.recipe_id, e.made_count + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatFishSuccessEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::FishSuccessEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Fishing Information", + fmt::format( + "{} ({})", + e.item_name, e.item_id + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatDeathEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DeathEvent &e +) +{ + std::string killer_info; + if (e.killer_id) { + killer_info = fmt::format("Killer: {} ({})", e.killer_name, e.killer_id); + } + + std::string spell_info; + if (e.spell_id < MAX_SPELL_DB_ID_VAL) { + spell_info = fmt::format("Spell: {} ({})", e.spell_name, e.spell_id); + } + + std::string skill_info; + if (e.skill_id) { + skill_info = fmt::format("Skill: {} ({})", e.skill_name, e.skill_id); + } + + std::vector f = {}; + BuildDiscordField( + &f, + "Death Information", + fmt::format( + "{} \nDamage: {} \n {} \n {}", + killer_info, e.damage, spell_info, skill_info + ) + ); + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatNPCHandinEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::HandinEvent &e +) +{ + std::string handin_items_info; + if (!e.handin_items.empty()) { + for (const auto &h: e.handin_items) { + handin_items_info += fmt::format( + "{} ({}){}{}\n", + h.item_name, + h.item_id, + h.charges > 1 ? fmt::format(" Charges: {}", h.charges) : "", + h.attuned ? " (Attuned)" : "" + ); + } + } + + std::string return_items_info; + if (!e.return_items.empty()) { + for (const auto &r: e.return_items) { + return_items_info += fmt::format( + "{} ({}){}{}\n", + r.item_name, + r.item_id, + r.charges > 1 ? fmt::format(" Charges: {}", r.charges) : "", + r.attuned ? " (Attuned)" : "" + ); + } + } + + std::string handin_money_info; + if (e.handin_money.platinum) { + handin_money_info += fmt::format( + ":moneybag: {} Platinum\n", + Strings::Commify(std::to_string(e.handin_money.platinum)) + ); + } + + if (e.handin_money.gold) { + handin_money_info += fmt::format( + ":moneybag: {} Gold\n", + Strings::Commify(std::to_string(e.handin_money.gold)) + ); + } + + if (e.handin_money.silver) { + handin_money_info += fmt::format( + ":moneybag: {} Silver\n", + Strings::Commify(std::to_string(e.handin_money.silver)) + ); + } + + if (e.handin_money.copper) { + handin_money_info += fmt::format( + ":moneybag: {} Copper", + Strings::Commify(std::to_string(e.handin_money.copper)) + ); + } + + + std::string return_money_info; + if (e.return_money.platinum) { + return_money_info += fmt::format( + ":moneybag: {} Platinum\n", + Strings::Commify(std::to_string(e.return_money.platinum)) + ); + } + + if (e.return_money.gold) { + return_money_info += fmt::format( + ":moneybag: {} Gold\n", + Strings::Commify(std::to_string(e.return_money.gold)) + ); + } + + if (e.return_money.silver) { + return_money_info += fmt::format( + ":moneybag: {} Silver\n", + Strings::Commify(std::to_string(e.return_money.silver)) + ); + } + + if (e.return_money.copper) { + return_money_info += fmt::format( + ":moneybag: {} Copper", + Strings::Commify(std::to_string(e.return_money.copper)) + ); + } + + std::vector f = {}; + + if (!handin_items_info.empty()) { + BuildDiscordField( + &f, + "Handin Items", + fmt::format( + "{}", + handin_items_info + ) + ); + } + + if (!handin_money_info.empty()) { + BuildDiscordField( + &f, + "Handin Money", + fmt::format( + "{}", + handin_money_info + ) + ); + } + + if (!return_items_info.empty()) { + BuildDiscordField( + &f, + "Return Items", + fmt::format( + "{}", + return_items_info + ) + ); + } + + if (!return_money_info.empty()) { + BuildDiscordField( + &f, + "Return Money", + fmt::format( + "{}", + return_money_info + ) + ); + } + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatDiscoverItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DiscoverItemEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Discovered Item", + fmt::format( + "{} ({})", + e.item_name, e.item_id + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatDroppedItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DroppedItemEvent &e +) +{ + std::vector f = {}; + BuildDiscordField( + &f, + "Dropped Item", + fmt::format( + "{} ({})\nSlot: {} ({})", + e.item_name, + e.item_id, + EQ::invslot::GetInvPossessionsSlotName(e.slot_id), + e.slot_id + ) + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatSplitMoneyEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::SplitMoneyEvent &e +) +{ + std::string money_info; + if (e.platinum) { + money_info += fmt::format( + ":moneybag: {} Platinum\n", + Strings::Commify(std::to_string(e.platinum)) + ); + } + + if (e.gold) { + money_info += fmt::format( + ":moneybag: {} Gold\n", + Strings::Commify(std::to_string(e.gold)) + ); + } + + if (e.silver) { + money_info += fmt::format( + ":moneybag: {} Silver\n", + Strings::Commify(std::to_string(e.silver)) + ); + } + + if (e.copper) { + money_info += fmt::format( + ":moneybag: {} Copper\n", + Strings::Commify(std::to_string(e.copper)) + ); + } + + money_info += fmt::format( + ":moneybag: [{}]", + Strings::Commify(std::to_string(e.player_money_balance)) + ); + + std::vector f = {}; + BuildDiscordField( + &f, + "Split Money", + money_info + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatTraderPurchaseEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TraderPurchaseEvent &e +) +{ + std::string purchase_info; + + purchase_info += fmt::format( + "Item: {} ({})\n", + e.item_name, + e.item_id + ); + + purchase_info += fmt::format( + "Trader: {} ({})\n", + e.trader_name, + e.trader_id + ); + + purchase_info += fmt::format( + "Price: {} Amount: {} Total: {}\n", + Strings::Commify(std::to_string(e.price)), + e.charges, + Strings::Commify(std::to_string(e.total_cost)) + ); + + purchase_info += fmt::format( + ":moneybag: [{}]\n", + Strings::Commify(std::to_string(e.player_money_balance)) + ); + + std::vector f = {}; + BuildDiscordField( + &f, + "Purchase Information", + purchase_info + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatTraderSellEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TraderSellEvent &e +) +{ + std::string sell_info; + + sell_info += fmt::format( + "Item: {} ({})\n", + e.item_name, + e.item_id + ); + + sell_info += fmt::format( + "Buyer: {} ({})\n", + e.buyer_name, + e.buyer_id + ); + + sell_info += fmt::format( + "Price: {} Amount: {} Total: {}\n", + Strings::Commify(std::to_string(e.price)), + e.charges, + Strings::Commify(std::to_string(e.total_cost)) + ); + + sell_info += fmt::format( + ":moneybag: [{}]\n", + Strings::Commify(std::to_string(e.player_money_balance)) + ); + + std::vector f = {}; + BuildDiscordField( + &f, + "Sale Information", + sell_info + ); + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +std::string PlayerEventDiscordFormatter::FormatTradeEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TradeEvent &e +) +{ + std::string character_1_item_info; + if (!e.character_1_give_items.empty()) { + for (const auto &i: e.character_1_give_items) { + std::string augment_info; + if (i.aug_1_item_id > 0) { + augment_info += fmt::format( + "Augment 1: {} ({})", + i.aug_1_item_name, + i.aug_1_item_id + ); + } + + if (i.aug_2_item_id > 0) { + augment_info += fmt::format( + "Augment 2: {} ({})", + i.aug_2_item_name, + i.aug_2_item_id + ); + } + + if (i.aug_3_item_id > 0) { + augment_info += fmt::format( + "Augment 3: {} ({})", + i.aug_3_item_name, + i.aug_3_item_id + ); + } + + if (i.aug_4_item_id > 0) { + augment_info += fmt::format( + "Augment 4: {} ({})\n", + i.aug_4_item_name, + i.aug_4_item_id + ); + } + + if (i.aug_5_item_id > 0) { + augment_info += fmt::format( + "Augment 5: {} ({})\n", + i.aug_5_item_name, + i.aug_5_item_id + ); + } + + if (i.aug_6_item_id > 0) { + augment_info += fmt::format( + "Augment 6: {} ({})", + i.aug_6_item_name, + i.aug_6_item_id + ); + } + + character_1_item_info += fmt::format( + "{} ({}){}\nSlot: {} ({}){}\n{}", + i.item_name, + i.item_id, + i.charges > 1 ? fmt::format(" Charges: {}", i.charges) : "", + EQ::invslot::GetInvPossessionsSlotName(i.slot), + i.slot, + i.in_bag ? " (Bagged Item)" : "", + augment_info + ); + } + } + + std::string character_2_item_info; + if (!e.character_2_give_items.empty()) { + for (const auto &i: e.character_2_give_items) { + std::string augment_info; + if (i.aug_1_item_id > 0) { + augment_info += fmt::format( + "Augment 1: {} ({})", + i.aug_1_item_name, + i.aug_1_item_id + ); + } + + if (i.aug_2_item_id > 0) { + augment_info += fmt::format( + "Augment 2: {} ({})", + i.aug_2_item_name, + i.aug_2_item_id + ); + } + + if (i.aug_3_item_id > 0) { + augment_info += fmt::format( + "Augment 3: {} ({})", + i.aug_3_item_name, + i.aug_3_item_id + ); + } + + if (i.aug_4_item_id > 0) { + augment_info += fmt::format( + "Augment 4: {} ({})\n", + i.aug_4_item_name, + i.aug_4_item_id + ); + } + + if (i.aug_5_item_id > 0) { + augment_info += fmt::format( + "Augment 5: {} ({})\n", + i.aug_5_item_name, + i.aug_5_item_id + ); + } + + if (i.aug_6_item_id > 0) { + augment_info += fmt::format( + "Augment 6: {} ({})", + i.aug_6_item_name, + i.aug_6_item_id + ); + } + + character_2_item_info += fmt::format( + "{} ({}){}\nSlot: {} ({}){}\n{}\n", + i.item_name, + i.item_id, + i.charges > 1 ? fmt::format(" Charges: {}", i.charges) : "", + EQ::invslot::GetInvPossessionsSlotName(i.slot), + i.slot, + i.in_bag ? " (Bagged Item)" : "", + augment_info + ); + } + } + + std::string character_1_money_info; + if (e.character_1_give_money.platinum) { + character_1_money_info += fmt::format( + ":moneybag: {} Platinum\n", + Strings::Commify(std::to_string(e.character_1_give_money.platinum)) + ); + } + + if (e.character_1_give_money.gold) { + character_1_money_info += fmt::format( + ":moneybag: {} Gold\n", + Strings::Commify(std::to_string(e.character_1_give_money.gold)) + ); + } + + if (e.character_1_give_money.silver) { + character_1_money_info += fmt::format( + ":moneybag: {} Silver\n", + Strings::Commify(std::to_string(e.character_1_give_money.silver)) + ); + } + + if (e.character_1_give_money.copper) { + character_1_money_info += fmt::format( + ":moneybag: {} Copper", + Strings::Commify(std::to_string(e.character_1_give_money.copper)) + ); + } + + std::string character_2_money_info; + if (e.character_2_give_money.platinum) { + character_2_money_info += fmt::format( + ":moneybag: {} Platinum\n", + Strings::Commify(std::to_string(e.character_2_give_money.platinum)) + ); + } + + if (e.character_2_give_money.gold) { + character_2_money_info += fmt::format( + ":moneybag: {} Gold\n", + Strings::Commify(std::to_string(e.character_2_give_money.gold)) + ); + } + + if (e.character_2_give_money.silver) { + character_2_money_info += fmt::format( + ":moneybag: {} Silver\n", + Strings::Commify(std::to_string(e.character_2_give_money.silver)) + ); + } + + if (e.character_2_give_money.copper) { + character_2_money_info += fmt::format( + ":moneybag: {} Copper", + Strings::Commify(std::to_string(e.character_2_give_money.copper)) + ); + } + + std::vector f = {}; + + if (!character_1_item_info.empty()) { + BuildDiscordField( + &f, + "Character 1 Items", + character_1_item_info + ); + } + + if (!character_1_money_info.empty()) { + BuildDiscordField( + &f, + "Character 1 Money", + character_1_money_info + ); + } + + if (!character_2_item_info.empty()) { + BuildDiscordField( + &f, + "Character 2 Items", + character_2_item_info + ); + } + + if (!character_2_money_info.empty()) { + BuildDiscordField( + &f, + "Character 2 Money", + character_2_money_info + ); + } + + std::vector embeds = {}; + BuildBaseEmbed(&embeds, f, c); + auto root = BuildDiscordWebhook(c, embeds); + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + root.serialize(ar); + } + + return ss.str(); +} + +DiscordWebhook PlayerEventDiscordFormatter::BuildDiscordWebhook( + const PlayerEvent::PlayerEventContainer &p, + std::vector &embeds +) +{ + DiscordWebhook w = DiscordWebhook{ + .embeds = embeds + }; + + std::string instance_info; + if (p.player_event.instance_id > 0) { + instance_info = fmt::format("Instance ID [{}]", p.player_event.instance_id); + } + + std::string guild_info; + if (!p.player_event.guild_name.empty()) { + guild_info = fmt::format(":shield: **Guild** [{}] ({})", p.player_event.guild_name, p.player_event.guild_id); + } + + std::string character = fmt::format( + "{} ({}) {}", + p.player_event.character_name, + p.player_event.character_id, + guild_info + ); + + std::string zone = fmt::format( + "[{}] ({}) ({}) {}", + p.player_event.zone_long_name, + p.player_event.zone_short_name, + p.player_event.zone_id, + instance_info + ); + + w.content = fmt::format(":trident: **Character** {} :map: **Zone** {}", character, zone); + +// w.avatar_url = "https://cdn.discordapp.com/icons/212663220849213441/a_710698e80c111a5674e1ef716d8e3f14.webp?size=96"; + + return w; +} diff --git a/common/events/player_event_discord_formatter.h b/common/events/player_event_discord_formatter.h new file mode 100644 index 000000000..aef518ace --- /dev/null +++ b/common/events/player_event_discord_formatter.h @@ -0,0 +1,214 @@ +#ifndef EQEMU_PLAYER_EVENT_DISCORD_FORMATTER_H +#define EQEMU_PLAYER_EVENT_DISCORD_FORMATTER_H + +#include +#include "player_events.h" +#include "../repositories/base/base_player_event_logs_repository.h" +#include +#include + +struct DiscordField { + std::string name; + std::string value; + bool is_inline; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(name), + CEREAL_NVP(value), + cereal::make_nvp("inline", is_inline) + ); + } +}; + +struct DiscordAuthor { + std::string name; + std::string icon_url; + std::string url; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(name), + CEREAL_NVP(icon_url), + CEREAL_NVP(url) + ); + } +}; + +struct DiscordEmbed { + std::vector fields; + std::string title; + std::string description; + std::string timestamp; + DiscordAuthor author; + + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(fields), + CEREAL_NVP(title), + CEREAL_NVP(description), + CEREAL_NVP(timestamp), + CEREAL_NVP(author) + ); + } +}; + +struct DiscordWebhook { + std::vector embeds; + std::string content; + std::string avatar_url; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(embeds), + CEREAL_NVP(avatar_url), + CEREAL_NVP(content) + ); + } +}; + + +class PlayerEventDiscordFormatter { +public: + static std::string GetCurrentTimestamp(); + static std::string FormatEventSay(const PlayerEvent::PlayerEventContainer &c, const PlayerEvent::SayEvent &e); + static std::string + FormatGMCommand(const PlayerEvent::PlayerEventContainer &c, const PlayerEvent::GMCommandEvent &e); + static void BuildDiscordField( + std::vector *f, + const std::string &name, + const std::string &value, + bool is_inline = true + ); + static void BuildBaseEmbed( + std::vector *e, + const std::vector &f, + PlayerEvent::PlayerEventContainer c + ); + static std::string FormatWithNodata(const PlayerEvent::PlayerEventContainer &c); + + static std::string FormatAAGainedEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::AAGainedEvent &e + ); + static std::string FormatAAPurchasedEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::AAPurchasedEvent &e + ); + static std::string FormatDeathEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DeathEvent &e + ); + static std::string FormatFishSuccessEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::FishSuccessEvent &e + ); + static std::string FormatForageSuccessEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::ForageSuccessEvent &e + ); + static std::string FormatDestroyItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DestroyItemEvent &e + ); + static std::string FormatDiscoverItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DiscoverItemEvent &e + ); + static std::string FormatDroppedItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::DroppedItemEvent &e + ); + static std::string FormatLevelGainedEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::LevelGainedEvent &e + ); + static std::string FormatLevelLostEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::LevelLostEvent &e + ); + static std::string FormatLootItemEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::LootItemEvent &e + ); + static std::string FormatGroundSpawnPickupEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::GroundSpawnPickupEvent &e + ); + static std::string FormatMerchantPurchaseEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::MerchantPurchaseEvent &e + ); + static std::string FormatMerchantSellEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::MerchantSellEvent &e + ); + static std::string FormatNPCHandinEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::HandinEvent &e + ); + static std::string FormatSkillUpEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::SkillUpEvent &e + ); + static std::string FormatTaskAcceptEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TaskAcceptEvent &e + ); + static std::string FormatTaskCompleteEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TaskCompleteEvent &e + ); + static std::string FormatTaskUpdateEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TaskUpdateEvent &e + ); + static std::string FormatTradeEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TradeEvent &e + ); + static std::string FormatTraderPurchaseEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TraderPurchaseEvent &e + ); + static std::string FormatTraderSellEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::TraderSellEvent &e + ); + static std::string FormatResurrectAcceptEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::ResurrectAcceptEvent &e + ); + static std::string FormatSplitMoneyEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::SplitMoneyEvent &e + ); + static std::string FormatCombineEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::CombineEvent &e + ); + static std::string FormatZoningEvent( + const PlayerEvent::PlayerEventContainer &c, + const PlayerEvent::ZoningEvent &e + ); + static DiscordWebhook BuildDiscordWebhook( + const PlayerEvent::PlayerEventContainer &p, + std::vector &embeds + ); +}; + + +#endif //EQEMU_PLAYER_EVENT_DISCORD_FORMATTER_H diff --git a/common/events/player_event_logs.cpp b/common/events/player_event_logs.cpp new file mode 100644 index 000000000..b2b432b8b --- /dev/null +++ b/common/events/player_event_logs.cpp @@ -0,0 +1,703 @@ +#include +#include "player_event_logs.h" +#include "player_event_discord_formatter.h" +#include "../platform.h" +#include "../rulesys.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); + + 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; + } + + SetSettingsDefaults(); + + // initialize settings from database + auto s = PlayerEventLogSettingsRepository::All(*m_database); + std::vector db{}; + db.reserve(s.size()); + for (auto &e: s) { + m_settings[e.id] = e; + db.emplace_back(e.id); + } + + // 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; + PlayerEventLogSettingsRepository::InsertOne(*m_database, c); + } + } + + 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) { + 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() +{ + if (m_record_batch_queue.empty()) { + return; + } + + BenchTimer benchmark; + + // flush many + PlayerEventLogsRepository::InsertMany(*m_database, m_record_batch_queue); + LogInfo( + "Processing batch player event log queue of [{}] took [{}]", + m_record_batch_queue.size(), + benchmark.elapsed() + ); + + // empty + m_batch_queue_lock.lock(); + m_record_batch_queue = {}; + m_batch_queue_lock.unlock(); +} + +// adds a player event to the queue +void PlayerEventLogs::AddToQueue(const 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 (!LogSys.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 (!LogSys.GetDiscordWebhooks()[m_settings[event_type_id].discord_webhook_id].webhook_url.empty()) { + return LogSys.GetDiscordWebhooks()[m_settings[event_type_id].discord_webhook_id].webhook_url; + } + + return ""; +} + +// 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: { + 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_FAILURE: { + payload = PlayerEventDiscordFormatter::FormatWithNodata(e); + 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::WENT_ONLINE: + case PlayerEvent::WENT_OFFLINE: { + payload = PlayerEventDiscordFormatter::FormatWithNodata(e); + 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: { + LogInfo( + "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 UCS depending on rule Logging:PlayerEventsQSProcess +void PlayerEventLogs::Process() +{ + if (m_process_batch_events_timer.Check()) { + ProcessBatchQueue(); + } + + if (m_process_retention_truncation_timer.Check()) { + ProcessRetentionTruncation(); + } +} + +void PlayerEventLogs::ProcessRetentionTruncation() +{ + LogInfo("Running truncation"); + + for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { + if (m_settings[i].retention_days > 0) { + int deleted_count = PlayerEventLogsRepository::DeleteWhere( + *m_database, + fmt::format( + "event_type_id = {} AND created_at < (NOW() - INTERVAL {} DAY)", + i, + m_settings[i].retention_days + ) + ); + + if (deleted_count > 0) { + LogInfo( + "Truncated [{}] events of type [{}] ({}) older than [{}] days", + deleted_count, + PlayerEvent::EventName[i], + i, + m_settings[i].retention_days + ); + } + } + } +} + +void PlayerEventLogs::ReloadSettings() +{ + for (auto &e: PlayerEventLogSettingsRepository::All(*m_database)) { + 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 = 1; + m_settings[PlayerEvent::KILLED_NAMED_NPC].event_enabled = 1; + m_settings[PlayerEvent::KILLED_RAID_NPC].event_enabled = 1; + + for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { + m_settings[i].retention_days = RETENTION_DAYS_DEFAULT; + } +} diff --git a/common/events/player_event_logs.h b/common/events/player_event_logs.h new file mode 100644 index 000000000..814f075ba --- /dev/null +++ b/common/events/player_event_logs.h @@ -0,0 +1,85 @@ +#ifndef EQEMU_PLAYER_EVENT_LOGS_H +#define EQEMU_PLAYER_EVENT_LOGS_H + +#include "../repositories/player_event_log_settings_repository.h" +#include "player_events.h" +#include "../servertalk.h" +#include "../repositories/player_event_logs_repository.h" +#include "../timer.h" +#include "../json/json_archive_single_line.h" +#include +#include + +class PlayerEventLogs { +public: + void Init(); + void ReloadSettings(); + PlayerEventLogs *SetDatabase(Database *db); + bool ValidateDatabaseConnection(); + bool IsEventEnabled(PlayerEvent::EventType event); + + void Process(); + + // batch queue + void AddToQueue(const PlayerEventLogsRepository::PlayerEventLogs &logs); + + // main event record generic function + // can ingest any struct event types + template + std::unique_ptr RecordEvent( + PlayerEvent::EventType t, + const PlayerEvent::PlayerEvent &p, + T e + ) + { + auto n = PlayerEventLogsRepository::NewEntity(); + FillPlayerEvent(p, n); + n.event_type_id = t; + + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + e.serialize(ar); + } + + n.event_type_name = PlayerEvent::EventName[t]; + n.event_data = Strings::Contains(ss.str(), "noop") ? "{}" : ss.str(); + n.created_at = std::time(nullptr); + + auto c = PlayerEvent::PlayerEventContainer{ + .player_event = p, + .player_event_log = n + }; + + return BuildPlayerEventPacket(c); + } + + [[nodiscard]] const PlayerEventLogSettingsRepository::PlayerEventLogSettings *GetSettings() const; + bool IsEventDiscordEnabled(int32_t event_type_id); + std::string GetDiscordWebhookUrlFromEventType(int32_t event_type_id); + + static std::string GetDiscordPayloadFromEvent(const PlayerEvent::PlayerEventContainer &e); +private: + Database *m_database; // reference to database + PlayerEventLogSettingsRepository::PlayerEventLogSettings m_settings[PlayerEvent::EventType::MAX]{}; + + // batch queue is used to record events in batch + std::vector m_record_batch_queue{}; + static void FillPlayerEvent(const PlayerEvent::PlayerEvent &p, PlayerEventLogsRepository::PlayerEventLogs &n); + static std::unique_ptr + BuildPlayerEventPacket(const PlayerEvent::PlayerEventContainer &e); + + // timers + Timer m_process_batch_events_timer; // events processing timer + Timer m_process_retention_truncation_timer; // timer for truncating events based on retention settings + + // processing + std::mutex m_batch_queue_lock{}; + void ProcessBatchQueue(); + void ProcessRetentionTruncation(); + void SetSettingsDefaults(); +}; + +extern PlayerEventLogs player_event_logs; + +#endif //EQEMU_PLAYER_EVENT_LOGS_H diff --git a/common/events/player_events.h b/common/events/player_events.h new file mode 100644 index 000000000..98625693d --- /dev/null +++ b/common/events/player_events.h @@ -0,0 +1,935 @@ +#ifndef EQEMU_PLAYER_EVENTS_H +#define EQEMU_PLAYER_EVENTS_H + +#include +#include +#include "../types.h" +#include "../repositories/player_event_logs_repository.h" + +namespace PlayerEvent { + + enum EventType { + GM_COMMAND = 1, + ZONING, + AA_GAIN, + AA_PURCHASE, + FORAGE_SUCCESS, + FORAGE_FAILURE, + FISH_SUCCESS, + FISH_FAILURE, + ITEM_DESTROY, + WENT_ONLINE, + WENT_OFFLINE, + LEVEL_GAIN, + LEVEL_LOSS, + LOOT_ITEM, + MERCHANT_PURCHASE, + MERCHANT_SELL, + GROUP_JOIN, // unimplemented + GROUP_LEAVE, // unimplemented + RAID_JOIN, // unimplemented + RAID_LEAVE, // unimplemented + GROUNDSPAWN_PICKUP, + NPC_HANDIN, + SKILL_UP, + TASK_ACCEPT, + TASK_UPDATE, + TASK_COMPLETE, + TRADE, + GIVE_ITEM, // unimplemented + SAY, + REZ_ACCEPTED, + DEATH, + COMBINE_FAILURE, + COMBINE_SUCCESS, + DROPPED_ITEM, + SPLIT_MONEY, + DZ_JOIN, // unimplemented + DZ_LEAVE, // unimplemented + TRADER_PURCHASE, + TRADER_SELL, + BANDOLIER_CREATE, // unimplemented + BANDOLIER_SWAP, // unimplemented + DISCOVER_ITEM, + POSSIBLE_HACK, + KILLED_NPC, + KILLED_NAMED_NPC, + KILLED_RAID_NPC, + MAX // dont remove + }; + + // Don't ever remove items, even if they are deprecated + // If event is deprecated just tag (Deprecated) in the name + // If event is unimplemented just tag (Unimplemented) in the name + // Events don't get saved to the database if unimplemented or deprecated + // Events tagged as deprecated will get automatically removed + static const char *EventName[PlayerEvent::MAX] = { + "None", + "GM Command", + "Zoning", + "AA Gain", + "AA Purchase", + "Forage Success", + "Forage Failure", + "Fish Success", + "Fish Failure", + "Item Destroy", + "Went Online", + "Went Offline", + "Level Gain", + "Level Loss", + "Loot Item", + "Merchant Purchase", + "Merchant Sell", + "Group Join (Unimplemented)", + "Group Leave (Unimplemented)", + "Raid Join (Unimplemented)", + "Raid Leave (Unimplemented)", + "Groundspawn Pickup", + "NPC Handin", + "Skill Up", + "Task Accept", + "Task Update", + "Task Complete", + "Trade", + "Given Item (Unimplemented)", + "Say", + "Rez Accepted", + "Death", + "Combine Failure", + "Combine Success", + "Dropped Item", + "Split Money", + "DZ Join (Unimplemented)", + "DZ Leave (Unimplemented)", + "Trader Purchase", + "Trader Sell", + "Bandolier Create (Unimplemented)", + "Bandolier Swap (Unimplemented)", + "Discover Item", + "Possible Hack", + "Killed NPC", + "Killed Named NPC", + "Killed Raid NPC" + }; + + // Generic struct used by all events + struct PlayerEvent { + int64 account_id; + std::string account_name; + int64 character_id; + std::string character_name; + int64 guild_id; + std::string guild_name; + int zone_id; + std::string zone_short_name; + std::string zone_long_name; + int instance_id; + float x; + float y; + float z; + float heading; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(account_id), + CEREAL_NVP(account_name), + CEREAL_NVP(character_id), + CEREAL_NVP(character_name), + CEREAL_NVP(guild_id), + CEREAL_NVP(guild_name), + CEREAL_NVP(zone_id), + CEREAL_NVP(zone_short_name), + CEREAL_NVP(zone_long_name), + CEREAL_NVP(instance_id), + CEREAL_NVP(x), + CEREAL_NVP(y), + CEREAL_NVP(z), + CEREAL_NVP(heading) + ); + } + }; + + // contains metadata in use for things like log/discord formatters + // along with the actual event to be persisted + struct PlayerEventContainer { + PlayerEvent player_event; + PlayerEventLogsRepository::PlayerEventLogs player_event_log; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(player_event), + CEREAL_NVP(player_event_log) + ); + } + }; + + // used in events with no extra data + struct EmptyEvent { + std::string noop; // noop, gets discard upstream + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(noop) + ); + } + }; + + // used in Trade event + struct TradeItem { + int64 item_id; + std::string item_name; + int32 slot; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(slot) + ); + } + }; + + // used in Trade event + class TradeItemEntry { + public: + uint16 slot; + uint32 item_id; + std::string item_name; + uint16 charges; + uint32 aug_1_item_id; + std::string aug_1_item_name; + uint32 aug_2_item_id; + std::string aug_2_item_name; + uint32 aug_3_item_id; + std::string aug_3_item_name; + uint32 aug_4_item_id; + std::string aug_4_item_name; + uint32 aug_5_item_id; + std::string aug_5_item_name; + uint32 aug_6_item_id; + std::string aug_6_item_name; + bool in_bag; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(slot), + CEREAL_NVP(item_id), + CEREAL_NVP(charges), + CEREAL_NVP(aug_1_item_id), + CEREAL_NVP(aug_2_item_id), + CEREAL_NVP(aug_3_item_id), + CEREAL_NVP(aug_4_item_id), + CEREAL_NVP(aug_5_item_id), + CEREAL_NVP(in_bag) + ); + } + }; + + /** + * Events + */ + struct Money { + int32 platinum; + int32 gold; + int32 silver; + int32 copper; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(platinum), + CEREAL_NVP(gold), + CEREAL_NVP(silver), + CEREAL_NVP(copper) + ); + } + }; + + struct TradeEvent { + uint32 character_1_id; + std::string character_1_name; + uint32 character_2_id; + std::string character_2_name; + Money character_1_give_money; + Money character_2_give_money; + std::vector character_1_give_items; + std::vector character_2_give_items; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(character_1_id), + CEREAL_NVP(character_1_name), + CEREAL_NVP(character_2_id), + CEREAL_NVP(character_2_name), + CEREAL_NVP(character_1_give_money), + CEREAL_NVP(character_2_give_money), + CEREAL_NVP(character_1_give_items), + CEREAL_NVP(character_2_give_items) + ); + } + }; + + struct GMCommandEvent { + std::string message; + std::string target; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(message), + CEREAL_NVP(target) + ); + } + }; + + struct ZoningEvent { + std::string from_zone_long_name; + std::string from_zone_short_name; + int32 from_zone_id; + int32 from_instance_id; + int32 from_instance_version; + std::string to_zone_long_name; + std::string to_zone_short_name; + int32 to_zone_id; + int32 to_instance_id; + int32 to_instance_version; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(from_zone_long_name), + CEREAL_NVP(from_zone_short_name), + CEREAL_NVP(from_zone_id), + CEREAL_NVP(from_instance_id), + CEREAL_NVP(from_instance_version), + CEREAL_NVP(to_zone_long_name), + CEREAL_NVP(to_zone_short_name), + CEREAL_NVP(to_zone_id), + CEREAL_NVP(to_instance_id), + CEREAL_NVP(to_instance_version) + ); + } + }; + + struct AAGainedEvent { + uint32 aa_gained; + + // cereal + template + void serialize(Archive &ar) + { + ar(CEREAL_NVP(aa_gained)); + } + }; + + struct AAPurchasedEvent { + int32 aa_id; + int32 aa_cost; + int32 aa_previous_id; + int32 aa_next_id; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(aa_id), + CEREAL_NVP(aa_cost), + CEREAL_NVP(aa_previous_id), + CEREAL_NVP(aa_next_id) + ); + } + }; + + struct ForageSuccessEvent { + uint32 item_id; + std::string item_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name) + ); + } + }; + + struct FishSuccessEvent { + uint32 item_id; + std::string item_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name) + ); + } + }; + + struct DestroyItemEvent { + uint32 item_id; + std::string item_name; + int16 charges; + std::string reason; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(reason), + CEREAL_NVP(charges) + ); + } + }; + + struct LevelGainedEvent { + uint32 from_level; + uint8 to_level; + int levels_gained; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(from_level), + CEREAL_NVP(to_level), + CEREAL_NVP(levels_gained) + ); + } + }; + + struct LevelLostEvent { + uint32 from_level; + uint8 to_level; + int levels_lost; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(from_level), + CEREAL_NVP(to_level), + CEREAL_NVP(levels_lost) + ); + } + }; + + struct LootItemEvent { + uint32 item_id; + std::string item_name; + int16 charges; + uint32 npc_id; + std::string corpse_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(charges), + CEREAL_NVP(npc_id), + CEREAL_NVP(corpse_name) + ); + } + }; + + struct MerchantPurchaseEvent { + uint32 npc_id; + std::string merchant_name; + uint32 merchant_type; + uint32 item_id; + std::string item_name; + int16 charges; + uint32 cost; + uint32 alternate_currency_id; + uint64 player_money_balance; + uint64 player_currency_balance; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(npc_id), + CEREAL_NVP(merchant_name), + CEREAL_NVP(merchant_type), + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(charges), + CEREAL_NVP(cost), + CEREAL_NVP(alternate_currency_id), + CEREAL_NVP(player_money_balance), + CEREAL_NVP(player_currency_balance) + ); + } + }; + + struct MerchantSellEvent { + uint32 npc_id; + std::string merchant_name; + uint32 merchant_type; + uint32 item_id; + std::string item_name; + int16 charges; + uint32 cost; + uint32 alternate_currency_id; + uint64 player_money_balance; + uint64 player_currency_balance; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(npc_id), + CEREAL_NVP(merchant_name), + CEREAL_NVP(merchant_type), + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(charges), + CEREAL_NVP(cost), + CEREAL_NVP(alternate_currency_id), + CEREAL_NVP(player_money_balance), + CEREAL_NVP(player_currency_balance) + ); + } + }; + + struct SkillUpEvent { + uint32 skill_id; + int value; + int16 max_skill; + std::string against_who; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(skill_id), + CEREAL_NVP(value), + CEREAL_NVP(max_skill), + CEREAL_NVP(against_who) + ); + } + }; + + struct TaskAcceptEvent { + uint32 npc_id; + std::string npc_name; + uint32 task_id; + std::string task_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(npc_id), + CEREAL_NVP(npc_name), + CEREAL_NVP(task_id), + CEREAL_NVP(task_name) + ); + } + }; + + struct TaskUpdateEvent { + uint32 task_id; + std::string task_name; + uint32 activity_id; + uint32 done_count; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(task_id), + CEREAL_NVP(task_name), + CEREAL_NVP(activity_id), + CEREAL_NVP(done_count) + ); + } + }; + + struct TaskCompleteEvent { + uint32 task_id; + std::string task_name; + uint32 activity_id; + uint32 done_count; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(task_id), + CEREAL_NVP(task_name), + CEREAL_NVP(activity_id), + CEREAL_NVP(done_count) + ); + } + }; + + struct GroundSpawnPickupEvent { + uint32 item_id; + std::string item_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name) + ); + } + }; + + struct SayEvent { + std::string message; + std::string target; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(message), + CEREAL_NVP(target) + ); + } + }; + + struct ResurrectAcceptEvent { + std::string resurrecter_name; + std::string spell_name; + uint32 spell_id; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(resurrecter_name), + CEREAL_NVP(spell_name), + CEREAL_NVP(spell_id) + ); + } + }; + + struct CombineEvent { + uint32 recipe_id; + std::string recipe_name; + uint32 made_count; + uint32 tradeskill_id; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(recipe_id), + CEREAL_NVP(recipe_name), + CEREAL_NVP(made_count), + CEREAL_NVP(tradeskill_id) + ); + } + }; + + struct DroppedItemEvent { + uint32 item_id; + std::string item_name; + int16 slot_id; + uint32 charges; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(slot_id), + CEREAL_NVP(charges) + ); + } + }; + + struct DeathEvent { + uint32 killer_id; + std::string killer_name; + int64 damage; + uint32 spell_id; + std::string spell_name; + int skill_id; + std::string skill_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(killer_id), + CEREAL_NVP(killer_name), + CEREAL_NVP(damage), + CEREAL_NVP(spell_id), + CEREAL_NVP(spell_name), + CEREAL_NVP(skill_id), + CEREAL_NVP(skill_name) + ); + } + }; + + struct SplitMoneyEvent { + uint32 copper; + uint32 silver; + uint32 gold; + uint32 platinum; + uint64 player_money_balance; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(copper), + CEREAL_NVP(silver), + CEREAL_NVP(gold), + CEREAL_NVP(platinum), + CEREAL_NVP(player_money_balance) + ); + } + }; + + struct TraderPurchaseEvent { + uint32 item_id; + std::string item_name; + uint32 trader_id; + std::string trader_name; + uint32 price; + uint32 charges; + uint32 total_cost; + uint64 player_money_balance; + + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(trader_id), + CEREAL_NVP(trader_name), + CEREAL_NVP(price), + CEREAL_NVP(charges), + CEREAL_NVP(total_cost), + CEREAL_NVP(player_money_balance) + ); + } + }; + + struct TraderSellEvent { + uint32 item_id; + std::string item_name; + uint32 buyer_id; + std::string buyer_name; + uint32 price; + uint32 charges; + uint32 total_cost; + uint64 player_money_balance; + + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(buyer_id), + CEREAL_NVP(buyer_name), + CEREAL_NVP(price), + CEREAL_NVP(charges), + CEREAL_NVP(total_cost), + CEREAL_NVP(player_money_balance) + ); + } + }; + + struct DiscoverItemEvent { + uint32 item_id; + std::string item_name; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name) + ); + } + }; + + class HandinEntry { + public: + uint32 item_id; + std::string item_name; + uint16 charges; + bool attuned; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(charges), + CEREAL_NVP(attuned) + ); + } + }; + + class HandinMoney { + public: + uint32 copper; + uint32 silver; + uint32 gold; + uint32 platinum; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(copper), + CEREAL_NVP(silver), + CEREAL_NVP(gold), + CEREAL_NVP(platinum) + ); + } + }; + + struct HandinEvent { + uint32 npc_id; + std::string npc_name; + std::vector handin_items; + HandinMoney handin_money; + std::vector return_items; + HandinMoney return_money; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(npc_id), + CEREAL_NVP(npc_name), + CEREAL_NVP(handin_items), + CEREAL_NVP(handin_money), + CEREAL_NVP(return_items), + CEREAL_NVP(return_money) + ); + } + }; + + struct PossibleHackEvent { + std::string message; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(message) + ); + } + }; + + struct KilledNPCEvent { + uint32 npc_id; + std::string npc_name; + uint32 combat_time_seconds; + uint64 total_damage_per_second_taken; + uint64 total_heal_per_second_taken; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(npc_id), + CEREAL_NVP(npc_name), + CEREAL_NVP(combat_time_seconds), + CEREAL_NVP(total_damage_per_second_taken), + CEREAL_NVP(total_heal_per_second_taken) + ); + } + }; +} + +#endif //EQEMU_PLAYER_EVENTS_H + +#define RecordPlayerEventLog(event_type, event_data) do {\ + if (player_event_logs.IsEventEnabled(event_type)) {\ + worldserver.SendPacket(\ + player_event_logs.RecordEvent(\ + event_type,\ + GetPlayerEvent(),\ + event_data\ + ).get()\ + );\ + }\ +} while (0) + +#define RecordPlayerEventLogWithClient(c, event_type, event_data) do {\ + if (player_event_logs.IsEventEnabled(event_type)) {\ + worldserver.SendPacket(\ + player_event_logs.RecordEvent(\ + event_type,\ + (c)->GetPlayerEvent(),\ + event_data\ + ).get()\ + );\ + }\ +} while (0) diff --git a/common/json/json_archive_single_line.h b/common/json/json_archive_single_line.h new file mode 100644 index 000000000..ed49cc35e --- /dev/null +++ b/common/json/json_archive_single_line.h @@ -0,0 +1,1010 @@ +/*! \file json.hpp + \brief JSON input and output archives */ +/* + Copyright (c) 2014, Randolph Voorhies, Shane Grant + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of cereal nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL RANDOLPH VOORHIES OR SHANE GRANT BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef CEREAL_ARCHIVES_JSON_SL_HPP_ +#define CEREAL_ARCHIVES_JSON_SL_HPP_ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace cereal +{ + // ###################################################################### + //! An output archive designed to save data to JSON + /*! This archive uses RapidJSON to build serialize data to JSON. + + JSON archives provides a human readable output but at decreased + performance (both in time and space) compared to binary archives. + + JSON archives are only guaranteed to finish flushing their contents + upon destruction and should thus be used in an RAII fashion. + + JSON benefits greatly from name-value pairs, which if present, will + name the nodes in the output. If these are not present, each level + of the output will be given an automatically generated delimited name. + + The precision of the output archive controls the number of decimals output + for floating point numbers and should be sufficiently large (i.e. at least 20) + if there is a desire to have binary equality between the numbers output and + those read in. In general you should expect a loss of precision when going + from floating point to text and back. + + JSON archives do not output the size information for any dynamically sized structure + and instead infer it from the number of children for a node. This means that data + can be hand edited for dynamic sized structures and will still be readable. This + is accomplished through the cereal::SizeTag object, which will cause the archive + to output the data as a JSON array (e.g. marked by [] instead of {}), which indicates + that the container is variable sized and may be edited. + + \ingroup Archives */ + class JSONOutputArchiveSingleLine : public OutputArchive, public traits::TextArchive + { + enum class NodeType { StartObject, InObject, StartArray, InArray }; + + using WriteStream = rapidjson::OStreamWrapper; + + public: + /*! @name Common Functionality + Common use cases for directly interacting with an JSONOutputArchive */ + //! @{ + + //! A class containing various advanced options for the JSON archive + class Options + { + public: + //! Default options +// static Options Default(){ return Options(); } + + //! Default options with no indentation + static Options NoIndent(){ return Options( JSONWriterSL::Writer::kDefaultMaxDecimalPlaces, IndentChar::space, 0 ); } + + //! Default options to output a single line + static Options Default(){ return Options( JSONWriterSL::Writer::kDefaultMaxDecimalPlaces, IndentChar::space, 0, true); } + + //! The character to use for indenting + enum class IndentChar : char + { + space = ' ', + tab = '\t', + newline = '\n', + carriage_return = '\r' + }; + + //! Specify specific options for the JSONOutputArchive + /*! @param precision The precision used for floating point numbers + @param indentChar The type of character to indent with + @param indentLength The number of indentChar to use for indentation + (0 corresponds to no indentation) */ + explicit Options( int precision = JSONWriterSL::Writer::kDefaultMaxDecimalPlaces, + IndentChar indentChar = IndentChar::space, + unsigned int indentLength = 4, + bool singleLine = false) : + itsPrecision( precision ), + itsIndentChar( static_cast(indentChar) ), + itsIndentLength( indentLength ), + itsSingleLine( singleLine ) { } + + private: + friend class JSONOutputArchiveSingleLine; + int itsPrecision; + char itsIndentChar; + unsigned int itsIndentLength; + bool itsSingleLine; + }; + + class JSONWriterSL + { + public: + using PrettyWriter = rapidjson::PrettyWriter; + using Writer = rapidjson::Writer; + + explicit JSONWriterSL(WriteStream &stream, const Options & options) { + if ((prettyPrint = !options.itsSingleLine)) { + new(&prettyWriter) PrettyWriter{stream}; + prettyWriter.SetIndent(options.itsIndentChar, options.itsIndentLength); + prettyWriter.SetMaxDecimalPlaces( options.itsPrecision ); + } else { + new(&writer) Writer{stream}; + writer.SetMaxDecimalPlaces( options.itsPrecision ); + } + } + + ~JSONWriterSL() { + if (prettyPrint) + prettyWriter.~PrettyWriter(); + else + writer.~Writer(); + } + + bool Bool(bool b) { return prettyPrint ? prettyWriter.Bool(b) : writer.Bool(b); } + bool Int(int i) { return prettyPrint ? prettyWriter.Int(i) : writer.Int(i); } + bool Uint(unsigned u) { return prettyPrint ? prettyWriter.Uint(u) : writer.Uint(u); } + bool Int64(int64_t i64) { return prettyPrint ? prettyWriter.Int64(i64) : writer.Int64(i64); } + bool Uint64(uint64_t u64) { return prettyPrint ? prettyWriter.Uint64(u64) : writer.Uint64(u64); } + bool Double(double d) { return prettyPrint ? prettyWriter.Double(d) : writer.Double(d); } + bool Null() { return prettyPrint ? prettyWriter.Null() : writer.Null(); } + + bool String(const char* str, unsigned length) { return prettyPrint ? prettyWriter.String(str, length) + : writer.String(str, length); } + bool String(const char* str) { return prettyPrint ? prettyWriter.String(str) + : writer.String(str); } + + bool StartArray() { return prettyPrint ? prettyWriter.StartArray() : writer.StartArray(); } + bool EndArray() { return prettyPrint ? prettyWriter.EndArray() : writer.EndArray(); } + + bool StartObject() { return prettyPrint ? prettyWriter.StartObject() : writer.StartObject(); } + bool EndObject() { return prettyPrint ? prettyWriter.EndObject() : writer.EndObject(); } + + private: + bool prettyPrint; + union { + Writer writer; + PrettyWriter prettyWriter; + }; + }; + + //! Construct, outputting to the provided stream + /*! @param stream The stream to output to. + @param options The JSON specific options to use. See the Options struct + for the values of default parameters */ + JSONOutputArchiveSingleLine(std::ostream & stream, Options const & options = Options::Default() ) : + OutputArchive(this), + itsWriteStream(stream), + itsWriter(itsWriteStream, options), + itsNextName(nullptr) + { + itsNameCounter.push(0); + itsNodeStack.push(NodeType::StartObject); + } + + //! Destructor, flushes the JSON + ~JSONOutputArchiveSingleLine() CEREAL_NOEXCEPT + { + if (itsNodeStack.top() == NodeType::InObject) + itsWriter.EndObject(); + else if (itsNodeStack.top() == NodeType::InArray) + itsWriter.EndArray(); + } + + //! Saves some binary data, encoded as a base64 string, with an optional name + /*! This will create a new node, optionally named, and insert a value that consists of + the data encoded as a base64 string */ + void saveBinaryValue( const void * data, size_t size, const char * name = nullptr ) + { + setNextName( name ); + writeName(); + + auto base64string = base64::encode( reinterpret_cast( data ), size ); + saveValue( base64string ); + }; + + //! @} + /*! @name Internal Functionality + Functionality designed for use by those requiring control over the inner mechanisms of + the JSONOutputArchive */ + //! @{ + + //! Starts a new node in the JSON output + /*! The node can optionally be given a name by calling setNextName prior + to creating the node + + Nodes only need to be started for types that are themselves objects or arrays */ + void startNode() + { + writeName(); + itsNodeStack.push(NodeType::StartObject); + itsNameCounter.push(0); + } + + //! Designates the most recently added node as finished + void finishNode() + { + // if we ended up serializing an empty object or array, writeName + // will never have been called - so start and then immediately end + // the object/array. + // + // We'll also end any object/arrays we happen to be in + switch(itsNodeStack.top()) + { + case NodeType::StartArray: + itsWriter.StartArray(); + case NodeType::InArray: + itsWriter.EndArray(); + break; + case NodeType::StartObject: + itsWriter.StartObject(); + case NodeType::InObject: + itsWriter.EndObject(); + break; + } + + itsNodeStack.pop(); + itsNameCounter.pop(); + } + + //! Sets the name for the next node created with startNode + void setNextName( const char * name ) + { + itsNextName = name; + } + + //! Saves a bool to the current node + void saveValue(bool b) { itsWriter.Bool(b); } + //! Saves an int to the current node + void saveValue(int i) { itsWriter.Int(i); } + //! Saves a uint to the current node + void saveValue(unsigned u) { itsWriter.Uint(u); } + //! Saves an int64 to the current node + void saveValue(int64_t i64) { itsWriter.Int64(i64); } + //! Saves a uint64 to the current node + void saveValue(uint64_t u64) { itsWriter.Uint64(u64); } + //! Saves a double to the current node + void saveValue(double d) { itsWriter.Double(d); } + //! Saves a string to the current node + void saveValue(std::string const & s) { itsWriter.String(s.c_str(), static_cast( s.size() )); } + //! Saves a const char * to the current node + void saveValue(char const * s) { itsWriter.String(s); } + //! Saves a nullptr to the current node + void saveValue(std::nullptr_t) { itsWriter.Null(); } + + private: + // Some compilers/OS have difficulty disambiguating the above for various flavors of longs, so we provide + // special overloads to handle these cases. + + //! 32 bit signed long saving to current node + template ::value> = traits::sfinae> inline + void saveLong(T l){ saveValue( static_cast( l ) ); } + + //! non 32 bit signed long saving to current node + template ::value> = traits::sfinae> inline + void saveLong(T l){ saveValue( static_cast( l ) ); } + + //! 32 bit unsigned long saving to current node + template ::value> = traits::sfinae> inline + void saveLong(T lu){ saveValue( static_cast( lu ) ); } + + //! non 32 bit unsigned long saving to current node + template ::value> = traits::sfinae> inline + void saveLong(T lu){ saveValue( static_cast( lu ) ); } + + public: +#ifdef _MSC_VER + //! MSVC only long overload to current node + void saveValue( unsigned long lu ){ saveLong( lu ); }; +#else // _MSC_VER + //! Serialize a long if it would not be caught otherwise + template ::value, + !std::is_same::value, + !std::is_same::value> = traits::sfinae> inline + void saveValue( T t ){ saveLong( t ); } + + //! Serialize an unsigned long if it would not be caught otherwise + template ::value, + !std::is_same::value, + !std::is_same::value> = traits::sfinae> inline + void saveValue( T t ){ saveLong( t ); } +#endif // _MSC_VER + + //! Save exotic arithmetic as strings to current node + /*! Handles long long (if distinct from other types), unsigned long (if distinct), and long double */ + template ::value, + !std::is_same::value, + !std::is_same::value, + !std::is_same::value, + !std::is_same::value, + (sizeof(T) >= sizeof(long double) || sizeof(T) >= sizeof(long long))> = traits::sfinae> inline + void saveValue(T const & t) + { + std::stringstream ss; ss.precision( std::numeric_limits::max_digits10 ); + ss << t; + saveValue( ss.str() ); + } + + //! Write the name of the upcoming node and prepare object/array state + /*! Since writeName is called for every value that is output, regardless of + whether it has a name or not, it is the place where we will do a deferred + check of our node state and decide whether we are in an array or an object. + + The general workflow of saving to the JSON archive is: + + 1. (optional) Set the name for the next node to be created, usually done by an NVP + 2. Start the node + 3. (if there is data to save) Write the name of the node (this function) + 4. (if there is data to save) Save the data (with saveValue) + 5. Finish the node + */ + void writeName() + { + NodeType const & nodeType = itsNodeStack.top(); + + // Start up either an object or an array, depending on state + if(nodeType == NodeType::StartArray) + { + itsWriter.StartArray(); + itsNodeStack.top() = NodeType::InArray; + } + else if(nodeType == NodeType::StartObject) + { + itsNodeStack.top() = NodeType::InObject; + itsWriter.StartObject(); + } + + // Array types do not output names + if(nodeType == NodeType::InArray) return; + + if(itsNextName == nullptr) + { + std::string name = "value" + std::to_string( itsNameCounter.top()++ ) + "\0"; + saveValue(name); + } + else + { + saveValue(itsNextName); + itsNextName = nullptr; + } + } + + //! Designates that the current node should be output as an array, not an object + void makeArray() + { + itsNodeStack.top() = NodeType::StartArray; + } + + //! @} + + private: + WriteStream itsWriteStream; //!< Rapidjson write stream + JSONWriterSL itsWriter; //!< Rapidjson writer + char const * itsNextName; //!< The next name + std::stack itsNameCounter; //!< Counter for creating unique names for unnamed nodes + std::stack itsNodeStack; + }; // JSONOutputArchive + + // ###################################################################### + //! An input archive designed to load data from JSON + /*! This archive uses RapidJSON to read in a JSON archive. + + As with the output JSON archive, the preferred way to use this archive is in + an RAII fashion, ensuring its destruction after all data has been read. + + Input JSON should have been produced by the JSONOutputArchive. Data can + only be added to dynamically sized containers (marked by JSON arrays) - + the input archive will determine their size by looking at the number of child nodes. + Only JSON originating from a JSONOutputArchive is officially supported, but data + from other sources may work if properly formatted. + + The JSONInputArchiveSingleLine does not require that nodes are loaded in the same + order they were saved by JSONOutputArchive. Using name value pairs (NVPs), + it is possible to load in an out of order fashion or otherwise skip/select + specific nodes to load. + + The default behavior of the input archive is to read sequentially starting + with the first node and exploring its children. When a given NVP does + not match the read in name for a node, the archive will search for that + node at the current level and load it if it exists. After loading an out of + order node, the archive will then proceed back to loading sequentially from + its new position. + + Consider this simple example where loading of some data is skipped: + + @code{cpp} + // imagine the input file has someData(1-9) saved in order at the top level node + ar( someData1, someData2, someData3 ); // XML loads in the order it sees in the file + ar( cereal::make_nvp( "hello", someData6 ) ); // NVP given does not + // match expected NVP name, so we search + // for the given NVP and load that value + ar( someData7, someData8, someData9 ); // with no NVP given, loading resumes at its + // current location, proceeding sequentially + @endcode + + \ingroup Archives */ + class JSONInputArchiveSingleLine : public InputArchive, public traits::TextArchive + { + private: + using ReadStream = rapidjson::IStreamWrapper; + typedef rapidjson::GenericValue> JSONValue; + typedef JSONValue::ConstMemberIterator MemberIterator; + typedef JSONValue::ConstValueIterator ValueIterator; + typedef rapidjson::Document::GenericValue GenericValue; + + public: + /*! @name Common Functionality + Common use cases for directly interacting with an JSONInputArchiveSingleLine */ + //! @{ + + //! Construct, reading from the provided stream + /*! @param stream The stream to read from */ + JSONInputArchiveSingleLine(std::istream & stream) : + InputArchive(this), + itsNextName( nullptr ), + itsReadStream(stream) + { + itsDocument.ParseStream<>(itsReadStream); + if (itsDocument.IsArray()) + itsIteratorStack.emplace_back(itsDocument.Begin(), itsDocument.End()); + else + itsIteratorStack.emplace_back(itsDocument.MemberBegin(), itsDocument.MemberEnd()); + } + + ~JSONInputArchiveSingleLine() CEREAL_NOEXCEPT = default; + + //! Loads some binary data, encoded as a base64 string + /*! This will automatically start and finish a node to load the data, and can be called directly by + users. + + Note that this follows the same ordering rules specified in the class description in regards + to loading in/out of order */ + void loadBinaryValue( void * data, size_t size, const char * name = nullptr ) + { + itsNextName = name; + + std::string encoded; + loadValue( encoded ); + auto decoded = base64::decode( encoded ); + + if( size != decoded.size() ) + throw Exception("Decoded binary data size does not match specified size"); + + std::memcpy( data, decoded.data(), decoded.size() ); + itsNextName = nullptr; + }; + + private: + //! @} + /*! @name Internal Functionality + Functionality designed for use by those requiring control over the inner mechanisms of + the JSONInputArchiveSingleLine */ + //! @{ + + //! An internal iterator that handles both array and object types + /*! This class is a variant and holds both types of iterators that + rapidJSON supports - one for arrays and one for objects. */ + class Iterator + { + public: + Iterator() : itsIndex( 0 ), itsType(Null_) {} + + Iterator(MemberIterator begin, MemberIterator end) : + itsMemberItBegin(begin), itsMemberItEnd(end), itsIndex(0), itsType(Member) + { } + + Iterator(ValueIterator begin, ValueIterator end) : + itsValueItBegin(begin), itsValueItEnd(end), itsIndex(0), itsType(Value) + { } + + //! Advance to the next node + Iterator & operator++() + { + ++itsIndex; + return *this; + } + + //! Get the value of the current node + GenericValue const & value() + { + switch(itsType) + { + case Value : return itsValueItBegin[itsIndex]; + case Member: return itsMemberItBegin[itsIndex].value; + default: throw cereal::Exception("Invalid Iterator Type!"); + } + } + + //! Get the name of the current node, or nullptr if it has no name + const char * name() const + { + if( itsType == Member && (itsMemberItBegin + itsIndex) != itsMemberItEnd ) + return itsMemberItBegin[itsIndex].name.GetString(); + else + return nullptr; + } + + //! Adjust our position such that we are at the node with the given name + /*! @throws Exception if no such named node exists */ + inline void search( const char * searchName ) + { + const auto len = std::strlen( searchName ); + size_t index = 0; + for( auto it = itsMemberItBegin; it != itsMemberItEnd; ++it, ++index ) + { + const auto currentName = it->name.GetString(); + if( ( std::strncmp( searchName, currentName, len ) == 0 ) && + ( std::strlen( currentName ) == len ) ) + { + itsIndex = index; + return; + } + } + + throw Exception("JSON Parsing failed - provided NVP (" + std::string(searchName) + ") not found"); + } + + private: + MemberIterator itsMemberItBegin, itsMemberItEnd; //!< The member iterator (object) + ValueIterator itsValueItBegin, itsValueItEnd; //!< The value iterator (array) + size_t itsIndex; //!< The current index of this iterator + enum Type {Value, Member, Null_} itsType; //!< Whether this holds values (array) or members (objects) or nothing + }; + + //! Searches for the expectedName node if it doesn't match the actualName + /*! This needs to be called before every load or node start occurs. This function will + check to see if an NVP has been provided (with setNextName) and if so, see if that name matches the actual + next name given. If the names do not match, it will search in the current level of the JSON for that name. + If the name is not found, an exception will be thrown. + + Resets the NVP name after called. + + @throws Exception if an expectedName is given and not found */ + inline void search() + { + // The name an NVP provided with setNextName() + if( itsNextName ) + { + // The actual name of the current node + auto const actualName = itsIteratorStack.back().name(); + + // Do a search if we don't see a name coming up, or if the names don't match + if( !actualName || std::strcmp( itsNextName, actualName ) != 0 ) + itsIteratorStack.back().search( itsNextName ); + } + + itsNextName = nullptr; + } + + public: + //! Starts a new node, going into its proper iterator + /*! This places an iterator for the next node to be parsed onto the iterator stack. If the next + node is an array, this will be a value iterator, otherwise it will be a member iterator. + + By default our strategy is to start with the document root node and then recursively iterate through + all children in the order they show up in the document. + We don't need to know NVPs to do this; we'll just blindly load in the order things appear in. + + If we were given an NVP, we will search for it if it does not match our the name of the next node + that would normally be loaded. This functionality is provided by search(). */ + void startNode() + { + search(); + + if(itsIteratorStack.back().value().IsArray()) + itsIteratorStack.emplace_back(itsIteratorStack.back().value().Begin(), itsIteratorStack.back().value().End()); + else + itsIteratorStack.emplace_back(itsIteratorStack.back().value().MemberBegin(), itsIteratorStack.back().value().MemberEnd()); + } + + //! Finishes the most recently started node + void finishNode() + { + itsIteratorStack.pop_back(); + ++itsIteratorStack.back(); + } + + //! Retrieves the current node name + /*! @return nullptr if no name exists */ + const char * getNodeName() const + { + return itsIteratorStack.back().name(); + } + + //! Sets the name for the next node created with startNode + void setNextName( const char * name ) + { + itsNextName = name; + } + + //! Loads a value from the current node - small signed overload + template ::value, + sizeof(T) < sizeof(int64_t)> = traits::sfinae> inline + void loadValue(T & val) + { + search(); + + val = static_cast( itsIteratorStack.back().value().GetInt() ); + ++itsIteratorStack.back(); + } + + //! Loads a value from the current node - small unsigned overload + template ::value, + sizeof(T) < sizeof(uint64_t), + !std::is_same::value> = traits::sfinae> inline + void loadValue(T & val) + { + search(); + + val = static_cast( itsIteratorStack.back().value().GetUint() ); + ++itsIteratorStack.back(); + } + + //! Loads a value from the current node - bool overload + void loadValue(bool & val) { search(); val = itsIteratorStack.back().value().GetBool(); ++itsIteratorStack.back(); } + //! Loads a value from the current node - int64 overload + void loadValue(int64_t & val) { search(); val = itsIteratorStack.back().value().GetInt64(); ++itsIteratorStack.back(); } + //! Loads a value from the current node - uint64 overload + void loadValue(uint64_t & val) { search(); val = itsIteratorStack.back().value().GetUint64(); ++itsIteratorStack.back(); } + //! Loads a value from the current node - float overload + void loadValue(float & val) { search(); val = static_cast(itsIteratorStack.back().value().GetDouble()); ++itsIteratorStack.back(); } + //! Loads a value from the current node - double overload + void loadValue(double & val) { search(); val = itsIteratorStack.back().value().GetDouble(); ++itsIteratorStack.back(); } + //! Loads a value from the current node - string overload + void loadValue(std::string & val) { search(); val = itsIteratorStack.back().value().GetString(); ++itsIteratorStack.back(); } + //! Loads a nullptr from the current node + void loadValue(std::nullptr_t&) { search(); CEREAL_RAPIDJSON_ASSERT(itsIteratorStack.back().value().IsNull()); ++itsIteratorStack.back(); } + + // Special cases to handle various flavors of long, which tend to conflict with + // the int32_t or int64_t on various compiler/OS combinations. MSVC doesn't need any of this. +#ifndef _MSC_VER + private: + //! 32 bit signed long loading from current node + template inline + typename std::enable_if::value, void>::type + loadLong(T & l){ loadValue( reinterpret_cast( l ) ); } + + //! non 32 bit signed long loading from current node + template inline + typename std::enable_if::value, void>::type + loadLong(T & l){ loadValue( reinterpret_cast( l ) ); } + + //! 32 bit unsigned long loading from current node + template inline + typename std::enable_if::value, void>::type + loadLong(T & lu){ loadValue( reinterpret_cast( lu ) ); } + + //! non 32 bit unsigned long loading from current node + template inline + typename std::enable_if::value, void>::type + loadLong(T & lu){ loadValue( reinterpret_cast( lu ) ); } + + public: + //! Serialize a long if it would not be caught otherwise + template inline + typename std::enable_if::value && + sizeof(T) >= sizeof(std::int64_t) && + !std::is_same::value, void>::type + loadValue( T & t ){ loadLong(t); } + + //! Serialize an unsigned long if it would not be caught otherwise + template inline + typename std::enable_if::value && + sizeof(T) >= sizeof(std::uint64_t) && + !std::is_same::value, void>::type + loadValue( T & t ){ loadLong(t); } +#endif // _MSC_VER + + private: + //! Convert a string to a long long + void stringToNumber( std::string const & str, long long & val ) { val = std::stoll( str ); } + //! Convert a string to an unsigned long long + void stringToNumber( std::string const & str, unsigned long long & val ) { val = std::stoull( str ); } + //! Convert a string to a long double + void stringToNumber( std::string const & str, long double & val ) { val = std::stold( str ); } + + public: + //! Loads a value from the current node - long double and long long overloads + template ::value, + !std::is_same::value, + !std::is_same::value, + !std::is_same::value, + !std::is_same::value, + (sizeof(T) >= sizeof(long double) || sizeof(T) >= sizeof(long long))> = traits::sfinae> + inline void loadValue(T & val) + { + std::string encoded; + loadValue( encoded ); + stringToNumber( encoded, val ); + } + + //! Loads the size for a SizeTag + void loadSize(size_type & size) + { + if (itsIteratorStack.size() == 1) + size = itsDocument.Size(); + else + size = (itsIteratorStack.rbegin() + 1)->value().Size(); + } + + //! @} + + private: + const char * itsNextName; //!< Next name set by NVP + ReadStream itsReadStream; //!< Rapidjson write stream + std::vector itsIteratorStack; //!< 'Stack' of rapidJSON iterators + rapidjson::Document itsDocument; //!< Rapidjson document + }; + + // ###################################################################### + // JSONArchive prologue and epilogue functions + // ###################################################################### + + // ###################################################################### + //! Prologue for NVPs for JSON archives + /*! NVPs do not start or finish nodes - they just set up the names */ + template inline + void prologue(JSONOutputArchiveSingleLine &, NameValuePair const & ) + { } + + //! Prologue for NVPs for JSON archives + template inline + void prologue( JSONInputArchiveSingleLine &, NameValuePair const & ) + { } + + // ###################################################################### + //! Epilogue for NVPs for JSON archives + /*! NVPs do not start or finish nodes - they just set up the names */ + template inline + void epilogue(JSONOutputArchiveSingleLine &, NameValuePair const & ) + { } + + //! Epilogue for NVPs for JSON archives + /*! NVPs do not start or finish nodes - they just set up the names */ + template inline + void epilogue( JSONInputArchiveSingleLine &, NameValuePair const & ) + { } + + // ###################################################################### + //! Prologue for SizeTags for JSON archives + /*! SizeTags are strictly ignored for JSON, they just indicate + that the current node should be made into an array */ + template inline + void prologue(JSONOutputArchiveSingleLine & ar, SizeTag const & ) + { + ar.makeArray(); + } + + //! Prologue for SizeTags for JSON archives + template inline + void prologue( JSONInputArchiveSingleLine &, SizeTag const & ) + { } + + // ###################################################################### + //! Epilogue for SizeTags for JSON archives + /*! SizeTags are strictly ignored for JSON */ + template inline + void epilogue(JSONOutputArchiveSingleLine &, SizeTag const & ) + { } + + //! Epilogue for SizeTags for JSON archives + template inline + void epilogue( JSONInputArchiveSingleLine &, SizeTag const & ) + { } + + // ###################################################################### + //! Prologue for all other types for JSON archives (except minimal types) + /*! Starts a new node, named either automatically or by some NVP, + that may be given data by the type about to be archived + + Minimal types do not start or finish nodes */ + template ::value, + !traits::has_minimal_base_class_serialization::value, + !traits::has_minimal_output_serialization::value> = traits::sfinae> + inline void prologue(JSONOutputArchiveSingleLine & ar, T const & ) + { + ar.startNode(); + } + + //! Prologue for all other types for JSON archives + template ::value, + !traits::has_minimal_base_class_serialization::value, + !traits::has_minimal_input_serialization::value> = traits::sfinae> + inline void prologue( JSONInputArchiveSingleLine & ar, T const & ) + { + ar.startNode(); + } + + // ###################################################################### + //! Epilogue for all other types other for JSON archives (except minimal types) + /*! Finishes the node created in the prologue + + Minimal types do not start or finish nodes */ + template ::value, + !traits::has_minimal_base_class_serialization::value, + !traits::has_minimal_output_serialization::value> = traits::sfinae> + inline void epilogue(JSONOutputArchiveSingleLine & ar, T const & ) + { + ar.finishNode(); + } + + //! Epilogue for all other types other for JSON archives + template ::value, + !traits::has_minimal_base_class_serialization::value, + !traits::has_minimal_input_serialization::value> = traits::sfinae> + inline void epilogue( JSONInputArchiveSingleLine & ar, T const & ) + { + ar.finishNode(); + } + + // ###################################################################### + //! Prologue for arithmetic types for JSON archives + inline + void prologue(JSONOutputArchiveSingleLine & ar, std::nullptr_t const & ) + { + ar.writeName(); + } + + //! Prologue for arithmetic types for JSON archives + inline + void prologue( JSONInputArchiveSingleLine &, std::nullptr_t const & ) + { } + + // ###################################################################### + //! Epilogue for arithmetic types for JSON archives + inline + void epilogue(JSONOutputArchiveSingleLine &, std::nullptr_t const & ) + { } + + //! Epilogue for arithmetic types for JSON archives + inline + void epilogue( JSONInputArchiveSingleLine &, std::nullptr_t const & ) + { } + + // ###################################################################### + //! Prologue for arithmetic types for JSON archives + template ::value> = traits::sfinae> inline + void prologue(JSONOutputArchiveSingleLine & ar, T const & ) + { + ar.writeName(); + } + + //! Prologue for arithmetic types for JSON archives + template ::value> = traits::sfinae> inline + void prologue( JSONInputArchiveSingleLine &, T const & ) + { } + + // ###################################################################### + //! Epilogue for arithmetic types for JSON archives + template ::value> = traits::sfinae> inline + void epilogue(JSONOutputArchiveSingleLine &, T const & ) + { } + + //! Epilogue for arithmetic types for JSON archives + template ::value> = traits::sfinae> inline + void epilogue( JSONInputArchiveSingleLine &, T const & ) + { } + + // ###################################################################### + //! Prologue for strings for JSON archives + template inline + void prologue(JSONOutputArchiveSingleLine & ar, std::basic_string const &) + { + ar.writeName(); + } + + //! Prologue for strings for JSON archives + template inline + void prologue(JSONInputArchiveSingleLine &, std::basic_string const &) + { } + + // ###################################################################### + //! Epilogue for strings for JSON archives + template inline + void epilogue(JSONOutputArchiveSingleLine &, std::basic_string const &) + { } + + //! Epilogue for strings for JSON archives + template inline + void epilogue(JSONInputArchiveSingleLine &, std::basic_string const &) + { } + + // ###################################################################### + // Common JSONArchive serialization functions + // ###################################################################### + //! Serializing NVP types to JSON + template inline + void CEREAL_SAVE_FUNCTION_NAME(JSONOutputArchiveSingleLine & ar, NameValuePair const & t ) + { + ar.setNextName( t.name ); + ar( t.value ); + } + + template inline + void CEREAL_LOAD_FUNCTION_NAME( JSONInputArchiveSingleLine & ar, NameValuePair & t ) + { + ar.setNextName( t.name ); + ar( t.value ); + } + + //! Saving for nullptr to JSON + inline + void CEREAL_SAVE_FUNCTION_NAME(JSONOutputArchiveSingleLine & ar, std::nullptr_t const & t) + { + ar.saveValue( t ); + } + + //! Loading arithmetic from JSON + inline + void CEREAL_LOAD_FUNCTION_NAME(JSONInputArchiveSingleLine & ar, std::nullptr_t & t) + { + ar.loadValue( t ); + } + + //! Saving for arithmetic to JSON + template ::value> = traits::sfinae> inline + void CEREAL_SAVE_FUNCTION_NAME(JSONOutputArchiveSingleLine & ar, T const & t) + { + ar.saveValue( t ); + } + + //! Loading arithmetic from JSON + template ::value> = traits::sfinae> inline + void CEREAL_LOAD_FUNCTION_NAME(JSONInputArchiveSingleLine & ar, T & t) + { + ar.loadValue( t ); + } + + //! saving string to JSON + template inline + void CEREAL_SAVE_FUNCTION_NAME(JSONOutputArchiveSingleLine & ar, std::basic_string const & str) + { + ar.saveValue( str ); + } + + //! loading string from JSON + template inline + void CEREAL_LOAD_FUNCTION_NAME(JSONInputArchiveSingleLine & ar, std::basic_string & str) + { + ar.loadValue( str ); + } + + // ###################################################################### + //! Saving SizeTags to JSON + template inline + void CEREAL_SAVE_FUNCTION_NAME(JSONOutputArchiveSingleLine &, SizeTag const & ) + { + // nothing to do here, we don't explicitly save the size + } + + //! Loading SizeTags from JSON + template inline + void CEREAL_LOAD_FUNCTION_NAME( JSONInputArchiveSingleLine & ar, SizeTag & st ) + { + ar.loadSize( st.size ); + } +} // namespace cereal + +// register archives for polymorphic support +CEREAL_REGISTER_ARCHIVE(cereal::JSONInputArchiveSingleLine) +CEREAL_REGISTER_ARCHIVE(cereal::JSONOutputArchiveSingleLine) + +// tie input and output archives together +CEREAL_SETUP_ARCHIVE_TRAITS(cereal::JSONInputArchiveSingleLine, cereal::JSONOutputArchiveSingleLine) + +#endif // CEREAL_ARCHIVES_JSON_SL_HPP_ diff --git a/common/platform.cpp b/common/platform.cpp index b126e6d33..cdfad9aed 100644 --- a/common/platform.cpp +++ b/common/platform.cpp @@ -22,26 +22,31 @@ EQEmuExePlatform exe_platform = ExePlatformNone; -void RegisterExecutablePlatform(EQEmuExePlatform p) { +void RegisterExecutablePlatform(EQEmuExePlatform p) +{ exe_platform = p; } -const EQEmuExePlatform& GetExecutablePlatform() { +const EQEmuExePlatform &GetExecutablePlatform() +{ return exe_platform; } -/** - * @return - */ -int GetExecutablePlatformInt(){ +int GetExecutablePlatformInt() +{ return exe_platform; } -/** - * Returns platform name by string - * - * @return - */ +bool IsWorld() +{ + return exe_platform == EQEmuExePlatform::ExePlatformWorld; +} + +bool IsQueryServ() +{ + return exe_platform == EQEmuExePlatform::ExePlatformQueryServ; +} + std::string GetPlatformName() { switch (GetExecutablePlatformInt()) { diff --git a/common/platform.h b/common/platform.h index a69738ff6..35569c8bf 100644 --- a/common/platform.h +++ b/common/platform.h @@ -44,5 +44,7 @@ void RegisterExecutablePlatform(EQEmuExePlatform p); const EQEmuExePlatform& GetExecutablePlatform(); int GetExecutablePlatformInt(); std::string GetPlatformName(); +bool IsWorld(); +bool IsQueryServ(); #endif diff --git a/common/repositories/base/base_eventlog_repository.h b/common/repositories/base/base_eventlog_repository.h deleted file mode 100644 index c2b008106..000000000 --- a/common/repositories/base/base_eventlog_repository.h +++ /dev/null @@ -1,412 +0,0 @@ -/** - * DO NOT MODIFY THIS FILE - * - * This repository was automatically generated and is NOT to be modified directly. - * Any repository modifications are meant to be made to the repository extending the base. - * Any modifications to base repositories are to be made by the generator only - * - * @generator ./utils/scripts/generators/repository-generator.pl - * @docs https://eqemu.gitbook.io/server/in-development/developer-area/repositories - */ - -#ifndef EQEMU_BASE_EVENTLOG_REPOSITORY_H -#define EQEMU_BASE_EVENTLOG_REPOSITORY_H - -#include "../../database.h" -#include "../../strings.h" -#include - -class BaseEventlogRepository { -public: - struct Eventlog { - uint32_t id; - std::string accountname; - uint32_t accountid; - int32_t status; - std::string charname; - std::string target; - std::string time; - std::string descriptiontype; - std::string description; - int32_t event_nid; - }; - - static std::string PrimaryKey() - { - return std::string("id"); - } - - static std::vector Columns() - { - return { - "id", - "accountname", - "accountid", - "status", - "charname", - "target", - "time", - "descriptiontype", - "description", - "event_nid", - }; - } - - static std::vector SelectColumns() - { - return { - "id", - "accountname", - "accountid", - "status", - "charname", - "target", - "time", - "descriptiontype", - "description", - "event_nid", - }; - } - - static std::string ColumnsRaw() - { - return std::string(Strings::Implode(", ", Columns())); - } - - static std::string SelectColumnsRaw() - { - return std::string(Strings::Implode(", ", SelectColumns())); - } - - static std::string TableName() - { - return std::string("eventlog"); - } - - static std::string BaseSelect() - { - return fmt::format( - "SELECT {} FROM {}", - SelectColumnsRaw(), - TableName() - ); - } - - static std::string BaseInsert() - { - return fmt::format( - "INSERT INTO {} ({}) ", - TableName(), - ColumnsRaw() - ); - } - - static Eventlog NewEntity() - { - Eventlog e{}; - - e.id = 0; - e.accountname = ""; - e.accountid = 0; - e.status = 0; - e.charname = ""; - e.target = "None"; - e.time = std::time(nullptr); - e.descriptiontype = ""; - e.description = ""; - e.event_nid = 0; - - return e; - } - - static Eventlog GetEventlog( - const std::vector &eventlogs, - int eventlog_id - ) - { - for (auto &eventlog : eventlogs) { - if (eventlog.id == eventlog_id) { - return eventlog; - } - } - - return NewEntity(); - } - - static Eventlog FindOne( - Database& db, - int eventlog_id - ) - { - auto results = db.QueryDatabase( - fmt::format( - "{} WHERE id = {} LIMIT 1", - BaseSelect(), - eventlog_id - ) - ); - - auto row = results.begin(); - if (results.RowCount() == 1) { - Eventlog e{}; - - e.id = static_cast(strtoul(row[0], nullptr, 10)); - e.accountname = row[1] ? row[1] : ""; - e.accountid = static_cast(strtoul(row[2], nullptr, 10)); - e.status = static_cast(atoi(row[3])); - e.charname = row[4] ? row[4] : ""; - e.target = row[5] ? row[5] : ""; - e.time = row[6] ? row[6] : ""; - e.descriptiontype = row[7] ? row[7] : ""; - e.description = row[8] ? row[8] : ""; - e.event_nid = static_cast(atoi(row[9])); - - return e; - } - - return NewEntity(); - } - - static int DeleteOne( - Database& db, - int eventlog_id - ) - { - auto results = db.QueryDatabase( - fmt::format( - "DELETE FROM {} WHERE {} = {}", - TableName(), - PrimaryKey(), - eventlog_id - ) - ); - - return (results.Success() ? results.RowsAffected() : 0); - } - - static int UpdateOne( - Database& db, - const Eventlog &e - ) - { - std::vector v; - - auto columns = Columns(); - - v.push_back(columns[1] + " = '" + Strings::Escape(e.accountname) + "'"); - v.push_back(columns[2] + " = " + std::to_string(e.accountid)); - v.push_back(columns[3] + " = " + std::to_string(e.status)); - v.push_back(columns[4] + " = '" + Strings::Escape(e.charname) + "'"); - v.push_back(columns[5] + " = '" + Strings::Escape(e.target) + "'"); - v.push_back(columns[6] + " = '" + Strings::Escape(e.time) + "'"); - v.push_back(columns[7] + " = '" + Strings::Escape(e.descriptiontype) + "'"); - v.push_back(columns[8] + " = '" + Strings::Escape(e.description) + "'"); - v.push_back(columns[9] + " = " + std::to_string(e.event_nid)); - - auto results = db.QueryDatabase( - fmt::format( - "UPDATE {} SET {} WHERE {} = {}", - TableName(), - Strings::Implode(", ", v), - PrimaryKey(), - e.id - ) - ); - - return (results.Success() ? results.RowsAffected() : 0); - } - - static Eventlog InsertOne( - Database& db, - Eventlog e - ) - { - std::vector v; - - v.push_back(std::to_string(e.id)); - v.push_back("'" + Strings::Escape(e.accountname) + "'"); - v.push_back(std::to_string(e.accountid)); - v.push_back(std::to_string(e.status)); - v.push_back("'" + Strings::Escape(e.charname) + "'"); - v.push_back("'" + Strings::Escape(e.target) + "'"); - v.push_back("'" + Strings::Escape(e.time) + "'"); - v.push_back("'" + Strings::Escape(e.descriptiontype) + "'"); - v.push_back("'" + Strings::Escape(e.description) + "'"); - v.push_back(std::to_string(e.event_nid)); - - auto results = db.QueryDatabase( - fmt::format( - "{} VALUES ({})", - BaseInsert(), - Strings::Implode(",", v) - ) - ); - - if (results.Success()) { - e.id = results.LastInsertedID(); - return e; - } - - e = NewEntity(); - - return e; - } - - static int InsertMany( - Database& db, - const std::vector &entries - ) - { - std::vector insert_chunks; - - for (auto &e: entries) { - std::vector v; - - v.push_back(std::to_string(e.id)); - v.push_back("'" + Strings::Escape(e.accountname) + "'"); - v.push_back(std::to_string(e.accountid)); - v.push_back(std::to_string(e.status)); - v.push_back("'" + Strings::Escape(e.charname) + "'"); - v.push_back("'" + Strings::Escape(e.target) + "'"); - v.push_back("'" + Strings::Escape(e.time) + "'"); - v.push_back("'" + Strings::Escape(e.descriptiontype) + "'"); - v.push_back("'" + Strings::Escape(e.description) + "'"); - v.push_back(std::to_string(e.event_nid)); - - insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); - } - - std::vector v; - - auto results = db.QueryDatabase( - fmt::format( - "{} VALUES {}", - BaseInsert(), - Strings::Implode(",", insert_chunks) - ) - ); - - return (results.Success() ? results.RowsAffected() : 0); - } - - static std::vector All(Database& db) - { - std::vector all_entries; - - auto results = db.QueryDatabase( - fmt::format( - "{}", - BaseSelect() - ) - ); - - all_entries.reserve(results.RowCount()); - - for (auto row = results.begin(); row != results.end(); ++row) { - Eventlog e{}; - - e.id = static_cast(strtoul(row[0], nullptr, 10)); - e.accountname = row[1] ? row[1] : ""; - e.accountid = static_cast(strtoul(row[2], nullptr, 10)); - e.status = static_cast(atoi(row[3])); - e.charname = row[4] ? row[4] : ""; - e.target = row[5] ? row[5] : ""; - e.time = row[6] ? row[6] : ""; - e.descriptiontype = row[7] ? row[7] : ""; - e.description = row[8] ? row[8] : ""; - e.event_nid = static_cast(atoi(row[9])); - - all_entries.push_back(e); - } - - return all_entries; - } - - static std::vector GetWhere(Database& db, const std::string &where_filter) - { - std::vector all_entries; - - auto results = db.QueryDatabase( - fmt::format( - "{} WHERE {}", - BaseSelect(), - where_filter - ) - ); - - all_entries.reserve(results.RowCount()); - - for (auto row = results.begin(); row != results.end(); ++row) { - Eventlog e{}; - - e.id = static_cast(strtoul(row[0], nullptr, 10)); - e.accountname = row[1] ? row[1] : ""; - e.accountid = static_cast(strtoul(row[2], nullptr, 10)); - e.status = static_cast(atoi(row[3])); - e.charname = row[4] ? row[4] : ""; - e.target = row[5] ? row[5] : ""; - e.time = row[6] ? row[6] : ""; - e.descriptiontype = row[7] ? row[7] : ""; - e.description = row[8] ? row[8] : ""; - e.event_nid = static_cast(atoi(row[9])); - - all_entries.push_back(e); - } - - return all_entries; - } - - static int DeleteWhere(Database& db, const std::string &where_filter) - { - auto results = db.QueryDatabase( - fmt::format( - "DELETE FROM {} WHERE {}", - TableName(), - where_filter - ) - ); - - return (results.Success() ? results.RowsAffected() : 0); - } - - static int Truncate(Database& db) - { - auto results = db.QueryDatabase( - fmt::format( - "TRUNCATE TABLE {}", - TableName() - ) - ); - - return (results.Success() ? results.RowsAffected() : 0); - } - - static int64 GetMaxId(Database& db) - { - auto results = db.QueryDatabase( - fmt::format( - "SELECT COALESCE(MAX({}), 0) FROM {}", - PrimaryKey(), - TableName() - ) - ); - - return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); - } - - static int64 Count(Database& db, const std::string &where_filter = "") - { - auto results = db.QueryDatabase( - fmt::format( - "SELECT COUNT(*) FROM {} {}", - TableName(), - (where_filter.empty() ? "" : "WHERE " + where_filter) - ) - ); - - return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); - } - -}; - -#endif //EQEMU_BASE_EVENTLOG_REPOSITORY_H diff --git a/common/repositories/base/base_hackers_repository.h b/common/repositories/base/base_player_event_log_settings_repository.h similarity index 56% rename from common/repositories/base/base_hackers_repository.h rename to common/repositories/base/base_player_event_log_settings_repository.h index 329c370d8..69156fa72 100644 --- a/common/repositories/base/base_hackers_repository.h +++ b/common/repositories/base/base_player_event_log_settings_repository.h @@ -9,22 +9,21 @@ * @docs https://eqemu.gitbook.io/server/in-development/developer-area/repositories */ -#ifndef EQEMU_BASE_HACKERS_REPOSITORY_H -#define EQEMU_BASE_HACKERS_REPOSITORY_H +#ifndef EQEMU_BASE_PLAYER_EVENT_LOG_SETTINGS_REPOSITORY_H +#define EQEMU_BASE_PLAYER_EVENT_LOG_SETTINGS_REPOSITORY_H #include "../../database.h" #include "../../strings.h" #include -class BaseHackersRepository { +class BasePlayerEventLogSettingsRepository { public: - struct Hackers { - int32_t id; - std::string account; - std::string name; - std::string hacked; - std::string zone; - std::string date; + struct PlayerEventLogSettings { + int64_t id; + std::string event_name; + int8_t event_enabled; + int32_t retention_days; + int32_t discord_webhook_id; }; static std::string PrimaryKey() @@ -36,11 +35,10 @@ public: { return { "id", - "account", - "name", - "hacked", - "zone", - "date", + "event_name", + "event_enabled", + "retention_days", + "discord_webhook_id", }; } @@ -48,11 +46,10 @@ public: { return { "id", - "account", - "name", - "hacked", - "zone", - "date", + "event_name", + "event_enabled", + "retention_days", + "discord_webhook_id", }; } @@ -68,7 +65,7 @@ public: static std::string TableName() { - return std::string("hackers"); + return std::string("player_event_log_settings"); } static std::string BaseSelect() @@ -89,57 +86,56 @@ public: ); } - static Hackers NewEntity() + static PlayerEventLogSettings NewEntity() { - Hackers e{}; + PlayerEventLogSettings e{}; - e.id = 0; - e.account = ""; - e.name = ""; - e.hacked = ""; - e.zone = ""; - e.date = std::time(nullptr); + e.id = 0; + e.event_name = ""; + e.event_enabled = 0; + e.retention_days = 0; + e.discord_webhook_id = 0; return e; } - static Hackers GetHackers( - const std::vector &hackerss, - int hackers_id + static PlayerEventLogSettings GetPlayerEventLogSettings( + const std::vector &player_event_log_settingss, + int player_event_log_settings_id ) { - for (auto &hackers : hackerss) { - if (hackers.id == hackers_id) { - return hackers; + for (auto &player_event_log_settings : player_event_log_settingss) { + if (player_event_log_settings.id == player_event_log_settings_id) { + return player_event_log_settings; } } return NewEntity(); } - static Hackers FindOne( + static PlayerEventLogSettings FindOne( Database& db, - int hackers_id + int player_event_log_settings_id ) { auto results = db.QueryDatabase( fmt::format( - "{} WHERE id = {} LIMIT 1", + "{} WHERE {} = {} LIMIT 1", BaseSelect(), - hackers_id + PrimaryKey(), + player_event_log_settings_id ) ); auto row = results.begin(); if (results.RowCount() == 1) { - Hackers e{}; + PlayerEventLogSettings e{}; - e.id = static_cast(atoi(row[0])); - e.account = row[1] ? row[1] : ""; - e.name = row[2] ? row[2] : ""; - e.hacked = row[3] ? row[3] : ""; - e.zone = row[4] ? row[4] : ""; - e.date = row[5] ? row[5] : ""; + e.id = strtoll(row[0], nullptr, 10); + e.event_name = row[1] ? row[1] : ""; + e.event_enabled = static_cast(atoi(row[2])); + e.retention_days = static_cast(atoi(row[3])); + e.discord_webhook_id = static_cast(atoi(row[4])); return e; } @@ -149,7 +145,7 @@ public: static int DeleteOne( Database& db, - int hackers_id + int player_event_log_settings_id ) { auto results = db.QueryDatabase( @@ -157,7 +153,7 @@ public: "DELETE FROM {} WHERE {} = {}", TableName(), PrimaryKey(), - hackers_id + player_event_log_settings_id ) ); @@ -166,18 +162,18 @@ public: static int UpdateOne( Database& db, - const Hackers &e + const PlayerEventLogSettings &e ) { std::vector v; auto columns = Columns(); - v.push_back(columns[1] + " = '" + Strings::Escape(e.account) + "'"); - v.push_back(columns[2] + " = '" + Strings::Escape(e.name) + "'"); - v.push_back(columns[3] + " = '" + Strings::Escape(e.hacked) + "'"); - v.push_back(columns[4] + " = '" + Strings::Escape(e.zone) + "'"); - v.push_back(columns[5] + " = '" + Strings::Escape(e.date) + "'"); + v.push_back(columns[0] + " = " + std::to_string(e.id)); + v.push_back(columns[1] + " = '" + Strings::Escape(e.event_name) + "'"); + v.push_back(columns[2] + " = " + std::to_string(e.event_enabled)); + v.push_back(columns[3] + " = " + std::to_string(e.retention_days)); + v.push_back(columns[4] + " = " + std::to_string(e.discord_webhook_id)); auto results = db.QueryDatabase( fmt::format( @@ -192,19 +188,18 @@ public: return (results.Success() ? results.RowsAffected() : 0); } - static Hackers InsertOne( + static PlayerEventLogSettings InsertOne( Database& db, - Hackers e + PlayerEventLogSettings e ) { std::vector v; v.push_back(std::to_string(e.id)); - v.push_back("'" + Strings::Escape(e.account) + "'"); - v.push_back("'" + Strings::Escape(e.name) + "'"); - v.push_back("'" + Strings::Escape(e.hacked) + "'"); - v.push_back("'" + Strings::Escape(e.zone) + "'"); - v.push_back("'" + Strings::Escape(e.date) + "'"); + v.push_back("'" + Strings::Escape(e.event_name) + "'"); + v.push_back(std::to_string(e.event_enabled)); + v.push_back(std::to_string(e.retention_days)); + v.push_back(std::to_string(e.discord_webhook_id)); auto results = db.QueryDatabase( fmt::format( @@ -226,7 +221,7 @@ public: static int InsertMany( Database& db, - const std::vector &entries + const std::vector &entries ) { std::vector insert_chunks; @@ -235,11 +230,10 @@ public: std::vector v; v.push_back(std::to_string(e.id)); - v.push_back("'" + Strings::Escape(e.account) + "'"); - v.push_back("'" + Strings::Escape(e.name) + "'"); - v.push_back("'" + Strings::Escape(e.hacked) + "'"); - v.push_back("'" + Strings::Escape(e.zone) + "'"); - v.push_back("'" + Strings::Escape(e.date) + "'"); + v.push_back("'" + Strings::Escape(e.event_name) + "'"); + v.push_back(std::to_string(e.event_enabled)); + v.push_back(std::to_string(e.retention_days)); + v.push_back(std::to_string(e.discord_webhook_id)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -257,9 +251,9 @@ public: return (results.Success() ? results.RowsAffected() : 0); } - static std::vector All(Database& db) + static std::vector All(Database& db) { - std::vector all_entries; + std::vector all_entries; auto results = db.QueryDatabase( fmt::format( @@ -271,14 +265,13 @@ public: all_entries.reserve(results.RowCount()); for (auto row = results.begin(); row != results.end(); ++row) { - Hackers e{}; + PlayerEventLogSettings e{}; - e.id = static_cast(atoi(row[0])); - e.account = row[1] ? row[1] : ""; - e.name = row[2] ? row[2] : ""; - e.hacked = row[3] ? row[3] : ""; - e.zone = row[4] ? row[4] : ""; - e.date = row[5] ? row[5] : ""; + e.id = strtoll(row[0], nullptr, 10); + e.event_name = row[1] ? row[1] : ""; + e.event_enabled = static_cast(atoi(row[2])); + e.retention_days = static_cast(atoi(row[3])); + e.discord_webhook_id = static_cast(atoi(row[4])); all_entries.push_back(e); } @@ -286,9 +279,9 @@ public: return all_entries; } - static std::vector GetWhere(Database& db, const std::string &where_filter) + static std::vector GetWhere(Database& db, const std::string &where_filter) { - std::vector all_entries; + std::vector all_entries; auto results = db.QueryDatabase( fmt::format( @@ -301,14 +294,13 @@ public: all_entries.reserve(results.RowCount()); for (auto row = results.begin(); row != results.end(); ++row) { - Hackers e{}; + PlayerEventLogSettings e{}; - e.id = static_cast(atoi(row[0])); - e.account = row[1] ? row[1] : ""; - e.name = row[2] ? row[2] : ""; - e.hacked = row[3] ? row[3] : ""; - e.zone = row[4] ? row[4] : ""; - e.date = row[5] ? row[5] : ""; + e.id = strtoll(row[0], nullptr, 10); + e.event_name = row[1] ? row[1] : ""; + e.event_enabled = static_cast(atoi(row[2])); + e.retention_days = static_cast(atoi(row[3])); + e.discord_webhook_id = static_cast(atoi(row[4])); all_entries.push_back(e); } @@ -369,4 +361,4 @@ public: }; -#endif //EQEMU_BASE_HACKERS_REPOSITORY_H +#endif //EQEMU_BASE_PLAYER_EVENT_LOG_SETTINGS_REPOSITORY_H diff --git a/common/repositories/base/base_player_event_logs_repository.h b/common/repositories/base/base_player_event_logs_repository.h new file mode 100644 index 000000000..a32c8ed0e --- /dev/null +++ b/common/repositories/base/base_player_event_logs_repository.h @@ -0,0 +1,465 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://eqemu.gitbook.io/server/in-development/developer-area/repositories + */ + +#ifndef EQEMU_BASE_PLAYER_EVENT_LOGS_REPOSITORY_H +#define EQEMU_BASE_PLAYER_EVENT_LOGS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include +#include + +class BasePlayerEventLogsRepository { +public: + struct PlayerEventLogs { + int64_t id; + int64_t account_id; + int64_t character_id; + int32_t zone_id; + int32_t instance_id; + float x; + float y; + float z; + float heading; + int32_t event_type_id; + std::string event_type_name; + std::string event_data; + time_t created_at; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(id), + CEREAL_NVP(account_id), + CEREAL_NVP(character_id), + CEREAL_NVP(zone_id), + CEREAL_NVP(instance_id), + CEREAL_NVP(x), + CEREAL_NVP(y), + CEREAL_NVP(z), + CEREAL_NVP(heading), + CEREAL_NVP(event_type_id), + CEREAL_NVP(event_type_name), + CEREAL_NVP(event_data), + CEREAL_NVP(created_at) + ); + } + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "account_id", + "character_id", + "zone_id", + "instance_id", + "x", + "y", + "z", + "heading", + "event_type_id", + "event_type_name", + "event_data", + "created_at", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "account_id", + "character_id", + "zone_id", + "instance_id", + "x", + "y", + "z", + "heading", + "event_type_id", + "event_type_name", + "event_data", + "UNIX_TIMESTAMP(created_at)", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("player_event_logs"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static PlayerEventLogs NewEntity() + { + PlayerEventLogs e{}; + + e.id = 0; + e.account_id = 0; + e.character_id = 0; + e.zone_id = 0; + e.instance_id = 0; + e.x = 0; + e.y = 0; + e.z = 0; + e.heading = 0; + e.event_type_id = 0; + e.event_type_name = ""; + e.event_data = ""; + e.created_at = 0; + + return e; + } + + static PlayerEventLogs GetPlayerEventLogs( + const std::vector &player_event_logss, + int player_event_logs_id + ) + { + for (auto &player_event_logs : player_event_logss) { + if (player_event_logs.id == player_event_logs_id) { + return player_event_logs; + } + } + + return NewEntity(); + } + + static PlayerEventLogs FindOne( + Database& db, + int player_event_logs_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + player_event_logs_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + PlayerEventLogs e{}; + + e.id = strtoll(row[0], nullptr, 10); + e.account_id = strtoll(row[1], nullptr, 10); + e.character_id = strtoll(row[2], nullptr, 10); + e.zone_id = static_cast(atoi(row[3])); + e.instance_id = static_cast(atoi(row[4])); + e.x = strtof(row[5], nullptr); + e.y = strtof(row[6], nullptr); + e.z = strtof(row[7], nullptr); + e.heading = strtof(row[8], nullptr); + e.event_type_id = static_cast(atoi(row[9])); + e.event_type_name = row[10] ? row[10] : ""; + e.event_data = row[11] ? row[11] : ""; + e.created_at = strtoll(row[12] ? row[12] : "-1", nullptr, 10); + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int player_event_logs_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + player_event_logs_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const PlayerEventLogs &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[1] + " = " + std::to_string(e.account_id)); + v.push_back(columns[2] + " = " + std::to_string(e.character_id)); + v.push_back(columns[3] + " = " + std::to_string(e.zone_id)); + v.push_back(columns[4] + " = " + std::to_string(e.instance_id)); + v.push_back(columns[5] + " = " + std::to_string(e.x)); + v.push_back(columns[6] + " = " + std::to_string(e.y)); + v.push_back(columns[7] + " = " + std::to_string(e.z)); + v.push_back(columns[8] + " = " + std::to_string(e.heading)); + v.push_back(columns[9] + " = " + std::to_string(e.event_type_id)); + v.push_back(columns[10] + " = '" + Strings::Escape(e.event_type_name) + "'"); + v.push_back(columns[11] + " = '" + Strings::Escape(e.event_data) + "'"); + v.push_back(columns[12] + " = FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static PlayerEventLogs InsertOne( + Database& db, + PlayerEventLogs e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.account_id)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.zone_id)); + v.push_back(std::to_string(e.instance_id)); + v.push_back(std::to_string(e.x)); + v.push_back(std::to_string(e.y)); + v.push_back(std::to_string(e.z)); + v.push_back(std::to_string(e.heading)); + v.push_back(std::to_string(e.event_type_id)); + v.push_back("'" + Strings::Escape(e.event_type_name) + "'"); + v.push_back("'" + Strings::Escape(e.event_data) + "'"); + v.push_back("FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.account_id)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.zone_id)); + v.push_back(std::to_string(e.instance_id)); + v.push_back(std::to_string(e.x)); + v.push_back(std::to_string(e.y)); + v.push_back(std::to_string(e.z)); + v.push_back(std::to_string(e.heading)); + v.push_back(std::to_string(e.event_type_id)); + v.push_back("'" + Strings::Escape(e.event_type_name) + "'"); + v.push_back("'" + Strings::Escape(e.event_data) + "'"); + v.push_back("FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + PlayerEventLogs e{}; + + e.id = strtoll(row[0], nullptr, 10); + e.account_id = strtoll(row[1], nullptr, 10); + e.character_id = strtoll(row[2], nullptr, 10); + e.zone_id = static_cast(atoi(row[3])); + e.instance_id = static_cast(atoi(row[4])); + e.x = strtof(row[5], nullptr); + e.y = strtof(row[6], nullptr); + e.z = strtof(row[7], nullptr); + e.heading = strtof(row[8], nullptr); + e.event_type_id = static_cast(atoi(row[9])); + e.event_type_name = row[10] ? row[10] : ""; + e.event_data = row[11] ? row[11] : ""; + e.created_at = strtoll(row[12] ? row[12] : "-1", nullptr, 10); + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + PlayerEventLogs e{}; + + e.id = strtoll(row[0], nullptr, 10); + e.account_id = strtoll(row[1], nullptr, 10); + e.character_id = strtoll(row[2], nullptr, 10); + e.zone_id = static_cast(atoi(row[3])); + e.instance_id = static_cast(atoi(row[4])); + e.x = strtof(row[5], nullptr); + e.y = strtof(row[6], nullptr); + e.z = strtof(row[7], nullptr); + e.heading = strtof(row[8], nullptr); + e.event_type_id = static_cast(atoi(row[9])); + e.event_type_name = row[10] ? row[10] : ""; + e.event_data = row[11] ? row[11] : ""; + e.created_at = strtoll(row[12] ? row[12] : "-1", nullptr, 10); + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + +}; + +#endif //EQEMU_BASE_PLAYER_EVENT_LOGS_REPOSITORY_H diff --git a/common/repositories/character_data_repository.h b/common/repositories/character_data_repository.h index f527e5537..c029b14ea 100644 --- a/common/repositories/character_data_repository.h +++ b/common/repositories/character_data_repository.h @@ -5,6 +5,8 @@ #include "../strings.h" #include "base/base_character_data_repository.h" + + class CharacterDataRepository: public BaseCharacterDataRepository { public: @@ -44,7 +46,6 @@ public: */ // Custom extended repository methods here - }; #endif //EQEMU_CHARACTER_DATA_REPOSITORY_H diff --git a/common/repositories/dynamic_zone_members_repository.h b/common/repositories/dynamic_zone_members_repository.h index 1e25fc950..b1afe41ca 100644 --- a/common/repositories/dynamic_zone_members_repository.h +++ b/common/repositories/dynamic_zone_members_repository.h @@ -172,7 +172,7 @@ public: DELETE FROM {} WHERE dynamic_zone_id IN ({}); ), - TableName(), fmt::join(dynamic_zone_ids, ",") + TableName(), Strings::Join(dynamic_zone_ids, ",") )); } } diff --git a/common/repositories/expedition_lockouts_repository.h b/common/repositories/expedition_lockouts_repository.h index 94dbe155d..4ccae0d39 100644 --- a/common/repositories/expedition_lockouts_repository.h +++ b/common/repositories/expedition_lockouts_repository.h @@ -75,7 +75,7 @@ public: FROM expedition_lockouts WHERE expedition_id IN ({}) ), - fmt::join(expedition_ids, ",") + Strings::Join(expedition_ids, ",") )); all_entries.reserve(results.RowCount()); diff --git a/common/repositories/expeditions_repository.h b/common/repositories/expeditions_repository.h index 502b9bc04..aab6acf5f 100644 --- a/common/repositories/expeditions_repository.h +++ b/common/repositories/expeditions_repository.h @@ -62,7 +62,7 @@ public: std::vector entries; - auto joined_character_names = fmt::format("'{}'", fmt::join(character_names, "','")); + auto joined_character_names = fmt::format("'{}'", Strings::Join(character_names, "','")); auto results = db.QueryDatabase(fmt::format(SQL( SELECT diff --git a/common/repositories/eventlog_repository.h b/common/repositories/player_event_log_settings_repository.h similarity index 68% rename from common/repositories/eventlog_repository.h rename to common/repositories/player_event_log_settings_repository.h index 83f21fb1a..770db7c3f 100644 --- a/common/repositories/eventlog_repository.h +++ b/common/repositories/player_event_log_settings_repository.h @@ -1,11 +1,11 @@ -#ifndef EQEMU_EVENTLOG_REPOSITORY_H -#define EQEMU_EVENTLOG_REPOSITORY_H +#ifndef EQEMU_PLAYER_EVENT_LOG_SETTINGS_REPOSITORY_H +#define EQEMU_PLAYER_EVENT_LOG_SETTINGS_REPOSITORY_H #include "../database.h" #include "../strings.h" -#include "base/base_eventlog_repository.h" +#include "base/base_player_event_log_settings_repository.h" -class EventlogRepository: public BaseEventlogRepository { +class PlayerEventLogSettingsRepository: public BasePlayerEventLogSettingsRepository { public: /** @@ -32,10 +32,10 @@ public: * * Example custom methods in a repository * - * EventlogRepository::GetByZoneAndVersion(int zone_id, int zone_version) - * EventlogRepository::GetWhereNeverExpires() - * EventlogRepository::GetWhereXAndY() - * EventlogRepository::DeleteWhereXAndY() + * PlayerEventLogSettingsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * PlayerEventLogSettingsRepository::GetWhereNeverExpires() + * PlayerEventLogSettingsRepository::GetWhereXAndY() + * PlayerEventLogSettingsRepository::DeleteWhereXAndY() * * Most of the above could be covered by base methods, but if you as a developer * find yourself re-using logic for other parts of the code, its best to just make a @@ -47,4 +47,4 @@ public: }; -#endif //EQEMU_EVENTLOG_REPOSITORY_H +#endif //EQEMU_PLAYER_EVENT_LOG_SETTINGS_REPOSITORY_H diff --git a/common/repositories/hackers_repository.h b/common/repositories/player_event_logs_repository.h similarity index 71% rename from common/repositories/hackers_repository.h rename to common/repositories/player_event_logs_repository.h index 516ab0e6f..97a6d6431 100644 --- a/common/repositories/hackers_repository.h +++ b/common/repositories/player_event_logs_repository.h @@ -1,11 +1,11 @@ -#ifndef EQEMU_HACKERS_REPOSITORY_H -#define EQEMU_HACKERS_REPOSITORY_H +#ifndef EQEMU_PLAYER_EVENT_LOGS_REPOSITORY_H +#define EQEMU_PLAYER_EVENT_LOGS_REPOSITORY_H #include "../database.h" #include "../strings.h" -#include "base/base_hackers_repository.h" +#include "base/base_player_event_logs_repository.h" -class HackersRepository: public BaseHackersRepository { +class PlayerEventLogsRepository: public BasePlayerEventLogsRepository { public: /** @@ -32,10 +32,10 @@ public: * * Example custom methods in a repository * - * HackersRepository::GetByZoneAndVersion(int zone_id, int zone_version) - * HackersRepository::GetWhereNeverExpires() - * HackersRepository::GetWhereXAndY() - * HackersRepository::DeleteWhereXAndY() + * PlayerEventLogsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * PlayerEventLogsRepository::GetWhereNeverExpires() + * PlayerEventLogsRepository::GetWhereXAndY() + * PlayerEventLogsRepository::DeleteWhereXAndY() * * Most of the above could be covered by base methods, but if you as a developer * find yourself re-using logic for other parts of the code, its best to just make a @@ -47,4 +47,4 @@ public: }; -#endif //EQEMU_HACKERS_REPOSITORY_H +#endif //EQEMU_PLAYER_EVENT_LOGS_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index b8102b989..967a77081 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -778,6 +778,8 @@ RULE_CATEGORY_END() RULE_CATEGORY(Logging) RULE_BOOL(Logging, PrintFileFunctionAndLine, false, "Ex: [World Server] [net.cpp::main:309] Loading variables...") RULE_BOOL(Logging, WorldGMSayLogging, true, "Relay worldserver logging to zone processes via GM say output") +RULE_BOOL(Logging, PlayerEventsQSProcess, false, "Have query server process player events instead of world. Useful when wanting to use a dedicated server and database for processing player events on separate disk") +RULE_INT(Logging, BatchPlayerEventProcessIntervalSeconds, 5, "This is the interval in which player events are processed in world or qs") RULE_CATEGORY_END() RULE_CATEGORY(HotReload) diff --git a/common/servertalk.h b/common/servertalk.h index 904477e21..d26319911 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -281,6 +281,9 @@ #define ServerOP_QSSendQuery 0x5006 #define ServerOP_QSPlayerDropItem 0x5007 +// player events +#define ServerOP_PlayerEvent 0x5100 + enum { CZUpdateType_Character, CZUpdateType_Group, @@ -1308,10 +1311,10 @@ struct Server_Speech_Struct { char message[0]; }; -struct QSTradeItems_Struct { - uint32 from_id; +struct PlayerLogTradeItemsEntry_Struct { + uint32 from_character_id; uint16 from_slot; - uint32 to_id; + uint32 to_character_id; uint16 to_slot; uint32 item_id; uint16 charges; @@ -1322,15 +1325,15 @@ struct QSTradeItems_Struct { uint32 aug_5; }; -struct QSPlayerLogTrade_Struct { - uint32 char1_id; - MoneyUpdate_Struct char1_money; - uint16 char1_count; - uint32 char2_id; - MoneyUpdate_Struct char2_money; - uint16 char2_count; - uint16 _detail_count; - QSTradeItems_Struct items[0]; +struct PlayerLogTrade_Struct { + uint32 character_1_id; + MoneyUpdate_Struct character_1_money; + uint16 character_1_item_count; + uint32 character_2_id; + MoneyUpdate_Struct character_2_money; + uint16 character_2_item_count; + uint16 _detail_count; + PlayerLogTradeItemsEntry_Struct item_entries[0]; }; struct QSDropItems_Struct { @@ -1806,6 +1809,11 @@ struct ServerDzCreateSerialized_Struct { char cereal_data[0]; }; +struct ServerSendPlayerEvent_Struct { + uint32_t cereal_size; + char cereal_data[0]; +}; + struct ServerFlagUpdate_Struct { uint32 account_id; int16 admin; diff --git a/common/strings.cpp b/common/strings.cpp index 46aeed759..74405fa86 100644 --- a/common/strings.cpp +++ b/common/strings.cpp @@ -225,6 +225,20 @@ std::string Strings::Join(const std::vector &ar, const std::string return ret; } +std::string Strings::Join(const std::vector &ar, const std::string &delim) +{ + std::string ret; + for (size_t i = 0; i < ar.size(); ++i) { + if (i != 0) { + ret += delim; + } + + ret += std::to_string(ar[i]); + } + + return ret; +} + void Strings::FindReplace(std::string &string_subject, const std::string &search_string, const std::string &replace_string) { diff --git a/common/strings.h b/common/strings.h index 2291a9e50..a25898f7e 100644 --- a/common/strings.h +++ b/common/strings.h @@ -108,6 +108,7 @@ public: static std::string GetBetween(const std::string &s, std::string start_delim, std::string stop_delim); static std::string Implode(std::string glue, std::vector src); static std::string Join(const std::vector &ar, const std::string &delim); + static std::string Join(const std::vector &ar, const std::string &delim); static std::string MillisecondsToTime(int duration); static std::string Money(uint32 platinum, uint32 gold = 0, uint32 silver = 0, uint32 copper = 0); static std::string NumberToWords(unsigned long long int n); diff --git a/common/timer.cpp b/common/timer.cpp index f4d931764..280b82518 100644 --- a/common/timer.cpp +++ b/common/timer.cpp @@ -16,7 +16,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ - // Disgrace: for windows compile #ifndef WIN32 #include diff --git a/common/version.h b/common/version.h index 6370b4418..765804563 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9219 +#define CURRENT_BINARY_DATABASE_VERSION 9220 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9037 #endif diff --git a/queryserv/database.cpp b/queryserv/database.cpp index 5b655a21e..7a442ae4c 100644 --- a/queryserv/database.cpp +++ b/queryserv/database.cpp @@ -50,7 +50,6 @@ #include "../common/strings.h" #include "../common/servertalk.h" - void QSDatabase::AddSpeech( const char *from, const char *to, @@ -125,7 +124,7 @@ void QSDatabase::LogPlayerDropItem(QSPlayerDropItem_Struct *QS) } } -void QSDatabase::LogPlayerTrade(QSPlayerLogTrade_Struct *QS, uint32 detailCount) +void QSDatabase::LogPlayerTrade(PlayerLogTrade_Struct *QS, uint32 detailCount) { std::string query = StringFormat( @@ -134,10 +133,10 @@ void QSDatabase::LogPlayerTrade(QSPlayerLogTrade_Struct *QS, uint32 detailCount) "`char1_sp` = '%i', `char1_cp` = '%i', `char1_items` = '%i', " "`char2_id` = '%i', `char2_pp` = '%i', `char2_gp` = '%i', " "`char2_sp` = '%i', `char2_cp` = '%i', `char2_items` = '%i'", - QS->char1_id, QS->char1_money.platinum, QS->char1_money.gold, - QS->char1_money.silver, QS->char1_money.copper, QS->char1_count, - QS->char2_id, QS->char2_money.platinum, QS->char2_money.gold, - QS->char2_money.silver, QS->char2_money.copper, QS->char2_count + QS->character_1_id, QS->character_1_money.platinum, QS->character_1_money.gold, + QS->character_1_money.silver, QS->character_1_money.copper, QS->character_1_item_count, + QS->character_2_id, QS->character_2_money.platinum, QS->character_2_money.gold, + QS->character_2_money.silver, QS->character_2_money.copper, QS->character_2_item_count ); auto results = QueryDatabase(query); if (!results.Success()) { @@ -157,10 +156,10 @@ void QSDatabase::LogPlayerTrade(QSPlayerLogTrade_Struct *QS, uint32 detailCount) "`from_id` = '%i', `from_slot` = '%i', `to_id` = '%i', `to_slot` = '%i', " "`item_id` = '%i', `charges` = '%i', `aug_1` = '%i', `aug_2` = '%i', " "`aug_3` = '%i', `aug_4` = '%i', `aug_5` = '%i'", - lastIndex, QS->items[i].from_id, QS->items[i].from_slot, - QS->items[i].to_id, QS->items[i].to_slot, QS->items[i].item_id, - QS->items[i].charges, QS->items[i].aug_1, QS->items[i].aug_2, - QS->items[i].aug_3, QS->items[i].aug_4, QS->items[i].aug_5 + lastIndex, QS->item_entries[i].from_character_id, QS->item_entries[i].from_slot, + QS->item_entries[i].to_character_id, QS->item_entries[i].to_slot, QS->item_entries[i].item_id, + QS->item_entries[i].charges, QS->item_entries[i].aug_1, QS->item_entries[i].aug_2, + QS->item_entries[i].aug_3, QS->item_entries[i].aug_4, QS->item_entries[i].aug_5 ); results = QueryDatabase(query); if (!results.Success()) { diff --git a/queryserv/database.h b/queryserv/database.h index 0741f9aa0..ed67b6db8 100644 --- a/queryserv/database.h +++ b/queryserv/database.h @@ -39,7 +39,7 @@ class QSDatabase : public Database { public: void AddSpeech(const char* from, const char* to, const char* message, uint16 minstatus, uint32 guilddbid, uint8 type); - void LogPlayerTrade(QSPlayerLogTrade_Struct* QS, uint32 DetailCount); + void LogPlayerTrade(PlayerLogTrade_Struct* QS, uint32 DetailCount); void LogPlayerDropItem(QSPlayerDropItem_Struct* QS); void LogPlayerHandin(QSPlayerLogHandin_Struct* QS, uint32 DetailCount); void LogPlayerNPCKill(QSPlayerLogNPCKill_Struct* QS, uint32 Members); diff --git a/queryserv/worldserver.cpp b/queryserv/worldserver.cpp index 70dc91577..8019197e6 100644 --- a/queryserv/worldserver.cpp +++ b/queryserv/worldserver.cpp @@ -100,7 +100,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } case ServerOP_QSPlayerLogTrades: { - QSPlayerLogTrade_Struct *QS = (QSPlayerLogTrade_Struct *) p.Data(); + PlayerLogTrade_Struct *QS = (PlayerLogTrade_Struct *) p.Data(); database.LogPlayerTrade(QS, QS->_detail_count); break; } diff --git a/ucs/database.h b/ucs/database.h index 26eead391..0b4bfe268 100644 --- a/ucs/database.h +++ b/ucs/database.h @@ -30,6 +30,7 @@ #include "../common/database.h" #include "clientlist.h" #include "chatchannel.h" +#include "../common/shareddb.h" #include #include #include diff --git a/ucs/ucs.cpp b/ucs/ucs.cpp index 78dabcc08..79b0bff09 100644 --- a/ucs/ucs.cpp +++ b/ucs/ucs.cpp @@ -37,9 +37,10 @@ #include "../common/net/tcp_server.h" #include "../common/net/servertalk_client_connection.h" -#include "../common/discord_manager.h" +#include "../common/discord/discord_manager.h" #include "../common/path_manager.h" #include "../common/zone_store.h" +#include "../common/events/player_event_logs.h" ChatChannelList *ChannelList; Clientlist *g_Clientlist; @@ -49,6 +50,7 @@ WorldServer *worldserver = nullptr; DiscordManager discord_manager; PathManager path; ZoneStore zone_store; +PlayerEventLogs player_event_logs; const ucsconfig *Config; @@ -93,7 +95,7 @@ void CatchSignal(int sig_num) { } } -void DiscordQueueListener() { +void PlayerEventQueueListener() { while (caught_loop == 0) { discord_manager.ProcessMessageQueue(); Sleep(100); @@ -177,7 +179,7 @@ int main() { std::signal(SIGKILL, CatchSignal); std::signal(SIGSEGV, CatchSignal); - std::thread(DiscordQueueListener).detach(); + std::thread(PlayerEventQueueListener).detach(); worldserver = new WorldServer; diff --git a/ucs/worldserver.cpp b/ucs/worldserver.cpp index f7522040c..39e076d7b 100644 --- a/ucs/worldserver.cpp +++ b/ucs/worldserver.cpp @@ -26,7 +26,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "clientlist.h" #include "ucsconfig.h" #include "database.h" -#include "../common/discord_manager.h" +#include "../common/discord/discord_manager.h" +#include "../common/events/player_event_logs.h" #include #include @@ -76,6 +77,18 @@ void WorldServer::ProcessMessage(uint16 opcode, EQ::Net::Packet &p) } case ServerOP_ReloadLogs: { LogSys.LoadLogDatabaseSettings(); + player_event_logs.ReloadSettings(); + break; + } + case ServerOP_PlayerEvent: { + auto n = PlayerEvent::PlayerEventContainer{}; + auto s = (ServerSendPlayerEvent_Struct*) pack->pBuffer; + EQ::Util::MemoryStreamReader ss(s->cereal_data, s->cereal_size); + cereal::BinaryInputArchive archive(ss); + archive(n); + + discord_manager.QueuePlayerEventMessage(n); + break; } case ServerOP_DiscordWebhookMessage: { diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 9d58be4d9..9e2532432 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -473,6 +473,7 @@ 9217|2023_01_15_chatchannel_reserved_names.sql|SHOW TABLES LIKE 'chatchannel_reserved_names'|empty| 9218|2023_01_24_item_recast.sql|show columns from character_item_recast like '%recast_type%'|contains|smallint 9219|2023_01_29_merchant_status_requirements.sql|SHOW COLUMNS FROM merchantlist LIKE 'min_status'|empty| +9220|2022_12_19_player_events_tables.sql|SHOW TABLES LIKE 'player_event_logs'|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2022_12_19_player_events_tables.sql b/utils/sql/git/required/2022_12_19_player_events_tables.sql new file mode 100644 index 000000000..df27b01c9 --- /dev/null +++ b/utils/sql/git/required/2022_12_19_player_events_tables.sql @@ -0,0 +1,34 @@ +CREATE TABLE `player_event_log_settings` +( + `id` bigint(20) NOT NULL, + `event_name` varchar(100) DEFAULT NULL, + `event_enabled` tinyint(1) DEFAULT NULL, + `retention_days` int(11) DEFAULT 0, + `discord_webhook_id` int(11) DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `player_event_logs` +( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `account_id` bigint(20) DEFAULT NULL, + `character_id` bigint(20) DEFAULT NULL, + `zone_id` int(11) DEFAULT NULL, + `instance_id` int(11) DEFAULT NULL, + `x` float DEFAULT NULL, + `y` float DEFAULT NULL, + `z` float DEFAULT NULL, + `heading` float DEFAULT NULL, + `event_type_id` int(11) DEFAULT NULL, + `event_type_name` varchar(255) DEFAULT NULL, + `event_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`event_data`)), + `created_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `event_created_at` (`event_type_id`,`created_at`), + KEY `zone_id` (`zone_id`), + KEY `character_id` (`character_id`,`zone_id`) USING BTREE, + KEY `created_at` (`created_at`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; + +DROP TABLE `hackers`; +DROP TABLE `eventlog`; diff --git a/world/cli/test.cpp b/world/cli/test.cpp index 39a923115..738741723 100644 --- a/world/cli/test.cpp +++ b/world/cli/test.cpp @@ -1,4 +1,6 @@ -#include "../../common/zone_store.h" +#include +#include +#include "../../common/events/player_events.h" void WorldserverCLI::TestCommand(int argc, char **argv, argh::parser &cmd, std::string &description) { @@ -8,14 +10,5 @@ void WorldserverCLI::TestCommand(int argc, char **argv, argh::parser &cmd, std:: return; } - zone_store.LoadZones(database); - - const char* zonename = ZoneName(0); - if (zonename == 0) { - LogInfo("Zone name is 0"); - } - if (zonename == nullptr) { - LogInfo("Zone name is nullptr"); - } - + } diff --git a/world/client.cpp b/world/client.cpp index 150e3724c..5574f1adb 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -49,6 +49,8 @@ #include "sof_char_create_data.h" #include "../common/zone_store.h" #include "../common/repositories/account_repository.h" +#include "../common/repositories/player_event_logs_repository.h" +#include "../common/events/player_event_logs.h" #include #include @@ -818,7 +820,8 @@ bool Client::HandleEnterWorldPacket(const EQApplicationPacket *app) { zone_id = database.MoveCharacterToBind(charid, 4); } else { LogInfo("[{}] is trying to go home before they're able.", char_name); - database.SetHackerFlag(GetAccountName(), char_name, "MQGoHome: player tried to go home before they were able."); + RecordPossibleHack("[MQGoHome] player tried to go home before they were able"); + eqs->Close(); return true; } @@ -844,7 +847,8 @@ bool Client::HandleEnterWorldPacket(const EQApplicationPacket *app) { database.MoveCharacterToZone(charid, zone_id); } else { LogInfo("[{}] is trying to go to the Tutorial but they are not allowed.", char_name); - database.SetHackerFlag(GetAccountName(), char_name, "MQTutorial: player tried to enter the tutorial without having tutorial enabled for this character."); + RecordPossibleHack("[MQTutorial] player tried to enter the tutorial without having tutorial enabled for this character"); + eqs->Close(); return true; } @@ -2360,3 +2364,24 @@ bool Client::StoreCharacter( return true; } + +void Client::RecordPossibleHack(const std::string& message) +{ + if (player_event_logs.IsEventEnabled(PlayerEvent::POSSIBLE_HACK)) { + auto event = PlayerEvent::PossibleHackEvent{.message = message}; + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + event.serialize(ar); + } + + auto e = PlayerEventLogsRepository::NewEntity(); + e.character_id = charid; + e.account_id = GetCLE() ? GetAccountID() : 0; + e.event_type_id = PlayerEvent::POSSIBLE_HACK; + e.event_type_name = PlayerEvent::EventName[PlayerEvent::POSSIBLE_HACK]; + e.event_data = ss.str(); + e.created_at = std::time(nullptr); + PlayerEventLogsRepository::InsertOne(database, e); + } +} diff --git a/world/client.h b/world/client.h index 93aacb31d..d4ff39f31 100644 --- a/world/client.h +++ b/world/client.h @@ -121,6 +121,7 @@ private: EQStreamInterface* eqs; bool CanTradeFVNoDropItem(); + void RecordPossibleHack(const std::string& message); }; bool CheckCharCreateInfoSoF(CharCreate_Struct *cc); diff --git a/world/dynamic_zone_manager.cpp b/world/dynamic_zone_manager.cpp index 2bed2a97e..751246b2d 100644 --- a/world/dynamic_zone_manager.cpp +++ b/world/dynamic_zone_manager.cpp @@ -28,9 +28,9 @@ void DynamicZoneManager::PurgeExpiredDynamicZones() LogDynamicZones("Purging [{}] dynamic zone(s)", dz_ids.size()); DynamicZoneMembersRepository::DeleteWhere(database, - fmt::format("dynamic_zone_id IN ({})", fmt::join(dz_ids, ","))); + fmt::format("dynamic_zone_id IN ({})", Strings::Join(dz_ids, ","))); DynamicZonesRepository::DeleteWhere(database, - fmt::format("id IN ({})", fmt::join(dz_ids, ","))); + fmt::format("id IN ({})", Strings::Join(dz_ids, ","))); } } @@ -145,7 +145,7 @@ void DynamicZoneManager::Process() // need to look up expedition ids until lockouts are moved to dynamic zones std::vector expedition_ids; auto expeditions = ExpeditionsRepository::GetWhere(database, - fmt::format("dynamic_zone_id IN ({})", fmt::join(dynamic_zone_ids, ","))); + fmt::format("dynamic_zone_id IN ({})", Strings::Join(dynamic_zone_ids, ","))); if (!expeditions.empty()) { @@ -154,14 +154,14 @@ void DynamicZoneManager::Process() expedition_ids.emplace_back(expedition.id); } ExpeditionLockoutsRepository::DeleteWhere(database, - fmt::format("expedition_id IN ({})", fmt::join(expedition_ids, ","))); + fmt::format("expedition_id IN ({})", Strings::Join(expedition_ids, ","))); } ExpeditionsRepository::DeleteWhere(database, - fmt::format("dynamic_zone_id IN ({})", fmt::join(dynamic_zone_ids, ","))); + fmt::format("dynamic_zone_id IN ({})", Strings::Join(dynamic_zone_ids, ","))); DynamicZoneMembersRepository::RemoveAllMembers(database, dynamic_zone_ids); DynamicZonesRepository::DeleteWhere(database, - fmt::format("id IN ({})", fmt::join(dynamic_zone_ids, ","))); + fmt::format("id IN ({})", Strings::Join(dynamic_zone_ids, ","))); } } diff --git a/world/expedition_database.cpp b/world/expedition_database.cpp index 670418a87..995be8c6e 100644 --- a/world/expedition_database.cpp +++ b/world/expedition_database.cpp @@ -50,11 +50,11 @@ void ExpeditionDatabase::PurgeExpiredExpeditions() auto results = database.QueryDatabase(query); if (results.Success()) { - std::vector expedition_ids; + std::vector expedition_ids; std::vector dynamic_zone_ids; for (auto row = results.begin(); row != results.end(); ++row) { - expedition_ids.emplace_back(static_cast(strtoul(row[0], nullptr, 10))); + expedition_ids.emplace_back(row[0]); dynamic_zone_ids.emplace_back(static_cast(strtoul(row[1], nullptr, 10))); } diff --git a/world/main.cpp b/world/main.cpp index b27ab7684..6e46d70ad 100644 --- a/world/main.cpp +++ b/world/main.cpp @@ -58,6 +58,7 @@ #include "../common/unix.h" #include +#include #if not defined (FREEBSD) && not defined (DARWIN) union semun { @@ -96,6 +97,7 @@ union semun { #include "shared_task_manager.h" #include "world_boot.h" #include "../common/path_manager.h" +#include "../common/events/player_event_logs.h" ZoneStore zone_store; @@ -118,6 +120,7 @@ EQEmuLogSys LogSys; WorldContentService content_service; WebInterfaceList web_interface; PathManager path; +PlayerEventLogs player_event_logs; void CatchSignal(int sig_num); @@ -128,6 +131,13 @@ inline void UpdateWindowTitle(std::string new_title) #endif } +void PlayerEventQueueListener() { + while (RunLoops) { + player_event_logs.Process(); + Sleep(1000); + } +} + /** * World process entrypoint * @@ -371,6 +381,13 @@ int main(int argc, char **argv) } ); + player_event_logs.SetDatabase(&database)->Init(); + + if (!RuleB(Logging, PlayerEventsQSProcess)) { + LogInfo("[PlayerEventQueueListener] Booting queue processor"); + std::thread(PlayerEventQueueListener).detach(); + } + auto loop_fn = [&](EQ::Timer* t) { Timer::SetCurrentTime(); diff --git a/world/queryserv.cpp b/world/queryserv.cpp index e3cf2a51f..8708b6c31 100644 --- a/world/queryserv.cpp +++ b/world/queryserv.cpp @@ -22,6 +22,7 @@ void QueryServConnection::AddConnection(std::shared_ptrOnMessage(ServerOP_QueryServGeneric, std::bind(&QueryServConnection::HandleGenericMessage, this, std::placeholders::_1, std::placeholders::_2)); connection->OnMessage(ServerOP_LFGuildUpdate, std::bind(&QueryServConnection::HandleLFGuildUpdateMessage, this, std::placeholders::_1, std::placeholders::_2)); m_streams.insert(std::make_pair(connection->GetUUID(), connection)); + m_keepalive = std::make_unique(1000, true, std::bind(&QueryServConnection::OnKeepAlive, this, std::placeholders::_1)); } void QueryServConnection::RemoveConnection(std::shared_ptr connection) @@ -51,4 +52,10 @@ bool QueryServConnection::SendPacket(ServerPacket* pack) } return true; -} \ No newline at end of file +} + +void QueryServConnection::OnKeepAlive(EQ::Timer *t) +{ + ServerPacket pack(ServerOP_KeepAlive, 0); + SendPacket(&pack); +} diff --git a/world/queryserv.h b/world/queryserv.h index f19520b7d..a02aab647 100644 --- a/world/queryserv.h +++ b/world/queryserv.h @@ -4,6 +4,7 @@ #include "../common/types.h" #include "../common/net/servertalk_server.h" #include "../common/servertalk.h" +#include "../common/event/timer.h" class QueryServConnection { @@ -14,8 +15,10 @@ public: void HandleGenericMessage(uint16_t opcode, EQ::Net::Packet &p); void HandleLFGuildUpdateMessage(uint16_t opcode, EQ::Net::Packet &p); bool SendPacket(ServerPacket* pack); + void OnKeepAlive(EQ::Timer *t); private: std::map> m_streams; + std::unique_ptr m_keepalive; }; #endif /*QueryServ_H_*/ diff --git a/world/shared_task_manager.cpp b/world/shared_task_manager.cpp index 1c539805a..50a3f5b1d 100644 --- a/world/shared_task_manager.cpp +++ b/world/shared_task_manager.cpp @@ -312,7 +312,7 @@ void SharedTaskManager::LoadSharedTaskState() shared_task_character_data = CharacterDataRepository::GetWhere( *m_database, - fmt::format("id IN ({})", fmt::join(character_ids, ",")) + fmt::format("id IN ({})", Strings::Join(character_ids, ",")) ); } @@ -1294,7 +1294,7 @@ std::vector SharedTaskManage OR (timer_group > 0 AND timer_type = {} AND timer_group = {})) AND expire_time > NOW() ORDER BY timer_type ASC LIMIT 1 ), - fmt::join(character_ids, ","), + Strings::Join(character_ids, ","), task.id, static_cast(TaskTimerType::Replay), task.replay_timer_group, @@ -1632,7 +1632,7 @@ void SharedTaskManager::AddReplayTimers(SharedTask *s) s->GetTaskData().id, s->GetTaskData().replay_timer_group, static_cast(TaskTimerType::Replay), - fmt::join(s->member_id_history, ",") + Strings::Join(s->member_id_history, ",") )); CharacterTaskTimersRepository::InsertMany(*m_database, task_timers); diff --git a/world/shared_task_world_messaging.cpp b/world/shared_task_world_messaging.cpp index a9f513592..01584c495 100644 --- a/world/shared_task_world_messaging.cpp +++ b/world/shared_task_world_messaging.cpp @@ -323,7 +323,7 @@ void SharedTaskWorldMessaging::HandleZoneMessage(ServerPacket *pack) } } - std::string player_list = fmt::format("{}", fmt::join(player_names, ", ")); + std::string player_list = fmt::format("{}", Strings::Join(player_names, ", ")); client_list.SendCharacterMessageID(buf->source_character_id, Chat::Yellow, TaskStr::MEMBERS_PRINT, {player_list}); } diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 0edd6e88f..f3cd59092 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -43,6 +43,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/shared_tasks.h" #include "shared_task_manager.h" #include "../common/content/world_content_service.h" +#include "../common/repositories/player_event_logs_repository.h" +#include "../common/events/player_event_logs.h" extern ClientList client_list; extern GroupLFPList LFPGroupList; @@ -369,6 +371,30 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { zoneserver_list.SendPacket(pack); break; } + case ServerOP_PlayerEvent: { + auto n = PlayerEvent::PlayerEventContainer{}; + auto s = (ServerSendPlayerEvent_Struct *) pack->pBuffer; + EQ::Util::MemoryStreamReader ss(s->cereal_data, s->cereal_size); + cereal::BinaryInputArchive archive(ss); + archive(n); + + // by default process events in world + // if set, process events in queryserver + // if you want to offload event recording to a dedicated QS instance + if (!RuleB(Logging, PlayerEventsQSProcess)) { + player_event_logs.AddToQueue(n.player_event_log); + } + else { + QSLink.SendPacket(pack); + } + + // if discord enabled for event, ship to UCS to process + if (player_event_logs.IsEventDiscordEnabled(n.player_event_log.event_type_id)) { + UCSLink.SendPacket(pack); + } + + break; + } case ServerOP_DetailsChange: { if (pack->size != sizeof(ServerRaidGeneralAction_Struct)) { break; @@ -1356,6 +1382,7 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { zoneserver_list.SendPacket(pack); UCSLink.SendPacket(pack); LogSys.LoadLogDatabaseSettings(); + player_event_logs.ReloadSettings(); break; } case ServerOP_ReloadTasks: { diff --git a/zone/aa.cpp b/zone/aa.cpp index 3bbd11ca9..fc96beac2 100644 --- a/zone/aa.cpp +++ b/zone/aa.cpp @@ -23,6 +23,7 @@ Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.net) #include "../common/races.h" #include "../common/spdat.h" #include "../common/strings.h" +#include "../common/events/player_event_logs.h" #include "aa.h" #include "client.h" #include "corpse.h" @@ -35,9 +36,11 @@ Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.net) #include "titles.h" #include "zonedb.h" #include "../common/zone_store.h" +#include "worldserver.h" #include "bot.h" +extern WorldServer worldserver; extern QueryServ* QServ; void Mob::TemporaryPets(uint16 spell_id, Mob *targ, const char *name_override, uint32 duration_override, bool followme, bool sticktarg, uint16 *eye_id) { @@ -1180,6 +1183,17 @@ void Client::FinishAlternateAdvancementPurchase(AA::Rank *rank, bool ignore_cost SendAlternateAdvancementPoints(); SendAlternateAdvancementStats(); + if (player_event_logs.IsEventEnabled(PlayerEvent::AA_PURCHASE)) { + auto e = PlayerEvent::AAPurchasedEvent{ + .aa_id = rank->id, + .aa_cost = cost, + .aa_previous_id = rank->prev_id, + .aa_next_id = rank->next_id + }; + + RecordPlayerEventLog(PlayerEvent::AA_PURCHASE, e); + } + if (rank->prev) { MessageString( Chat::Yellow, diff --git a/zone/attack.cpp b/zone/attack.cpp index 6bd2add41..f089c16ad 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -25,6 +25,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/strings.h" #include "../common/data_verification.h" #include "../common/misc_functions.h" +#include "../common/events/player_event_logs.h" #include "queryserv.h" #include "quest_parser_collection.h" #include "string_ids.h" @@ -2064,6 +2065,20 @@ bool Client::Death(Mob* killerMob, int64 damage, uint16 spell, EQ::skills::Skill QServ->PlayerLogEvent(Player_Log_Deaths, CharacterID(), event_desc); } + if (player_event_logs.IsEventEnabled(PlayerEvent::DEATH)) { + auto e = PlayerEvent::DeathEvent{ + .killer_id = killerMob ? static_cast(killerMob->GetID()) : static_cast(0), + .killer_name = killerMob ? killerMob->GetCleanName() : "No Killer", + .damage = damage, + .spell_id = spell, + .spell_name = IsValidSpell(spell) ? spells[spell].name : "No Spell", + .skill_id = static_cast(attack_skill), + .skill_name = !EQ::skills::GetSkillName(attack_skill).empty() ? EQ::skills::GetSkillName(attack_skill) : "No Skill", + }; + + RecordPlayerEventLog(PlayerEvent::DEATH, e); + } + std::vector args = { new_corpse }; parse->EventPlayer(EVENT_DEATH_COMPLETE, this, export_string, 0, &args); return true; @@ -2518,6 +2533,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy if (kr->members[i].member != nullptr && kr->members[i].member->IsClient()) { // If Group Member is Client Client *c = kr->members[i].member; parse->EventNPC(EVENT_KILLED_MERIT, this, c, "killed", 0); + c->RecordKilledNPCEvent(this); if (RuleB(NPC, EnableMeritBasedFaction)) c->SetFactionLevel(c->CharacterID(), GetNPCFactionID(), c->GetBaseClass(), c->GetBaseRace(), c->GetDeity()); @@ -2565,6 +2581,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy if (kg->members[i] != nullptr && kg->members[i]->IsClient()) { // If Group Member is Client Client *c = kg->members[i]->CastToClient(); parse->EventNPC(EVENT_KILLED_MERIT, this, c, "killed", 0); + c->RecordKilledNPCEvent(this); if (RuleB(NPC, EnableMeritBasedFaction)) c->SetFactionLevel(c->CharacterID(), GetNPCFactionID(), c->GetBaseClass(), c->GetBaseRace(), c->GetDeity()); @@ -2612,6 +2629,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy /* Send the EVENT_KILLED_MERIT event */ parse->EventNPC(EVENT_KILLED_MERIT, this, give_exp_client, "killed", 0); + give_exp_client->RecordKilledNPCEvent(this); if (RuleB(NPC, EnableMeritBasedFaction)) give_exp_client->SetFactionLevel(give_exp_client->CharacterID(), GetNPCFactionID(), give_exp_client->GetBaseClass(), @@ -2789,7 +2807,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy std::vector args = { corpse }; parse->EventNPC(EVENT_DEATH_COMPLETE, this, oos, export_string, 0, &args); - combat_record.Stop(); + m_combat_record.Stop(); /* Zone controller process EVENT_DEATH_ZONE (Death events) */ args.push_back(this); diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index c70a73e6f..b6e492d0a 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -1624,8 +1624,6 @@ int bot_command_real_dispatch(Client *c, const char *message) { Seperator sep(message, ' ', 10, 100, true); // "three word argument" should be considered 1 arg - bot_command_log_command(c, message); - std::string cstr(sep.arg[0]+1); if(bot_command_list.count(cstr) != 1) { @@ -1659,77 +1657,6 @@ int bot_command_real_dispatch(Client *c, const char *message) } -void bot_command_log_command(Client *c, const char *message) -{ -int admin = c->Admin(); - - bool continueevents = false; - switch (zone->loglevelvar){ //catch failsafe - case 9: { // log only LeadGM - if ( - admin >= AccountStatus::GMLeadAdmin && - admin < AccountStatus::GMMgmt - ) { - continueevents = true; - } - break; - } - case 8: { // log only GM - if ( - admin >= AccountStatus::GMAdmin && - admin < AccountStatus::GMLeadAdmin - ) { - continueevents = true; - } - break; - } - case 1: { - if (admin >= AccountStatus::GMMgmt) { - continueevents = true; - } - break; - } - case 2: { - if (admin >= AccountStatus::GMLeadAdmin) { - continueevents = true; - } - break; - } - case 3: { - if (admin >= AccountStatus::GMAdmin) { - continueevents = true; - } - break; - } - case 4: { - if (admin >= AccountStatus::QuestTroupe) { - continueevents = true; - } - break; - } - case 5: { - if (admin >= AccountStatus::ApprenticeGuide) { - continueevents = true; - } - break; - } - case 6: { - if (admin >= AccountStatus::Steward) { - continueevents = true; - } - break; - } - case 7: { - continueevents = true; - break; - } - } - - if (continueevents) - database.logevents(c->AccountName(), c->AccountID(), admin,c->GetName(), c->GetTarget()?c->GetTarget()->GetName():"None", "BotCommand", message, 1); -} - - /* * helper functions by use */ diff --git a/zone/bot_command.h b/zone/bot_command.h index 270631ebd..9e8715e75 100644 --- a/zone/bot_command.h +++ b/zone/bot_command.h @@ -543,7 +543,6 @@ void bot_command_deinit(void); int bot_command_add(std::string bot_command_name, const char *desc, int access, BotCmdFuncPtr function); int bot_command_not_avail(Client *c, const char *message); int bot_command_real_dispatch(Client *c, char const *message); -void bot_command_log_command(Client *c, const char *message); // Bot Commands void bot_command_actionable(Client *c, const Seperator *sep); diff --git a/zone/cheat_manager.cpp b/zone/cheat_manager.cpp index f780bb5f1..cd6af2fb3 100644 --- a/zone/cheat_manager.cpp +++ b/zone/cheat_manager.cpp @@ -1,6 +1,10 @@ #include "cheat_manager.h" #include "client.h" #include "quest_parser_collection.h" +#include "../common/events/player_event_logs.h" +#include "worldserver.h" + +extern WorldServer worldserver; void CheatManager::SetClient(Client *cli) { @@ -36,12 +40,9 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position2.z, Distance(position1, position2) ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + LogCheat(fmt::runtime(message)); std::string export_string = fmt::format( "{} {} {}", @@ -65,12 +66,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position2.z, Distance(position1, position2) ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); std::string export_string = fmt::format( "{} {} {}", @@ -91,12 +87,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } break; @@ -109,12 +100,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } break; @@ -129,12 +115,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } } @@ -149,12 +130,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } break; @@ -167,12 +143,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } break; @@ -185,12 +156,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } break; @@ -199,17 +165,13 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 if (RuleB(Cheat, EnableMQGhostDetector) && ((m_target->Admin() < RuleI(Cheat, MQGhostExemptStatus) || (RuleI(Cheat, MQGhostExemptStatus)) == -1))) { - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - "Packet blocking detected.", - zone->GetShortName() - ); - LogCheat( + std::string message = fmt::format( "[MQGhost] [{}] [{}] was caught not sending the proper packets as regularly as they were suppose to.", m_target->AccountName(), m_target->GetName() ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + LogCheat("{}", message); } break; case MQFastMem: @@ -222,12 +184,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); } break; @@ -238,12 +195,7 @@ void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position1.y, position1.z ); - database.SetMQDetectionFlag( - m_target->AccountName(), - m_target->GetName(), - message.c_str(), - zone->GetShortName() - ); + RecordPlayerEventLogWithClient(m_target, PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); LogCheat(fmt::runtime(message)); break; } diff --git a/zone/client.cpp b/zone/client.cpp index fed889585..5a4bdcc2f 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -66,6 +66,9 @@ extern volatile bool RunLoops; #include "../common/repositories/character_spells_repository.h" #include "../common/repositories/character_disciplines_repository.h" #include "../common/repositories/character_data_repository.h" +#include "../common/repositories/discovered_items_repository.h" +#include "../common/events/player_events.h" +#include "../common/events/player_event_logs.h" extern QueryServ* QServ; @@ -1122,6 +1125,17 @@ void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_s break; } case ChatChannel_Say: { /* Say */ + if (player_event_logs.IsEventEnabled(PlayerEvent::SAY)) { + std::string msg = message; + if (!msg.empty() && msg.at(0) != '#' && msg.at(0) != '^') { + auto e = PlayerEvent::SayEvent{ + .message = message, + .target = GetTarget() ? GetTarget()->GetCleanName() : "" + }; + RecordPlayerEventLog(PlayerEvent::SAY, e); + } + } + if (message[0] == COMMAND_CHAR) { if (command_dispatch(this, message, false) == -2) { if (parse->PlayerHasQuestSub(EVENT_COMMAND)) { @@ -2542,6 +2556,17 @@ bool Client::CheckIncreaseSkill(EQ::skills::SkillType skillid, Mob *against_who, 0 ); parse->EventPlayer(EVENT_SKILL_UP, this, export_string, 0); + + if (player_event_logs.IsEventEnabled(PlayerEvent::SKILL_UP)) { + auto e = PlayerEvent::SkillUpEvent{ + .skill_id = static_cast(skillid), + .value = (skillval + 1), + .max_skill = static_cast(maxskill), + .against_who = (against_who) ? against_who->GetCleanName() : GetCleanName(), + }; + RecordPlayerEventLog(PlayerEvent::SKILL_UP, e); + } + LogSkills("Skill [{}] at value [{}] successfully gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); return true; } else { @@ -2764,35 +2789,6 @@ void Client::MemorizeSpell(uint32 slot,uint32 spellid,uint32 scribing, uint32 re safe_delete(outapp); } -void Client::LogMerchant(Client* player, Mob* merchant, uint32 quantity, uint32 price, const EQ::ItemData* item, bool buying) -{ - if(!player || !merchant || !item) - return; - - std::string LogText = "Qty: "; - - char Buffer[255]; - memset(Buffer, 0, sizeof(Buffer)); - - snprintf(Buffer, sizeof(Buffer)-1, "%3i", quantity); - LogText += Buffer; - snprintf(Buffer, sizeof(Buffer)-1, "%10i", price); - LogText += " TotalValue: "; - LogText += Buffer; - snprintf(Buffer, sizeof(Buffer)-1, " ItemID: %7i", item->ID); - LogText += Buffer; - LogText += " "; - snprintf(Buffer, sizeof(Buffer)-1, " %s", item->Name); - LogText += Buffer; - - if (buying==true) { - database.logevents(player->AccountName(),player->AccountID(),player->admin,player->GetName(),merchant->GetName(),"Buying from Merchant",LogText.c_str(),2); - } - else { - database.logevents(player->AccountName(),player->AccountID(),player->admin,player->GetName(),merchant->GetName(),"Selling to Merchant",LogText.c_str(),3); - } -} - void Client::Disarm(Client* disarmer, int chance) { int16 slot = EQ::invslot::SLOT_INVALID; const EQ::ItemInstance *inst = GetInv().GetItem(EQ::invslot::slotPrimary); @@ -4044,36 +4040,42 @@ void Client::KeyRingList() } } -bool Client::IsDiscovered(uint32 itemid) { - - std::string query = StringFormat("SELECT count(*) FROM discovered_items WHERE item_id = '%lu'", itemid); - auto results = database.QueryDatabase(query); - if (!results.Success()) { +bool Client::IsDiscovered(uint32 item_id) { + const auto& l = DiscoveredItemsRepository::GetWhere( + database, + fmt::format( + "item_id = {}", + item_id + ) + ); + if (l.empty()) { return false; } - auto row = results.begin(); - if (!atoi(row[0])) - return false; - return true; } -void Client::DiscoverItem(uint32 itemid) { +void Client::DiscoverItem(uint32 item_id) { + auto e = DiscoveredItemsRepository::NewEntity(); - std::string query = StringFormat("INSERT INTO discovered_items " - "SET item_id = %lu, char_name = '%s', " - "discovered_date = UNIX_TIMESTAMP(), account_status = %i", - itemid, GetName(), Admin()); - auto results = database.QueryDatabase(query); + e.account_status = Admin(); + e.char_name = GetCleanName(); + e.discovered_date = std::time(nullptr); + e.item_id = item_id; - auto* inst = database.CreateItem(itemid); + auto d = DiscoveredItemsRepository::InsertOne(database, e); - std::vector args; + parse->EventPlayer(EVENT_DISCOVER_ITEM, this, "", item_id); - args.emplace_back(inst); + if (player_event_logs.IsEventEnabled(PlayerEvent::DISCOVER_ITEM)) { + const auto* item = database.GetItem(item_id); - parse->EventPlayer(EVENT_DISCOVER_ITEM, this, "", itemid, &args); + auto e = PlayerEvent::DiscoverItemEvent{ + .item_id = item_id, + .item_name = item->Name, + }; + RecordPlayerEventLog(PlayerEvent::DISCOVER_ITEM, e); + } } void Client::UpdateLFP() { @@ -6870,7 +6872,7 @@ void Client::SetAlternateCurrencyValue(uint32 currency_id, uint32 new_amount) SendAlternateCurrencyValue(currency_id); } -void Client::AddAlternateCurrencyValue(uint32 currency_id, int32 amount, int8 method) +int Client::AddAlternateCurrencyValue(uint32 currency_id, int32 amount, int8 method) { /* Added via Quest, rest of the logging methods may be done inline due to information available in that area of the code */ @@ -6883,12 +6885,12 @@ void Client::AddAlternateCurrencyValue(uint32 currency_id, int32 amount, int8 me } if(amount == 0) { - return; + return 0; } if(!alternate_currency_loaded) { alternate_currency_queued_operations.push(std::make_pair(currency_id, amount)); - return; + return 0; } int new_value = 0; @@ -6907,6 +6909,8 @@ void Client::AddAlternateCurrencyValue(uint32 currency_id, int32 amount, int8 me database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_value); } SendAlternateCurrencyValue(currency_id); + + return new_value; } void Client::SendAlternateCurrencyValues() @@ -11859,7 +11863,7 @@ void Client::SendPath(Mob* target) target->CastToClient()->Trader || target->CastToClient()->Buyer ) - ) { + ) { Message( Chat::Yellow, fmt::format( @@ -11894,7 +11898,8 @@ void Client::SendPath(Mob* target) points.push_back(a); points.push_back(b); - } else { + } + else { glm::vec3 path_start( GetX(), GetY(), @@ -11907,8 +11912,8 @@ void Client::SendPath(Mob* target) target->GetZ() + (target->GetSize() < 6.0 ? 6 : target->GetSize()) * HEAD_POSITION ); - bool partial = false; - bool stuck = false; + bool partial = false; + bool stuck = false; auto path_list = zone->pathing->FindRoute(path_start, path_end, partial, stuck); if (path_list.empty() || partial) { @@ -11939,7 +11944,7 @@ void Client::SendPath(Mob* target) p.z = GetZ(); points.push_back(p); - for (const auto& n : path_list) { + for (const auto &n: path_list) { if (n.teleport) { leads_to_teleporter = true; break; @@ -11967,10 +11972,201 @@ void Client::SendPath(Mob* target) SendPathPacket(points); } -void Client::UseAugmentContainer(int container_slot) { +void Client::UseAugmentContainer(int container_slot) +{ auto in_augment = new AugmentItem_Struct[sizeof(AugmentItem_Struct)]; in_augment->container_slot = container_slot; - in_augment->augment_slot = -1; + in_augment->augment_slot = -1; Object::HandleAugmentation(this, in_augment, nullptr); safe_delete_array(in_augment); } + +PlayerEvent::PlayerEvent Client::GetPlayerEvent() +{ + auto e = PlayerEvent::PlayerEvent{}; + e.account_id = AccountID(); + e.character_id = CharacterID(); + e.character_name = GetCleanName(); + e.x = GetX(); + e.y = GetY(); + e.z = GetZ(); + e.heading = GetHeading(); + e.zone_id = GetZoneID(); + e.zone_short_name = zone ? zone->GetShortName() : ""; + e.zone_long_name = zone ? zone->GetLongName() : ""; + e.instance_id = GetInstanceID(); + e.guild_id = GuildID(); + e.guild_name = guild_mgr.GetGuildName(GuildID()); + e.account_name = AccountName(); + + return e; +} + +void Client::PlayerTradeEventLog(Trade *t, Trade *t2) +{ + Client *trader = t->GetOwner()->CastToClient(); + Client *trader2 = t2->GetOwner()->CastToClient(); + uint8 t_item_count = 0; + uint8 t2_item_count = 0; + + auto money_t = PlayerEvent::Money{ + .platinum = t->pp, + .gold = t->gp, + .silver = t->sp, + .copper = t->cp, + }; + auto money_t2 = PlayerEvent::Money{ + .platinum = t2->pp, + .gold = t2->gp, + .silver = t2->sp, + .copper = t2->cp, + }; + + // trader 1 item count + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { + if (trader->GetInv().GetItem(i)) { + t_item_count++; + } + } + + // trader 2 item count + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { + if (trader2->GetInv().GetItem(i)) { + t2_item_count++; + } + } + + std::vector t_entries = {}; + t_entries.reserve(t_item_count); + if (t_item_count > 0) { + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { + const EQ::ItemInstance *inst = trader->GetInv().GetItem(i); + if (inst) { + t_entries.emplace_back( + PlayerEvent::TradeItemEntry{ + .slot = i, + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .aug_1_item_id = inst->GetAugmentItemID(0), + .aug_1_item_name = inst->GetAugment(0) ? inst->GetAugment(0)->GetItem()->Name : "", + .aug_2_item_id = inst->GetAugmentItemID(1), + .aug_2_item_name = inst->GetAugment(1) ? inst->GetAugment(1)->GetItem()->Name : "", + .aug_3_item_id = inst->GetAugmentItemID(2), + .aug_3_item_name = inst->GetAugment(2) ? inst->GetAugment(2)->GetItem()->Name : "", + .aug_4_item_id = inst->GetAugmentItemID(3), + .aug_4_item_name = inst->GetAugment(3) ? inst->GetAugment(3)->GetItem()->Name : "", + .aug_5_item_id = inst->GetAugmentItemID(4), + .aug_5_item_name = inst->GetAugment(4) ? inst->GetAugment(4)->GetItem()->Name : "", + .aug_6_item_id = inst->GetAugmentItemID(5), + .aug_6_item_name = inst->GetAugment(5) ? inst->GetAugment(5)->GetItem()->Name : "", + .in_bag = false, + } + ); + + if (inst->IsClassBag()) { + for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { + inst = trader->GetInv().GetItem(i, j); + if (inst) { + t_entries.emplace_back( + PlayerEvent::TradeItemEntry{ + .slot = j, + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .aug_1_item_id = inst->GetAugmentItemID(0), + .aug_1_item_name = inst->GetAugment(0) ? inst->GetAugment(0)->GetItem()->Name : "", + .aug_2_item_id = inst->GetAugmentItemID(1), + .aug_2_item_name = inst->GetAugment(1) ? inst->GetAugment(1)->GetItem()->Name : "", + .aug_3_item_id = inst->GetAugmentItemID(2), + .aug_3_item_name = inst->GetAugment(2) ? inst->GetAugment(2)->GetItem()->Name : "", + .aug_4_item_id = inst->GetAugmentItemID(3), + .aug_4_item_name = inst->GetAugment(3) ? inst->GetAugment(3)->GetItem()->Name : "", + .aug_5_item_id = inst->GetAugmentItemID(4), + .aug_5_item_name = inst->GetAugment(4) ? inst->GetAugment(4)->GetItem()->Name : "", + .aug_6_item_id = inst->GetAugmentItemID(5), + .aug_6_item_name = inst->GetAugment(5) ? inst->GetAugment(5)->GetItem()->Name : "", + .in_bag = true, + } + ); + } + } + } + } + } + } + + std::vector t2_entries = {}; + t_entries.reserve(t2_item_count); + if (t2_item_count > 0) { + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { + const EQ::ItemInstance *inst = trader2->GetInv().GetItem(i); + if (inst) { + t2_entries.emplace_back( + PlayerEvent::TradeItemEntry{ + .slot = i, + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .aug_1_item_id = inst->GetAugmentItemID(0), + .aug_1_item_name = inst->GetAugment(0) ? inst->GetAugment(0)->GetItem()->Name : "", + .aug_2_item_id = inst->GetAugmentItemID(1), + .aug_2_item_name = inst->GetAugment(1) ? inst->GetAugment(1)->GetItem()->Name : "", + .aug_3_item_id = inst->GetAugmentItemID(2), + .aug_3_item_name = inst->GetAugment(2) ? inst->GetAugment(2)->GetItem()->Name : "", + .aug_4_item_id = inst->GetAugmentItemID(3), + .aug_4_item_name = inst->GetAugment(3) ? inst->GetAugment(3)->GetItem()->Name : "", + .aug_5_item_id = inst->GetAugmentItemID(4), + .aug_5_item_name = inst->GetAugment(4) ? inst->GetAugment(4)->GetItem()->Name : "", + .aug_6_item_id = inst->GetAugmentItemID(5), + .aug_6_item_name = inst->GetAugment(5) ? inst->GetAugment(5)->GetItem()->Name : "", + .in_bag = false, + } + ); + + if (inst->IsClassBag()) { + for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { + inst = trader2->GetInv().GetItem(i, j); + if (inst) { + t2_entries.emplace_back( + PlayerEvent::TradeItemEntry{ + .slot = j, + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .aug_1_item_id = inst->GetAugmentItemID(0), + .aug_1_item_name = inst->GetAugment(0) ? inst->GetAugment(0)->GetItem()->Name : "", + .aug_2_item_id = inst->GetAugmentItemID(1), + .aug_2_item_name = inst->GetAugment(1) ? inst->GetAugment(1)->GetItem()->Name : "", + .aug_3_item_id = inst->GetAugmentItemID(2), + .aug_3_item_name = inst->GetAugment(2) ? inst->GetAugment(2)->GetItem()->Name : "", + .aug_4_item_id = inst->GetAugmentItemID(3), + .aug_4_item_name = inst->GetAugment(3) ? inst->GetAugment(3)->GetItem()->Name : "", + .aug_5_item_id = inst->GetAugmentItemID(4), + .aug_5_item_name = inst->GetAugment(4) ? inst->GetAugment(4)->GetItem()->Name : "", + .aug_6_item_id = inst->GetAugmentItemID(5), + .aug_6_item_name = inst->GetAugment(5) ? inst->GetAugment(5)->GetItem()->Name : "", + .in_bag = true, + } + ); + } + } + } + } + } + } + + auto e = PlayerEvent::TradeEvent{ + .character_1_id = trader->CharacterID(), + .character_1_name = trader->GetCleanName(), + .character_2_id = trader2->CharacterID(), + .character_2_name = trader2->GetCleanName(), + .character_1_give_money = money_t, + .character_2_give_money = money_t2, + .character_1_give_items = t_entries, + .character_2_give_items = t2_entries + }; + + RecordPlayerEventLogWithClient(trader, PlayerEvent::TRADE, e); + RecordPlayerEventLogWithClient(trader2, PlayerEvent::TRADE, e); +} diff --git a/zone/client.h b/zone/client.h index 9c8990d63..bc97cabbc 100644 --- a/zone/client.h +++ b/zone/client.h @@ -67,6 +67,7 @@ namespace EQ #include "task_manager.h" #include "task_client_state.h" #include "cheat_manager.h" +#include "../common/events/player_events.h" #ifdef _WINDOWS // since windows defines these within windef.h (which windows.h include) @@ -329,7 +330,6 @@ public: bool ShouldISpawnFor(Client *c) { return !GMHideMe(c) && !IsHoveringForRespawn(); } virtual bool Process(); void ProcessPackets(); - void LogMerchant(Client* player, Mob* merchant, uint32 quantity, uint32 price, const EQ::ItemData* item, bool buying); void QueuePacket(const EQApplicationPacket* app, bool ack_req = true, CLIENT_CONN_STATUS = CLIENT_CONNECTINGALL, eqFilterType filter=FilterNone); void FastQueuePacket(EQApplicationPacket** app, bool ack_req = true, CLIENT_CONN_STATUS = CLIENT_CONNECTINGALL); void ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_skill, const char* orig_message, const char* targetname = nullptr, bool is_silent = false); @@ -1487,7 +1487,7 @@ public: void ConsentCorpses(std::string consent_name, bool deny = false); void SendAltCurrencies(); void SetAlternateCurrencyValue(uint32 currency_id, uint32 new_amount); - void AddAlternateCurrencyValue(uint32 currency_id, int32 amount, int8 method = 0); + int AddAlternateCurrencyValue(uint32 currency_id, int32 amount, int8 method = 0); void SendAlternateCurrencyValues(); void SendAlternateCurrencyValue(uint32 currency_id, bool send_if_null = true); uint32 GetAlternateCurrencyValue(uint32 currency_id) const; @@ -1679,6 +1679,8 @@ public: std::string GetGuildPublicNote(); + PlayerEvent::PlayerEvent GetPlayerEvent(); + void RecordKilledNPCEvent(NPC *n); protected: friend class Mob; void CalcItemBonuses(StatBonuses* newbon); @@ -2086,6 +2088,7 @@ private: bool CanTradeFVNoDropItem(); void SendMobPositions(); + void PlayerTradeEventLog(Trade *t, Trade *t2); }; #endif diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 31a5aa361..0b2edef8d 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -71,6 +71,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/repositories/account_repository.h" #include "bot.h" +#include "../common/events/player_event_logs.h" extern QueryServ* QServ; extern Zone* zone; @@ -790,6 +791,7 @@ void Client::CompleteConnect() /* This sub event is for if a player logs in for the first time since entering world. */ if (firstlogon == 1) { parse->EventPlayer(EVENT_CONNECT, this, "", 0); + RecordPlayerEventLog(PlayerEvent::WENT_ONLINE, PlayerEvent::EmptyEvent{}); /* QS: PlayerLogConnectDisconnect */ if (RuleB(QueryServ, PlayerLogConnectDisconnect)) { std::string event_desc = StringFormat("Connect :: Logged into zoneid:%i instid:%i", GetZoneID(), GetInstanceID()); @@ -2240,9 +2242,6 @@ void Client::Handle_OP_AdventureMerchantSell(const EQApplicationPacket *app) return; } - if (RuleB(EventLog, RecordSellToMerchant)) - LogMerchant(this, vendor, ams_in->charges, price, item, false); - if (!inst->IsStackable()) { DeleteItemInInventory(ams_in->slot); @@ -2600,10 +2599,28 @@ void Client::Handle_OP_AltCurrencyPurchase(const EQApplicationPacket *app) ); parse->EventPlayer(EVENT_ALT_CURRENCY_MERCHANT_BUY, this, export_string, 0); - AddAlternateCurrencyValue(alt_cur_id, -((int32)cost)); - int16 charges = 1; - if (item->MaxCharges != 0) + uint64 current_balance = AddAlternateCurrencyValue(alt_cur_id, -((int32) cost)); + int16 charges = 1; + if (item->MaxCharges != 0) { charges = item->MaxCharges; + } + + if (player_event_logs.IsEventEnabled(PlayerEvent::MERCHANT_PURCHASE)) { + auto e = PlayerEvent::MerchantPurchaseEvent{ + .npc_id = tar->GetNPCTypeID(), + .merchant_name = tar->GetCleanName(), + .merchant_type = tar->MerchantType, + .item_id = item->ID, + .item_name = item->Name, + .charges = charges, + .cost = cost, + .alternate_currency_id = alt_cur_id, + .player_money_balance = GetCarriedMoney(), + .player_currency_balance = current_balance, + }; + + RecordPlayerEventLog(PlayerEvent::MERCHANT_PURCHASE, e); + } EQ::ItemInstance *inst = database.CreateItem(item, charges); if (!AutoPutLootInInventory(*inst, true, true)) @@ -2774,7 +2791,25 @@ void Client::Handle_OP_AltCurrencySell(const EQApplicationPacket *app) parse->EventPlayer(EVENT_ALT_CURRENCY_MERCHANT_SELL, this, export_string, 0); FastQueuePacket(&outapp); - AddAlternateCurrencyValue(alt_cur_id, cost); + uint64 new_balance = AddAlternateCurrencyValue(alt_cur_id, cost); + + if (player_event_logs.IsEventEnabled(PlayerEvent::MERCHANT_SELL)) { + auto e = PlayerEvent::MerchantSellEvent{ + .npc_id = tar->GetNPCTypeID(), + .merchant_name = tar->GetCleanName(), + .merchant_type = tar->CastToNPC()->MerchantType, + .item_id = item->ID, + .item_name = item->Name, + .charges = static_cast(sell->charges), + .cost = cost, + .alternate_currency_id = 0, + .player_money_balance = GetCarriedMoney(), + .player_currency_balance = new_balance, + }; + + RecordPlayerEventLog(PlayerEvent::MERCHANT_SELL, e); + } + Save(1); } } @@ -3441,10 +3476,10 @@ void Client::Handle_OP_BankerChange(const EQApplicationPacket *app) if (!banker || distance > USE_NPC_RANGE2) { - auto hacked_string = fmt::format( + auto message = fmt::format( "Player tried to make use of a banker(money) but {} is non-existant or too far away ({} units).", banker ? banker->GetName() : "UNKNOWN NPC", distance); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); return; } @@ -4164,7 +4199,8 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) const EQ::ItemData* item = inst->GetItem(); if (item->Click.Effect != (uint32)castspell->spell_id) { - database.SetMQDetectionFlag(account_name, name, "OP_CastSpell with item, tried to cast a different spell.", zone->GetShortName()); + std::string message = fmt::format("OP_CastSpell with item, tried to cast a different spell than what was on item - item spell id [{}] attempted [{}]", item->Click.Effect, (uint32)castspell->spell_id); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); InterruptSpell(castspell->spell_id); //CHEATER!! return; } @@ -4205,7 +4241,7 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) } else { - database.SetMQDetectionFlag(account_name, name, "OP_CastSpell with item, did not meet req level.", zone->GetShortName()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "OP_CastSpell with item, did not meet req level."}); Message(Chat::Red, "Error: level not high enough.", castspell->inventoryslot); InterruptSpell(castspell->spell_id); } @@ -5145,8 +5181,8 @@ void Client::Handle_OP_ControlBoat(const EQApplicationPacket *app) if (!boat->IsNPC() || !boat->IsControllableBoat()) { - auto hacked_string = fmt::format("OP_Control Boat was sent against {} which is of race {}", boat->GetName(), boat->GetRace()); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + auto message = fmt::format("OP_Control Boat was sent against {} which is of race {}", boat->GetName(), boat->GetRace()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); return; } @@ -5415,6 +5451,17 @@ void Client::Handle_OP_DeleteItem(const EQApplicationPacket *app) } DeleteItemInInventory(alc->from_slot, 1); + if (player_event_logs.IsEventEnabled(PlayerEvent::ITEM_DESTROY)) { + auto e = PlayerEvent::DestroyItemEvent{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = inst->GetCharges(), + .reason = "Client deleted", + }; + + RecordPlayerEventLog(PlayerEvent::ITEM_DESTROY, e); + } + return; } @@ -5466,8 +5513,9 @@ void Client::Handle_OP_Disarm(const EQApplicationPacket *app) { return; if (pmob->GetID() != GetID()) { // Client sent a disarm request with an originator ID not matching their own ID. - auto hack_str = fmt::format("Player {} ({}) sent OP_Disarm with source ID of: {}", GetCleanName(), GetID(), pmob->GetID()); - database.SetMQDetectionFlag(account_name, name, hack_str, zone->GetShortName()); + auto message = fmt::format("Player {} ({}) sent OP_Disarm with source ID of: {}", GetCleanName(), GetID(), pmob->GetID()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + return; } // No disarm on corpses @@ -6194,7 +6242,7 @@ void Client::Handle_OP_GMBecomeNPC(const EQApplicationPacket *app) { if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/becomenpc"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /becomenpc when they shouldn't be able to"}); return; } if (app->size != sizeof(BecomeNPC_Struct)) { @@ -6234,7 +6282,7 @@ void Client::Handle_OP_GMDelCorpse(const EQApplicationPacket *app) return; if (Admin() < commandEditPlayerCorpses) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/delcorpse"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /delcorpse"}); return; } GMDelCorpse_Struct* dc = (GMDelCorpse_Struct *)app->pBuffer; @@ -6255,7 +6303,6 @@ void Client::Handle_OP_GMEmoteZone(const EQApplicationPacket *app) { if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/emote"); return; } if (app->size != sizeof(GMEmoteZone_Struct)) { @@ -6288,7 +6335,7 @@ void Client::Handle_OP_GMFind(const EQApplicationPacket *app) { if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/find"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /find"}); return; } if (app->size != sizeof(GMSummon_Struct)) { @@ -6326,7 +6373,7 @@ void Client::Handle_OP_GMGoto(const EQApplicationPacket *app) } if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/goto"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /goto"}); return; } GMSummon_Struct* gmg = (GMSummon_Struct*)app->pBuffer; @@ -6353,7 +6400,7 @@ void Client::Handle_OP_GMHideMe(const EQApplicationPacket *app) { if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/hideme"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /hideme"}); return; } if (app->size != sizeof(SpawnAppearance_Struct)) { @@ -6373,7 +6420,7 @@ void Client::Handle_OP_GMKick(const EQApplicationPacket *app) return; if (Admin() < minStatusToKick) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/kick"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /kick"}); return; } GMKick_Struct* gmk = (GMKick_Struct *)app->pBuffer; @@ -6403,7 +6450,7 @@ void Client::Handle_OP_GMKill(const EQApplicationPacket *app) { if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/kill"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /kill"}); return; } if (app->size != sizeof(GMKill_Struct)) { @@ -6455,7 +6502,7 @@ void Client::Handle_OP_GMLastName(const EQApplicationPacket *app) else { if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(client->account_name, client->name, "/lastname"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /lastname"}); return; } else @@ -6480,7 +6527,7 @@ void Client::Handle_OP_GMNameChange(const EQApplicationPacket *app) const GMName_Struct* gmn = (const GMName_Struct *)app->pBuffer; if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/name"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /name"}); return; } Client* client = entity_list.GetClientByName(gmn->oldname); @@ -6623,7 +6670,7 @@ void Client::Handle_OP_GMToggle(const EQApplicationPacket *app) } if (Admin() < minStatusToUseGMCommands) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/toggle"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /toggle"}); return; } GMToggle_Struct *ts = (GMToggle_Struct *)app->pBuffer; @@ -6670,7 +6717,7 @@ void Client::Handle_OP_GMZoneRequest(const EQApplicationPacket *app) } if (Admin() < minStatusToBeGM) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/zone"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /zone"}); return; } @@ -6726,7 +6773,7 @@ void Client::Handle_OP_GMZoneRequest2(const EQApplicationPacket *app) { if (Admin() < minStatusToBeGM) { Message(Chat::Red, "Your account has been reported for hacking."); - database.SetHackerFlag(account_name, name, "/zone"); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Used /zone"}); return; } if (app->size < sizeof(uint32)) { @@ -8400,9 +8447,12 @@ void Client::Handle_OP_Illusion(const EQApplicationPacket *app) return; } - if (!GetGM()) - { - database.SetMQDetectionFlag(AccountName(), GetName(), "OP_Illusion sent by non Game Master.", zone->GetShortName()); + if (!GetGM()) { + RecordPlayerEventLog( + PlayerEvent::POSSIBLE_HACK, + PlayerEvent::PossibleHackEvent{.message = "OP_Illusion sent by non Game Master"} + ); + return; } @@ -10199,13 +10249,13 @@ void Client::Handle_OP_MoveItem(const EQApplicationPacket *app) { const EQ::ItemInstance *itm_from = GetInv().GetItem(mi->from_slot); const EQ::ItemInstance *itm_to = GetInv().GetItem(mi->to_slot); - auto detect = fmt::format("Player issued a move item from {}(item id {}) to {}(item id {}) while casting {}.", + auto message = fmt::format("Player issued a move item from {}(item id {}) to {}(item id {}) while casting {}.", mi->from_slot, itm_from ? itm_from->GetID() : 0, mi->to_slot, itm_to ? itm_to->GetID() : 0, casting_spell_id); - database.SetMQDetectionFlag(AccountName(), GetName(), detect, zone->GetShortName()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); Kick("Inventory desync"); // Kick client to prevent client and server from getting out-of-sync inventory slots return; } @@ -11188,7 +11238,7 @@ void Client::Handle_OP_PickPocket(const EQApplicationPacket *app) if (!p_timers.Expired(&database, pTimerBeggingPickPocket, false)) { Message(Chat::Red, "Ability recovery time not yet met."); - database.SetMQDetectionFlag(AccountName(), GetName(), "OP_PickPocket was sent again too quickly.", zone->GetShortName()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "OP_PickPocket was sent again too quickly."}); return; } PickPocket_Struct* pick_in = (PickPocket_Struct*)app->pBuffer; @@ -11210,7 +11260,7 @@ void Client::Handle_OP_PickPocket(const EQApplicationPacket *app) } else if (Distance(GetPosition(), victim->GetPosition()) > 20) { Message(Chat::Red, "Attempt to pickpocket out of range detected."); - database.SetMQDetectionFlag(AccountName(), GetName(), "OP_PickPocket was sent from outside combat range.", zone->GetShortName()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "OP_PickPocket was sent from outside combat range"}); } else if (victim->IsNPC()) { auto body = victim->GetBodyType(); @@ -12696,16 +12746,27 @@ void Client::Handle_OP_RezzAnswer(const EQApplicationPacket *app) { VERIFY_PACKET_LENGTH(OP_RezzAnswer, app, Resurrect_Struct); - const Resurrect_Struct* ra = (const Resurrect_Struct*)app->pBuffer; + const auto* r = (const Resurrect_Struct*) app->pBuffer; - LogSpells("[Client::Handle_OP_RezzAnswer] Received OP_RezzAnswer from client. Pendingrezzexp is [{}] action is [{}]", - PendingRezzXP, ra->action ? "ACCEPT" : "DECLINE"); + LogSpells( + "[Client::Handle_OP_RezzAnswer] Received OP_RezzAnswer from client. Pendingrezzexp is [{}] action is [{}]", + PendingRezzXP, + r->action ? "ACCEPT" : "DECLINE" + ); - OPRezzAnswer(ra->action, ra->spellid, ra->zone_id, ra->instance_id, ra->x, ra->y, ra->z); + OPRezzAnswer(r->action, r->spellid, r->zone_id, r->instance_id, r->x, r->y, r->z); + + if (r->action == ResurrectionActions::Accept) { + if (player_event_logs.IsEventEnabled(PlayerEvent::REZ_ACCEPTED)) { + auto e = PlayerEvent::ResurrectAcceptEvent{ + .resurrecter_name = r->rezzer_name, + .spell_name = spells[r->spellid].name, + .spell_id = r->spellid, + }; + RecordPlayerEventLog(PlayerEvent::REZ_ACCEPTED, e); + } - if (ra->action == 1) - { EQApplicationPacket* outapp = app->Copy(); // Send the OP_RezzComplete to the world server. This finds it's way to the zone that // the rezzed corpse is in to mark the corpse as rezzed. @@ -12713,7 +12774,6 @@ void Client::Handle_OP_RezzAnswer(const EQApplicationPacket *app) worldserver.RezzPlayer(outapp, 0, 0, OP_RezzComplete); safe_delete(outapp); } - return; } void Client::Handle_OP_Sacrifice(const EQApplicationPacket *app) @@ -13236,10 +13296,19 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) if (!TakeMoneyFromPP(mpo->price)) { - auto hacker_str = fmt::format("Vendor Cheat: attempted to buy {} of {}: {} that cost {} cp but only has {} pp {} gp {} sp {} cp", - mpo->quantity, item->ID, item->Name, - mpo->price, m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); - database.SetMQDetectionFlag(AccountName(), GetName(), Strings::Escape(hacker_str), zone->GetShortName()); + auto message = fmt::format( + "Vendor Cheat attempted to buy qty [{}] of item_id [{}] item_name[{}] that cost [{}] copper but only has platinum [{}] gold [{}] silver [{}] copper [{}]", + mpo->quantity, + item->ID, + item->Name, + mpo->price, + m_pp.platinum, + m_pp.gold, + m_pp.silver, + m_pp.copper + ); + + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); safe_delete(outapp); safe_delete(inst); return; @@ -13304,6 +13373,23 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) safe_delete(inst); safe_delete(outapp); + if (player_event_logs.IsEventEnabled(PlayerEvent::MERCHANT_PURCHASE)) { + auto e = PlayerEvent::MerchantPurchaseEvent{ + .npc_id = tmp->GetNPCTypeID(), + .merchant_name = tmp->GetCleanName(), + .merchant_type = tmp->CastToNPC()->MerchantType, + .item_id = item->ID, + .item_name = item->Name, + .charges = static_cast(mpo->quantity), + .cost = mpo->price, + .alternate_currency_id = 0, + .player_money_balance = GetCarriedMoney(), + .player_currency_balance = 0, + }; + + RecordPlayerEventLog(PlayerEvent::MERCHANT_PURCHASE, e); + } + // start QS code // stacking purchases not supported at this time - entire process will need some work to catch them properly if (RuleB(QueryServ, PlayerLogMerchantTransactions)) { @@ -13359,9 +13445,6 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) } // end QS code - if (RuleB(EventLog, RecordBuyFromMerchant)) - LogMerchant(this, tmp, mpo->quantity, mpo->price, item, true); - const auto& export_string = fmt::format( "{} {} {} {} {}", tmp->GetNPCTypeID(), @@ -13372,10 +13455,24 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) ); parse->EventPlayer(EVENT_MERCHANT_BUY, this, export_string, 0); - if ((RuleB(Character, EnableDiscoveredItems))) - { - if (!GetGM() && !IsDiscovered(item_id)) - DiscoverItem(item_id); + if (player_event_logs.IsEventEnabled(PlayerEvent::MERCHANT_PURCHASE)) { + auto e = PlayerEvent::MerchantPurchaseEvent{ + .npc_id = tmp->GetNPCTypeID(), + .merchant_name = tmp->GetCleanName(), + .merchant_type = tmp->CastToNPC()->MerchantType, + .item_id = item->ID, + .item_name = item->Name, + .charges = static_cast(mpo->quantity), + .cost = mpo->price, + .alternate_currency_id = 0, + .player_money_balance = GetCarriedMoney(), + .player_currency_balance = 0, + }; + RecordPlayerEventLog(PlayerEvent::MERCHANT_PURCHASE, e); + } + + if (RuleB(Character, EnableDiscoveredItems) && !GetGM() && !IsDiscovered(item_id)) { + DiscoverItem(item_id); } t1.stop(); @@ -13460,9 +13557,6 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) else mp->quantity = 1; - if (RuleB(EventLog, RecordSellToMerchant)) - LogMerchant(this, vendor, mp->quantity, price, item, false); - int charges = mp->quantity; if (vendor->GetKeepsSoldItems()) { @@ -13475,7 +13569,7 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) charges, true ) - ) > 0) { + ) > 0) { EQ::ItemInstance *inst2 = inst->Clone(); while (true) { @@ -13557,6 +13651,22 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) ); parse->EventPlayer(EVENT_MERCHANT_SELL, this, export_string, 0); + if (player_event_logs.IsEventEnabled(PlayerEvent::MERCHANT_SELL)) { + auto e = PlayerEvent::MerchantSellEvent{ + .npc_id = vendor->GetNPCTypeID(), + .merchant_name = vendor->GetCleanName(), + .merchant_type = vendor->CastToNPC()->MerchantType, + .item_id = item->ID, + .item_name = item->Name, + .charges = static_cast(mp->quantity), + .cost = price, + .alternate_currency_id = 0, + .player_money_balance = GetCarriedMoney(), + .player_currency_balance = 0, + }; + + RecordPlayerEventLog(PlayerEvent::MERCHANT_SELL, e); + } // Now remove the item from the player, this happens regardless of outcome DeleteItemInInventory( @@ -13747,8 +13857,8 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app) { if (ClientVersion() < EQ::versions::ClientVersion::SoF) { - auto hack_str = fmt::format("Player sent OP_SpawnAppearance with AT_Invis: {}", sa->parameter); - database.SetMQDetectionFlag(account_name, name, hack_str, zone->GetShortName()); + auto message = fmt::format("Player sent OP_SpawnAppearance with AT_Invis [{}]", sa->parameter); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } } return; @@ -13847,8 +13957,8 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app) { if (!HasSkill(EQ::skills::SkillSneak)) { - auto hack_str = fmt::format("Player sent OP_SpawnAppearance with AT_Sneak: {}", sa->parameter); - database.SetMQDetectionFlag(account_name, name, hack_str, zone->GetShortName()); + auto message = fmt::format("Player sent OP_SpawnAppearance with AT_Sneak [{}]", sa->parameter); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } return; } @@ -13857,8 +13967,8 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app) } else if (sa->type == AT_Size) { - auto hack_str = fmt::format("Player sent OP_SpawnAppearance with AT_Size: {}", sa->parameter); - database.SetMQDetectionFlag(account_name, name, hack_str, zone->GetShortName()); + auto message = fmt::format("Player sent OP_SpawnAppearance with AT_Size [{}]", sa->parameter); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } else if (sa->type == AT_Light) // client emitting light (lightstone, shiny shield) { @@ -14201,9 +14311,14 @@ void Client::Handle_OP_TargetCommand(const EQApplicationPacket *app) else if (GetTarget()->GetBodyType() == BT_NoTarget2 || GetTarget()->GetBodyType() == BT_Special || GetTarget()->GetBodyType() == BT_NoTarget) { - auto hacker_str = fmt::format("{} attempting to target something untargetable, {} bodytype: {}", - GetName(), GetTarget()->GetName(), (int)GetTarget()->GetBodyType()); - database.SetMQDetectionFlag(AccountName(), GetName(), Strings::Escape(hacker_str), zone->GetShortName()); + auto message = fmt::format( + "[{}] attempting to target something untargetable [{}] bodytype [{}]", + GetName(), + GetTarget()->GetName(), + (int) GetTarget()->GetBodyType() + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + SetTarget((Mob*)nullptr); return; } @@ -14232,15 +14347,16 @@ void Client::Handle_OP_TargetCommand(const EQApplicationPacket *app) { if (DistanceSquared(m_Position, GetTarget()->GetPosition()) > (zone->newzone_data.maxclip*zone->newzone_data.maxclip)) { - auto hacker_str = fmt::format( - "{} attempting to target something beyond the clip plane of {:.2f} " + auto message = fmt::format( + "[{}] attempting to target something beyond the clip plane of {:.2f} " "units, from ({:.2f}, {:.2f}, {:.2f}) to {} ({:.2f}, {:.2f}, " "{:.2f})", GetName(), (zone->newzone_data.maxclip * zone->newzone_data.maxclip), GetX(), GetY(), GetZ(), GetTarget()->GetName(), GetTarget()->GetX(), GetTarget()->GetY(), GetTarget()->GetZ()); - database.SetMQDetectionFlag(AccountName(), GetName(), Strings::Escape(hacker_str), zone->GetShortName()); + + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); SetTarget(nullptr); return; } @@ -14248,13 +14364,21 @@ void Client::Handle_OP_TargetCommand(const EQApplicationPacket *app) } else if (DistanceSquared(m_Position, GetTarget()->GetPosition()) > (zone->newzone_data.maxclip*zone->newzone_data.maxclip)) { - auto hacker_str = - fmt::format("{} attempting to target something beyond the clip plane of {:.2f} " - "units, from ({:.2f}, {:.2f}, {:.2f}) to {} ({:.2f}, {:.2f}, {:.2f})", - GetName(), (zone->newzone_data.maxclip * zone->newzone_data.maxclip), - GetX(), GetY(), GetZ(), GetTarget()->GetName(), GetTarget()->GetX(), - GetTarget()->GetY(), GetTarget()->GetZ()); - database.SetMQDetectionFlag(AccountName(), GetName(), Strings::Escape(hacker_str), zone->GetShortName()); + auto message = fmt::format( + "{} attempting to target something beyond the clip plane of {:.2f} " + "units, from ({:.2f}, {:.2f}, {:.2f}) to {} ({:.2f}, {:.2f}, {:.2f})", + GetName(), + (zone->newzone_data.maxclip * zone->newzone_data.maxclip), + GetX(), + GetY(), + GetZ(), + GetTarget()->GetName(), + GetTarget()->GetX(), + GetTarget()->GetY(), + GetTarget()->GetZ() + ); + + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); SetTarget(nullptr); return; } @@ -14411,16 +14535,14 @@ void Client::Handle_OP_TradeAcceptClick(const EQApplicationPacket *app) // TODO: query (other) as a hacker } else { - // Audit trade to database for both trade streams - other->trade->LogTrade(); - trade->LogTrade(); + other->PlayerTradeEventLog(other->trade, trade); // start QS code if (RuleB(QueryServ, PlayerLogTrades)) { - QSPlayerLogTrade_Struct event_entry; + PlayerLogTrade_Struct event_entry; std::list event_details; - memset(&event_entry, 0, sizeof(QSPlayerLogTrade_Struct)); + memset(&event_entry, 0, sizeof(PlayerLogTrade_Struct)); // Perform actual trade FinishTrade(other, true, &event_entry, &event_details); @@ -14430,18 +14552,18 @@ void Client::Handle_OP_TradeAcceptClick(const EQApplicationPacket *app) auto qs_pack = new ServerPacket( ServerOP_QSPlayerLogTrades, - sizeof(QSPlayerLogTrade_Struct) + - (sizeof(QSTradeItems_Struct) * event_entry._detail_count)); - QSPlayerLogTrade_Struct* qs_buf = (QSPlayerLogTrade_Struct*)qs_pack->pBuffer; + sizeof(PlayerLogTrade_Struct) + + (sizeof(PlayerLogTradeItemsEntry_Struct) * event_entry._detail_count)); + PlayerLogTrade_Struct* qs_buf = (PlayerLogTrade_Struct*)qs_pack->pBuffer; - memcpy(qs_buf, &event_entry, sizeof(QSPlayerLogTrade_Struct)); + memcpy(qs_buf, &event_entry, sizeof(PlayerLogTrade_Struct)); int offset = 0; for (auto iter = event_details.begin(); iter != event_details.end(); ++iter, ++offset) { - QSTradeItems_Struct* detail = reinterpret_cast(*iter); - qs_buf->items[offset] = *detail; + PlayerLogTradeItemsEntry_Struct* detail = reinterpret_cast(*iter); + qs_buf->item_entries[offset] = *detail; safe_delete(detail); } @@ -15767,3 +15889,32 @@ void Client::SendMobPositions() } safe_delete(p); } + +struct RecordKillCheck { + PlayerEvent::EventType event; + bool check; +}; + +void Client::RecordKilledNPCEvent(NPC *n) +{ + bool is_named = Strings::Contains(n->GetName(), "#") && !n->IsRaidTarget(); + + std::vector checks = { + RecordKillCheck{.event = PlayerEvent::KILLED_NPC, .check = true}, + RecordKillCheck{.event = PlayerEvent::KILLED_NAMED_NPC, .check = is_named}, + RecordKillCheck{.event = PlayerEvent::KILLED_RAID_NPC, .check = n->IsRaidTarget()}, + }; + + for (auto &c: checks) { + if (c.check && player_event_logs.IsEventEnabled(c.event)) { + auto e = PlayerEvent::KilledNPCEvent{ + .npc_id = n->GetNPCTypeID(), + .npc_name = n->GetCleanName(), + .combat_time_seconds = static_cast(n->GetCombatRecord().TimeInCombat()), + .total_damage_per_second_taken = static_cast(n->GetCombatRecord().GetDamageReceivedPerSecond()), + .total_heal_per_second_taken = static_cast(n->GetCombatRecord().GetHealedReceivedPerSecond()), + }; + RecordPlayerEventLog(c.event, e); + } + } +} diff --git a/zone/client_process.cpp b/zone/client_process.cpp index c6ff4e02f..7f6ed7302 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -55,6 +55,7 @@ #include "zone.h" #include "zonedb.h" #include "../common/zone_store.h" +#include "../common/events/player_event_logs.h" extern QueryServ* QServ; extern Zone* zone; @@ -184,6 +185,7 @@ bool Client::Process() { SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Offline); parse->EventPlayer(EVENT_DISCONNECT, this, "", 0); + RecordPlayerEventLog(PlayerEvent::WENT_OFFLINE, PlayerEvent::EmptyEvent{}); return false; //delete client } @@ -542,7 +544,7 @@ bool Client::Process() { if (client_state == DISCONNECTED) { OnDisconnect(true); std::cout << "Client disconnected (cs=d): " << GetName() << std::endl; - database.SetMQDetectionFlag(AccountName(), GetName(), "/MQInstantCamp: Possible instant camp disconnect.", zone->GetShortName()); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "/MQInstantCamp: Possible instant camp disconnect"}); return false; } @@ -693,6 +695,7 @@ void Client::OnDisconnect(bool hard_disconnect) { MyRaid->MemberZoned(this); parse->EventPlayer(EVENT_DISCONNECT, this, "", 0); + RecordPlayerEventLog(PlayerEvent::WENT_OFFLINE, PlayerEvent::EmptyEvent{}); /* QS: PlayerLogConnectDisconnect */ if (RuleB(QueryServ, PlayerLogConnectDisconnect)){ @@ -1155,12 +1158,8 @@ void Client::OPMemorizeSpell(const EQApplicationPacket* app) if (HasSpellScribed(m->spell_id)) { MemSpell(m->spell_id, m->slot); } else { - database.SetMQDetectionFlag( - AccountName(), - GetName(), - "OP_MemorizeSpell but we don't have this spell scribed...", - zone->GetShortName() - ); + std::string message = fmt::format("OP_MemorizeSpell [{}] but we don't have this spell scribed", m->spell_id); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } break; } @@ -1299,10 +1298,13 @@ void Client::OPMoveCoin(const EQApplicationPacket* app) NPC *banker = entity_list.GetClosestBanker(this, distance); if(!banker || distance > USE_NPC_RANGE2) { - auto hacked_string = fmt::format("Player tried to make use of a banker(coin move) but " - "{} is non-existant or too far away ({} units).", - banker ? banker->GetName() : "UNKNOWN NPC", distance); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + auto message = fmt::format( + "Player tried to make use of a banker (coin move) but " + "banker [{}] is non-existent or too far away [{}] units", + banker ? banker->GetName() : "UNKNOWN NPC", distance + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + return; } @@ -1330,11 +1332,13 @@ void Client::OPMoveCoin(const EQApplicationPacket* app) NPC *banker = entity_list.GetClosestBanker(this, distance); if(!banker || distance > USE_NPC_RANGE2) { - auto hacked_string = - fmt::format("Player tried to make use of a banker(shared coin move) but {} is " - "non-existant or too far away ({} units).", - banker ? banker->GetName() : "UNKNOWN NPC", distance); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + auto message = fmt::format( + "Player tried to make use of a banker (shared coin move) but banker [{}] is " + "non-existent or too far away [{}] units", + banker ? banker->GetName() : "UNKNOWN NPC", distance + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + return; } if(mc->cointype1 == COINTYPE_PP) // there's only platinum here @@ -1386,10 +1390,13 @@ void Client::OPMoveCoin(const EQApplicationPacket* app) NPC *banker = entity_list.GetClosestBanker(this, distance); if(!banker || distance > USE_NPC_RANGE2) { - auto hacked_string = fmt::format("Player tried to make use of a banker(coin move) but " - "{} is non-existant or too far away ({} units).", - banker ? banker->GetName() : "UNKNOWN NPC", distance); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + auto message = fmt::format( + "Player tried to make use of a banker(coin move) but " + "banker [{}] is non-existent or too far away [{}] units", + banker ? banker->GetName() : "UNKNOWN NPC", distance + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + return; } switch(mc->cointype2) @@ -1429,11 +1436,13 @@ void Client::OPMoveCoin(const EQApplicationPacket* app) NPC *banker = entity_list.GetClosestBanker(this, distance); if(!banker || distance > USE_NPC_RANGE2) { - auto hacked_string = - fmt::format("Player tried to make use of a banker(shared coin move) but {} is " - "non-existant or too far away ({} units).", - banker ? banker->GetName() : "UNKNOWN NPC", distance); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + auto message = fmt::format( + "Player tried to make use of a banker (shared coin move) but banker [{}] is " + "non-existent or too far away [{}] units", + banker ? banker->GetName() : "UNKNOWN NPC", distance + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + return; } if(mc->cointype2 == COINTYPE_PP) // there's only platinum here diff --git a/zone/combat_record.cpp b/zone/combat_record.cpp index 8d746b52d..814c2d256 100644 --- a/zone/combat_record.cpp +++ b/zone/combat_record.cpp @@ -4,50 +4,51 @@ void CombatRecord::Start(std::string in_mob_name) { - start_time = std::time(nullptr); - end_time = 0; - damage_received = 0; - heal_received = 0; - mob_name = in_mob_name; + m_start_time = std::time(nullptr); + m_end_time = 0; + m_damage_received = 0; + m_heal_received = 0; + m_mob_name = in_mob_name; } + void CombatRecord::Stop() { - end_time = std::time(nullptr); + m_end_time = std::time(nullptr); double time_in_combat = TimeInCombat(); LogCombatRecord( "[Summary] Mob [{}] [Received] DPS [{:.0f}] Heal/s [{:.0f}] Duration [{}] ({}s)", - mob_name, - time_in_combat > 0 ? (damage_received / time_in_combat) : damage_received, - time_in_combat > 0 ? (heal_received / time_in_combat) : heal_received, + m_mob_name, + GetDamageReceivedPerSecond(), + GetHealedReceivedPerSecond(), time_in_combat > 0 ? Strings::SecondsToTime(time_in_combat) : "", time_in_combat ); } -bool CombatRecord::InCombat() +bool CombatRecord::InCombat() const { - return start_time > 0; + return m_start_time > 0; } void CombatRecord::ProcessHPEvent(int64 hp, int64 current_hp) { // damage if (hp < current_hp) { - damage_received = damage_received + std::llabs(current_hp - hp); + m_damage_received = m_damage_received + std::llabs(current_hp - hp); } // heal if (hp > current_hp && current_hp > 0) { - heal_received = heal_received + std::llabs(current_hp - hp); + m_heal_received = m_heal_received + std::llabs(current_hp - hp); } LogCombatRecordDetail( "damage_received [{}] heal_received [{}] current_hp [{}] hp [{}] calc [{}]", - damage_received, - heal_received, + m_damage_received, + m_heal_received, current_hp, hp, std::llabs(current_hp - hp) @@ -56,5 +57,27 @@ void CombatRecord::ProcessHPEvent(int64 hp, int64 current_hp) double CombatRecord::TimeInCombat() const { - return difftime(end_time, start_time); + return m_end_time > m_start_time ? difftime(m_end_time, m_start_time) : 0; +} + +float CombatRecord::GetDamageReceivedPerSecond() const +{ + double time_in_combat = TimeInCombat(); + return time_in_combat > 0 ? (m_damage_received / time_in_combat) : m_damage_received; +} + +float CombatRecord::GetHealedReceivedPerSecond() const +{ + double time_in_combat = TimeInCombat(); + return time_in_combat > 0 ? (m_heal_received / time_in_combat) : m_heal_received; +} + +int64 CombatRecord::GetDamageReceived() const +{ + return m_damage_received; +} + +int64 CombatRecord::GetHealReceived() const +{ + return m_heal_received; } diff --git a/zone/combat_record.h b/zone/combat_record.h index 77e67c57f..eff98a42c 100644 --- a/zone/combat_record.h +++ b/zone/combat_record.h @@ -9,15 +9,19 @@ class CombatRecord { public: void Start(std::string in_mob_name); void Stop(); - bool InCombat(); + bool InCombat() const; void ProcessHPEvent(int64 hp, int64 current_hp); double TimeInCombat() const; + int64 GetDamageReceived() const; + int64 GetHealReceived() const; + float GetDamageReceivedPerSecond() const; + float GetHealedReceivedPerSecond() const; private: - std::string mob_name; - time_t start_time = 0; - time_t end_time = 0; - int64 damage_received = 0; - int64 heal_received = 0; + std::string m_mob_name; + time_t m_start_time = 0; + time_t m_end_time = 0; + int64 m_damage_received = 0; + int64 m_heal_received = 0; }; #endif //EQEMU_COMBAT_RECORD_H diff --git a/zone/command.cpp b/zone/command.cpp index e44e590d6..a27ea2c2f 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -36,6 +36,7 @@ #include "fastmath.h" #include "mob_movement_manager.h" #include "npc_scale_manager.h" +#include "../common/events/player_event_logs.h" extern QueryServ* QServ; extern WorldServer worldserver; @@ -551,8 +552,6 @@ int command_realdispatch(Client *c, std::string message, bool ignore_status) { Seperator sep(message.c_str(), ' ', 10, 100, true); // "three word argument" should be considered 1 arg - command_logcommand(c, message.c_str()); - std::string cstr(sep.arg[0] + 1); if (commandlist.count(cstr) != 1) { @@ -593,6 +592,15 @@ int command_realdispatch(Client *c, std::string message, bool ignore_status) parse->EventPlayer(EVENT_GM_COMMAND, c, message, 0); + if (player_event_logs.IsEventEnabled(PlayerEvent::GM_COMMAND) && message != "#help") { + auto e = PlayerEvent::GMCommandEvent{ + .message = message, + .target = c->GetTarget() ? c->GetTarget()->GetName() : "NONE" + }; + + RecordPlayerEventLogWithClient(c, PlayerEvent::GM_COMMAND, e); + } + cur->function(c, &sep); // Dispatch C++ Command return 0; @@ -1035,7 +1043,6 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/listpetition.cpp" #include "gm_commands/lootsim.cpp" #include "gm_commands/loc.cpp" -#include "gm_commands/logcommand.cpp" #include "gm_commands/logs.cpp" #include "gm_commands/makepet.cpp" #include "gm_commands/mana.cpp" diff --git a/zone/command.h b/zone/command.h index 452579ffa..079a15280 100644 --- a/zone/command.h +++ b/zone/command.h @@ -26,7 +26,6 @@ void command_deinit(void); int command_add(std::string command_name, std::string description, uint8 admin, CmdFuncPtr function); int command_notavail(Client *c, std::string message, bool ignore_status); int command_realdispatch(Client *c, std::string message, bool ignore_status); -void command_logcommand(Client *c, std::string message); uint8 GetCommandStatus(Client *c, std::string command_name); void ListModifyNPCStatMap(Client *c); std::map GetModifyNPCStatMap(); diff --git a/zone/common.h b/zone/common.h index 798795130..08c9e078c 100644 --- a/zone/common.h +++ b/zone/common.h @@ -848,11 +848,6 @@ public: // Add item from cursor slot to trade bucket (automatically does bag data too) void AddEntity(uint16 trade_slot_id, uint32 stack_size); - // Audit trade - void LogTrade(); - - void DumpTrade(); - public: // Object state @@ -868,6 +863,8 @@ private: uint32 with_id; Mob* owner; +public: + Mob *GetOwner() const; }; struct ExtraAttackOptions { diff --git a/zone/corpse.cpp b/zone/corpse.cpp index 66cb21d93..5e4b042d6 100644 --- a/zone/corpse.cpp +++ b/zone/corpse.cpp @@ -49,6 +49,7 @@ Child of the Mob class. #include "quest_parser_collection.h" #include "string_ids.h" #include "worldserver.h" +#include "../common/events/player_event_logs.h" #include @@ -1443,6 +1444,18 @@ void Corpse::LootItem(Client *client, const EQApplicationPacket *app) prevent_loot = true; } + if (player_event_logs.IsEventEnabled(PlayerEvent::LOOT_ITEM) && !IsPlayerCorpse()) { + auto e = PlayerEvent::LootItemEvent{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = inst->GetCharges(), + .npc_id = GetNPCTypeID(), + .corpse_name = EntityList::RemoveNumbers(corpse_name) + }; + + RecordPlayerEventLogWithClient(client, PlayerEvent::LOOT_ITEM, e); + } + if (!IsPlayerCorpse()) { // dynamic zones may prevent looting by non-members or based on lockouts @@ -1471,9 +1484,14 @@ void Corpse::LootItem(Client *client, const EQApplicationPacket *app) // safe to ACK now client->QueuePacket(app); - if (!IsPlayerCorpse() && RuleB(Character, EnableDiscoveredItems)) { - if (client && !client->GetGM() && !client->IsDiscovered(inst->GetItem()->ID)) - client->DiscoverItem(inst->GetItem()->ID); + if ( + !IsPlayerCorpse() && + RuleB(Character, EnableDiscoveredItems) && + client && + !client->GetGM() && + !client->IsDiscovered(inst->GetItem()->ID) + ) { + client->DiscoverItem(inst->GetItem()->ID); } if (zone->adv_data) { diff --git a/zone/embparser_api.cpp b/zone/embparser_api.cpp index 1342ddf54..30b681052 100644 --- a/zone/embparser_api.cpp +++ b/zone/embparser_api.cpp @@ -4091,6 +4091,11 @@ int8 Perl__GetRecipeSuccessCount(uint32 recipe_id, uint32 item_id) return content_db.GetRecipeComponentCount(RecipeCountType::Success, recipe_id, item_id); } +void Perl__send_player_handin_event() +{ + quest_manager.SendPlayerHandinEvent(); +} + void perl_register_quest() { perl::interpreter perl(PERL_GET_THX); @@ -4632,6 +4637,7 @@ void perl_register_quest() package.add("scribespells", (int(*)(int, int))&Perl__scribespells); package.add("secondstotime", &Perl__secondstotime); package.add("selfcast", &Perl__selfcast); + package.add("send_player_handin_event", &Perl__send_player_handin_event); package.add("setaaexpmodifierbycharid", (void(*)(uint32, uint32, double))&Perl__setaaexpmodifierbycharid); package.add("setaaexpmodifierbycharid", (void(*)(uint32, uint32, double, int16))&Perl__setaaexpmodifierbycharid); package.add("set_proximity", (void(*)(float, float, float, float))&Perl__set_proximity); diff --git a/zone/embperl.h b/zone/embperl.h index d639a4311..a728eecd6 100644 --- a/zone/embperl.h +++ b/zone/embperl.h @@ -25,6 +25,8 @@ Eglin #include namespace perl = perlbind; +#undef Null + #ifdef WIN32 #define snprintf _snprintf #endif diff --git a/zone/exp.cpp b/zone/exp.cpp index 27d7987dd..d902bec4e 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -34,6 +34,10 @@ #include "../common/data_verification.h" #include "bot.h" +#include "../common/events/player_event_logs.h" +#include "worldserver.h" + +extern WorldServer worldserver; extern QueryServ* QServ; @@ -728,6 +732,8 @@ void Client::SetEXP(uint64 set_exp, uint64 set_aaxp, bool isrezzexp) { parse->EventPlayer(EVENT_AA_GAIN, this, export_string, 0); + RecordPlayerEventLog(PlayerEvent::AA_GAIN, PlayerEvent::AAGainedEvent{gained}); + /* QS: PlayerLogAARate */ if (RuleB(QueryServ, PlayerLogAARate)){ int add_points = (m_pp.aapoints - last_unspentAA); @@ -867,8 +873,19 @@ void Client::SetLevel(uint8 set_level, bool command) } if (set_level > m_pp.level) { - const auto export_string = fmt::format("{}", (set_level - m_pp.level)); + int levels_gained = (set_level - m_pp.level); + const auto export_string = fmt::format("{}", levels_gained); parse->EventPlayer(EVENT_LEVEL_UP, this, export_string, 0); + if (player_event_logs.IsEventEnabled(PlayerEvent::LEVEL_GAIN)) { + auto e = PlayerEvent::LevelGainedEvent{ + .from_level = m_pp.level, + .to_level = set_level, + .levels_gained = levels_gained + }; + + RecordPlayerEventLog(PlayerEvent::LEVEL_GAIN, e); + } + if (RuleB(QueryServ, PlayerLogLevels)) { const auto event_desc = fmt::format( @@ -881,8 +898,18 @@ void Client::SetLevel(uint8 set_level, bool command) QServ->PlayerLogEvent(Player_Log_Levels, CharacterID(), event_desc); } } else if (set_level < m_pp.level) { - const auto export_string = fmt::format("{}", (m_pp.level - set_level)); + int levels_lost = (m_pp.level - set_level); + const auto export_string = fmt::format("{}", levels_lost); parse->EventPlayer(EVENT_LEVEL_DOWN, this, export_string, 0); + if (player_event_logs.IsEventEnabled(PlayerEvent::LEVEL_LOSS)) { + auto e = PlayerEvent::LevelLostEvent{ + .from_level = m_pp.level, + .to_level = set_level, + .levels_lost = levels_lost + }; + + RecordPlayerEventLog(PlayerEvent::LEVEL_LOSS, e); + } if (RuleB(QueryServ, PlayerLogLevels)) { const auto event_desc = fmt::format( diff --git a/zone/expedition.h b/zone/expedition.h index d9d47a34e..1379b724d 100644 --- a/zone/expedition.h +++ b/zone/expedition.h @@ -29,6 +29,7 @@ #include #include #include +#include class Client; class EQApplicationPacket; diff --git a/zone/forage.cpp b/zone/forage.cpp index 21074b4a4..ce5e05fed 100644 --- a/zone/forage.cpp +++ b/zone/forage.cpp @@ -32,6 +32,10 @@ #include "zonedb.h" #include "../common/zone_store.h" #include "../common/repositories/criteria/content_filter_criteria.h" +#include "../common/events/player_event_logs.h" +#include "worldserver.h" + +extern WorldServer worldserver; #include @@ -377,6 +381,15 @@ void Client::GoFish() std::vector args; args.push_back(inst); parse->EventPlayer(EVENT_FISH_SUCCESS, this, "", inst->GetID(), &args); + + if (player_event_logs.IsEventEnabled(PlayerEvent::FISH_SUCCESS)) { + auto e = PlayerEvent::FishSuccessEvent{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + }; + + RecordPlayerEventLog(PlayerEvent::FISH_SUCCESS, e); + } } } } @@ -396,6 +409,7 @@ void Client::GoFish() } parse->EventPlayer(EVENT_FISH_FAILURE, this, "", 0); + RecordPlayerEventLog(PlayerEvent::FISH_FAILURE, PlayerEvent::EmptyEvent{}); } //chance to break fishing pole... @@ -497,6 +511,14 @@ void Client::ForageItem(bool guarantee) { std::vector args; args.push_back(inst); parse->EventPlayer(EVENT_FORAGE_SUCCESS, this, "", inst->GetID(), &args); + + if (player_event_logs.IsEventEnabled(PlayerEvent::FORAGE_SUCCESS)) { + auto e = PlayerEvent::ForageSuccessEvent{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name + }; + RecordPlayerEventLog(PlayerEvent::FORAGE_SUCCESS, e); + } } } @@ -508,6 +530,7 @@ void Client::ForageItem(bool guarantee) { } else { MessageString(Chat::Skills, FORAGE_FAILED); parse->EventPlayer(EVENT_FORAGE_FAILURE, this, "", 0); + RecordPlayerEventLog(PlayerEvent::FORAGE_FAILURE, PlayerEvent::EmptyEvent{}); } CheckIncreaseSkill(EQ::skills::SkillForage, nullptr, 5); diff --git a/zone/gm_commands/logcommand.cpp b/zone/gm_commands/logcommand.cpp deleted file mode 100755 index 33fe83516..000000000 --- a/zone/gm_commands/logcommand.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "../client.h" - -void command_logcommand(Client *c, std::string message) -{ - int admin = c->Admin(); - - bool log = false; - switch (zone->loglevelvar) { //catch failsafe - case 9: { // log only LeadGM - if ( - admin >= AccountStatus::GMLeadAdmin && - admin < AccountStatus::GMMgmt - ) { - log = true; - } - - break; - } - case 8: { // log only GM - if ( - admin >= AccountStatus::GMAdmin && - admin < AccountStatus::GMLeadAdmin - ) { - log = true; - } - - break; - } - case 1: { - if (admin >= AccountStatus::GMMgmt) { - log = true; - } - - break; - } - case 2: { - if (admin >= AccountStatus::GMLeadAdmin) { - log = true; - } - - break; - } - case 3: { - if (admin >= AccountStatus::GMAdmin) { - log = true; - } - - break; - } - case 4: { - if (admin >= AccountStatus::QuestTroupe) { - log = true; - } - - break; - } - case 5: { - if (admin >= AccountStatus::ApprenticeGuide) { - log = true; - } - - break; - } - case 6: { - if (admin >= AccountStatus::Steward) { - log = true; - } - - break; - } - case 7: { - log = true; - break; - } - } - - if (log) { - database.logevents( - c->AccountName(), - c->AccountID(), - admin, - c->GetName(), - c->GetTarget() ? c->GetTarget()->GetName() : "None", - "Command", - message.c_str(), - 1 - ); - } -} diff --git a/zone/groups.cpp b/zone/groups.cpp index 910d5b21c..01d7c58bb 100644 --- a/zone/groups.cpp +++ b/zone/groups.cpp @@ -26,6 +26,7 @@ #include "../common/strings.h" #include "worldserver.h" #include "string_ids.h" +#include "../common/events/player_event_logs.h" extern EntityList entity_list; extern WorldServer worldserver; @@ -177,6 +178,18 @@ void Group::SplitMoney(uint32 copper, uint32 silver, uint32 gold, uint32 platinu true ); + if (player_event_logs.IsEventEnabled(PlayerEvent::SPLIT_MONEY)) { + auto e = PlayerEvent::SplitMoneyEvent{ + .copper = copper_split, + .silver = silver_split, + .gold = gold_split, + .platinum = platinum_split, + .player_money_balance = members[i]->CastToClient()->GetCarriedMoney(), + }; + + RecordPlayerEventLogWithClient(members[i]->CastToClient(), PlayerEvent::SPLIT_MONEY, e); + } + members[i]->CastToClient()->MessageString( Chat::MoneySplit, YOU_RECEIVE_AS_SPLIT, diff --git a/zone/inventory.cpp b/zone/inventory.cpp index 3b582ea4f..56dc64609 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -24,8 +24,10 @@ #include "worldserver.h" #include "zonedb.h" #include "../common/zone_store.h" +#include "../common/events/player_event_logs.h" #include "bot.h" +#include "../common/events/player_event_logs.h" extern WorldServer worldserver; @@ -803,17 +805,12 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, safe_delete(inst); // discover item and any augments - if((RuleB(Character, EnableDiscoveredItems)) && !GetGM()) { - if(!IsDiscovered(item_id)) { - DiscoverItem(item_id); - } - /* - // Augments should have been discovered prior to being placed on an item. - for (int iter = AUG_BEGIN; iter < EQ::constants::ITEM_COMMON_SIZE; ++iter) { - if(augments[iter] && !IsDiscovered(augments[iter])) - DiscoverItem(augments[iter]); - } - */ + if ( + RuleB(Character, EnableDiscoveredItems) && + !GetGM() && + !IsDiscovered(item_id) + ) { + DiscoverItem(item_id); } return true; @@ -822,11 +819,14 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, // Drop item from inventory to ground (generally only dropped from SLOT_CURSOR) void Client::DropItem(int16 slot_id, bool recurse) { - LogInventory("[{}] (char_id: [{}]) Attempting to drop item from slot [{}] on the ground", - GetCleanName(), CharacterID(), slot_id); + LogInventory( + "[{}] (char_id: [{}]) Attempting to drop item from slot [{}] on the ground", + GetCleanName(), + CharacterID(), + slot_id + ); - if(GetInv().CheckNoDrop(slot_id, recurse) && !CanTradeFVNoDropItem()) - { + if (GetInv().CheckNoDrop(slot_id, recurse) && !CanTradeFVNoDropItem()) { auto invalid_drop = m_inv.GetItem(slot_id); if (!invalid_drop) { LogInventory("Error in InventoryProfile::CheckNoDrop() - returned 'true' for empty slot"); @@ -850,48 +850,84 @@ void Client::DropItem(int16 slot_id, bool recurse) } invalid_drop = nullptr; - database.SetHackerFlag(AccountName(), GetCleanName(), "Tried to drop an item on the ground that was nodrop!"); + std::string message = fmt::format( + "Tried to drop an item on the ground that was no-drop! item_name [{}] item_id ({})", + invalid_drop->GetItem()->Name, + invalid_drop->GetItem()->ID + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); GetInv().DeleteItem(slot_id); return; } // Take control of item in client inventory - EQ::ItemInstance *inst = m_inv.PopItem(slot_id); - if(inst) { + auto* inst = m_inv.PopItem(slot_id); + if (inst) { if (LogSys.log_settings[Logs::Inventory].is_category_enabled) { LogInventory("DropItem() Processing - full item parse:"); - LogInventory("depth: 0, Item: [{}] (id: [{}]), IsDroppable: [{}]", - (inst->GetItem() ? inst->GetItem()->Name : "null data"), inst->GetID(), (inst->IsDroppable(false) ? "true" : "false")); + LogInventory( + "depth: 0, Item: [{}] (id: [{}]), IsDroppable: [{}]", + (inst->GetItem() ? inst->GetItem()->Name : "null data"), + inst->GetID(), + (inst->IsDroppable(false) ? "true" : "false") + ); - if (!inst->IsDroppable(false)) + if (!inst->IsDroppable(false)) { LogError("Non-droppable item being processed for drop by [{}]", GetCleanName()); + } for (auto iter1 : *inst->GetContents()) { // depth 1 - LogInventory("-depth: 1, Item: [{}] (id: [{}]), IsDroppable: [{}]", - (iter1.second->GetItem() ? iter1.second->GetItem()->Name : "null data"), iter1.second->GetID(), (iter1.second->IsDroppable(false) ? "true" : "false")); + LogInventory( + "-depth: 1, Item: [{}] (id: [{}]), IsDroppable: [{}]", + (iter1.second->GetItem() ? iter1.second->GetItem()->Name : "null data"), + iter1.second->GetID(), + (iter1.second->IsDroppable(false) ? "true" : "false") + ); - if (!iter1.second->IsDroppable(false)) + if (!iter1.second->IsDroppable(false)) { LogError("Non-droppable item being processed for drop by [{}]", GetCleanName()); + } for (auto iter2 : *iter1.second->GetContents()) { // depth 2 - LogInventory("--depth: 2, Item: [{}] (id: [{}]), IsDroppable: [{}]", - (iter2.second->GetItem() ? iter2.second->GetItem()->Name : "null data"), iter2.second->GetID(), (iter2.second->IsDroppable(false) ? "true" : "false")); + LogInventory( + "--depth: 2, Item: [{}] (id: [{}]), IsDroppable: [{}]", + (iter2.second->GetItem() ? iter2.second->GetItem()->Name : "null data"), + iter2.second->GetID(), + (iter2.second->IsDroppable(false) ? "true" : "false") + ); - if (!iter2.second->IsDroppable(false)) + if (!iter2.second->IsDroppable(false)) { LogError("Non-droppable item being processed for drop by [{}]", GetCleanName()); + } } } } + if (player_event_logs.IsEventEnabled(PlayerEvent::DROPPED_ITEM)) { + auto e = PlayerEvent::DroppedItemEvent{ + .item_id = inst->GetID(), + .item_name = inst->GetItem()->Name, + .slot_id = slot_id, + .charges = (uint32) inst->GetCharges() + }; + RecordPlayerEventLog(PlayerEvent::DROPPED_ITEM, e); + } + int i = parse->EventItem(EVENT_DROP_ITEM, this, inst, nullptr, "", slot_id); - if(i != 0) { + if (i != 0) { LogInventory("Item drop handled by [EVENT_DROP_ITEM]"); safe_delete(inst); } } else { // Item doesn't exist in inventory! LogInventory("DropItem() - No item found in slot [{}]", slot_id); - Message(Chat::Red, "Error: Item not found in slot %i", slot_id); + Message( + Chat::Red, + fmt::format( + "Error: Item not found in slot {}.", + slot_id + ).c_str() + ); return; } @@ -904,15 +940,16 @@ void Client::DropItem(int16 slot_id, bool recurse) database.SaveInventory(CharacterID(), nullptr, slot_id); } - if(!inst) + if (!inst) { return; + } // Package as zone object auto object = new Object(this, inst); entity_list.AddObject(object, true); object->StartDecay(); - LogInventory("Item drop handled ut assolet"); + LogInventory("[{}] dropped [{}] from slot [{}]", GetCleanName(), inst->GetItem()->Name, slot_id); DropItemQS(inst, false); safe_delete(inst); @@ -1822,6 +1859,18 @@ bool Client::SwapItem(MoveItem_Struct* move_in) { MessageString(Chat::Loot, 290); parse->EventItem(EVENT_DESTROY_ITEM, this, test_inst, nullptr, "", 0); DeleteItemInInventory(EQ::invslot::slotCursor, 0, true); + + if (player_event_logs.IsEventEnabled(PlayerEvent::ITEM_DESTROY)) { + auto e = PlayerEvent::DestroyItemEvent{ + .item_id = test_inst->GetItem()->ID, + .item_name = test_inst->GetItem()->Name, + .charges = test_inst->GetCharges(), + .reason = "Duplicate lore item", + }; + + RecordPlayerEventLog(PlayerEvent::ITEM_DESTROY, e); + } + } } return true; @@ -1835,6 +1884,16 @@ bool Client::SwapItem(MoveItem_Struct* move_in) { EQ::ItemInstance *inst = m_inv.GetItem(EQ::invslot::slotCursor); if(inst) { parse->EventItem(EVENT_DESTROY_ITEM, this, inst, nullptr, "", 0); + if (player_event_logs.IsEventEnabled(PlayerEvent::ITEM_DESTROY)) { + auto e = PlayerEvent::DestroyItemEvent{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = inst->GetCharges(), + .reason = "Client destroy cursor", + }; + + RecordPlayerEventLog(PlayerEvent::ITEM_DESTROY, e); + } } DeleteItemInInventory(move_in->from_slot); @@ -1867,10 +1926,13 @@ bool Client::SwapItem(MoveItem_Struct* move_in) { if(!banker || distance > USE_NPC_RANGE2) { - auto hacked_string = fmt::format("Player tried to make use of a banker(items) but {} is " - "non-existant or too far away ({} units).", - banker ? banker->GetName() : "UNKNOWN NPC", distance); - database.SetMQDetectionFlag(AccountName(), GetName(), hacked_string, zone->GetShortName()); + auto message = fmt::format( + "Player tried to make use of a banker (items) but banker [{}] is " + "non-existent or too far away [{}] units", + banker ? banker->GetName() : "UNKNOWN NPC", distance + ); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); + Kick("Inventory desync"); // Kicking player to avoid item loss do to client and server inventories not being sync'd return false; } diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index 0b4f5b035..6883ce518 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -3803,6 +3803,11 @@ int8 lua_get_recipe_success_count(uint32 recipe_id, uint32 item_id) return content_db.GetRecipeComponentCount(RecipeCountType::Success, recipe_id, item_id); } +void lua_send_player_handin_event() +{ + quest_manager.SendPlayerHandinEvent(); +} + #define LuaCreateNPCParse(name, c_type, default_value) do { \ cur = table[#name]; \ if(luabind::type(cur) != LUA_TNIL) { \ @@ -4331,6 +4336,7 @@ luabind::scope lua_register_general() { luabind::def("get_recipe_fail_count", (int8(*)(uint32,uint32))&lua_get_recipe_fail_count), luabind::def("get_recipe_salvage_count", (int8(*)(uint32,uint32))&lua_get_recipe_salvage_count), luabind::def("get_recipe_success_count", (int8(*)(uint32,uint32))&lua_get_recipe_success_count), + luabind::def("send_player_handin_event", (void(*)(void))&lua_send_player_handin_event), /* Cross Zone */ diff --git a/zone/lua_parser_events.cpp b/zone/lua_parser_events.cpp index 20fd3d76d..b4bc6eec4 100644 --- a/zone/lua_parser_events.cpp +++ b/zone/lua_parser_events.cpp @@ -59,7 +59,7 @@ void handle_npc_event_trade( uint32 extra_data, std::vector *extra_pointers ) { - Lua_Client l_client(reinterpret_cast(init)); + Lua_Client l_client(reinterpret_cast(init)); luabind::adl::object l_client_o = luabind::adl::object(L, l_client); l_client_o.push(L); lua_setfield(L, -2, "other"); @@ -105,6 +105,10 @@ void handle_npc_event_trade( lua_pushinteger(L, money_value); lua_setfield(L, -2, "copper"); + // set a reference to the client inside of the trade object as well for plugins to process + l_client_o.push(L); + lua_setfield(L, -2, "other"); + lua_setfield(L, -2, "trade"); } diff --git a/zone/main.cpp b/zone/main.cpp index d8f8ed1af..7aa9d3ce3 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -89,6 +89,7 @@ extern volatile bool is_zone_loaded; #include "zone_event_scheduler.h" #include "../common/file.h" #include "../common/path_manager.h" +#include "../common/events/player_event_logs.h" EntityList entity_list; WorldServer worldserver; @@ -107,6 +108,7 @@ EQEmuLogSys LogSys; ZoneEventScheduler event_scheduler; WorldContentService content_service; PathManager path; +PlayerEventLogs player_event_logs; const SPDat_Spell_Struct* spells; int32 SPDAT_RECORDS = -1; @@ -266,6 +268,8 @@ int main(int argc, char** argv) { ->SetGMSayHandler(&Zone::GMSayHookCallBackProcess) ->StartFileLogs(); + player_event_logs.SetDatabase(&database)->Init(); + /* Guilds */ guild_mgr.SetDatabase(&database); GuildBanks = nullptr; diff --git a/zone/mob.h b/zone/mob.h index 63c47d146..c1471d1db 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1741,7 +1741,9 @@ protected: bool spawned_in_water; bool is_boat; - CombatRecord combat_record{}; + CombatRecord m_combat_record{}; +public: + const CombatRecord &GetCombatRecord() const; public: bool GetWasSpawnedInWater() const; diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index 5cfdd9f15..3cba8a814 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -1929,7 +1929,7 @@ void Mob::AI_Event_Engaged(Mob *attacker, bool yell_for_help) } std::string mob_name = GetCleanName(); - combat_record.Start(mob_name); + m_combat_record.Start(mob_name); CastToNPC()->SetCombatEvent(true); } } @@ -1970,7 +1970,7 @@ void Mob::AI_Event_NoLongerEngaged() { CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::LeaveCombat, emoteid); } - combat_record.Stop(); + m_combat_record.Stop(); CastToNPC()->SetCombatEvent(false); } } diff --git a/zone/object.cpp b/zone/object.cpp index 1add7adff..276a8435e 100644 --- a/zone/object.cpp +++ b/zone/object.cpp @@ -25,9 +25,11 @@ #include "object.h" #include "quest_parser_collection.h" +#include "worldserver.h" #include "zonedb.h" #include "../common/zone_store.h" #include "../common/repositories/criteria/content_filter_criteria.h" +#include "../common/events/player_event_logs.h" #include @@ -37,6 +39,7 @@ const char DEFAULT_OBJECT_NAME_SUFFIX[] = "_ACTORDEF"; extern Zone* zone; extern EntityList entity_list; +extern WorldServer worldserver; // Loading object from database Object::Object(uint32 id, uint32 type, uint32 icon, const Object_Struct& object, const EQ::ItemInstance* inst) @@ -517,6 +520,14 @@ bool Object::HandleClick(Client* sender, const ClickObject_Struct* click_object) } } + if (player_event_logs.IsEventEnabled(PlayerEvent::GROUNDSPAWN_PICKUP)) { + auto e = PlayerEvent::GroundSpawnPickupEvent{ + .item_id = item->ID, + .item_name = item->Name, + }; + RecordPlayerEventLogWithClient(sender, PlayerEvent::GROUNDSPAWN_PICKUP, e); + } + std::string export_string = fmt::format("{}", item->ID); std::vector args; args.push_back(m_inst); @@ -542,12 +553,13 @@ bool Object::HandleClick(Client* sender, const ClickObject_Struct* click_object) sender->SendItemPacket(EQ::invslot::slotCursor, m_inst, ItemPacketTrade); // Could be an undiscovered ground_spawn - if (m_ground_spawn && (RuleB(Character, EnableDiscoveredItems))) - { - if (!sender->GetGM() && !sender->IsDiscovered(item->ID)) - { - sender->DiscoverItem(item->ID); - } + if ( + m_ground_spawn && + RuleB(Character, EnableDiscoveredItems) && + !sender->GetGM() && + !sender->IsDiscovered(item->ID) + ) { + sender->DiscoverItem(item->ID); } if(cursordelete) // delete the item if it's a duplicate lore. We have to do this because the client expects the item packet diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index 356951810..c84b74730 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -23,6 +23,7 @@ #include "../common/spdat.h" #include "../common/strings.h" #include "../common/say_link.h" +#include "../common/events/player_event_logs.h" #include "entity.h" #include "event_codes.h" @@ -2431,6 +2432,7 @@ bool QuestManager::createBot(const char *name, const char *lastname, uint8 level ); parse->EventPlayer(EVENT_BOT_CREATE, initiator, export_string, 0); + return true; } } @@ -2940,7 +2942,6 @@ std::string QuestManager::varlink( return linker.GenerateLink(); } - std::string QuestManager::getitemname(uint32 item_id) { const EQ::ItemData* item_data = database.GetItem(item_id); if (!item_data) { @@ -3986,3 +3987,171 @@ int8 QuestManager::DoesAugmentFit(EQ::ItemInstance* inst, uint32 augment_id, uin return inst->AvailableAugmentSlot(aug_inst->AugType); } + +void QuestManager::SendPlayerHandinEvent() { + QuestManagerCurrentQuestVars(); + if (!owner || !owner->IsNPC() || !initiator) { + return; + } + + if ( + !initiator->EntityVariableExists("HANDIN_ITEMS") && + !initiator->EntityVariableExists("HANDIN_MONEY") && + !initiator->EntityVariableExists("RETURN_ITEMS") && + !initiator->EntityVariableExists("RETURN_MONEY") + ) { + return; + } + + auto handin_items = initiator->GetEntityVariable("HANDIN_ITEMS"); + auto return_items = initiator->GetEntityVariable("RETURN_ITEMS"); + auto handin_money = initiator->GetEntityVariable("HANDIN_MONEY"); + auto return_money = initiator->GetEntityVariable("RETURN_MONEY"); + + std::vector hi = {}; + std::vector ri = {}; + PlayerEvent::HandinMoney hm{}; + PlayerEvent::HandinMoney rm{}; + + // Handin Items + if (!handin_items.empty()) { + if (Strings::Contains(handin_items, ",")) { + const auto handin_data = Strings::Split(handin_items, ","); + + for (const auto &h: handin_data) { + const auto item_data = Strings::Split(h, "-"); + + if ( + item_data.size() == 3 && + Strings::IsNumber(item_data[0]) && + Strings::IsNumber(item_data[1]) && + Strings::IsNumber(item_data[2]) + ) { + const auto item_id = static_cast(std::stoul(item_data[0])); + if (item_id != 0) { + const auto *item = database.GetItem(item_id); + + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(std::stoul(item_data[1])), + .attuned = std::stoi(item_data[2]) ? true : false + } + ); + } + } + } + } + else if (Strings::Contains(handin_items, "|")) { + const auto item_data = Strings::Split(handin_items, "|"); + + if ( + item_data.size() == 3 && + Strings::IsNumber(item_data[0]) && + Strings::IsNumber(item_data[1]) && + Strings::IsNumber(item_data[2]) + ) { + const auto item_id = static_cast(std::stoul(item_data[0])); + const auto *item = database.GetItem(item_id); + + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(std::stoul(item_data[1])), + .attuned = std::stoi(item_data[2]) ? true : false + } + ); + } + } + } + + // Handin Money + if (!handin_money.empty()) { + const auto hms = Strings::Split(handin_money, "|"); + hm.copper = static_cast(std::stoul(hms[0])); + hm.silver = static_cast(std::stoul(hms[1])); + hm.gold = static_cast(std::stoul(hms[2])); + hm.platinum = static_cast(std::stoul(hms[3])); + } + + // Return Items + if (!return_items.empty()) { + if (Strings::Contains(return_items, ",")) { + const auto return_data = Strings::Split(return_items, ","); + + for (const auto &r: return_data) { + const auto item_data = Strings::Split(r, "|"); + + if ( + item_data.size() == 3 && + Strings::IsNumber(item_data[0]) && + Strings::IsNumber(item_data[1]) && + Strings::IsNumber(item_data[2]) + ) { + const auto item_id = static_cast(std::stoul(item_data[0])); + const auto *item = database.GetItem(item_id); + + ri.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(std::stoul(item_data[1])), + .attuned = std::stoi(item_data[2]) ? true : false + } + ); + } + } + } + else if (Strings::Contains(return_items, "|")) { + const auto item_data = Strings::Split(return_items, "|"); + + if ( + item_data.size() == 3 && + Strings::IsNumber(item_data[0]) && + Strings::IsNumber(item_data[1]) && + Strings::IsNumber(item_data[2]) + ) { + const auto item_id = static_cast(std::stoul(item_data[0])); + const auto *item = database.GetItem(item_id); + + ri.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(std::stoul(item_data[1])), + .attuned = std::stoi(item_data[2]) ? true : false + } + ); + } + } + } + + // Return Money + if (!return_money.empty()) { + const auto rms = Strings::Split(return_money, "|"); + rm.copper = static_cast(std::stoul(rms[0])); + rm.silver = static_cast(std::stoul(rms[1])); + rm.gold = static_cast(std::stoul(rms[2])); + rm.platinum = static_cast(std::stoul(rms[3])); + } + + initiator->DeleteEntityVariable("HANDIN_ITEMS"); + initiator->DeleteEntityVariable("HANDIN_MONEY"); + initiator->DeleteEntityVariable("RETURN_ITEMS"); + initiator->DeleteEntityVariable("RETURN_MONEY"); + + if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN)) { + auto e = PlayerEvent::HandinEvent{ + .npc_id = owner->CastToNPC()->GetNPCTypeID(), + .npc_name = owner->GetCleanName(), + .handin_items = hi, + .handin_money = hm, + .return_items = ri, + .return_money = rm + }; + + RecordPlayerEventLogWithClient(initiator, PlayerEvent::NPC_HANDIN, e); + } +} diff --git a/zone/questmgr.h b/zone/questmgr.h index 71c166685..469ff2615 100644 --- a/zone/questmgr.h +++ b/zone/questmgr.h @@ -347,6 +347,7 @@ public: bool HasRecipeLearned(uint32 recipe_id); bool DoAugmentSlotsMatch(uint32 item_one, uint32 item_two); int8 DoesAugmentFit(EQ::ItemInstance* inst, uint32 augment_id, uint8 augment_slot = 255); + void SendPlayerHandinEvent(); Bot *GetBot() const; Client *GetInitiator() const; diff --git a/zone/raids.cpp b/zone/raids.cpp index b27cae0e0..65938dc42 100644 --- a/zone/raids.cpp +++ b/zone/raids.cpp @@ -17,6 +17,7 @@ */ #include "../common/strings.h" +#include "../common/events/player_event_logs.h" #include "client.h" #include "entity.h" @@ -814,6 +815,18 @@ void Raid::SplitMoney(uint32 gid, uint32 copper, uint32 silver, uint32 gold, uin true ); + if (player_event_logs.IsEventEnabled(PlayerEvent::SPLIT_MONEY)) { + auto e = PlayerEvent::SplitMoneyEvent{ + .copper = copper_split, + .silver = silver_split, + .gold = gold_split, + .platinum = platinum_split, + .player_money_balance = members[i].member->GetCarriedMoney(), + }; + + RecordPlayerEventLogWithClient(members[i].member, PlayerEvent::SPLIT_MONEY, e); + } + members[i].member->MessageString( Chat::MoneySplit, YOU_RECEIVE_AS_SPLIT, diff --git a/zone/spells.cpp b/zone/spells.cpp index fc5cb94ff..c16cfa6af 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -77,6 +77,7 @@ Copyright (C) 2001-2002 EQEMu Development Team (http://eqemu.org) #include "../common/strings.h" #include "../common/data_verification.h" #include "../common/misc_functions.h" +#include "../common/events/player_event_logs.h" #include "data_bucket.h" #include "quest_parser_collection.h" @@ -6914,10 +6915,13 @@ bool Mob::CheckItemRaceClassDietyRestrictionsOnCast(uint32 inventory_slot) { if (itm && itm->GetItem()->Classes != 65535) { if ((itm->GetItem()->Click.Type == EQ::item::ItemEffectEquipClick) && !(itm->GetItem()->Classes & bitmask)) { if (CastToClient()->ClientVersion() < EQ::versions::ClientVersion::SoF) { - // They are casting a spell from an item that requires equipping but shouldn't let them equip it - LogError("HACKER: [{}] (account: [{}]) attempted to click an equip-only effect on item [{}] (id: [{}]) which they shouldn't be able to equip!", - CastToClient()->GetCleanName(), CastToClient()->AccountName(), itm->GetItem()->Name, itm->GetItem()->ID); - database.SetHackerFlag(CastToClient()->AccountName(), CastToClient()->GetCleanName(), "Clicking equip-only item with an invalid class"); + std::string message = fmt::format( + "Attempted to click an equip-only effect on item_name [{}] item_id [{}] which they shouldn't be able to equip!", + itm->GetItem()->Name, + itm->GetItem()->ID + ); + + RecordPlayerEventLogWithClient(CastToClient(), PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } else { MessageString(Chat::Red, MUST_EQUIP_ITEM); @@ -6926,10 +6930,13 @@ bool Mob::CheckItemRaceClassDietyRestrictionsOnCast(uint32 inventory_slot) { } if ((itm->GetItem()->Click.Type == EQ::item::ItemEffectClick2) && !(itm->GetItem()->Classes & bitmask)) { if (CastToClient()->ClientVersion() < EQ::versions::ClientVersion::SoF) { - // They are casting a spell from an item that they don't meet the race/class requirements to cast - LogError("HACKER: [{}] (account: [{}]) attempted to click a race/class restricted effect on item [{}] (id: [{}]) which they shouldn't be able to click!", - CastToClient()->GetCleanName(), CastToClient()->AccountName(), itm->GetItem()->Name, itm->GetItem()->ID); - database.SetHackerFlag(CastToClient()->AccountName(), CastToClient()->GetCleanName(), "Clicking race/class restricted item with an invalid class"); + std::string message = fmt::format( + "Attempted to click a race/class restricted effect on item_name [{}] item_id [{}] which they shouldn't be able to click!", + itm->GetItem()->Name, + itm->GetItem()->ID + ); + + RecordPlayerEventLogWithClient(CastToClient(), PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } else { if (CastToClient()->ClientVersion() >= EQ::versions::ClientVersion::RoF) @@ -6947,9 +6954,13 @@ bool Mob::CheckItemRaceClassDietyRestrictionsOnCast(uint32 inventory_slot) { } if (itm && (itm->GetItem()->Click.Type == EQ::item::ItemEffectEquipClick) && inventory_slot > EQ::invslot::EQUIPMENT_END) { if (CastToClient()->ClientVersion() < EQ::versions::ClientVersion::SoF) { - // They are attempting to cast a must equip clicky without having it equipped - LogError("HACKER: [{}] (account: [{}]) attempted to click an equip-only effect on item [{}] (id: [{}]) without equiping it!", CastToClient()->GetCleanName(), CastToClient()->AccountName(), itm->GetItem()->Name, itm->GetItem()->ID); - database.SetHackerFlag(CastToClient()->AccountName(), CastToClient()->GetCleanName(), "Clicking equip-only item without equiping it"); + std::string message = fmt::format( + "Attempted to click an equip-only effect on item_name [{}] item_id [{}] without equipping it!", + itm->GetItem()->Name, + itm->GetItem()->ID + ); + + RecordPlayerEventLogWithClient(CastToClient(), PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = message}); } else { MessageString(Chat::Red, MUST_EQUIP_ITEM); @@ -6975,8 +6986,8 @@ void Mob::SetHP(int64 hp) return; } - if (combat_record.InCombat()) { - combat_record.ProcessHPEvent(hp, current_hp); + if (m_combat_record.InCombat()) { + m_combat_record.ProcessHPEvent(hp, current_hp); } current_hp = hp; @@ -6984,8 +6995,8 @@ void Mob::SetHP(int64 hp) void Mob::DrawDebugCoordinateNode(std::string node_name, const glm::vec4 vec) { - NPC* node = nullptr; - for (const auto& n : entity_list.GetNPCList()) { + NPC *node = nullptr; + for (const auto &n: entity_list.GetNPCList()) { if (n.second->GetCleanName() == node_name) { node = n.second; break; @@ -6995,3 +7006,8 @@ void Mob::DrawDebugCoordinateNode(std::string node_name, const glm::vec4 vec) node = NPC::SpawnNodeNPC(node_name, "", GetPosition()); } } + +const CombatRecord &Mob::GetCombatRecord() const +{ + return m_combat_record; +} diff --git a/zone/task_client_state.cpp b/zone/task_client_state.cpp index 87b148747..bbe043126 100644 --- a/zone/task_client_state.cpp +++ b/zone/task_client_state.cpp @@ -14,6 +14,7 @@ #include "worldserver.h" #include "dynamic_zone.h" #include "string_ids.h" +#include "../common/events/player_event_logs.h" #define EBON_CRYSTAL 40902 #define RADIANT_CRYSTAL 40903 @@ -925,6 +926,16 @@ int ClientTaskState::IncrementDoneCount( int event_res = DispatchEventTaskComplete(client, *info, activity_id); + if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_COMPLETE)) { + auto e = PlayerEvent::TaskCompleteEvent{ + .task_id = static_cast(info->task_id), + .task_name = task_manager->GetTaskName(static_cast(info->task_id)), + .activity_id = static_cast(info->activity[activity_id].activity_id), + .done_count = static_cast(info->activity[activity_id].done_count) + }; + RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_COMPLETE, e); + } + /* QS: PlayerLogTaskUpdates :: Complete */ if (RuleB(QueryServ, PlayerLogTaskUpdates)) { std::string event_desc = StringFormat( @@ -963,6 +974,16 @@ int ClientTaskState::IncrementDoneCount( activity_id, task_index ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_UPDATE)) { + auto e = PlayerEvent::TaskUpdateEvent{ + .task_id = static_cast(info->task_id), + .task_name = task_manager->GetTaskName(static_cast(info->task_id)), + .activity_id = static_cast(info->activity[activity_id].activity_id), + .done_count = static_cast(info->activity[activity_id].done_count) + }; + RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_UPDATE, e); + } } task_manager->SaveClientState(client, this); @@ -2134,6 +2155,26 @@ void ClientTaskState::AcceptNewTask( NPC *npc = entity_list.GetID(npc_type_id)->CastToNPC(); if (npc) { parse->EventNPC(EVENT_TASK_ACCEPTED, npc, client, export_string, 0); + + if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_ACCEPT)) { + auto e = PlayerEvent::TaskAcceptEvent{ + .npc_id = static_cast(npc_type_id), + .npc_name = npc->GetCleanName(), + .task_id = static_cast(task_id), + .task_name = task_manager->GetTaskName(static_cast(task_id)), + }; + RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_ACCEPT, e); + } + } else { + if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_ACCEPT)) { + auto e = PlayerEvent::TaskAcceptEvent{ + .npc_id = 0, + .npc_name = "No NPC", + .task_id = static_cast(task_id), + .task_name = task_manager->GetTaskName(static_cast(task_id)), + }; + RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_ACCEPT, e); + } } parse->EventPlayer(EVENT_TASK_ACCEPTED, client, export_string, 0); } diff --git a/zone/task_manager.cpp b/zone/task_manager.cpp index bee348227..4b8871998 100644 --- a/zone/task_manager.cpp +++ b/zone/task_manager.cpp @@ -700,7 +700,7 @@ void TaskManager::SharedTaskSelector(Client* client, Mob* mob, const std::vector if (request.group_type != SharedTaskRequestGroupType::Solo) { auto shared_task_members = SharedTaskMembersRepository::GetWhere( database, - fmt::format("character_id IN ({}) LIMIT 1", fmt::join(request.character_ids, ","))); + fmt::format("character_id IN ({}) LIMIT 1", Strings::Join(request.character_ids, ","))); if (!shared_task_members.empty()) { validation_failed = true; diff --git a/zone/tradeskills.cpp b/zone/tradeskills.cpp index 52ec95947..c2f9a3076 100644 --- a/zone/tradeskills.cpp +++ b/zone/tradeskills.cpp @@ -17,6 +17,7 @@ */ #include "../common/global_define.h" +#include "../common/events/player_event_logs.h" #include #include @@ -33,12 +34,14 @@ #include "string_ids.h" #include "titles.h" #include "zonedb.h" +#include "worldserver.h" #include "../common/repositories/char_recipe_list_repository.h" #include "../common/zone_store.h" #include "../common/repositories/tradeskill_recipe_repository.h" #include "../common/repositories/tradeskill_recipe_entries_repository.h" extern QueryServ* QServ; +extern WorldServer worldserver; static const EQ::skills::SkillType TradeskillUnknown = EQ::skills::Skill1HBlunt; /* an arbitrary non-tradeskill */ @@ -488,8 +491,28 @@ void Object::HandleCombine(Client* user, const NewCombine_Struct* in_combine, Ob } if (success) { + if (player_event_logs.IsEventEnabled(PlayerEvent::COMBINE_SUCCESS)) { + auto e = PlayerEvent::CombineEvent{ + .recipe_id = spec.recipe_id, + .recipe_name = spec.name, + .made_count = spec.madecount, + .tradeskill_id = (uint32)spec.tradeskill + }; + RecordPlayerEventLogWithClient(user, PlayerEvent::COMBINE_SUCCESS, e); + } + parse->EventPlayer(EVENT_COMBINE_SUCCESS, user, spec.name, spec.recipe_id); } else { + if (player_event_logs.IsEventEnabled(PlayerEvent::COMBINE_FAILURE)) { + auto e = PlayerEvent::CombineEvent{ + .recipe_id = spec.recipe_id, + .recipe_name = spec.name, + .made_count = spec.madecount, + .tradeskill_id = (uint32)spec.tradeskill + }; + RecordPlayerEventLogWithClient(user, PlayerEvent::COMBINE_FAILURE, e); + } + parse->EventPlayer(EVENT_COMBINE_FAILURE, user, spec.name, spec.recipe_id); } } diff --git a/zone/trading.cpp b/zone/trading.cpp index b74ccb856..ba6606b70 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -21,6 +21,7 @@ #include "../common/rulesys.h" #include "../common/strings.h" #include "../common/misc_functions.h" +#include "../common/events/player_event_logs.h" #include "client.h" #include "entity.h" @@ -187,149 +188,9 @@ void Trade::SendItemData(const EQ::ItemInstance* inst, int16 dest_slot_id) } } -// Audit trade: The part logged is what travels owner -> with -void Trade::LogTrade() +Mob *Trade::GetOwner() const { - Mob* with = With(); - if (!owner->IsClient() || !with) - return; // Should never happen - - Client* trader = owner->CastToClient(); - bool logtrade = false; - int admin_level = 0; - uint8 item_count = 0; - - if (zone->tradevar != 0) { - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { - if (trader->GetInv().GetItem(i)) - item_count++; - } - - if ((cp + sp + gp + pp) || item_count) { - admin_level = trader->Admin(); - } else { - admin_level = (AccountStatus::Max + 1); - } - - if (zone->tradevar == 7) { - logtrade = true; - } else if ( - admin_level >= AccountStatus::Steward && - admin_level < AccountStatus::ApprenticeGuide - ) { - if (zone->tradevar < 8 && zone->tradevar > 5) { - logtrade = true; - } - } else if (admin_level <= AccountStatus::ApprenticeGuide) { - if (zone->tradevar < 8 && zone->tradevar > 4) { - logtrade = true; - } - } else if (admin_level <= AccountStatus::QuestTroupe) { - if (zone->tradevar < 8 && zone->tradevar > 3) { - logtrade = true; - } - } else if (admin_level <= AccountStatus::GMAdmin) { - if (zone->tradevar < 9 && zone->tradevar > 2) { - logtrade = true; - } - } else if (admin_level <= AccountStatus::GMLeadAdmin) { - if ((zone->tradevar < 8 && zone->tradevar > 1) || zone->tradevar == 9) { - logtrade = true; - } - } else if (admin_level <= AccountStatus::Max){ - if (zone->tradevar < 8 && zone->tradevar > 0) { - logtrade = true; - } - } - } - - if (logtrade) { - char logtext[1000] = {0}; - uint32 cash = 0; - bool comma = false; - - // Log items offered by owner - cash = cp + sp + gp + pp; - if ((cash>0) || (item_count>0)) { - sprintf(logtext, "%s gave %s ", trader->GetName(), with->GetName()); - - if (item_count > 0) { - strcat(logtext, "items {"); - - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { - const EQ::ItemInstance* inst = trader->GetInv().GetItem(i); - - if (!comma) - comma = true; - else { - if (inst) - strcat(logtext, ","); - } - - if (inst) { - char item_num[15] = {0}; - sprintf(item_num, "%i", inst->GetItem()->ID); - strcat(logtext, item_num); - - if (inst->IsClassBag()) { - for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { - inst = trader->GetInv().GetItem(i, j); - if (inst) { - strcat(logtext, ","); - sprintf(item_num, "%i", inst->GetItem()->ID); - strcat(logtext, item_num); - } - } - } - } - } - } - - if (cash > 0) { - char money[100] = {0}; - sprintf(money, " %ipp, %igp, %isp, %icp", trader->trade->pp, trader->trade->gp, trader->trade->sp, trader->trade->cp); - strcat(logtext, money); - } - - database.logevents(trader->AccountName(), trader->AccountID(), - trader->Admin(), trader->GetName(), with->GetName(), "Trade", logtext, 6); - } - } -} - - -void Trade::DumpTrade() -{ - Mob* with = With(); - LogTrading("Dumping trade data: [{}] in TradeState [{}] with [{}]", - owner->GetName(), state, ((with==nullptr)?"(null)":with->GetName())); - - if (!owner->IsClient()) - return; - - Client* trader = owner->CastToClient(); - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; i++) { - const EQ::ItemInstance* inst = trader->GetInv().GetItem(i); - - if (inst) { - LogTrading("Item [{}] (Charges=[{}], Slot=[{}], IsBag=[{}])", - inst->GetItem()->ID, inst->GetCharges(), - i, ((inst->IsClassBag()) ? "True" : "False")); - - if (inst->IsClassBag()) { - for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { - inst = trader->GetInv().GetItem(i, j); - if (inst) { - LogTrading("\tBagItem [{}] (Charges=[{}], Slot=[{}])", - inst->GetItem()->ID, inst->GetCharges(), - EQ::InventoryProfile::CalcSlotId(i, j)); - } - } - } - } - } - - LogTrading("\tpp:[{}], gp:[{}], sp:[{}], cp:[{}]", pp, gp, sp, cp); + return owner; } @@ -458,8 +319,8 @@ void Client::ResetTrade() { void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, std::list* event_details) { if(tradingWith && tradingWith->IsClient()) { - Client* other = tradingWith->CastToClient(); - QSPlayerLogTrade_Struct* qs_audit = nullptr; + Client * other = tradingWith->CastToClient(); + PlayerLogTrade_Struct * qs_audit = nullptr; bool qs_log = false; if(other) { @@ -470,24 +331,24 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st // step 0: pre-processing // QS code if (RuleB(QueryServ, PlayerLogTrades) && event_entry && event_details) { - qs_audit = (QSPlayerLogTrade_Struct*)event_entry; + qs_audit = (PlayerLogTrade_Struct*)event_entry; qs_log = true; if (finalizer) { - qs_audit->char2_id = character_id; + qs_audit->character_2_id = character_id; - qs_audit->char2_money.platinum = trade->pp; - qs_audit->char2_money.gold = trade->gp; - qs_audit->char2_money.silver = trade->sp; - qs_audit->char2_money.copper = trade->cp; + qs_audit->character_2_money.platinum = trade->pp; + qs_audit->character_2_money.gold = trade->gp; + qs_audit->character_2_money.silver = trade->sp; + qs_audit->character_2_money.copper = trade->cp; } else { - qs_audit->char1_id = character_id; + qs_audit->character_1_id = character_id; - qs_audit->char1_money.platinum = trade->pp; - qs_audit->char1_money.gold = trade->gp; - qs_audit->char1_money.silver = trade->sp; - qs_audit->char1_money.copper = trade->cp; + qs_audit->character_1_money.platinum = trade->pp; + qs_audit->character_1_money.gold = trade->gp; + qs_audit->character_1_money.silver = trade->sp; + qs_audit->character_1_money.copper = trade->cp; } } @@ -510,12 +371,12 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st if (other->PutItemInInventory(free_slot, *inst, true)) { LogTrading("Container [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID); if (qs_log) { - auto detail = new QSTradeItems_Struct; + auto detail = new PlayerLogTradeItemsEntry_Struct; - detail->from_id = character_id; - detail->from_slot = trade_slot; - detail->to_id = other->CharacterID(); - detail->to_slot = free_slot; + detail->from_character_id = character_id; + detail->from_slot = trade_slot; + detail->to_character_id = other->CharacterID(); + detail->to_slot = free_slot; detail->item_id = inst->GetID(); detail->charges = 1; detail->aug_1 = inst->GetAugmentItemID(1); @@ -527,20 +388,20 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st event_details->push_back(detail); if (finalizer) - qs_audit->char2_count += detail->charges; + qs_audit->character_2_item_count += detail->charges; else - qs_audit->char1_count += detail->charges; + qs_audit->character_1_item_count += detail->charges; for (uint8 sub_slot = EQ::invbag::SLOT_BEGIN; (sub_slot <= EQ::invbag::SLOT_END); ++sub_slot) { // this is to catch ALL items const EQ::ItemInstance* bag_inst = inst->GetItem(sub_slot); if (bag_inst) { - detail = new QSTradeItems_Struct; + detail = new PlayerLogTradeItemsEntry_Struct; - detail->from_id = character_id; - detail->from_slot = EQ::InventoryProfile::CalcSlotId(trade_slot, sub_slot); - detail->to_id = other->CharacterID(); - detail->to_slot = EQ::InventoryProfile::CalcSlotId(free_slot, sub_slot); + detail->from_character_id = character_id; + detail->from_slot = EQ::InventoryProfile::CalcSlotId(trade_slot, sub_slot); + detail->to_character_id = other->CharacterID(); + detail->to_slot = EQ::InventoryProfile::CalcSlotId(free_slot, sub_slot); detail->item_id = bag_inst->GetID(); detail->charges = (!bag_inst->IsStackable() ? 1 : bag_inst->GetCharges()); detail->aug_1 = bag_inst->GetAugmentItemID(1); @@ -552,9 +413,9 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st event_details->push_back(detail); if (finalizer) - qs_audit->char2_count += detail->charges; + qs_audit->character_2_item_count += detail->charges; else - qs_audit->char1_count += detail->charges; + qs_audit->character_1_item_count += detail->charges; } } } @@ -620,12 +481,12 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st LogTrading("Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot", inst->GetItem()->Name, inst->GetItem()->ID, (old_charges - inst->GetCharges())); if (qs_log) { - auto detail = new QSTradeItems_Struct; + auto detail = new PlayerLogTradeItemsEntry_Struct; - detail->from_id = character_id; - detail->from_slot = trade_slot; - detail->to_id = other->CharacterID(); - detail->to_slot = partial_slot; + detail->from_character_id = character_id; + detail->from_slot = trade_slot; + detail->to_character_id = other->CharacterID(); + detail->to_slot = partial_slot; detail->item_id = inst->GetID(); detail->charges = (old_charges - inst->GetCharges()); detail->aug_1 = 0; @@ -637,9 +498,9 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st event_details->push_back(detail); if (finalizer) - qs_audit->char2_count += detail->charges; + qs_audit->character_2_item_count += detail->charges; else - qs_audit->char1_count += detail->charges; + qs_audit->character_1_item_count += detail->charges; } } else { @@ -688,12 +549,12 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st } if (qs_log) { - auto detail = new QSTradeItems_Struct; + auto detail = new PlayerLogTradeItemsEntry_Struct; - detail->from_id = character_id; - detail->from_slot = trade_slot; - detail->to_id = character_id; - detail->to_slot = bias_slot; + detail->from_character_id = character_id; + detail->from_slot = trade_slot; + detail->to_character_id = character_id; + detail->to_slot = bias_slot; detail->item_id = inst->GetID(); detail->charges = (old_charges - inst->GetCharges()); detail->aug_1 = 0; @@ -728,12 +589,12 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st if (other->PutItemInInventory(free_slot, *inst, true)) { LogTrading("Item [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID); if (qs_log) { - auto detail = new QSTradeItems_Struct; + auto detail = new PlayerLogTradeItemsEntry_Struct; - detail->from_id = character_id; - detail->from_slot = trade_slot; - detail->to_id = other->CharacterID(); - detail->to_slot = free_slot; + detail->from_character_id = character_id; + detail->from_slot = trade_slot; + detail->to_character_id = other->CharacterID(); + detail->to_slot = free_slot; detail->item_id = inst->GetID(); detail->charges = (!inst->IsStackable() ? 1 : inst->GetCharges()); detail->aug_1 = inst->GetAugmentItemID(1); @@ -745,21 +606,21 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st event_details->push_back(detail); if (finalizer) - qs_audit->char2_count += detail->charges; + qs_audit->character_2_item_count += detail->charges; else - qs_audit->char1_count += detail->charges; + qs_audit->character_1_item_count += detail->charges; // 'step 3' should never really see containers..but, just in case... for (uint8 sub_slot = EQ::invbag::SLOT_BEGIN; (sub_slot <= EQ::invbag::SLOT_END); ++sub_slot) { // this is to catch ALL items const EQ::ItemInstance* bag_inst = inst->GetItem(sub_slot); if (bag_inst) { - detail = new QSTradeItems_Struct; + detail = new PlayerLogTradeItemsEntry_Struct; - detail->from_id = character_id; - detail->from_slot = trade_slot; - detail->to_id = other->CharacterID(); - detail->to_slot = free_slot; + detail->from_character_id = character_id; + detail->from_slot = trade_slot; + detail->to_character_id = other->CharacterID(); + detail->to_slot = free_slot; detail->item_id = bag_inst->GetID(); detail->charges = (!bag_inst->IsStackable() ? 1 : bag_inst->GetCharges()); detail->aug_1 = bag_inst->GetAugmentItemID(1); @@ -771,9 +632,9 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st event_details->push_back(detail); if (finalizer) - qs_audit->char2_count += detail->charges; + qs_audit->character_2_item_count += detail->charges; else - qs_audit->char1_count += detail->charges; + qs_audit->character_1_item_count += detail->charges; } } } @@ -1697,7 +1558,7 @@ void Client::BuyTraderItem(TraderBuy_Struct* tbs, Client* Trader, const EQApplic } if(!TakeMoneyFromPP(TotalCost)) { - database.SetHackerFlag(account_name, name, "Attempted to buy something in bazaar but did not have enough money."); + RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Attempted to buy something in bazaar but did not have enough money."}); TradeRequestFailed(app); safe_delete(outapp); return; @@ -1715,6 +1576,37 @@ void Client::BuyTraderItem(TraderBuy_Struct* tbs, Client* Trader, const EQApplic Trader->AddMoneyToPP(copper, silver, gold, platinum, true); + + if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { + auto e = PlayerEvent::TraderPurchaseEvent{ + .item_id = BuyItem->GetID(), + .item_name = BuyItem->GetItem()->Name, + .trader_id = Trader->CharacterID(), + .trader_name = Trader->GetCleanName(), + .price = tbs->Price, + .charges = outtbs->Quantity, + .total_cost = (tbs->Price * outtbs->Quantity), + .player_money_balance = GetCarriedMoney(), + }; + + RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); + } + + if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) { + auto e = PlayerEvent::TraderSellEvent{ + .item_id = BuyItem->GetID(), + .item_name = BuyItem->GetItem()->Name, + .buyer_id = CharacterID(), + .buyer_name = GetCleanName(), + .price = tbs->Price, + .charges = outtbs->Quantity, + .total_cost = (tbs->Price * outtbs->Quantity), + .player_money_balance = Trader->GetCarriedMoney(), + }; + + RecordPlayerEventLogWithClient(Trader, PlayerEvent::TRADER_SELL, e); + } + LogTrading("Trader Received: [{}] Platinum, [{}] Gold, [{}] Silver, [{}] Copper", platinum, gold, silver, copper); ReturnTraderReq(app, outtbs->Quantity, ItemID); diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 13e824cf5..82d69e7d5 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -57,6 +57,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "shared_task_zone_messaging.h" #include "dialogue_window.h" #include "bot_command.h" +#include "queryserv.h" +#include "../common/events/player_event_logs.h" extern EntityList entity_list; extern Zone* zone; @@ -1958,8 +1960,9 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) } case ServerOP_ReloadLogs: { - zone->SendReloadMessage("Log Settings"); - LogSys.LoadLogDatabaseSettings(); + zone->SendReloadMessage("Log Settings"); + LogSys.LoadLogDatabaseSettings(); + player_event_logs.ReloadSettings(); break; } case ServerOP_ReloadMerchants: { diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 7145ce8ea..4aab38f50 100755 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -170,34 +170,6 @@ void ZoneDatabase::UpdateSpawn2Status(uint32 id, uint8 new_status) QueryDatabase(query); } -bool ZoneDatabase::logevents(const char* accountname,uint32 accountid,uint8 status,const char* charname, const char* target,const char* descriptiontype, const char* description,int event_nid){ - - uint32 len = strlen(description); - uint32 len2 = strlen(target); - auto descriptiontext = new char[2 * len + 1]; - auto targetarr = new char[2 * len2 + 1]; - memset(descriptiontext, 0, 2*len+1); - memset(targetarr, 0, 2*len2+1); - DoEscapeString(descriptiontext, description, len); - DoEscapeString(targetarr, target, len2); - - std::string query = StringFormat("INSERT INTO eventlog (accountname, accountid, status, " - "charname, target, descriptiontype, description, event_nid) " - "VALUES('%s', %i, %i, '%s', '%s', '%s', '%s', '%i')", - accountname, accountid, status, charname, targetarr, - descriptiontype, descriptiontext, event_nid); - safe_delete_array(descriptiontext); - safe_delete_array(targetarr); - auto results = QueryDatabase(query); - if (!results.Success()) { - return false; - } - - return true; -} - - - bool ZoneDatabase::SetSpecialAttkFlag(uint8 id, const char* flag) { std::string query = StringFormat("UPDATE npc_types SET npcspecialattks='%s' WHERE id = %i;", flag, id); diff --git a/zone/zonedb.h b/zone/zonedb.h index 2256ca90d..4346a1338 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -612,7 +612,6 @@ public: * PLEASE DO NOT ADD TO THIS COLLECTION OF CRAP UNLESS YOUR METHOD * REALLY HAS NO BETTER SECTION */ - bool logevents(const char* accountname,uint32 accountid,uint8 status,const char* charname,const char* target, const char* descriptiontype, const char* description,int event_nid); uint32 GetKarma(uint32 acct_id); void UpdateKarma(uint32 acct_id, uint32 amount); diff --git a/zone/zoning.cpp b/zone/zoning.cpp index f2ea57e1b..4da1dd26a 100644 --- a/zone/zoning.cpp +++ b/zone/zoning.cpp @@ -38,6 +38,7 @@ extern Zone* zone; #include "../common/repositories/character_peqzone_flags_repository.h" #include "../common/repositories/zone_repository.h" +#include "../common/events/player_event_logs.h" void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) { @@ -216,6 +217,22 @@ void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) { return; } + if (player_event_logs.IsEventEnabled(PlayerEvent::ZONING)) { + auto e = PlayerEvent::ZoningEvent{}; + e.from_zone_long_name = zone->GetLongName(); + e.from_zone_short_name = zone->GetShortName(); + e.from_zone_id = zone->GetZoneID(); + e.from_instance_id = zone->GetInstanceID(); + e.from_instance_version = zone->GetInstanceVersion(); + e.to_zone_long_name = ZoneLongName(target_zone_id); + e.to_zone_short_name = ZoneName(target_zone_id); + e.to_zone_id = target_zone_id; + e.to_instance_id = target_instance_id; + e.to_instance_version = target_instance_version; + + RecordPlayerEventLog(PlayerEvent::ZONING, e); + } + //handle circumvention of zone restrictions //we need the value when creating the outgoing packet as well. uint8 ignore_restrictions = zonesummon_ignorerestrictions;