From 9098420e0fd4dcc28a8943ec1232a573c593d826 Mon Sep 17 00:00:00 2001 From: Catapultam Habeo Date: Fri, 10 Jan 2025 03:39:31 -0600 Subject: [PATCH 1/7] rebase\tidy up to address commends --- common/database.cpp | 7 + common/database/database_update_manifest.cpp | 13 + common/database_schema.h | 817 +- common/emu_oplist.h | 3 + common/eq_packet_structs.h | 15 + .../base/base_character_pet_name_repository.h | 404 + .../character_pet_name_repository.h | 50 + common/version.h | 2 +- utils/patches/patch_RoF2.conf | 9 + zone/client.cpp | 23199 ++++++++-------- zone/client.h | 6 + zone/client_packet.cpp | 29 + zone/lua_client.cpp | 7 + zone/lua_client.h | 1 + zone/perl_client.cpp | 6 + zone/pets.cpp | 8 + 16 files changed, 12606 insertions(+), 11970 deletions(-) create mode 100644 common/repositories/base/base_character_pet_name_repository.h create mode 100644 common/repositories/character_pet_name_repository.h diff --git a/common/database.cpp b/common/database.cpp index a3405970dd..f1d3a3757b 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -50,6 +50,7 @@ #include "../common/repositories/raid_members_repository.h" #include "../common/repositories/reports_repository.h" #include "../common/repositories/variables_repository.h" +#include "../common/repositories/character_pet_name_repository.h" #include "../common/events/player_event_logs.h" // Disgrace: for windows compile @@ -313,6 +314,12 @@ bool Database::ReserveName(uint32 account_id, const std::string& name) return false; } + const auto& p = CharacterPetNameRepository::GetWhere(*this, where_filter); + if (!p.empty()) { + LogInfo("Account [{}] requested name [{}] but name is already taken by an Pet", account_id, name); + return false; + } + auto e = CharacterDataRepository::NewEntity(); e.account_id = account_id; diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 582864c434..57819b700a 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -6320,6 +6320,19 @@ ALTER TABLE `data_buckets` ADD KEY `idx_account_id_key` (`account_id`, `key`); )", .content_schema_update = false }, + ManifestEntry{ + .version = 9293, + .description = "2025_01_10_create_pet_names_table.sql", + .check = "SHOW TABLES LIKE 'character_pet_name'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE `character_pet_name` ( + `character_id` INT(11) NOT NULL PRIMARY KEY, + `name` VARCHAR(64) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +)", + }, // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/database_schema.h b/common/database_schema.h index b86e16f448..1b0fbb7410 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -26,426 +26,427 @@ namespace DatabaseSchema { - /** - * Character-specific tables - * - * Does not included related meta-data tables such as 'guilds', 'accounts' - * @return - */ - static std::map GetCharacterTables() - { - return { - {"adventure_stats", "player_id"}, - {"char_recipe_list", "char_id"}, - {"character_activities", "charid"}, - {"character_alt_currency", "char_id"}, - {"character_alternate_abilities", "id"}, - {"character_auras", "id"}, - {"character_bandolier", "id"}, - {"character_bind", "id"}, - {"character_buffs", "character_id"}, - {"character_corpses", "id"}, - {"character_currency", "id"}, - {"character_data", "id"}, - {"character_disciplines", "id"}, - {"character_enabledtasks", "charid"}, - {"character_expedition_lockouts", "character_id"}, - {"character_exp_modifiers", "character_id"}, + /** + * Character-specific tables + * + * Does not included related meta-data tables such as 'guilds', 'accounts' + * @return + */ + static std::map GetCharacterTables() + { + return { + {"adventure_stats", "player_id"}, + {"char_recipe_list", "char_id"}, + {"character_activities", "charid"}, + {"character_alt_currency", "char_id"}, + {"character_alternate_abilities", "id"}, + {"character_auras", "id"}, + {"character_bandolier", "id"}, + {"character_bind", "id"}, + {"character_buffs", "character_id"}, + {"character_corpses", "id"}, + {"character_currency", "id"}, + {"character_data", "id"}, + {"character_disciplines", "id"}, + {"character_enabledtasks", "charid"}, + {"character_expedition_lockouts", "character_id"}, + {"character_exp_modifiers", "character_id"}, {"character_evolving_items", "character_id"}, - {"character_inspect_messages", "id"}, - {"character_instance_safereturns", "character_id"}, - {"character_item_recast", "id"}, - {"character_languages", "id"}, - {"character_leadership_abilities", "id"}, - {"character_material", "id"}, - {"character_memmed_spells", "id"}, - {"character_parcels", "char_id"}, - {"character_parcels_containers", "id"}, - {"character_pet_buffs", "char_id"}, - {"character_pet_info", "char_id"}, - {"character_pet_inventory", "char_id"}, - {"character_peqzone_flags", "id"}, - {"character_potionbelt", "id"}, - {"character_skills", "id"}, - {"character_spells", "id"}, - {"character_stats_record", "character_id"}, - {"character_task_timers", "character_id"}, - {"character_tasks", "charid"}, - {"character_tribute", "character_id"}, - {"completed_tasks", "charid"}, - {"data_buckets", "character_id"}, - {"faction_values", "char_id"}, - {"friends", "charid"}, - {"guild_members", "char_id"}, - {"guilds", "id"}, - {"instance_list_player", "id"}, - {"inventory", "charid"}, - {"inventory_snapshots", "charid"}, - {"keyring", "char_id"}, - {"mail", "charid"}, - {"player_titlesets", "char_id"}, - {"quest_globals", "charid"}, - {"timers", "char_id"}, - {"trader", "char_id"}, - {"zone_flags", "charID"} - }; - } + {"character_inspect_messages", "id"}, + {"character_instance_safereturns", "character_id"}, + {"character_item_recast", "id"}, + {"character_languages", "id"}, + {"character_leadership_abilities", "id"}, + {"character_material", "id"}, + {"character_memmed_spells", "id"}, + {"character_parcels", "char_id"}, + {"character_parcels_containers", "id"}, + {"character_pet_buffs", "char_id"}, + {"character_pet_info", "char_id"}, + {"character_pet_inventory", "char_id"}, + {"character_pet_name", "character_id"}, + {"character_peqzone_flags", "id"}, + {"character_potionbelt", "id"}, + {"character_skills", "id"}, + {"character_spells", "id"}, + {"character_stats_record", "character_id"}, + {"character_task_timers", "character_id"}, + {"character_tasks", "charid"}, + {"character_tribute", "character_id"}, + {"completed_tasks", "charid"}, + {"data_buckets", "character_id"}, + {"faction_values", "char_id"}, + {"friends", "charid"}, + {"guild_members", "char_id"}, + {"guilds", "id"}, + {"instance_list_player", "id"}, + {"inventory", "charid"}, + {"inventory_snapshots", "charid"}, + {"keyring", "char_id"}, + {"mail", "charid"}, + {"player_titlesets", "char_id"}, + {"quest_globals", "charid"}, + {"timers", "char_id"}, + {"trader", "char_id"}, + {"zone_flags", "charID"} + }; + } - /** - * @description Gets all player and meta-data tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetPlayerTables() - { - return { - "account", - "account_ip", - "account_flags", - "account_rewards", - "adventure_details", - "adventure_stats", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "char_recipe_list", - "character_activities", - "character_alt_currency", - "character_alternate_abilities", - "character_auras", - "character_bandolier", - "character_bind", - "character_buffs", - "character_corpse_items", - "character_corpses", - "character_currency", - "character_data", - "character_disciplines", - "character_enabledtasks", - "character_expedition_lockouts", - "character_exp_modifiers", + /** + * @description Gets all player and meta-data tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetPlayerTables() + { + return { + "account", + "account_ip", + "account_flags", + "account_rewards", + "adventure_details", + "adventure_stats", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "char_recipe_list", + "character_activities", + "character_alt_currency", + "character_alternate_abilities", + "character_auras", + "character_bandolier", + "character_bind", + "character_buffs", + "character_corpse_items", + "character_corpses", + "character_currency", + "character_data", + "character_disciplines", + "character_enabledtasks", + "character_expedition_lockouts", + "character_exp_modifiers", "character_evolving_items", - "character_inspect_messages", - "character_instance_safereturns", - "character_item_recast", - "character_languages", - "character_leadership_abilities", - "character_material", - "character_memmed_spells", - "character_parcels", - "character_parcels_containers", - "character_pet_buffs", - "character_pet_info", - "character_pet_inventory", - "character_peqzone_flags", - "character_potionbelt", - "character_skills", - "character_spells", - "character_stats_record", - "character_task_timers", - "character_tasks", - "character_tribute", - "completed_tasks", - "data_buckets", - "discovered_items", - "faction_values", - "friends", - "guild_bank", - "guild_members", - "guild_permissions", - "guild_ranks", - "guild_relations", - "guild_tributes", - "guilds", - "instance_list_player", - "inventory", - "inventory_snapshots", - "keyring", - "mail", - "petitions", - "player_titlesets", - "quest_globals", - "sharedbank", - "spell_buckets", - "spell_globals", - "timers", - "trader", - "trader_audit", - "zone_flags" - }; - } + "character_inspect_messages", + "character_instance_safereturns", + "character_item_recast", + "character_languages", + "character_leadership_abilities", + "character_material", + "character_memmed_spells", + "character_parcels", + "character_parcels_containers", + "character_pet_buffs", + "character_pet_info", + "character_pet_inventory", + "character_peqzone_flags", + "character_potionbelt", + "character_skills", + "character_spells", + "character_stats_record", + "character_task_timers", + "character_tasks", + "character_tribute", + "completed_tasks", + "data_buckets", + "discovered_items", + "faction_values", + "friends", + "guild_bank", + "guild_members", + "guild_permissions", + "guild_ranks", + "guild_relations", + "guild_tributes", + "guilds", + "instance_list_player", + "inventory", + "inventory_snapshots", + "keyring", + "mail", + "petitions", + "player_titlesets", + "quest_globals", + "sharedbank", + "spell_buckets", + "spell_globals", + "timers", + "trader", + "trader_audit", + "zone_flags" + }; + } - /** - * Gets content tables - * - * @return - */ - static std::vector GetContentTables() - { - return { - "aa_ability", - "aa_rank_effects", - "aa_rank_prereqs", - "aa_ranks", - "adventure_template", - "adventure_template_entry", - "adventure_template_entry_flavor", - "alternate_currency", - "auras", - "base_data", - "blocked_spells", - "books", - "char_create_combinations", - "char_create_point_allocations", - "damageshieldtypes", - "doors", - "dynamic_zone_templates", - "faction_association", - "faction_base_data", - "faction_list", - "faction_list_mod", - "fishing", - "forage", - "global_loot", - "graveyard", - "grid", - "grid_entries", - "ground_spawns", - "horses", - "items", + /** + * Gets content tables + * + * @return + */ + static std::vector GetContentTables() + { + return { + "aa_ability", + "aa_rank_effects", + "aa_rank_prereqs", + "aa_ranks", + "adventure_template", + "adventure_template_entry", + "adventure_template_entry_flavor", + "alternate_currency", + "auras", + "base_data", + "blocked_spells", + "books", + "char_create_combinations", + "char_create_point_allocations", + "damageshieldtypes", + "doors", + "dynamic_zone_templates", + "faction_association", + "faction_base_data", + "faction_list", + "faction_list_mod", + "fishing", + "forage", + "global_loot", + "graveyard", + "grid", + "grid_entries", + "ground_spawns", + "horses", + "items", "items_evolving_details", - "ldon_trap_entries", - "ldon_trap_templates", - "lootdrop", - "lootdrop_entries", - "loottable", - "loottable_entries", - "merchantlist", - "npc_emotes", - "npc_faction", - "npc_faction_entries", - "npc_scale_global_base", - "npc_spells", - "npc_spells_effects", - "npc_spells_effects_entries", - "npc_spells_entries", - "npc_types", - "npc_types_tint", - "object", - "pets", - "pets_beastlord_data", - "pets_equipmentset", - "pets_equipmentset_entries", - "skill_caps", - "spawn2", - "spawn_conditions", - "spawnentry", - "spawngroup", - "spells_new", - "start_zones", - "starting_items", - "task_activities", - "tasks", - "tasksets", - "tradeskill_recipe", - "tradeskill_recipe_entries", - "traps", - "tribute_levels", - "tributes", - "veteran_reward_templates", - "zone", - "zone_points", - }; - } + "ldon_trap_entries", + "ldon_trap_templates", + "lootdrop", + "lootdrop_entries", + "loottable", + "loottable_entries", + "merchantlist", + "npc_emotes", + "npc_faction", + "npc_faction_entries", + "npc_scale_global_base", + "npc_spells", + "npc_spells_effects", + "npc_spells_effects_entries", + "npc_spells_entries", + "npc_types", + "npc_types_tint", + "object", + "pets", + "pets_beastlord_data", + "pets_equipmentset", + "pets_equipmentset_entries", + "skill_caps", + "spawn2", + "spawn_conditions", + "spawnentry", + "spawngroup", + "spells_new", + "start_zones", + "starting_items", + "task_activities", + "tasks", + "tasksets", + "tradeskill_recipe", + "tradeskill_recipe_entries", + "traps", + "tribute_levels", + "tributes", + "veteran_reward_templates", + "zone", + "zone_points", + }; + } - /** - * Gets server tables - * - * @return - */ - static std::vector GetServerTables() - { - return { - "chatchannels", - "chatchannel_reserved_names", - "command_settings", - "command_subsettings", - "content_flags", - "db_str", - "eqtime", - "launcher", - "launcher_zones", - "spawn_condition_values", - "spawn_events", - "level_exp_mods", - "logsys_categories", - "name_filter", - "perl_event_export_settings", - "profanity_list", - "rule_sets", - "titles", - "rule_values", - "variables", - }; - } + /** + * Gets server tables + * + * @return + */ + static std::vector GetServerTables() + { + return { + "chatchannels", + "chatchannel_reserved_names", + "command_settings", + "command_subsettings", + "content_flags", + "db_str", + "eqtime", + "launcher", + "launcher_zones", + "spawn_condition_values", + "spawn_events", + "level_exp_mods", + "logsys_categories", + "name_filter", + "perl_event_export_settings", + "profanity_list", + "rule_sets", + "titles", + "rule_values", + "variables", + }; + } - /** - * Gets QueryServer tables - * - * @return - */ - static std::vector GetQueryServerTables() - { - return { - "qs_merchant_transaction_record", - "qs_merchant_transaction_record_entries", - "qs_player_aa_rate_hourly", - "qs_player_delete_record", - "qs_player_delete_record_entries", - "qs_player_events", - "qs_player_handin_record", - "qs_player_handin_record_entries", - "qs_player_move_record", - "qs_player_move_record_entries", - "qs_player_npc_kill_record", - "qs_player_npc_kill_record_entries", - "qs_player_speech", - "qs_player_trade_record", - "qs_player_trade_record_entries", - }; - } + /** + * Gets QueryServer tables + * + * @return + */ + static std::vector GetQueryServerTables() + { + return { + "qs_merchant_transaction_record", + "qs_merchant_transaction_record_entries", + "qs_player_aa_rate_hourly", + "qs_player_delete_record", + "qs_player_delete_record_entries", + "qs_player_events", + "qs_player_handin_record", + "qs_player_handin_record_entries", + "qs_player_move_record", + "qs_player_move_record_entries", + "qs_player_npc_kill_record", + "qs_player_npc_kill_record_entries", + "qs_player_speech", + "qs_player_trade_record", + "qs_player_trade_record_entries", + }; + } - /** - * Gets state tables - * Tables that keep track of server state - * - * @return - */ - static std::vector GetStateTables() - { - return { - "adventure_members", - "banned_ips", - "bug_reports", - "bugs", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "completed_shared_task_activity_state", - "completed_shared_task_members", - "completed_shared_tasks", - "discord_webhooks", - "dynamic_zone_members", - "dynamic_zones", - "expedition_lockouts", - "expeditions", - "gm_ips", - "group_id", - "group_leaders", - "instance_list", - "ip_exemptions", - "lfguild", - "merc_buffs", - "merchantlist_temp", - "mercs", - "object_contents", - "raid_details", - "raid_leaders", - "raid_members", - "reports", - "respawn_times", - "saylink", - "server_scheduled_events", - "spawn2_disabled", - "player_event_log_settings", - "player_event_logs", - "shared_task_activity_state", - "shared_task_dynamic_zones", - "shared_task_members", - "shared_tasks", - }; - } + /** + * Gets state tables + * Tables that keep track of server state + * + * @return + */ + static std::vector GetStateTables() + { + return { + "adventure_members", + "banned_ips", + "bug_reports", + "bugs", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "completed_shared_task_activity_state", + "completed_shared_task_members", + "completed_shared_tasks", + "discord_webhooks", + "dynamic_zone_members", + "dynamic_zones", + "expedition_lockouts", + "expeditions", + "gm_ips", + "group_id", + "group_leaders", + "instance_list", + "ip_exemptions", + "lfguild", + "merc_buffs", + "merchantlist_temp", + "mercs", + "object_contents", + "raid_details", + "raid_leaders", + "raid_members", + "reports", + "respawn_times", + "saylink", + "server_scheduled_events", + "spawn2_disabled", + "player_event_log_settings", + "player_event_logs", + "shared_task_activity_state", + "shared_task_dynamic_zones", + "shared_task_members", + "shared_tasks", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetLoginTables() - { - return { - "login_accounts", - "login_api_tokens", - "login_server_admins", - "login_server_list_types", - "login_world_servers", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetLoginTables() + { + return { + "login_accounts", + "login_api_tokens", + "login_server_admins", + "login_server_list_types", + "login_world_servers", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetVersionTables() - { - return { - "db_version", - "inventory_versions", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetVersionTables() + { + return { + "db_version", + "inventory_versions", + }; + } - /** - * @description Gets all player bot tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetBotTables() - { - return { - "bot_buffs", - "bot_command_settings", - "bot_create_combinations", - "bot_data", - "bot_heal_rotation_members", - "bot_heal_rotation_targets", - "bot_heal_rotations", - "bot_inspect_messages", - "bot_inventories", - "bot_owner_options", - "bot_pet_buffs", - "bot_pet_inventories", - "bot_pets", - "bot_spell_casting_chances", - "bot_spell_settings", - "bot_spells_entries", - "bot_stances", - "bot_timers" - }; - } + /** + * @description Gets all player bot tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetBotTables() + { + return { + "bot_buffs", + "bot_command_settings", + "bot_create_combinations", + "bot_data", + "bot_heal_rotation_members", + "bot_heal_rotation_targets", + "bot_heal_rotations", + "bot_inspect_messages", + "bot_inventories", + "bot_owner_options", + "bot_pet_buffs", + "bot_pet_inventories", + "bot_pets", + "bot_spell_casting_chances", + "bot_spell_settings", + "bot_spells_entries", + "bot_stances", + "bot_timers" + }; + } - static std::vector GetMercTables() - { - return { - "merc_armorinfo", - "merc_inventory", - "merc_merchant_entries", - "merc_merchant_template_entries", - "merc_merchant_templates", - "merc_name_types", - "merc_npc_types", - "merc_spell_list_entries", - "merc_spell_lists", - "merc_stance_entries", - "merc_stats", - "merc_subtypes", - "merc_templates", - "merc_types", - "merc_weaponinfo" - }; - } + static std::vector GetMercTables() + { + return { + "merc_armorinfo", + "merc_inventory", + "merc_merchant_entries", + "merc_merchant_template_entries", + "merc_merchant_templates", + "merc_name_types", + "merc_npc_types", + "merc_spell_list_entries", + "merc_spell_lists", + "merc_stance_entries", + "merc_stats", + "merc_subtypes", + "merc_templates", + "merc_types", + "merc_weaponinfo" + }; + } } diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 6b5ab12ab9..eefcdfee87 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -77,6 +77,7 @@ N(OP_CashReward), N(OP_CastSpell), N(OP_ChangeSize), N(OP_ChannelMessage), +N(OP_ChangePetName), N(OP_CharacterCreate), N(OP_CharacterCreateRequest), N(OP_CharInventory), @@ -284,6 +285,8 @@ N(OP_InspectMessageUpdate), N(OP_InspectRequest), N(OP_InstillDoubt), N(OP_InterruptCast), +N(OP_InvokeChangePetName), +N(OP_InvokeChangePetNameImmediate), N(OP_ItemLinkClick), N(OP_ItemLinkResponse), N(OP_ItemLinkText), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index bfe5abf442..922ff040e6 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -5819,6 +5819,21 @@ struct ChangeSize_Struct /*16*/ }; +struct ChangePetName_Struct { +/*00*/ char new_pet_name[64]; +/*40*/ char pet_owner_name[64]; +/*80*/ int response_code; +}; + +enum ChangePetNameResponse : int { + Denied = 0, // 5167 You have requested an invalid name or a Customer Service Representative has denied your name request. Please try another name. + Accepted = 1, // 5976 Your request for a name change was successful. + Timeout = -3, // 5979 You must wait longer before submitting another name request. Please try again in a few minutes. + NotEligible = -4, // 5980 Your character is not eligible for a name change. + Pending = -5, // 5193 You already have a name change pending. Please wait until it is fully processed before attempting another name change. + Unhandled = -1 +}; + // New OpCode/Struct for SoD+ struct GroupMakeLeader_Struct { diff --git a/common/repositories/base/base_character_pet_name_repository.h b/common/repositories/base/base_character_pet_name_repository.h new file mode 100644 index 0000000000..5c4307666d --- /dev/null +++ b/common/repositories/base/base_character_pet_name_repository.h @@ -0,0 +1,404 @@ +/** + * 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://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_CHARACTER_PET_NAME_REPOSITORY_H +#define EQEMU_BASE_CHARACTER_PET_NAME_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseCharacterPetNameRepository { +public: + struct CharacterPetName { + int32_t char_id; + std::string name; + int8_t class_id; + }; + + static std::string PrimaryKey() + { + return std::string("char_id"); + } + + static std::vector Columns() + { + return { + "char_id", + "name", + "class_id", + }; + } + + static std::vector SelectColumns() + { + return { + "char_id", + "name", + "class_id", + }; + } + + 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("character_pet_name"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static CharacterPetName NewEntity() + { + CharacterPetName e{}; + + e.char_id = 0; + e.name = ""; + e.class_id = -1; + + return e; + } + + static CharacterPetName GetCharacterPetName( + const std::vector &character_pet_names, + int character_pet_name_id + ) + { + for (auto &character_pet_name : character_pet_names) { + if (character_pet_name.char_id == character_pet_name_id) { + return character_pet_name; + } + } + + return NewEntity(); + } + + static CharacterPetName FindOne( + Database& db, + int character_pet_name_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + character_pet_name_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + CharacterPetName e{}; + + e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.name = row[1] ? row[1] : ""; + e.class_id = row[2] ? static_cast(atoi(row[2])) : -1; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int character_pet_name_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + character_pet_name_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const CharacterPetName &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[0] + " = " + std::to_string(e.char_id)); + v.push_back(columns[1] + " = '" + Strings::Escape(e.name) + "'"); + v.push_back(columns[2] + " = " + std::to_string(e.class_id)); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.char_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static CharacterPetName InsertOne( + Database& db, + CharacterPetName e + ) + { + std::vector v; + + v.push_back(std::to_string(e.char_id)); + v.push_back("'" + Strings::Escape(e.name) + "'"); + v.push_back(std::to_string(e.class_id)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.char_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.char_id)); + v.push_back("'" + Strings::Escape(e.name) + "'"); + v.push_back(std::to_string(e.class_id)); + + 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) { + CharacterPetName e{}; + + e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.name = row[1] ? row[1] : ""; + e.class_id = row[2] ? static_cast(atoi(row[2])) : -1; + + 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) { + CharacterPetName e{}; + + e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.name = row[1] ? row[1] : ""; + e.class_id = row[2] ? static_cast(atoi(row[2])) : -1; + + 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); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const CharacterPetName &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.char_id)); + v.push_back("'" + Strings::Escape(e.name) + "'"); + v.push_back(std::to_string(e.class_id)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.char_id)); + v.push_back("'" + Strings::Escape(e.name) + "'"); + v.push_back(std::to_string(e.class_id)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_CHARACTER_PET_NAME_REPOSITORY_H diff --git a/common/repositories/character_pet_name_repository.h b/common/repositories/character_pet_name_repository.h new file mode 100644 index 0000000000..47c6279a9d --- /dev/null +++ b/common/repositories/character_pet_name_repository.h @@ -0,0 +1,50 @@ +#ifndef EQEMU_CHARACTER_PET_NAME_REPOSITORY_H +#define EQEMU_CHARACTER_PET_NAME_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_character_pet_name_repository.h" + +class CharacterPetNameRepository: public BaseCharacterPetNameRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * CharacterPetNameRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * CharacterPetNameRepository::GetWhereNeverExpires() + * CharacterPetNameRepository::GetWhereXAndY() + * CharacterPetNameRepository::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 + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_CHARACTER_PET_NAME_REPOSITORY_H diff --git a/common/version.h b/common/version.h index 671a0c921d..a99b965751 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 9292 +#define CURRENT_BINARY_DATABASE_VERSION 9293 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9045 #endif diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 93d6f04637..a4e6d68ed9 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -735,3 +735,12 @@ OP_PickZone=0xaaba #evolve item related OP_EvolveItem=0x7cfb + +# This is bugged in ROF2 +# OP_InvokeChangePetNameImmediate is supposed to write DisablePetNameChangeReminder=0 to reset the 'nag reminder' +# It actually sets DisableNameChangeReminder=0 (player name change nag reminder). Additionally, clicking the +# 'Don't remind me later' button sets DisableNameChangeReminder=1 +# This can be fixed with a client patch. +OP_InvokeChangePetNameImmediate=0x046d +OP_InvokeChangePetName=0x4506 +OP_ChangePetName=0x5dab \ No newline at end of file diff --git a/zone/client.cpp b/zone/client.cpp index 697d7c752a..797a8938ab 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -1,19 +1,19 @@ /* EQEMu: Everquest Server Emulator - Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) + Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; version 2 of the License. + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY except by those people which sell it, which - are required to give you total support for your newly bought product; - without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY except by those people which sell it, which + are required to give you total support for your newly bought product; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "../common/global_define.h" #include @@ -23,10 +23,10 @@ // for windows compile #ifndef _WINDOWS - #include - #include - #include - #include "../common/unix.h" + #include + #include + #include + #include "../common/unix.h" #endif extern volatile bool RunLoops; @@ -70,11 +70,13 @@ extern volatile bool RunLoops; #include "../common/repositories/inventory_repository.h" #include "../common/repositories/keyring_repository.h" #include "../common/repositories/tradeskill_recipe_repository.h" +#include "../common/repositories/character_pet_name_repository.h" #include "../common/events/player_events.h" #include "../common/events/player_event_logs.h" #include "dialogue_window.h" #include "../common/zone_store.h" #include "../common/skill_caps.h" +#include "client.h" extern QueryServ* QServ; @@ -88,62 +90,62 @@ extern PetitionList petition_list; void UpdateWindowTitle(char* iNewTitle); Client::Client(EQStreamInterface *ieqs) : Mob( - "No name", // in_name - "", // in_lastname - 0, // in_cur_hp - 0, // in_max_hp - Gender::Male, // in_gender - Race::Doug, // in_race - Class::None, // in_class - BodyType::Humanoid, // in_bodytype - Deity::Unknown, // in_deity - 0, // in_level - 0, // in_npctype_id - 0.0f, // in_size - 0.7f, // in_runspeed - glm::vec4(), // position - 0, // in_light - 0xFF, // in_texture - 0xFF, // in_helmtexture - 0, // in_ac - 0, // in_atk - 0, // in_str - 0, // in_sta - 0, // in_dex - 0, // in_agi - 0, // in_int - 0, // in_wis - 0, // in_cha - 0, // in_haircolor - 0, // in_beardcolor - 0, // in_eyecolor1 - 0, // in_eyecolor2 - 0, // in_hairstyle - 0, // in_luclinface - 0, // in_beard - 0, // in_drakkin_heritage - 0, // in_drakkin_tattoo - 0, // in_drakkin_details - EQ::TintProfile(), // in_armor_tint - 0xff, // in_aa_title - 0, // in_see_invis - 0, // in_see_invis_undead - 0, // in_see_hide - 0, // in_see_improved_hide - 0, // in_hp_regen - 0, // in_mana_regen - 0, // in_qglobal - 0, // in_maxlevel - 0, // in_scalerate - 0, // in_armtexture - 0, // in_bracertexture - 0, // in_handtexture - 0, // in_legtexture - 0, // in_feettexture - 0, // in_usemodel - false, // in_always_aggros_foes - 0, // in_heroic_strikethrough - false // in_keeps_sold_items + "No name", // in_name + "", // in_lastname + 0, // in_cur_hp + 0, // in_max_hp + Gender::Male, // in_gender + Race::Doug, // in_race + Class::None, // in_class + BodyType::Humanoid, // in_bodytype + Deity::Unknown, // in_deity + 0, // in_level + 0, // in_npctype_id + 0.0f, // in_size + 0.7f, // in_runspeed + glm::vec4(), // position + 0, // in_light + 0xFF, // in_texture + 0xFF, // in_helmtexture + 0, // in_ac + 0, // in_atk + 0, // in_str + 0, // in_sta + 0, // in_dex + 0, // in_agi + 0, // in_int + 0, // in_wis + 0, // in_cha + 0, // in_haircolor + 0, // in_beardcolor + 0, // in_eyecolor1 + 0, // in_eyecolor2 + 0, // in_hairstyle + 0, // in_luclinface + 0, // in_beard + 0, // in_drakkin_heritage + 0, // in_drakkin_tattoo + 0, // in_drakkin_details + EQ::TintProfile(), // in_armor_tint + 0xff, // in_aa_title + 0, // in_see_invis + 0, // in_see_invis_undead + 0, // in_see_hide + 0, // in_see_improved_hide + 0, // in_hp_regen + 0, // in_mana_regen + 0, // in_qglobal + 0, // in_maxlevel + 0, // in_scalerate + 0, // in_armtexture + 0, // in_bracertexture + 0, // in_handtexture + 0, // in_legtexture + 0, // in_feettexture + 0, // in_usemodel + false, // in_always_aggros_foes + 0, // in_heroic_strikethrough + false // in_keeps_sold_items ), hpupdate_timer(2000), camp_timer(29000), @@ -188,12756 +190,12831 @@ Client::Client(EQStreamInterface *ieqs) : Mob( lazy_load_bank_check_timer(1000), bandolier_throttle_timer(0) { - for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) { - SetFilter(client_filter, FilterShow); - } - - cheat_manager.SetClient(this); - mMovementManager->AddClient(this); - character_id = 0; - conn_state = NoPacketsReceived; - client_data_loaded = false; - berserk = false; - dead = false; - eqs = ieqs; - ip = eqs->GetRemoteIP(); - port = ntohs(eqs->GetRemotePort()); - client_state = CLIENT_CONNECTING; - SetTrader(false); - Haste = 0; - SetCustomerID(0); - SetTraderID(0); - TrackingID = 0; - WID = 0; - account_id = 0; - admin = AccountStatus::Player; - lsaccountid = 0; - guild_id = GUILD_NONE; - guildrank = 0; - guild_tribute_opt_in = 0; - SetGuildListDirty(false); - GuildBanker = false; - memset(lskey, 0, sizeof(lskey)); - strcpy(account_name, ""); - tellsoff = false; - last_reported_mana = 0; - last_reported_endurance = 0; - last_reported_endurance_percent = 0; - last_reported_mana_percent = 0; - gm_hide_me = false; - AFK = false; - LFG = false; - LFGFromLevel = 0; - LFGToLevel = 0; - LFGMatchFilter = false; - LFGComments[0] = '\0'; - LFP = false; - gmspeed = 0; - gminvul = false; - playeraction = 0; - SetTarget(0); - auto_attack = false; - auto_fire = false; - runmode = false; - linkdead_timer.Disable(); - zonesummon_id = 0; - zonesummon_ignorerestrictions = 0; - bZoning = false; - m_lock_save_position = false; - zone_mode = ZoneUnsolicited; - casting_spell_id = 0; - npcflag = false; - npclevel = 0; - fishing_timer.Disable(); - dead_timer.Disable(); - camp_timer.Disable(); - autosave_timer.Disable(); - GetMercTimer()->Disable(); - instalog = false; - m_pp.autosplit = false; - // initialise haste variable - m_tradeskill_object = nullptr; - delaytimer = false; - PendingRezzXP = -1; - PendingRezzDBID = 0; - PendingRezzSpellID = 0; - numclients++; - // emuerror; - UpdateWindowTitle(nullptr); - horseId = 0; - tgb = false; - tribute_master_id = 0xFFFFFFFF; - tribute_timer.Disable(); - task_state = nullptr; - TotalSecondsPlayed = 0; - keyring.clear(); - bind_sight_target = nullptr; - p_raid_instance = nullptr; - mercid = 0; - mercSlot = 0; - InitializeMercInfo(); - SetMerc(0); - if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) SetPVP(true, false); - dynamiczone_removal_timer.Disable(); - - //for good measure: - memset(&m_pp, 0, sizeof(m_pp)); - memset(&m_epp, 0, sizeof(m_epp)); - PendingTranslocate = false; - PendingSacrifice = false; - sacrifice_caster_id = 0; - controlling_boat_id = 0; - controlled_mob_id = 0; - qGlobals = nullptr; - - if (!RuleB(Character, PerCharacterQglobalMaxLevel) && !RuleB(Character, PerCharacterBucketMaxLevel)) { - SetClientMaxLevel(0); - } else if (RuleB(Character, PerCharacterQglobalMaxLevel)) { - SetClientMaxLevel(GetCharMaxLevelFromQGlobal()); - } else if (RuleB(Character, PerCharacterBucketMaxLevel)) { - SetClientMaxLevel(GetCharMaxLevelFromBucket()); - } - - KarmaUpdateTimer = new Timer(RuleI(Chat, KarmaUpdateIntervalMS)); - GlobalChatLimiterTimer = new Timer(RuleI(Chat, IntervalDurationMS)); - AttemptedMessages = 0; - TotalKarma = 0; - m_ClientVersion = EQ::versions::ClientVersion::Unknown; - m_ClientVersionBit = 0; - AggroCount = 0; - ooc_regen = false; - AreaHPRegen = 1.0f; - AreaManaRegen = 1.0f; - AreaEndRegen = 1.0f; - XPRate = 100; - current_endurance = 0; - - CanUseReport = true; - aa_los_them_mob = nullptr; - los_status = false; - los_status_facing = false; - HideCorpseMode = HideCorpseNone; - PendingGuildInvitation = false; - - InitializeBuffSlots(); - - adventure_request_timer = nullptr; - adventure_create_timer = nullptr; - adventure_leave_timer = nullptr; - adventure_door_timer = nullptr; - adv_requested_data = nullptr; - adventure_stats_timer = nullptr; - adventure_leaderboard_timer = nullptr; - adv_data = nullptr; - adv_requested_theme = LDoNTheme::Unused; - adv_requested_id = 0; - adv_requested_member_count = 0; - - for(int i = 0; i < XTARGET_HARDCAP; ++i) - { - XTargets[i].Type = Auto; - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - XTargets[i].dirty = false; - } - MaxXTargets = 5; - XTargetAutoAddHaters = true; - m_autohatermgr.SetOwner(this, nullptr, nullptr); - m_activeautohatermgr = &m_autohatermgr; - - initial_respawn_selection = 0; - alternate_currency_loaded = false; - - interrogateinv_flag = false; - - trapid = 0; - - for (int i = 0; i < InnateSkillMax; ++i) - m_pp.InnateSkills[i] = InnateDisabled; + for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) { + SetFilter(client_filter, FilterShow); + } + + cheat_manager.SetClient(this); + mMovementManager->AddClient(this); + character_id = 0; + conn_state = NoPacketsReceived; + client_data_loaded = false; + berserk = false; + dead = false; + eqs = ieqs; + ip = eqs->GetRemoteIP(); + port = ntohs(eqs->GetRemotePort()); + client_state = CLIENT_CONNECTING; + SetTrader(false); + Haste = 0; + SetCustomerID(0); + SetTraderID(0); + TrackingID = 0; + WID = 0; + account_id = 0; + admin = AccountStatus::Player; + lsaccountid = 0; + guild_id = GUILD_NONE; + guildrank = 0; + guild_tribute_opt_in = 0; + SetGuildListDirty(false); + GuildBanker = false; + memset(lskey, 0, sizeof(lskey)); + strcpy(account_name, ""); + tellsoff = false; + last_reported_mana = 0; + last_reported_endurance = 0; + last_reported_endurance_percent = 0; + last_reported_mana_percent = 0; + gm_hide_me = false; + AFK = false; + LFG = false; + LFGFromLevel = 0; + LFGToLevel = 0; + LFGMatchFilter = false; + LFGComments[0] = '\0'; + LFP = false; + gmspeed = 0; + gminvul = false; + playeraction = 0; + SetTarget(0); + auto_attack = false; + auto_fire = false; + runmode = false; + linkdead_timer.Disable(); + zonesummon_id = 0; + zonesummon_ignorerestrictions = 0; + bZoning = false; + m_lock_save_position = false; + zone_mode = ZoneUnsolicited; + casting_spell_id = 0; + npcflag = false; + npclevel = 0; + fishing_timer.Disable(); + dead_timer.Disable(); + camp_timer.Disable(); + autosave_timer.Disable(); + GetMercTimer()->Disable(); + instalog = false; + m_pp.autosplit = false; + // initialise haste variable + m_tradeskill_object = nullptr; + delaytimer = false; + PendingRezzXP = -1; + PendingRezzDBID = 0; + PendingRezzSpellID = 0; + numclients++; + // emuerror; + UpdateWindowTitle(nullptr); + horseId = 0; + tgb = false; + tribute_master_id = 0xFFFFFFFF; + tribute_timer.Disable(); + task_state = nullptr; + TotalSecondsPlayed = 0; + keyring.clear(); + bind_sight_target = nullptr; + p_raid_instance = nullptr; + mercid = 0; + mercSlot = 0; + InitializeMercInfo(); + SetMerc(0); + if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) SetPVP(true, false); + dynamiczone_removal_timer.Disable(); + + //for good measure: + memset(&m_pp, 0, sizeof(m_pp)); + memset(&m_epp, 0, sizeof(m_epp)); + PendingTranslocate = false; + PendingSacrifice = false; + sacrifice_caster_id = 0; + controlling_boat_id = 0; + controlled_mob_id = 0; + qGlobals = nullptr; + + if (!RuleB(Character, PerCharacterQglobalMaxLevel) && !RuleB(Character, PerCharacterBucketMaxLevel)) { + SetClientMaxLevel(0); + } else if (RuleB(Character, PerCharacterQglobalMaxLevel)) { + SetClientMaxLevel(GetCharMaxLevelFromQGlobal()); + } else if (RuleB(Character, PerCharacterBucketMaxLevel)) { + SetClientMaxLevel(GetCharMaxLevelFromBucket()); + } + + KarmaUpdateTimer = new Timer(RuleI(Chat, KarmaUpdateIntervalMS)); + GlobalChatLimiterTimer = new Timer(RuleI(Chat, IntervalDurationMS)); + AttemptedMessages = 0; + TotalKarma = 0; + m_ClientVersion = EQ::versions::ClientVersion::Unknown; + m_ClientVersionBit = 0; + AggroCount = 0; + ooc_regen = false; + AreaHPRegen = 1.0f; + AreaManaRegen = 1.0f; + AreaEndRegen = 1.0f; + XPRate = 100; + current_endurance = 0; + + CanUseReport = true; + aa_los_them_mob = nullptr; + los_status = false; + los_status_facing = false; + HideCorpseMode = HideCorpseNone; + PendingGuildInvitation = false; + + InitializeBuffSlots(); + + adventure_request_timer = nullptr; + adventure_create_timer = nullptr; + adventure_leave_timer = nullptr; + adventure_door_timer = nullptr; + adv_requested_data = nullptr; + adventure_stats_timer = nullptr; + adventure_leaderboard_timer = nullptr; + adv_data = nullptr; + adv_requested_theme = LDoNTheme::Unused; + adv_requested_id = 0; + adv_requested_member_count = 0; + + for(int i = 0; i < XTARGET_HARDCAP; ++i) + { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + XTargets[i].dirty = false; + } + MaxXTargets = 5; + XTargetAutoAddHaters = true; + m_autohatermgr.SetOwner(this, nullptr, nullptr); + m_activeautohatermgr = &m_autohatermgr; + + initial_respawn_selection = 0; + alternate_currency_loaded = false; + + interrogateinv_flag = false; + + trapid = 0; + + for (int i = 0; i < InnateSkillMax; ++i) + m_pp.InnateSkills[i] = InnateDisabled; + + temp_pvp = false; + + moving = false; + + environment_damage_modifier = 0; + invulnerable_environment_damage = false; + + // rate limiter + m_list_task_timers_rate_limit.Start(1000); + + // gm + SetDisplayMobInfoWindow(true); + SetDevToolsEnabled(true); + + bot_owner_options[booDeathMarquee] = false; + bot_owner_options[booStatsUpdate] = false; + bot_owner_options[booSpawnMessageSay] = false; + bot_owner_options[booSpawnMessageTell] = true; + bot_owner_options[booSpawnMessageClassSpecific] = true; + bot_owner_options[booAutoDefend] = RuleB(Bots, AllowOwnerOptionAutoDefend); + bot_owner_options[booBuffCounter] = false; + bot_owner_options[booMonkWuMessage] = false; + + m_parcel_platinum = 0; + m_parcel_gold = 0; + m_parcel_silver = 0; + m_parcel_copper = 0; + m_parcel_count = 0; + m_parcel_enabled = true; + m_parcel_merchant_engaged = false; + m_parcels.clear(); + + m_buyer_id = 0; + + SetBotPulling(false); + SetBotPrecombat(false); + + AI_Init(); +} - temp_pvp = false; +Client::~Client() { + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) { + DoParcelCancel(); + } - moving = false; + mMovementManager->RemoveClient(this); - environment_damage_modifier = 0; - invulnerable_environment_damage = false; + DataBucket::DeleteCachedBuckets(DataBucketLoadType::Account, AccountID()); + DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID()); - // rate limiter - m_list_task_timers_rate_limit.Start(1000); + if (RuleB(Bots, Enabled)) { + Bot::ProcessBotOwnerRefDelete(this); + } + + if (zone) { + zone->ClearEXPModifier(this); + } + + if (!IsZoning()) { + if(IsInAGuild()) { + guild_mgr.UpdateDbMemberOnline(CharacterID(), false); + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); + } + } + + Mob* horse = entity_list.GetMob(CastToClient()->GetHorseId()); + if (horse) + horse->Depop(); + + Mob* merc = entity_list.GetMob(GetMercenaryID()); + if (merc) + merc->Depop(); + + if(IsTrader()) { + TraderEndTrader(); + } + + if(IsBuyer()) { + ToggleBuyerMode(false); + } + + if(conn_state != ClientConnectFinished) { + LogDebug("Client [{}] was destroyed before reaching the connected state:", GetName()); + ReportConnectingState(); + } + + if(m_tradeskill_object != nullptr) { + m_tradeskill_object->Close(); + m_tradeskill_object = nullptr; + } + + m_close_mobs.clear(); + + if(IsDueling() && GetDuelTarget() != 0) { + Entity* entity = entity_list.GetID(GetDuelTarget()); + if(entity != nullptr && entity->IsClient()) { + entity->CastToClient()->SetDueling(false); + entity->CastToClient()->SetDuelTarget(0); + entity_list.DuelMessage(entity->CastToClient(),this,true); + } + } + + if(GetTarget()) + GetTarget()->IsTargeted(-1); + + //if we are in a group and we are not zoning, force leave the group + if(isgrouped && !bZoning && is_zone_loaded) + LeaveGroup(); + + UpdateWho(2); + + if(IsHoveringForRespawn()) + { + m_pp.zone_id = m_pp.binds[0].zone_id; + m_pp.zoneInstance = m_pp.binds[0].instance_id; + m_Position.x = m_pp.binds[0].x; + m_Position.y = m_pp.binds[0].y; + m_Position.z = m_pp.binds[0].z; + } + + // we save right now, because the client might be zoning and the world + // will need this data right away + Save(2); // This fails when database destructor is called first on shutdown + + safe_delete(task_state); + safe_delete(KarmaUpdateTimer); + safe_delete(GlobalChatLimiterTimer); + safe_delete(qGlobals); + safe_delete(adventure_request_timer); + safe_delete(adventure_create_timer); + safe_delete(adventure_leave_timer); + safe_delete(adventure_door_timer); + safe_delete(adventure_stats_timer); + safe_delete(adventure_leaderboard_timer); + safe_delete_array(adv_requested_data); + safe_delete_array(adv_data); + + ClearRespawnOptions(); + + numclients--; + UpdateWindowTitle(nullptr); + if(zone) + zone->RemoveAuth(GetName(), lskey); + + //let the stream factory know were done with this stream + eqs->Close(); + eqs->ReleaseFromUse(); + safe_delete(eqs); + + UninitializeBuffSlots(); +} - // gm - SetDisplayMobInfoWindow(true); - SetDevToolsEnabled(true); +void Client::SendZoneInPackets() +{ - bot_owner_options[booDeathMarquee] = false; - bot_owner_options[booStatsUpdate] = false; - bot_owner_options[booSpawnMessageSay] = false; - bot_owner_options[booSpawnMessageTell] = true; - bot_owner_options[booSpawnMessageClassSpecific] = true; - bot_owner_options[booAutoDefend] = RuleB(Bots, AllowOwnerOptionAutoDefend); - bot_owner_options[booBuffCounter] = false; - bot_owner_options[booMonkWuMessage] = false; + ////////////////////////////////////////////////////// + // Spawn Appearance Packet + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* sa = (SpawnAppearance_Struct*)outapp->pBuffer; + sa->type = AppearanceType::SpawnID; // Is 0x10 used to set the player id? + sa->parameter = GetID(); // Four bytes for this parameter... + outapp->priority = 6; + QueuePacket(outapp); + safe_delete(outapp); + + // Inform the world about the client + outapp = new EQApplicationPacket(); + + CreateSpawnPacket(outapp); + outapp->priority = 6; + if (!GetHideMe()) entity_list.QueueClients(this, outapp, true); + safe_delete(outapp); + SetSpawned(); + if (GetPVP(false)) //force a PVP update until we fix the spawn struct + SendAppearancePacket(AppearanceType::PVP, GetPVP(false), true, false); + + //Send AA Exp packet: + if (GetLevel() >= 51) + SendAlternateAdvancementStats(); + + // Send exp packets + outapp = new EQApplicationPacket(OP_ExpUpdate, sizeof(ExpUpdate_Struct)); + ExpUpdate_Struct* eu = (ExpUpdate_Struct*)outapp->pBuffer; + uint32 tmpxp1 = GetEXPForLevel(GetLevel() + 1); + uint32 tmpxp2 = GetEXPForLevel(GetLevel()); + + // Crash bug fix... Divide by zero when tmpxp1 and 2 equalled each other, most likely the error case from GetEXPForLevel() (invalid class, etc) + if (tmpxp1 != tmpxp2 && tmpxp1 != 0xFFFFFFFF && tmpxp2 != 0xFFFFFFFF) { + float tmpxp = (float)((float)m_pp.exp - tmpxp2) / ((float)tmpxp1 - tmpxp2); + eu->exp = (uint32)(330.0f * tmpxp); + outapp->priority = 6; + QueuePacket(outapp); + } + safe_delete(outapp); + + SendAlternateAdvancementTimers(); + + outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(ZoneInSendName_Struct)); + ZoneInSendName_Struct* zonesendname = (ZoneInSendName_Struct*)outapp->pBuffer; + strcpy(zonesendname->name, m_pp.name); + strcpy(zonesendname->name2, m_pp.name); + zonesendname->unknown0 = 0x0A; + QueuePacket(outapp); + safe_delete(outapp); + + if (IsInAGuild()) { + guild_mgr.UpdateDbMemberOnline(CharacterID(), true); + //SendGuildMembers(); + SendGuildURL(); + SendGuildChannel(); + SendGuildLFGuildStatus(); + } + SendLFGuildStatus(); + + //No idea why live sends this if even were not in a guild + SendGuildMOTD(); +} - m_parcel_platinum = 0; - m_parcel_gold = 0; - m_parcel_silver = 0; - m_parcel_copper = 0; - m_parcel_count = 0; - m_parcel_enabled = true; - m_parcel_merchant_engaged = false; - m_parcels.clear(); +void Client::SendLogoutPackets() { - m_buyer_id = 0; + auto outapp = new EQApplicationPacket(OP_CancelTrade, sizeof(CancelTrade_Struct)); + CancelTrade_Struct* ct = (CancelTrade_Struct*) outapp->pBuffer; + ct->fromid = GetID(); + ct->action = groupActUpdate; + FastQueuePacket(&outapp); - SetBotPulling(false); - SetBotPrecombat(false); + outapp = new EQApplicationPacket(OP_PreLogoutReply); + FastQueuePacket(&outapp); - AI_Init(); } -Client::~Client() { - if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) { - DoParcelCancel(); - } - - mMovementManager->RemoveClient(this); - - DataBucket::DeleteCachedBuckets(DataBucketLoadType::Account, AccountID()); - DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID()); +void Client::ReportConnectingState() { + switch(conn_state) { + case NoPacketsReceived: //havent gotten anything + LogDebug("Client has not sent us an initial zone entry packet"); + break; + case ReceivedZoneEntry: //got the first packet, loading up PP + LogDebug("Client sent initial zone packet, but we never got their player info from the database"); + break; + case PlayerProfileLoaded: //our DB work is done, sending it + LogDebug("We were sending the player profile, tributes, tasks, spawns, time and weather, but never finished"); + break; + case ZoneInfoSent: //includes PP, tributes, tasks, spawns, time and weather + LogDebug("We successfully sent player info and spawns, waiting for client to request new zone"); + break; + case NewZoneRequested: //received and sent new zone request + LogDebug("We received client's new zone request, waiting for client spawn request"); + break; + case ClientSpawnRequested: //client sent ReqClientSpawn + LogDebug("We received the client spawn request, and were sending objects, doors, zone points and some other stuff, but never finished"); + break; + case ZoneContentsSent: //objects, doors, zone points + LogDebug("The rest of the zone contents were successfully sent, waiting for client ready notification"); + break; + case ClientReadyReceived: //client told us its ready, send them a bunch of crap like guild MOTD, etc + LogDebug("We received client ready notification, but never finished Client::CompleteConnect"); + break; + case ClientConnectFinished: //client finally moved to finished state, were done here + LogDebug("Client is successfully connected"); + break; + }; +} - if (RuleB(Bots, Enabled)) { - Bot::ProcessBotOwnerRefDelete(this); - } +bool Client::SaveAA() +{ + std::vector v; - if (zone) { - zone->ClearEXPModifier(this); - } + uint32 aa_points_spent = 0; - if (!IsZoning()) { - if(IsInAGuild()) { - guild_mgr.UpdateDbMemberOnline(CharacterID(), false); - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); - } - } + auto e = CharacterAlternateAbilitiesRepository::NewEntity(); - Mob* horse = entity_list.GetMob(CastToClient()->GetHorseId()); - if (horse) - horse->Depop(); + for (auto &rank : aa_ranks) { + auto a = zone->GetAlternateAdvancementAbility(rank.first); + if (!a) { + continue; + } - Mob* merc = entity_list.GetMob(GetMercenaryID()); - if (merc) - merc->Depop(); + if (rank.second.first > 0) { + auto r = a->GetRankByPointsSpent(rank.second.first); + if (!r) { + continue; + } - if(IsTrader()) { - TraderEndTrader(); - } + aa_points_spent += r->total_cost; - if(IsBuyer()) { - ToggleBuyerMode(false); - } + e.id = character_id; + e.aa_id = a->first_rank_id; + e.aa_value = rank.second.first; + e.charges = rank.second.second; - if(conn_state != ClientConnectFinished) { - LogDebug("Client [{}] was destroyed before reaching the connected state:", GetName()); - ReportConnectingState(); - } + v.emplace_back(e); + } + } - if(m_tradeskill_object != nullptr) { - m_tradeskill_object->Close(); - m_tradeskill_object = nullptr; - } + m_pp.aapoints_spent = aa_points_spent + m_epp.expended_aa; - m_close_mobs.clear(); + return CharacterAlternateAbilitiesRepository::ReplaceMany(database, v); +} - if(IsDueling() && GetDuelTarget() != 0) { - Entity* entity = entity_list.GetID(GetDuelTarget()); - if(entity != nullptr && entity->IsClient()) { - entity->CastToClient()->SetDueling(false); - entity->CastToClient()->SetDuelTarget(0); - entity_list.DuelMessage(entity->CastToClient(),this,true); - } - } +void Client::RemoveExpendedAA(int aa_id) +{ + CharacterAlternateAbilitiesRepository::DeleteWhere( + database, + fmt::format( + "`id` = {} AND `aa_id` = {}", + CharacterID(), + aa_id + ) + ); +} - if(GetTarget()) - GetTarget()->IsTargeted(-1); +bool Client::Save(uint8 iCommitNow) { + if(!ClientDataLoaded()) + return false; + + /* Wrote current basics to PP for saves */ + if (!m_lock_save_position) { + m_pp.x = m_Position.x; + m_pp.y = m_Position.y; + m_pp.z = m_Position.z; + m_pp.heading = m_Position.w; + } + + m_pp.guildrank = guildrank; + + if (dead && GetHP() <= 0) { + m_pp.cur_hp = GetMaxHP(); + m_pp.mana = current_mana; + if (RuleB(Character, FullManaOnDeath)) { + m_pp.mana = GetMaxMana(); + } + + m_pp.endurance = current_endurance; + if (RuleB(Character, FullEndurOnDeath)) { + m_pp.endurance = GetMaxEndurance(); + } + } else { // Otherwise, no changes. + m_pp.cur_hp = GetHP(); + m_pp.mana = current_mana; + m_pp.endurance = current_endurance; + } + + /* Save Character Currency */ + database.SaveCharacterCurrency(CharacterID(), &m_pp); + + // save character binds + // this may not need to be called in Save() but it's here for now + // to maintain the current behavior + database.SaveCharacterBinds(this); + + /* Save Character Buffs */ + database.SaveBuffs(this); + + /* Total Time Played */ + TotalSecondsPlayed += (time(nullptr) - m_pp.lastlogin); + m_pp.timePlayedMin = (TotalSecondsPlayed / 60); + m_pp.RestTimer = GetRestTimer(); + + /* Save Mercs */ + if (GetMercInfo().MercTimerRemaining > RuleI(Mercs, UpkeepIntervalMS)) { + GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS); + } + + if (GetMercTimer()->Enabled()) { + GetMercInfo().MercTimerRemaining = GetMercTimer()->GetRemainingTime(); + } + + if (dead || (!GetMerc() && !GetMercInfo().IsSuspended)) { + memset(&m_mercinfo, 0, sizeof(struct MercInfo)); + } + + m_pp.lastlogin = time(nullptr); + + if (GetPet() && GetPet()->CastToNPC()->GetPetSpellID() && !dead) { + NPC *pet = GetPet()->CastToNPC(); + m_petinfo.SpellID = pet->CastToNPC()->GetPetSpellID(); + m_petinfo.HP = pet->GetHP(); + m_petinfo.Mana = pet->GetMana(); + pet->GetPetState(m_petinfo.Buffs, m_petinfo.Items, m_petinfo.Name); + m_petinfo.petpower = pet->GetPetPower(); + m_petinfo.size = pet->GetSize(); + m_petinfo.taunting = pet->CastToNPC()->IsTaunting(); + } else { + memset(&m_petinfo, 0, sizeof(struct PetInfo)); + } + database.SavePetInfo(this); + + if(tribute_timer.Enabled()) { + m_pp.tribute_time_remaining = tribute_timer.GetRemainingTime(); + } + else { + m_pp.tribute_time_remaining = 0xFFFFFFFF; m_pp.tribute_active = 0; + } + + if (m_pp.hunger_level < 0) + m_pp.hunger_level = 0; + + if (m_pp.thirst_level < 0) + m_pp.thirst_level = 0; + + p_timers.Store(&database); + + database.SaveCharacterTribute(this); + SaveTaskState(); /* Save Character Task */ + + LogFood("Client::Save - hunger_level: [{}] thirst_level: [{}]", m_pp.hunger_level, m_pp.thirst_level); + + // perform snapshot before SaveCharacterData() so that m_epp will contain the updated time + if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { + if (database.SaveCharacterInvSnapshot(CharacterID())) { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); + } + else { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); + } + } + + database.SaveCharacterData(this, &m_pp, &m_epp); /* Save Character Data */ + + database.SaveCharacterEXPModifier(this); + + return true; +} - //if we are in a group and we are not zoning, force leave the group - if(isgrouped && !bZoning && is_zone_loaded) - LeaveGroup(); +CLIENTPACKET::CLIENTPACKET() +{ + app = nullptr; + ack_req = false; +} - UpdateWho(2); +CLIENTPACKET::~CLIENTPACKET() +{ + safe_delete(app); +} - if(IsHoveringForRespawn()) - { - m_pp.zone_id = m_pp.binds[0].zone_id; - m_pp.zoneInstance = m_pp.binds[0].instance_id; - m_Position.x = m_pp.binds[0].x; - m_Position.y = m_pp.binds[0].y; - m_Position.z = m_pp.binds[0].z; - } +//this assumes we do not own pApp, and clones it. +bool Client::AddPacket(const EQApplicationPacket *pApp, bool bAckreq) { + if (!pApp) + return false; + if(!zoneinpacket_timer.Enabled()) { + //drop the packet because it will never get sent. + return(false); + } - // we save right now, because the client might be zoning and the world - // will need this data right away - Save(2); // This fails when database destructor is called first on shutdown + auto c = std::make_unique(); - safe_delete(task_state); - safe_delete(KarmaUpdateTimer); - safe_delete(GlobalChatLimiterTimer); - safe_delete(qGlobals); - safe_delete(adventure_request_timer); - safe_delete(adventure_create_timer); - safe_delete(adventure_leave_timer); - safe_delete(adventure_door_timer); - safe_delete(adventure_stats_timer); - safe_delete(adventure_leaderboard_timer); - safe_delete_array(adv_requested_data); - safe_delete_array(adv_data); + c->ack_req = bAckreq; + c->app = pApp->Copy(); - ClearRespawnOptions(); + clientpackets.push_back(std::move(c)); + return true; +} - numclients--; - UpdateWindowTitle(nullptr); - if(zone) - zone->RemoveAuth(GetName(), lskey); +//this assumes that it owns the object pointed to by *pApp +bool Client::AddPacket(EQApplicationPacket** pApp, bool bAckreq) { + if (!pApp || !(*pApp)) + return false; + if(!zoneinpacket_timer.Enabled()) { + //drop the packet because it will never get sent. + return(false); + } + auto c = std::make_unique(); - //let the stream factory know were done with this stream - eqs->Close(); - eqs->ReleaseFromUse(); - safe_delete(eqs); + c->ack_req = bAckreq; + c->app = *pApp; + *pApp = nullptr; - UninitializeBuffSlots(); + clientpackets.push_back(std::move(c)); + return true; } -void Client::SendZoneInPackets() -{ - - ////////////////////////////////////////////////////// - // Spawn Appearance Packet - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* sa = (SpawnAppearance_Struct*)outapp->pBuffer; - sa->type = AppearanceType::SpawnID; // Is 0x10 used to set the player id? - sa->parameter = GetID(); // Four bytes for this parameter... - outapp->priority = 6; - QueuePacket(outapp); - safe_delete(outapp); - - // Inform the world about the client - outapp = new EQApplicationPacket(); - - CreateSpawnPacket(outapp); - outapp->priority = 6; - if (!GetHideMe()) entity_list.QueueClients(this, outapp, true); - safe_delete(outapp); - SetSpawned(); - if (GetPVP(false)) //force a PVP update until we fix the spawn struct - SendAppearancePacket(AppearanceType::PVP, GetPVP(false), true, false); - - //Send AA Exp packet: - if (GetLevel() >= 51) - SendAlternateAdvancementStats(); - - // Send exp packets - outapp = new EQApplicationPacket(OP_ExpUpdate, sizeof(ExpUpdate_Struct)); - ExpUpdate_Struct* eu = (ExpUpdate_Struct*)outapp->pBuffer; - uint32 tmpxp1 = GetEXPForLevel(GetLevel() + 1); - uint32 tmpxp2 = GetEXPForLevel(GetLevel()); - - // Crash bug fix... Divide by zero when tmpxp1 and 2 equalled each other, most likely the error case from GetEXPForLevel() (invalid class, etc) - if (tmpxp1 != tmpxp2 && tmpxp1 != 0xFFFFFFFF && tmpxp2 != 0xFFFFFFFF) { - float tmpxp = (float)((float)m_pp.exp - tmpxp2) / ((float)tmpxp1 - tmpxp2); - eu->exp = (uint32)(330.0f * tmpxp); - outapp->priority = 6; - QueuePacket(outapp); - } - safe_delete(outapp); - - SendAlternateAdvancementTimers(); - - outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(ZoneInSendName_Struct)); - ZoneInSendName_Struct* zonesendname = (ZoneInSendName_Struct*)outapp->pBuffer; - strcpy(zonesendname->name, m_pp.name); - strcpy(zonesendname->name2, m_pp.name); - zonesendname->unknown0 = 0x0A; - QueuePacket(outapp); - safe_delete(outapp); - - if (IsInAGuild()) { - guild_mgr.UpdateDbMemberOnline(CharacterID(), true); - //SendGuildMembers(); - SendGuildURL(); - SendGuildChannel(); - SendGuildLFGuildStatus(); - } - SendLFGuildStatus(); - - //No idea why live sends this if even were not in a guild - SendGuildMOTD(); +bool Client::SendAllPackets() { + CLIENTPACKET* cp = nullptr; + while (!clientpackets.empty()) { + cp = clientpackets.front().get(); + if(eqs) + eqs->FastQueuePacket((EQApplicationPacket **)&cp->app, cp->ack_req); + clientpackets.pop_front(); + } + return true; } -void Client::SendLogoutPackets() { +void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CONN_STATUS required_state, eqFilterType filter) { + if (filter != FilterNone && GetFilter(filter) == FilterHide) { + return; + } - auto outapp = new EQApplicationPacket(OP_CancelTrade, sizeof(CancelTrade_Struct)); - CancelTrade_Struct* ct = (CancelTrade_Struct*) outapp->pBuffer; - ct->fromid = GetID(); - ct->action = groupActUpdate; - FastQueuePacket(&outapp); + if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { + AddPacket(app, ack_req); + return; + } - outapp = new EQApplicationPacket(OP_PreLogoutReply); - FastQueuePacket(&outapp); + // if the program doesnt care about the status or if the status isnt what we requested + if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { + // todo: save packets for later use + AddPacket(app, ack_req); + } + else if (eqs) { + eqs->QueuePacket(app, ack_req); + } +} +void Client::FastQueuePacket(EQApplicationPacket** app, bool ack_req, CLIENT_CONN_STATUS required_state) { + // if the program doesnt care about the status or if the status isnt what we requested + if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { + // todo: save packets for later use + AddPacket(app, ack_req); + return; + } + else { + if(eqs) + eqs->FastQueuePacket((EQApplicationPacket **)app, ack_req); + else if (app && (*app)) + delete *app; + *app = nullptr; + } + return; } -void Client::ReportConnectingState() { - switch(conn_state) { - case NoPacketsReceived: //havent gotten anything - LogDebug("Client has not sent us an initial zone entry packet"); - break; - case ReceivedZoneEntry: //got the first packet, loading up PP - LogDebug("Client sent initial zone packet, but we never got their player info from the database"); - break; - case PlayerProfileLoaded: //our DB work is done, sending it - LogDebug("We were sending the player profile, tributes, tasks, spawns, time and weather, but never finished"); - break; - case ZoneInfoSent: //includes PP, tributes, tasks, spawns, time and weather - LogDebug("We successfully sent player info and spawns, waiting for client to request new zone"); - break; - case NewZoneRequested: //received and sent new zone request - LogDebug("We received client's new zone request, waiting for client spawn request"); - break; - case ClientSpawnRequested: //client sent ReqClientSpawn - LogDebug("We received the client spawn request, and were sending objects, doors, zone points and some other stuff, but never finished"); - break; - case ZoneContentsSent: //objects, doors, zone points - LogDebug("The rest of the zone contents were successfully sent, waiting for client ready notification"); - break; - case ClientReadyReceived: //client told us its ready, send them a bunch of crap like guild MOTD, etc - LogDebug("We received client ready notification, but never finished Client::CompleteConnect"); - break; - case ClientConnectFinished: //client finally moved to finished state, were done here - LogDebug("Client is successfully connected"); - break; - }; +void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_skill, const char* orig_message, const char* targetname, bool is_silent) { + char message[4096]; + strn0cpy(message, orig_message, sizeof(message)); + + LogDebug("Client::ChannelMessageReceived() Channel:[{}] message:[{}]", chan_num, message); + + if (targetname == nullptr) { + targetname = (!GetTarget()) ? "" : GetTarget()->GetName(); + } + + if(RuleB(Chat, EnableAntiSpam)) + { + if(strcmp(targetname, "discard") != 0) + { + if(chan_num == ChatChannel_Shout || chan_num == ChatChannel_Auction || chan_num == ChatChannel_OOC || chan_num == ChatChannel_Tell) + { + if(GlobalChatLimiterTimer) + { + if(GlobalChatLimiterTimer->Check(false)) + { + GlobalChatLimiterTimer->Start(RuleI(Chat, IntervalDurationMS)); + AttemptedMessages = 0; + } + } + + uint32 AllowedMessages = RuleI(Chat, MinimumMessagesPerInterval) + TotalKarma; + AllowedMessages = AllowedMessages > RuleI(Chat, MaximumMessagesPerInterval) ? RuleI(Chat, MaximumMessagesPerInterval) : AllowedMessages; + + if(RuleI(Chat, MinStatusToBypassAntiSpam) <= Admin()) + AllowedMessages = 10000; + + AttemptedMessages++; + if(AttemptedMessages > AllowedMessages) + { + if(AttemptedMessages > RuleI(Chat, MaxMessagesBeforeKick)) + { + Kick("Sent too many chat messages at once."); + return; + } + if(GlobalChatLimiterTimer) + { + Message(0, "You have been rate limited, you can send more messages in %i seconds.", + GlobalChatLimiterTimer->GetRemainingTime() / 1000); + return; + } + else + { + Message(0, "You have been rate limited, you can send more messages in 60 seconds."); + return; + } + } + } + } + } + + /* Logs Player Chat */ + if (RuleB(QueryServ, PlayerLogChat)) { + auto pack = new ServerPacket(ServerOP_Speech, sizeof(Server_Speech_Struct) + strlen(message) + 1); + Server_Speech_Struct* sem = (Server_Speech_Struct*) pack->pBuffer; + + if(chan_num == ChatChannel_Guild) + sem->guilddbid = GuildID(); + else + sem->guilddbid = 0; + + strcpy(sem->message, message); + sem->minstatus = Admin(); + sem->type = chan_num; + if(targetname != 0) + strcpy(sem->to, targetname); + + if(GetName() != 0) + strcpy(sem->from, GetName()); + + if(worldserver.Connected()) + worldserver.SendPacket(pack); + safe_delete(pack); + } + + // Garble the message based on drunkness + if (GetIntoxication() > 0 && !(RuleB(Chat, ServerWideOOC) && chan_num == ChatChannel_OOC) && !GetGM()) { + GarbleMessage(message, (int)(GetIntoxication() / 3)); + language = Language::CommonTongue; // No need for language when drunk + lang_skill = Language::MaxValue; + } + + // some channels don't use languages + if ( + chan_num == ChatChannel_OOC || + chan_num == ChatChannel_GMSAY || + chan_num == ChatChannel_Broadcast || + chan_num == ChatChannel_Petition + ) { + language = Language::CommonTongue; + lang_skill = Language::MaxValue; + } + + // Censor the message + if (EQ::ProfanityManager::IsCensorshipActive() && (chan_num != ChatChannel_Say)) + EQ::ProfanityManager::RedactMessage(message); + + switch(chan_num) + { + case ChatChannel_Guild: { /* Guild Chat */ + if (!IsInAGuild()) { + MessageString(Chat::DefaultText, GUILD_NOT_MEMBER2); //You are not a member of any guild. + } else if (!guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_GUILD_CHAT_SPEAK_IN)) { + MessageString(Chat::EchoGuild, NO_PROPER_ACCESS); + } else if (!worldserver.SendChannelMessage(this, targetname, chan_num, GuildID(), language, lang_skill, message)) { + Message(Chat::White, "Error: World server disconnected"); + } + break; + } + case ChatChannel_Group: { /* Group Chat */ + Raid* raid = entity_list.GetRaidByClient(this); + if(raid) { + raid->RaidGroupSay((const char*) message, this, language, lang_skill); + break; + } + + Group* group = GetGroup(); + if(group != nullptr) { + group->GroupMessage(this,language,lang_skill,(const char*) message); + } + break; + } + case ChatChannel_Raid: { /* Raid Say */ + Raid* raid = entity_list.GetRaidByClient(this); + if(raid){ + raid->RaidSay((const char*) message, this, language, lang_skill); + } + break; + } + case ChatChannel_Shout: { /* Shout */ + Mob *sender = this; + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) + sender = GetPet(); + + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + break; + } + case ChatChannel_Auction: { /* Auction */ + if(RuleB(Chat, ServerWideAuction)) + { + if(!global_channel_timer.Check()) + { + if(strlen(targetname) == 0) + ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? + else + return; + } + + if(GetRevoked()) + { + Message(0, "You have been revoked. You may not talk on Auction."); + return; + } + + if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) + { + if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) + { + Message(0, "You do not have permission to talk in Auction at this time."); + return; + } + } + + if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + } + else if(!RuleB(Chat, ServerWideAuction)) { + Mob *sender = this; + + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) + sender = GetPet(); + + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + } + break; + } + case ChatChannel_OOC: { /* OOC */ + if(RuleB(Chat, ServerWideOOC)) + { + if(!global_channel_timer.Check()) + { + if(strlen(targetname) == 0) + ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? + else + return; + } + if(worldserver.IsOOCMuted() && admin < AccountStatus::GMAdmin) + { + Message(0,"OOC has been muted. Try again later."); + return; + } + + if(GetRevoked()) + { + Message(0, "You have been revoked. You may not talk on OOC."); + return; + } + + if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) + { + if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) + { + Message(0, "You do not have permission to talk in OOC at this time."); + return; + } + } + + if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) + { + Message(0, "Error: World server disconnected"); + } + } + else if(!RuleB(Chat, ServerWideOOC)) + { + Mob *sender = this; + + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) + sender = GetPet(); + + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + } + break; + } + case ChatChannel_Broadcast: /* Broadcast */ + case ChatChannel_GMSAY: { /* GM Say */ + if (!(admin >= AccountStatus::QuestTroupe)) + Message(0, "Error: Only GMs can use this channel"); + else if (!worldserver.SendChannelMessage(this, targetname, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + break; + } + case ChatChannel_Tell: { /* Tell */ + if(!global_channel_timer.Check()) + { + if(strlen(targetname) == 0) + ChannelMessageReceived(ChatChannel_Tell, language, lang_skill, message, "discard"); //Fast typer or spammer?? + else + return; + } + + if(GetRevoked()) + { + Message(0, "You have been revoked. You may not send tells."); + return; + } + + if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) + { + if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) + { + Message(0, "You do not have permission to send tells at this time."); + return; + } + } + + char target_name[64]; + + if(targetname) + { + size_t i = strlen(targetname); + int x; + for(x = 0; x < i; ++x) + { + if(targetname[x] == '%') + { + target_name[x] = '/'; + } + else + { + target_name[x] = targetname[x]; + } + } + target_name[x] = '\0'; + } + + if(!worldserver.SendChannelMessage(this, target_name, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + 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)) { + int i = parse->EventPlayer(EVENT_COMMAND, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Command '%s' not recognized.", message); + } + } + else if (parse->PlayerHasQuestSub(EVENT_SAY)) { + int i = parse->EventPlayer(EVENT_SAY, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Command '%s' not recognized.", message); + } + } + else { + if (!RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Command '%s' not recognized.", message); + } + } + } + break; + } + + if (message[0] == BOT_COMMAND_CHAR) { + if (RuleB(Bots, Enabled)) { + if (bot_command_dispatch(this, message) == -2) { + if (parse->PlayerHasQuestSub(EVENT_BOT_COMMAND)) { + int i = parse->EventPlayer(EVENT_BOT_COMMAND, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Bot command '%s' not recognized.", message); + } + } + else if (parse->PlayerHasQuestSub(EVENT_SAY)) { + int i = parse->EventPlayer(EVENT_SAY, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Bot command '%s' not recognized.", message); + } + } + else { + if (!RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Bot command '%s' not recognized.", message); + } + } + } + } else { + Message(Chat::Red, "Bots are disabled on this server."); + } + break; + } + + if (EQ::ProfanityManager::IsCensorshipActive()) { + EQ::ProfanityManager::RedactMessage(message); + } + + Mob* sender = this; + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) { + sender = GetPet(); + } + + if (!is_silent) { + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + } + + if (parse->PlayerHasQuestSub(EVENT_SAY)) { + parse->EventPlayer(EVENT_SAY, this, message, language); + } + + if (sender != this) { + break; + } + + if (quest_manager.ProximitySayInUse()) { + entity_list.ProcessProximitySay(message, this, language); + } + + Mob* t = GetTarget(); + + if ( + t && + !IsInvisible(t) && + DistanceNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say) + ) { + const bool is_engaged = t->IsEngaged(); + + if (is_engaged) { + parse->EventBotMercNPC(EVENT_AGGRO_SAY, t, this, [&]() { return message; }, language); + } else { + parse->EventBotMercNPC(EVENT_SAY, t, this, [&]() { return message; }, language); + } + + if (t->IsNPC() && !is_engaged) { + CheckLDoNHail(t->CastToNPC()); + CheckEmoteHail(t->CastToNPC(), message); + + if (RuleB(TaskSystem, EnableTaskSystem)) { + if (UpdateTasksOnSpeakWith(t->CastToNPC())) { + t->CastToNPC()->DoQuestPause(this); + } + } + } + } + break; + } + case ChatChannel_UCSRelay: + { + // UCS Relay for Underfoot and later. + if(!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + break; + } + case ChatChannel_Emotes: + { + // Emotes for Underfoot and later. + // crash protection -- cheater + message[1023] = '\0'; + size_t msg_len = strlen(message); + if (msg_len > 512) + message[512] = '\0'; + + auto outapp = new EQApplicationPacket(OP_Emote, 4 + msg_len + strlen(GetName()) + 2); + Emote_Struct* es = (Emote_Struct*)outapp->pBuffer; + char *Buffer = (char *)es; + Buffer += 4; + snprintf(Buffer, sizeof(Emote_Struct) - 4, "%s %s", GetName(), message); + entity_list.QueueCloseClients(this, outapp, true, RuleI(Range, Emote), 0, true, FilterSocials); + safe_delete(outapp); + break; + } + default: { + Message(0, "Channel (%i) not implemented", (uint16)chan_num); + } + } } -bool Client::SaveAA() +void Client::ChannelMessageSend( + const char *from, + const char *to, + uint8 channel_id, + uint8 language_id, + uint8 language_skill, + const char *message, + ... +) { - std::vector v; + if ( + (channel_id == ChatChannel_Petition && Admin() < AccountStatus::QuestTroupe) || + (channel_id == ChatChannel_GMSAY && !GetGM()) + ) { + return; + } + + va_list argptr; + char buffer[4096]; + char message_sender[64]; + + va_start(argptr, message); + vsnprintf(buffer, 4096, message, argptr); + va_end(argptr); + + EQApplicationPacket app(OP_ChannelMessage, sizeof(ChannelMessage_Struct) + strlen(buffer) + 1); + + auto* cm = (ChannelMessage_Struct *) app.pBuffer; + + if (from == 0) { + strcpy(cm->sender, "ZServer"); + } else if (from[0] == 0) { + strcpy(cm->sender, "ZServer"); + } else { + CleanMobName(from, message_sender); + strcpy(cm->sender, message_sender); + } + + if (to != 0) { + strcpy((char *) cm->targetname, to); + } else if (channel_id == ChatChannel_Tell) { + strcpy(cm->targetname, m_pp.name); + } else { + cm->targetname[0] = 0; + } + + uint8 listener_skill; + + const bool is_valid_language = EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27); + + if (is_valid_language) { + listener_skill = m_pp.languages[language_id]; + cm->language = listener_skill < 24 ? Language::Unknown27 : language_id; + } else { + listener_skill = m_pp.languages[Language::CommonTongue]; + cm->language = Language::CommonTongue; + } + + // set effective language skill = lower of sender and receiver skills + uint8 effective_skill = (language_skill < listener_skill ? language_skill : listener_skill); + if (effective_skill > Language::MaxValue) { + effective_skill = Language::MaxValue; + } + + cm->skill_in_language = effective_skill; + + cm->chan_num = channel_id; + strcpy(&cm->message[0], buffer); + + QueuePacket(&app); + + const bool can_train_self = RuleB(Client, SelfLanguageLearning); + const bool is_not_sender = strcmp(GetCleanName(), cm->sender); + + if (can_train_self || is_not_sender) { + if ( + channel_id == ChatChannel_Group && + listener_skill < Language::MaxValue + ) { // group message in non-mastered language, check for skill up + if (is_valid_language && m_pp.languages[language_id] <= language_skill) { + CheckLanguageSkillIncrease(language_id, language_skill); + } + } + } +} - uint32 aa_points_spent = 0; +void Client::Message(uint32 type, const char* message, ...) { + if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) + return; + if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... + return; + if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) + return; + + va_list argptr; + auto buffer = new char[4096]; + va_start(argptr, message); + vsnprintf(buffer, 4096, message, argptr); + va_end(argptr); + + SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); + buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); + buf.WriteInt8(static_cast(Journal::Mode::None)); + buf.WriteInt8(0); // language + buf.WriteUInt32(type); + buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here + buf.WriteString(""); // send name, not applicable here + buf.WriteInt32(0); // location, client seems to ignore + buf.WriteInt32(0); + buf.WriteInt32(0); + buf.WriteString(buffer); + + auto app = new EQApplicationPacket(OP_SpecialMesg, buf); + + FastQueuePacket(&app); + + safe_delete_array(buffer); +} - auto e = CharacterAlternateAbilitiesRepository::NewEntity(); +void Client::FilteredMessage(Mob *sender, uint32 type, eqFilterType filter, const char* message, ...) { + if (!FilteredMessageCheck(sender, filter)) + return; - for (auto &rank : aa_ranks) { - auto a = zone->GetAlternateAdvancementAbility(rank.first); - if (!a) { - continue; - } + va_list argptr; + auto buffer = new char[4096]; + va_start(argptr, message); + vsnprintf(buffer, 4096, message, argptr); + va_end(argptr); - if (rank.second.first > 0) { - auto r = a->GetRankByPointsSpent(rank.second.first); - if (!r) { - continue; - } + SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); + buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); + buf.WriteInt8(static_cast(Journal::Mode::None)); + buf.WriteInt8(0); // language + buf.WriteUInt32(type); + buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here + buf.WriteString(""); // send name, not applicable here + buf.WriteInt32(0); // location, client seems to ignore + buf.WriteInt32(0); + buf.WriteInt32(0); + buf.WriteString(buffer); - aa_points_spent += r->total_cost; + auto app = new EQApplicationPacket(OP_SpecialMesg, buf); - e.id = character_id; - e.aa_id = a->first_rank_id; - e.aa_value = rank.second.first; - e.charges = rank.second.second; + FastQueuePacket(&app); - v.emplace_back(e); - } - } + safe_delete_array(buffer); +} - m_pp.aapoints_spent = aa_points_spent + m_epp.expended_aa; +void Client::SetMaxHP() { + if(dead) + return; + SetHP(CalcMaxHP()); + SendHPUpdate(); + Save(); +} - return CharacterAlternateAbilitiesRepository::ReplaceMany(database, v); +bool Client::UpdateLDoNPoints(uint32 theme_id, int points) +{ + if (points < 0) { + if (m_pp.ldon_points_available < (0 - points)) { + return false; + } + } + + bool is_loss = false; + + switch (theme_id) { + case LDoNTheme::Unused: { // No theme, so distribute evenly across all + int split_points = (points / 5); + + int guk_points = (split_points + (points % 5)); + int mir_points = split_points; + int mmc_points = split_points; + int ruj_points = split_points; + int tak_points = split_points; + + split_points = 0; + + if (points < 0) { + if (m_pp.ldon_points_available < (0 - points)) { + return false; + } + + is_loss = true; + + if (m_pp.ldon_points_guk < (0 - guk_points)) { + mir_points += (guk_points + m_pp.ldon_points_guk); + guk_points = (0 - m_pp.ldon_points_guk); + } + + if (m_pp.ldon_points_mir < (0 - mir_points)) { + mmc_points += (mir_points + m_pp.ldon_points_mir); + mir_points = (0 - m_pp.ldon_points_mir); + } + + if (m_pp.ldon_points_mmc < (0 - mmc_points)) { + ruj_points += (mmc_points + m_pp.ldon_points_mmc); + mmc_points = (0 - m_pp.ldon_points_mmc); + } + + if (m_pp.ldon_points_ruj < (0 - ruj_points)) { + tak_points += (ruj_points + m_pp.ldon_points_ruj); + ruj_points = (0 - m_pp.ldon_points_ruj); + } + + if (m_pp.ldon_points_tak < (0 - tak_points)) { + split_points = (tak_points + m_pp.ldon_points_tak); + tak_points = (0 - m_pp.ldon_points_tak); + } + } + + m_pp.ldon_points_guk += guk_points; + m_pp.ldon_points_mir += mir_points; + m_pp.ldon_points_mmc += mmc_points; + m_pp.ldon_points_ruj += ruj_points; + m_pp.ldon_points_tak += tak_points; + + points -= split_points; + + if (split_points != 0) { // if anything left, recursively loop thru again + UpdateLDoNPoints(LDoNTheme::Unused, split_points); + } + + break; + } + case LDoNTheme::GUK: { + if (points < 0) { + if (m_pp.ldon_points_guk < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_guk += points; + break; + } + case LDoNTheme::MIR: { + if (points < 0) { + if (m_pp.ldon_points_mir < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_mir += points; + break; + } + case LDoNTheme::MMC: { + if (points < 0) { + if (m_pp.ldon_points_mmc < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_mmc += points; + break; + } + case LDoNTheme::RUJ: { + if (points < 0) { + if (m_pp.ldon_points_ruj < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_ruj += points; + break; + } + case LDoNTheme::TAK: { + if (points < 0) { + if (m_pp.ldon_points_tak < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_tak += points; + break; + } + } + + m_pp.ldon_points_available += points; + + QuestEventID event_id = is_loss ? EVENT_LDON_POINTS_LOSS : EVENT_LDON_POINTS_GAIN; + + if (parse->PlayerHasQuestSub(event_id)) { + const std::string &export_string = fmt::format( + "{} {}", + theme_id, + std::abs(points) + ); + + parse->EventPlayer(event_id, this, export_string, 0); + } + + auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); + auto *apus = (AdventurePoints_Update_Struct *) outapp->pBuffer; + + apus->ldon_available_points = m_pp.ldon_points_available; + apus->ldon_guk_points = m_pp.ldon_points_guk; + apus->ldon_mirugal_points = m_pp.ldon_points_mir; + apus->ldon_mistmoore_points = m_pp.ldon_points_mmc; + apus->ldon_rujarkian_points = m_pp.ldon_points_ruj; + apus->ldon_takish_points = m_pp.ldon_points_tak; + + outapp->priority = 6; + + QueuePacket(outapp); + safe_delete(outapp); + return true; } -void Client::RemoveExpendedAA(int aa_id) +void Client::SetLDoNPoints(uint32 theme_id, uint32 points) { - CharacterAlternateAbilitiesRepository::DeleteWhere( - database, - fmt::format( - "`id` = {} AND `aa_id` = {}", - CharacterID(), - aa_id - ) - ); + switch (theme_id) { + case LDoNTheme::GUK: { + m_pp.ldon_points_guk = points; + break; + } + case LDoNTheme::MIR: { + m_pp.ldon_points_mir = points; + break; + } + case LDoNTheme::MMC: { + m_pp.ldon_points_mmc = points; + break; + } + case LDoNTheme::RUJ: { + m_pp.ldon_points_ruj = points; + break; + } + case LDoNTheme::TAK: { + m_pp.ldon_points_tak = points; + break; + } + } + + m_pp.ldon_points_available = ( + m_pp.ldon_points_guk + + m_pp.ldon_points_mir + + m_pp.ldon_points_mmc + + m_pp.ldon_points_ruj + + m_pp.ldon_points_tak + ); + + auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); + + auto a = (AdventurePoints_Update_Struct*) outapp->pBuffer; + + a->ldon_available_points = m_pp.ldon_points_available; + a->ldon_guk_points = m_pp.ldon_points_guk; + a->ldon_mirugal_points = m_pp.ldon_points_mir; + a->ldon_mistmoore_points = m_pp.ldon_points_mmc; + a->ldon_rujarkian_points = m_pp.ldon_points_ruj; + a->ldon_takish_points = m_pp.ldon_points_tak; + + outapp->priority = 6; + + QueuePacket(outapp); + safe_delete(outapp); } -bool Client::Save(uint8 iCommitNow) { - if(!ClientDataLoaded()) - return false; - - /* Wrote current basics to PP for saves */ - if (!m_lock_save_position) { - m_pp.x = m_Position.x; - m_pp.y = m_Position.y; - m_pp.z = m_Position.z; - m_pp.heading = m_Position.w; - } +void Client::SetSkill(EQ::skills::SkillType skillid, uint16 value) { + if (skillid > EQ::skills::HIGHEST_SKILL) + return; + m_pp.skills[skillid] = value; // We need to be able to #setskill 254 and 255 to reset skills - m_pp.guildrank = guildrank; + database.SaveCharacterSkill(CharacterID(), skillid, value); + auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); + SkillUpdate_Struct* skill = (SkillUpdate_Struct*)outapp->pBuffer; + skill->skillId=skillid; + skill->value=value; + QueuePacket(outapp); + safe_delete(outapp); +} - if (dead && GetHP() <= 0) { - m_pp.cur_hp = GetMaxHP(); - m_pp.mana = current_mana; - if (RuleB(Character, FullManaOnDeath)) { - m_pp.mana = GetMaxMana(); - } +void Client::IncreaseLanguageSkill(uint8 language_id, uint8 increase) +{ + if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { + return; + } - m_pp.endurance = current_endurance; - if (RuleB(Character, FullEndurOnDeath)) { - m_pp.endurance = GetMaxEndurance(); - } - } else { // Otherwise, no changes. - m_pp.cur_hp = GetHP(); - m_pp.mana = current_mana; - m_pp.endurance = current_endurance; - } + m_pp.languages[language_id] += increase; - /* Save Character Currency */ - database.SaveCharacterCurrency(CharacterID(), &m_pp); + if (m_pp.languages[language_id] > Language::MaxValue) { + m_pp.languages[language_id] = Language::MaxValue; + } - // save character binds - // this may not need to be called in Save() but it's here for now - // to maintain the current behavior - database.SaveCharacterBinds(this); + database.SaveCharacterLanguage(CharacterID(), language_id, m_pp.languages[language_id]); - /* Save Character Buffs */ - database.SaveBuffs(this); + auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); + auto* s = (SkillUpdate_Struct*) outapp->pBuffer; - /* Total Time Played */ - TotalSecondsPlayed += (time(nullptr) - m_pp.lastlogin); - m_pp.timePlayedMin = (TotalSecondsPlayed / 60); - m_pp.RestTimer = GetRestTimer(); + s->skillId = 100 + language_id; + s->value = m_pp.languages[language_id]; - /* Save Mercs */ - if (GetMercInfo().MercTimerRemaining > RuleI(Mercs, UpkeepIntervalMS)) { - GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS); - } + QueuePacket(outapp); + safe_delete(outapp); - if (GetMercTimer()->Enabled()) { - GetMercInfo().MercTimerRemaining = GetMercTimer()->GetRemainingTime(); - } + MessageString(Chat::Skills, LANG_SKILL_IMPROVED); +} - if (dead || (!GetMerc() && !GetMercInfo().IsSuspended)) { - memset(&m_mercinfo, 0, sizeof(struct MercInfo)); - } +void Client::AddSkill(EQ::skills::SkillType skillid, uint16 value) { + if (skillid > EQ::skills::HIGHEST_SKILL) + return; + value = GetRawSkill(skillid) + value; + uint16 max = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); + if (value > max) + value = max; + SetSkill(skillid, value); +} - m_pp.lastlogin = time(nullptr); - - if (GetPet() && GetPet()->CastToNPC()->GetPetSpellID() && !dead) { - NPC *pet = GetPet()->CastToNPC(); - m_petinfo.SpellID = pet->CastToNPC()->GetPetSpellID(); - m_petinfo.HP = pet->GetHP(); - m_petinfo.Mana = pet->GetMana(); - pet->GetPetState(m_petinfo.Buffs, m_petinfo.Items, m_petinfo.Name); - m_petinfo.petpower = pet->GetPetPower(); - m_petinfo.size = pet->GetSize(); - m_petinfo.taunting = pet->CastToNPC()->IsTaunting(); - } else { - memset(&m_petinfo, 0, sizeof(struct PetInfo)); - } - database.SavePetInfo(this); +void Client::SendSound(){//Makes a sound. + auto outapp = new EQApplicationPacket(OP_Sound, 68); + unsigned char x[68]; + memset(x, 0, 68); + x[0]=0x22; + memset(&x[4],0x8002,sizeof(uint16)); + memset(&x[8],0x8624,sizeof(uint16)); + memset(&x[12],0x4A01,sizeof(uint16)); + x[16]=0x05; + x[28]=0x00;//change this value to give gold to the client + memset(&x[40],0xFFFFFFFF,sizeof(uint32)); + memset(&x[44],0xFFFFFFFF,sizeof(uint32)); + memset(&x[48],0xFFFFFFFF,sizeof(uint32)); + memset(&x[52],0xFFFFFFFF,sizeof(uint32)); + memset(&x[56],0xFFFFFFFF,sizeof(uint32)); + memset(&x[60],0xFFFFFFFF,sizeof(uint32)); + memset(&x[64],0xffffffff,sizeof(uint32)); + memcpy(outapp->pBuffer,x,outapp->size); + QueuePacket(outapp); + safe_delete(outapp); - if(tribute_timer.Enabled()) { - m_pp.tribute_time_remaining = tribute_timer.GetRemainingTime(); - } - else { - m_pp.tribute_time_remaining = 0xFFFFFFFF; m_pp.tribute_active = 0; - } +} +void Client::UpdateWho(uint8 remove) +{ + if (account_id == 0) { + return; + } + if (!worldserver.Connected()) { + return; + } + + auto pack = new ServerPacket(ServerOP_ClientList, sizeof(ServerClientList_Struct)); + auto *s = (ServerClientList_Struct *) pack->pBuffer; + s->remove = remove; + s->wid = GetWID(); + s->IP = GetIP(); + s->charid = CharacterID(); + strcpy(s->name, GetName()); + + s->gm = GetGM(); + s->Admin = Admin(); + s->AccountID = AccountID(); + strcpy(s->AccountName, AccountName()); + + s->LSAccountID = LSAccountID(); + strn0cpy(s->lskey, lskey, sizeof(s->lskey)); + + s->zone = zone->GetZoneID(); + s->instance_id = zone->GetInstanceID(); + s->race = GetRace(); + s->class_ = GetClass(); + s->level = GetLevel(); + + if (m_pp.anon == 0) { + s->anon = 0; + } + else if (m_pp.anon == 1) { + s->anon = 1; + } + else if (m_pp.anon >= 2) { + s->anon = 2; + } + + s->ClientVersion = static_cast(ClientVersion()); + s->tellsoff = tellsoff; + s->guild_id = guild_id; + s->guild_rank = guildrank; + s->guild_tribute_opt_in = guild_tribute_opt_in; + s->LFG = LFG; + if (LFG) { + s->LFGFromLevel = LFGFromLevel; + s->LFGToLevel = LFGToLevel; + s->LFGMatchFilter = LFGMatchFilter; + memcpy(s->LFGComments, LFGComments, sizeof(s->LFGComments)); + } + + worldserver.SendPacket(pack); + safe_delete(pack); +} - if (m_pp.hunger_level < 0) - m_pp.hunger_level = 0; +void Client::WhoAll(Who_All_Struct* whom) { - if (m_pp.thirst_level < 0) - m_pp.thirst_level = 0; + if (!worldserver.Connected()) + Message(0, "Error: World server disconnected"); + else { + auto pack = new ServerPacket(ServerOP_Who, sizeof(ServerWhoAll_Struct)); + ServerWhoAll_Struct* whoall = (ServerWhoAll_Struct*) pack->pBuffer; + whoall->admin = Admin(); + whoall->fromid=GetID(); + strcpy(whoall->from, GetName()); + strn0cpy(whoall->whom, whom->whom, 64); + whoall->lvllow = whom->lvllow; + whoall->lvlhigh = whom->lvlhigh; + whoall->gmlookup = whom->gmlookup; + whoall->wclass = whom->wclass; + whoall->wrace = whom->wrace; + worldserver.SendPacket(pack); + safe_delete(pack); + } +} - p_timers.Store(&database); +void Client::FriendsWho(char *FriendsString) { - database.SaveCharacterTribute(this); - SaveTaskState(); /* Save Character Task */ + if (!worldserver.Connected()) + Message(0, "Error: World server disconnected"); + else { + auto pack = + new ServerPacket(ServerOP_FriendsWho, sizeof(ServerFriendsWho_Struct) + strlen(FriendsString)); + ServerFriendsWho_Struct* FriendsWho = (ServerFriendsWho_Struct*) pack->pBuffer; + FriendsWho->FromID = GetID(); + strcpy(FriendsWho->FromName, GetName()); + strcpy(FriendsWho->FriendsString, FriendsString); + worldserver.SendPacket(pack); + safe_delete(pack); + } +} - LogFood("Client::Save - hunger_level: [{}] thirst_level: [{}]", m_pp.hunger_level, m_pp.thirst_level); +void Client::UpdateAdmin(bool from_database) { + int16 tmp = admin; + if (from_database) { + admin = database.GetAccountStatus(account_id); + } - // perform snapshot before SaveCharacterData() so that m_epp will contain the updated time - if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { - if (database.SaveCharacterInvSnapshot(CharacterID())) { - SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); - } - else { - SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); - } - } + if (tmp == admin && from_database) { + return; + } - database.SaveCharacterData(this, &m_pp, &m_epp); /* Save Character Data */ + if (m_pp.gm) { + LogInfo("[{}] - [{}] is a GM", __FUNCTION__ , GetName()); + petition_list.UpdateGMQueue(); + } - database.SaveCharacterEXPModifier(this); + UpdateWho(); +} - return true; +void Client::SetStats(uint8 type,int16 set_val){ + if(type>STAT_DISEASE){ + printf("Error in Client::IncStats, received invalid type of: %i\n",type); + return; + } + auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); + IncreaseStat_Struct* iss=(IncreaseStat_Struct*)outapp->pBuffer; + switch(type){ + case STAT_STR: + if(set_val>0) + iss->str=set_val; + if(set_val<0) + m_pp.STR=0; + else if(set_val>255) + m_pp.STR=255; + else + m_pp.STR=set_val; + break; + case STAT_STA: + if(set_val>0) + iss->sta=set_val; + if(set_val<0) + m_pp.STA=0; + else if(set_val>255) + m_pp.STA=255; + else + m_pp.STA=set_val; + break; + case STAT_AGI: + if(set_val>0) + iss->agi=set_val; + if(set_val<0) + m_pp.AGI=0; + else if(set_val>255) + m_pp.AGI=255; + else + m_pp.AGI=set_val; + break; + case STAT_DEX: + if(set_val>0) + iss->dex=set_val; + if(set_val<0) + m_pp.DEX=0; + else if(set_val>255) + m_pp.DEX=255; + else + m_pp.DEX=set_val; + break; + case STAT_INT: + if(set_val>0) + iss->int_=set_val; + if(set_val<0) + m_pp.INT=0; + else if(set_val>255) + m_pp.INT=255; + else + m_pp.INT=set_val; + break; + case STAT_WIS: + if(set_val>0) + iss->wis=set_val; + if(set_val<0) + m_pp.WIS=0; + else if(set_val>255) + m_pp.WIS=255; + else + m_pp.WIS=set_val; + break; + case STAT_CHA: + if(set_val>0) + iss->cha=set_val; + if(set_val<0) + m_pp.CHA=0; + else if(set_val>255) + m_pp.CHA=255; + else + m_pp.CHA=set_val; + break; + } + QueuePacket(outapp); + safe_delete(outapp); } -CLIENTPACKET::CLIENTPACKET() +void Client::IncStats(uint8 type, int16 increase_val) { - app = nullptr; - ack_req = false; + if (type > STAT_DISEASE) { + printf("Error in Client::IncStats, received invalid type of: %i\n", type); + return; + } + auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); + IncreaseStat_Struct *iss = (IncreaseStat_Struct *) outapp->pBuffer; + switch (type) { + case STAT_STR: + if (increase_val > 0) { + iss->str = increase_val; + } + + if ((m_pp.STR + increase_val * 2) > 255) { + m_pp.STR = 255; + } else { + m_pp.STR += increase_val * 2; + } + break; + case STAT_STA: + if (increase_val > 0) { + iss->sta = increase_val; + } + + if ((m_pp.STA + increase_val * 2) > 255) { + m_pp.STA = 255; + } else { + m_pp.STA += increase_val * 2; + } + break; + case STAT_AGI: + if (increase_val > 0) { + iss->agi = increase_val; + } + if ((m_pp.AGI + increase_val * 2) > 255) { + m_pp.AGI = 255; + } else { + m_pp.AGI += increase_val * 2; + } + break; + case STAT_DEX: + if (increase_val > 0) { + iss->dex = increase_val; + } + + if ((m_pp.DEX + increase_val * 2) > 255) { + m_pp.DEX = 255; + } else { + m_pp.DEX += increase_val * 2; + } + break; + case STAT_INT: + if (increase_val > 0) { + iss->int_ = increase_val; + } + + if ((m_pp.INT + increase_val * 2) > 255) { + m_pp.INT = 255; + } else { + m_pp.INT += increase_val * 2; + } + break; + case STAT_WIS: + if (increase_val > 0) { + iss->wis = increase_val; + } + + if ((m_pp.WIS + increase_val * 2) > 255) { + m_pp.WIS = 255; + } else { + m_pp.WIS += increase_val * 2; + } + break; + case STAT_CHA: + if (increase_val > 0) { + iss->cha = increase_val; + } + + if ((m_pp.CHA + increase_val * 2) > 255) { + m_pp.CHA = 255; + } else { + m_pp.CHA += increase_val * 2; + } + break; + } + QueuePacket(outapp); + safe_delete(outapp); } -CLIENTPACKET::~CLIENTPACKET() -{ - safe_delete(app); +const int64& Client::SetMana(int64 amount) { + bool update = false; + if (amount < 0) + amount = 0; + if (amount > GetMaxMana()) + amount = GetMaxMana(); + if (amount != current_mana) + update = true; + current_mana = amount; + if (update) + Mob::SetMana(amount); + CheckManaEndUpdate(); + return current_mana; } -//this assumes we do not own pApp, and clones it. -bool Client::AddPacket(const EQApplicationPacket *pApp, bool bAckreq) { - if (!pApp) - return false; - if(!zoneinpacket_timer.Enabled()) { - //drop the packet because it will never get sent. - return(false); - } +void Client::CheckManaEndUpdate() { + if (!Connected()) + return; + + if (last_reported_mana != current_mana || last_reported_endurance != current_endurance) { + + if (ClientVersion() >= EQ::versions::ClientVersion::SoD) { + SendManaUpdate(); + SendEnduranceUpdate(); + } + + auto outapp = new EQApplicationPacket(OP_ManaChange, sizeof(ManaChange_Struct)); + ManaChange_Struct* mana_change = (ManaChange_Struct*)outapp->pBuffer; + mana_change->new_mana = current_mana; + mana_change->stamina = current_endurance; + mana_change->spell_id = casting_spell_id; + mana_change->keepcasting = 1; + mana_change->slot = -1; + outapp->priority = 6; + QueuePacket(outapp); + safe_delete(outapp); + + /* Let others know when our mana percent has changed */ + if (GetManaPercent() != last_reported_mana_percent) { + Group *group = GetGroup(); + Raid *raid = GetRaid(); + + if (raid) { + raid->SendManaPacketFrom(this); + } + else if (group) { + group->SendManaPacketFrom(this); + } + + auto mana_packet = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); + ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_packet->pBuffer; + mana_update->cur_mana = GetMana(); + mana_update->max_mana = GetMaxMana(); + mana_update->spawn_id = GetID(); + if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) + QueuePacket(mana_packet); // do we need this with the OP_ManaChange packet above? + entity_list.QueueClientsByXTarget(this, mana_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); + safe_delete(mana_packet); + + last_reported_mana_percent = GetManaPercent(); + } + + /* Let others know when our endurance percent has changed */ + if (GetEndurancePercent() != last_reported_endurance_percent) { + Group *group = GetGroup(); + Raid *raid = GetRaid(); + + if (raid) { + raid->SendEndurancePacketFrom(this); + } + else if (group) { + group->SendEndurancePacketFrom(this); + } + + auto endurance_packet = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); + EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)endurance_packet->pBuffer; + endurance_update->cur_end = GetEndurance(); + endurance_update->max_end = GetMaxEndurance(); + endurance_update->spawn_id = GetID(); + if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) + QueuePacket(endurance_packet); // do we need this with the OP_ManaChange packet above? + entity_list.QueueClientsByXTarget(this, endurance_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); + safe_delete(endurance_packet); + + last_reported_endurance_percent = GetEndurancePercent(); + } + + last_reported_mana = current_mana; + last_reported_endurance = current_endurance; + } +} + +// sends mana update to self +void Client::SendManaUpdate() +{ + auto mana_app = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); + ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_app->pBuffer; + mana_update->cur_mana = GetMana(); + mana_update->max_mana = GetMaxMana(); + mana_update->spawn_id = GetID(); + QueuePacket(mana_app); + safe_delete(mana_app); +} - auto c = std::make_unique(); +// sends endurance update to self +void Client::SendEnduranceUpdate() +{ + auto end_app = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); + EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)end_app->pBuffer; + endurance_update->cur_end = GetEndurance(); + endurance_update->max_end = GetMaxEndurance(); + endurance_update->spawn_id = GetID(); + QueuePacket(end_app); + safe_delete(end_app); +} - c->ack_req = bAckreq; - c->app = pApp->Copy(); +void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) +{ + Mob::FillSpawnStruct(ns, ForWho); - clientpackets.push_back(std::move(c)); - return true; + // Populate client-specific spawn information + ns->spawn.afk = AFK; + ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live + ns->spawn.anon = m_pp.anon; + ns->spawn.gm = GetGM() ? 1 : 0; + ns->spawn.guildID = GuildID(); + ns->spawn.trader = IsTrader(); + ns->spawn.buyer = IsBuyer(); +// ns->spawn.linkdead = IsLD() ? 1 : 0; +// ns->spawn.pvp = GetPVP(false) ? 1 : 0; + ns->spawn.show_name = true; + + strcpy(ns->spawn.title, m_pp.title); + strcpy(ns->spawn.suffix, m_pp.suffix); + + if (IsBecomeNPC() == true) + ns->spawn.NPC = 1; + else if (ForWho == this) + ns->spawn.NPC = 10; + else + ns->spawn.NPC = 0; + ns->spawn.is_pet = 0; + + if (!IsInAGuild()) { + ns->spawn.guildrank = 0xFF; + } else { + ns->spawn.guildrank = guild_mgr.GetDisplayedRank(GuildID(), GuildRank(), CharacterID()); + ns->spawn.guild_show = guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_DISPLAY_GUILD_NAME); + } + ns->spawn.size = 0; // Changing size works, but then movement stops! (wth?) + ns->spawn.runspeed = (gmspeed == 0) ? runspeed : 3.125f; + ns->spawn.showhelm = m_pp.showhelm ? 1 : 0; + + UpdateEquipmentLight(); + UpdateActiveLight(); + ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive]; } -//this assumes that it owns the object pointed to by *pApp -bool Client::AddPacket(EQApplicationPacket** pApp, bool bAckreq) { - if (!pApp || !(*pApp)) - return false; - if(!zoneinpacket_timer.Enabled()) { - //drop the packet because it will never get sent. - return(false); - } - auto c = std::make_unique(); +bool Client::GMHideMe(Client* client) { + if (gm_hide_me) { + if (client == 0) + return true; + else if (admin > client->Admin()) + return true; + else + return false; + } + else + return false; +} - c->ack_req = bAckreq; - c->app = *pApp; - *pApp = nullptr; +void Client::Duck() { + SetAppearance(eaCrouching, false); +} - clientpackets.push_back(std::move(c)); - return true; +void Client::Stand() { + SetAppearance(eaStanding, false); } -bool Client::SendAllPackets() { - CLIENTPACKET* cp = nullptr; - while (!clientpackets.empty()) { - cp = clientpackets.front().get(); - if(eqs) - eqs->FastQueuePacket((EQApplicationPacket **)&cp->app, cp->ack_req); - clientpackets.pop_front(); - } - return true; +void Client::Sit() { + SetAppearance(eaSitting, false); } -void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CONN_STATUS required_state, eqFilterType filter) { - if (filter != FilterNone && GetFilter(filter) == FilterHide) { - return; - } +void Client::ChangeLastName(std::string last_name) { + memset(m_pp.last_name, 0, sizeof(m_pp.last_name)); + strn0cpy(m_pp.last_name, last_name.c_str(), sizeof(m_pp.last_name)); + auto outapp = new EQApplicationPacket(OP_GMLastName, sizeof(GMLastName_Struct)); + auto gmn = (GMLastName_Struct*) outapp->pBuffer; + strn0cpy(gmn->name, name, sizeof(gmn->name)); + strn0cpy(gmn->gmname, name, sizeof(gmn->gmname)); + strn0cpy(gmn->lastname, last_name.c_str(), sizeof(gmn->lastname)); - if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { - AddPacket(app, ack_req); - return; - } + gmn->unknown[0] = 1; + gmn->unknown[1] = 1; + gmn->unknown[2] = 1; + gmn->unknown[3] = 1; - // if the program doesnt care about the status or if the status isnt what we requested - if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { - // todo: save packets for later use - AddPacket(app, ack_req); - } - else if (eqs) { - eqs->QueuePacket(app, ack_req); - } + entity_list.QueueClients(this, outapp, false); + + safe_delete(outapp); } -void Client::FastQueuePacket(EQApplicationPacket** app, bool ack_req, CLIENT_CONN_STATUS required_state) { - // if the program doesnt care about the status or if the status isnt what we requested - if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { - // todo: save packets for later use - AddPacket(app, ack_req); - return; - } - else { - if(eqs) - eqs->FastQueuePacket((EQApplicationPacket **)app, ack_req); - else if (app && (*app)) - delete *app; - *app = nullptr; - } - return; +bool Client::ChangeFirstName(const char* in_firstname, const char* gmname) +{ + // check duplicate name + bool used_name = database.IsNameUsed((const char*) in_firstname); + if (used_name) { + return false; + } + + // update character_ + if(!database.UpdateName(GetName(), in_firstname)) + return false; + + // update pp + memset(m_pp.name, 0, sizeof(m_pp.name)); + snprintf(m_pp.name, sizeof(m_pp.name), "%s", in_firstname); + strcpy(name, m_pp.name); + Save(); + + // send name update packet + auto outapp = new EQApplicationPacket(OP_GMNameChange, sizeof(GMName_Struct)); + GMName_Struct* gmn=(GMName_Struct*)outapp->pBuffer; + strn0cpy(gmn->gmname,gmname,64); + strn0cpy(gmn->oldname,GetName(),64); + strn0cpy(gmn->newname,in_firstname,64); + gmn->unknown[0] = 1; + gmn->unknown[1] = 1; + gmn->unknown[2] = 1; + entity_list.QueueClients(this, outapp, false); + safe_delete(outapp); + + // finally, update the /who list + UpdateWho(); + + // success + return true; } -void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_skill, const char* orig_message, const char* targetname, bool is_silent) { - char message[4096]; - strn0cpy(message, orig_message, sizeof(message)); +void Client::SetGM(bool toggle) { + m_pp.gm = toggle ? 1 : 0; + m_inv.SetGMInventory((bool)m_pp.gm); + Message( + Chat::White, + fmt::format( + "You are {} flagged as a GM.", + m_pp.gm ? "now" : "no longer" + ).c_str() + ); + SendAppearancePacket(AppearanceType::GM, m_pp.gm); + Save(); + UpdateWho(); +} - LogDebug("Client::ChannelMessageReceived() Channel:[{}] message:[{}]", chan_num, message); +void Client::ReadBook(BookRequest_Struct* book) +{ + const std::string& text_file = book->txtfile; + + if (text_file.empty()) { + return; + } + + auto b = content_db.GetBook(text_file); + + if (!b.text.empty()) { + auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); + auto inst = const_cast(m_inv[book->invslot]); + + auto t = (BookText_Struct*) outapp->pBuffer; + + t->window = book->window; + t->type = book->type; + t->invslot = book->invslot; + t->target_id = book->target_id; + t->can_cast = 0; // todo: implement + t->can_scribe = false; + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END) { + if (inst && inst->GetItem()) { + auto recipe = TradeskillRecipeRepository::GetWhere( + content_db, + fmt::format( + "learned_by_item_id = {} LIMIT 1", + inst->GetItem()->ID + ) + ); + + t->type = inst->GetItem()->Book; + t->can_scribe = !recipe.empty(); + } + } + + memcpy(t->booktext, b.text.c_str(), b.text.size()); + + if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { + if (m_pp.languages[b.language] < Language::MaxValue) { + GarbleMessage(t->booktext, (Language::MaxValue - m_pp.languages[b.language])); + } + } + + // Send only books and scrolls to this event + if (parse->PlayerHasQuestSub(EVENT_READ_ITEM) && t->type != BookType::ItemInfo) { + std::vector args = { + b.text, + t->can_cast, + t->can_scribe, + t->invslot, + t->target_id, + t->type, + inst + }; + + parse->EventPlayer(EVENT_READ_ITEM, this, book->txtfile, inst ? inst->GetID() : 0, &args); + } + + QueuePacket(outapp); + safe_delete(outapp); + } +} - if (targetname == nullptr) { - targetname = (!GetTarget()) ? "" : GetTarget()->GetName(); - } +void Client::QuestReadBook(const char* text, uint8 type) { + std::string booktxt2 = text; + int length = booktxt2.length(); + if (booktxt2[0] != '\0') { + auto outapp = new EQApplicationPacket(OP_ReadBook, length + sizeof(BookText_Struct)); + BookText_Struct *out = (BookText_Struct *) outapp->pBuffer; + out->window = 0xFF; + out->type = type; + out->invslot = 0; + memcpy(out->booktext, booktxt2.c_str(), length); + QueuePacket(outapp); + safe_delete(outapp); + } +} - if(RuleB(Chat, EnableAntiSpam)) - { - if(strcmp(targetname, "discard") != 0) - { - if(chan_num == ChatChannel_Shout || chan_num == ChatChannel_Auction || chan_num == ChatChannel_OOC || chan_num == ChatChannel_Tell) - { - if(GlobalChatLimiterTimer) - { - if(GlobalChatLimiterTimer->Check(false)) - { - GlobalChatLimiterTimer->Start(RuleI(Chat, IntervalDurationMS)); - AttemptedMessages = 0; - } - } +uint32 Client::GetCarriedPlatinum() { + return ( + GetMoney(MoneyTypes::Platinum, MoneySubtypes::Personal) + + (GetMoney(MoneyTypes::Gold, MoneySubtypes::Personal) / 10) + + (GetMoney(MoneyTypes::Silver, MoneySubtypes::Personal) / 100) + + (GetMoney(MoneyTypes::Copper, MoneySubtypes::Personal) / 1000) + ); +} - uint32 AllowedMessages = RuleI(Chat, MinimumMessagesPerInterval) + TotalKarma; - AllowedMessages = AllowedMessages > RuleI(Chat, MaximumMessagesPerInterval) ? RuleI(Chat, MaximumMessagesPerInterval) : AllowedMessages; - - if(RuleI(Chat, MinStatusToBypassAntiSpam) <= Admin()) - AllowedMessages = 10000; - - AttemptedMessages++; - if(AttemptedMessages > AllowedMessages) - { - if(AttemptedMessages > RuleI(Chat, MaxMessagesBeforeKick)) - { - Kick("Sent too many chat messages at once."); - return; - } - if(GlobalChatLimiterTimer) - { - Message(0, "You have been rate limited, you can send more messages in %i seconds.", - GlobalChatLimiterTimer->GetRemainingTime() / 1000); - return; - } - else - { - Message(0, "You have been rate limited, you can send more messages in 60 seconds."); - return; - } - } - } - } - } +bool Client::TakePlatinum(uint32 platinum, bool update_client) { + if (GetCarriedPlatinum() >= platinum) { + const auto copper = static_cast(platinum) * 1000; + return TakeMoneyFromPP(copper, update_client); + } - /* Logs Player Chat */ - if (RuleB(QueryServ, PlayerLogChat)) { - auto pack = new ServerPacket(ServerOP_Speech, sizeof(Server_Speech_Struct) + strlen(message) + 1); - Server_Speech_Struct* sem = (Server_Speech_Struct*) pack->pBuffer; + return false; +} - if(chan_num == ChatChannel_Guild) - sem->guilddbid = GuildID(); - else - sem->guilddbid = 0; +bool Client::TakeMoneyFromPP(uint64 copper, bool update_client) { + int64 player_copper, silver, gold, platinum; + player_copper = m_pp.copper; + silver = static_cast(m_pp.silver) * 10; + gold = static_cast(m_pp.gold) * 100; + platinum = static_cast(m_pp.platinum) * 1000; + + int64 client_total = player_copper + silver + gold + platinum; + + client_total -= copper; + if (client_total < 0) { + return false; // Not enough money! + } else { + player_copper -= copper; + if(player_copper <= 0) { + copper = std::abs(player_copper); + m_pp.copper = 0; + } else { + m_pp.copper = player_copper; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + return true; + } + + silver -= copper; + if (silver <= 0) { + copper = std::abs(silver); + m_pp.silver = 0; + } else { + m_pp.silver = silver / 10; + m_pp.copper += (silver - (m_pp.silver * 10)); + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + return true; + } + + gold -=copper; + + if (gold <= 0) { + copper = std::abs(gold); + m_pp.gold = 0; + } else { + m_pp.gold = gold / 100; + uint64 silver_test = (gold - (static_cast(m_pp.gold) * 100)) / 10; + m_pp.silver += silver_test; + uint64 copper_test = (gold - (static_cast(m_pp.gold) * 100 + silver_test * 10)); + m_pp.copper += copper_test; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + return true; + } + + platinum -= copper; + + //Impossible for plat to be negative, already checked above + + m_pp.platinum = platinum / 1000; + uint64 gold_test = (platinum - (static_cast(m_pp.platinum) * 1000)) / 100; + m_pp.gold += gold_test; + uint64 silver_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100)) / 10; + m_pp.silver += silver_test; + uint64 copper_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100 + silver_test * 10)); + m_pp.copper = copper_test; + + if (update_client) { + SendMoneyUpdate(); + } + + RecalcWeight(); + SaveCurrency(); + return true; + } +} - strcpy(sem->message, message); - sem->minstatus = Admin(); - sem->type = chan_num; - if(targetname != 0) - strcpy(sem->to, targetname); +void Client::AddPlatinum(uint32 platinum, bool update_client) { + const auto copper = static_cast(platinum) * 1000; + AddMoneyToPP(copper, update_client); +} - if(GetName() != 0) - strcpy(sem->from, GetName()); +void Client::AddMoneyToPP(uint64 copper, bool update_client){ + uint64 temporary_copper; + uint64 temporary_copper_two; + temporary_copper = copper; - if(worldserver.Connected()) - worldserver.SendPacket(pack); - safe_delete(pack); - } + /* Add Amount of Platinum */ + temporary_copper_two = temporary_copper / 1000; + int32 new_value = m_pp.platinum + temporary_copper_two; - // Garble the message based on drunkness - if (GetIntoxication() > 0 && !(RuleB(Chat, ServerWideOOC) && chan_num == ChatChannel_OOC) && !GetGM()) { - GarbleMessage(message, (int)(GetIntoxication() / 3)); - language = Language::CommonTongue; // No need for language when drunk - lang_skill = Language::MaxValue; - } + if (new_value < 0) { + m_pp.platinum = 0; + } else { + m_pp.platinum = m_pp.platinum + temporary_copper_two; + } - // some channels don't use languages - if ( - chan_num == ChatChannel_OOC || - chan_num == ChatChannel_GMSAY || - chan_num == ChatChannel_Broadcast || - chan_num == ChatChannel_Petition - ) { - language = Language::CommonTongue; - lang_skill = Language::MaxValue; - } + temporary_copper -= temporary_copper_two * 1000; - // Censor the message - if (EQ::ProfanityManager::IsCensorshipActive() && (chan_num != ChatChannel_Say)) - EQ::ProfanityManager::RedactMessage(message); - - switch(chan_num) - { - case ChatChannel_Guild: { /* Guild Chat */ - if (!IsInAGuild()) { - MessageString(Chat::DefaultText, GUILD_NOT_MEMBER2); //You are not a member of any guild. - } else if (!guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_GUILD_CHAT_SPEAK_IN)) { - MessageString(Chat::EchoGuild, NO_PROPER_ACCESS); - } else if (!worldserver.SendChannelMessage(this, targetname, chan_num, GuildID(), language, lang_skill, message)) { - Message(Chat::White, "Error: World server disconnected"); - } - break; - } - case ChatChannel_Group: { /* Group Chat */ - Raid* raid = entity_list.GetRaidByClient(this); - if(raid) { - raid->RaidGroupSay((const char*) message, this, language, lang_skill); - break; - } + /* Add Amount of Gold */ + temporary_copper_two = temporary_copper / 100; + new_value = m_pp.gold + temporary_copper_two; - Group* group = GetGroup(); - if(group != nullptr) { - group->GroupMessage(this,language,lang_skill,(const char*) message); - } - break; - } - case ChatChannel_Raid: { /* Raid Say */ - Raid* raid = entity_list.GetRaidByClient(this); - if(raid){ - raid->RaidSay((const char*) message, this, language, lang_skill); - } - break; - } - case ChatChannel_Shout: { /* Shout */ - Mob *sender = this; - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) - sender = GetPet(); + if (new_value < 0) { + m_pp.gold = 0; + } else { + m_pp.gold = m_pp.gold + temporary_copper_two; + } - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - break; - } - case ChatChannel_Auction: { /* Auction */ - if(RuleB(Chat, ServerWideAuction)) - { - if(!global_channel_timer.Check()) - { - if(strlen(targetname) == 0) - ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? - else - return; - } + temporary_copper -= temporary_copper_two * 100; - if(GetRevoked()) - { - Message(0, "You have been revoked. You may not talk on Auction."); - return; - } + /* Add Amount of Silver */ + temporary_copper_two = temporary_copper / 10; + new_value = m_pp.silver + temporary_copper_two; - if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) - { - if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) - { - Message(0, "You do not have permission to talk in Auction at this time."); - return; - } - } + if (new_value < 0) { + m_pp.silver = 0; + } else { + m_pp.silver = m_pp.silver + temporary_copper_two; + } - if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - } - else if(!RuleB(Chat, ServerWideAuction)) { - Mob *sender = this; + temporary_copper -= temporary_copper_two * 10; - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) - sender = GetPet(); + /* Add Amount of Copper */ + temporary_copper_two = temporary_copper; + new_value = m_pp.copper + temporary_copper_two; - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - } - break; - } - case ChatChannel_OOC: { /* OOC */ - if(RuleB(Chat, ServerWideOOC)) - { - if(!global_channel_timer.Check()) - { - if(strlen(targetname) == 0) - ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? - else - return; - } - if(worldserver.IsOOCMuted() && admin < AccountStatus::GMAdmin) - { - Message(0,"OOC has been muted. Try again later."); - return; - } + if (new_value < 0) { + m_pp.copper = 0; + } else { + m_pp.copper = m_pp.copper + temporary_copper_two; + } - if(GetRevoked()) - { - Message(0, "You have been revoked. You may not talk on OOC."); - return; - } + //send them all at once, since the above code stopped working. + if (update_client) { + SendMoneyUpdate(); + } - if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) - { - if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) - { - Message(0, "You do not have permission to talk in OOC at this time."); - return; - } - } + RecalcWeight(); - if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) - { - Message(0, "Error: World server disconnected"); - } - } - else if(!RuleB(Chat, ServerWideOOC)) - { - Mob *sender = this; + SaveCurrency(); - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) - sender = GetPet(); + LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); +} - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - } - break; - } - case ChatChannel_Broadcast: /* Broadcast */ - case ChatChannel_GMSAY: { /* GM Say */ - if (!(admin >= AccountStatus::QuestTroupe)) - Message(0, "Error: Only GMs can use this channel"); - else if (!worldserver.SendChannelMessage(this, targetname, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - break; - } - case ChatChannel_Tell: { /* Tell */ - if(!global_channel_timer.Check()) - { - if(strlen(targetname) == 0) - ChannelMessageReceived(ChatChannel_Tell, language, lang_skill, message, "discard"); //Fast typer or spammer?? - else - return; - } +void Client::EVENT_ITEM_ScriptStopReturn(){ + /* Set a timestamp in an entity variable for plugin check_handin.pl in return_items + This will stopgap players from items being returned if global_npc.pl has a catch all return_items + */ + struct timeval read_time; + char buffer[50]; + gettimeofday(&read_time, 0); + sprintf(buffer, "%li.%li \n", read_time.tv_sec, read_time.tv_usec); + SetEntityVariable("Stop_Return", buffer); +} - if(GetRevoked()) - { - Message(0, "You have been revoked. You may not send tells."); - return; - } +void Client::AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 platinum, bool update_client){ + EVENT_ITEM_ScriptStopReturn(); - if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) - { - if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) - { - Message(0, "You do not have permission to send tells at this time."); - return; - } - } + int32 new_value = m_pp.platinum + platinum; + if (new_value >= 0 && new_value > m_pp.platinum) { + m_pp.platinum += platinum; + } - char target_name[64]; - - if(targetname) - { - size_t i = strlen(targetname); - int x; - for(x = 0; x < i; ++x) - { - if(targetname[x] == '%') - { - target_name[x] = '/'; - } - else - { - target_name[x] = targetname[x]; - } - } - target_name[x] = '\0'; - } + new_value = m_pp.gold + gold; + if (new_value >= 0 && new_value > m_pp.gold) { + m_pp.gold += gold; + } - if(!worldserver.SendChannelMessage(this, target_name, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - 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); - } - } + new_value = m_pp.silver + silver; + if (new_value >= 0 && new_value > m_pp.silver) { + m_pp.silver += silver; + } - if (message[0] == COMMAND_CHAR) { - if (command_dispatch(this, message, false) == -2) { - if (parse->PlayerHasQuestSub(EVENT_COMMAND)) { - int i = parse->EventPlayer(EVENT_COMMAND, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Command '%s' not recognized.", message); - } - } - else if (parse->PlayerHasQuestSub(EVENT_SAY)) { - int i = parse->EventPlayer(EVENT_SAY, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Command '%s' not recognized.", message); - } - } - else { - if (!RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Command '%s' not recognized.", message); - } - } - } - break; - } + new_value = m_pp.copper + copper; + if (new_value >= 0 && new_value > m_pp.copper) { + m_pp.copper += copper; + } - if (message[0] == BOT_COMMAND_CHAR) { - if (RuleB(Bots, Enabled)) { - if (bot_command_dispatch(this, message) == -2) { - if (parse->PlayerHasQuestSub(EVENT_BOT_COMMAND)) { - int i = parse->EventPlayer(EVENT_BOT_COMMAND, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Bot command '%s' not recognized.", message); - } - } - else if (parse->PlayerHasQuestSub(EVENT_SAY)) { - int i = parse->EventPlayer(EVENT_SAY, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Bot command '%s' not recognized.", message); - } - } - else { - if (!RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Bot command '%s' not recognized.", message); - } - } - } - } else { - Message(Chat::Red, "Bots are disabled on this server."); - } - break; - } + if (update_client) { + SendMoneyUpdate(); + } - if (EQ::ProfanityManager::IsCensorshipActive()) { - EQ::ProfanityManager::RedactMessage(message); - } + RecalcWeight(); + SaveCurrency(); - Mob* sender = this; - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) { - sender = GetPet(); - } +#if (EQDEBUG>=5) + LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", + GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); +#endif +} - if (!is_silent) { - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - } +void Client::SendMoneyUpdate() { + auto outapp = new EQApplicationPacket(OP_MoneyUpdate, sizeof(MoneyUpdate_Struct)); + MoneyUpdate_Struct* mus= (MoneyUpdate_Struct*)outapp->pBuffer; - if (parse->PlayerHasQuestSub(EVENT_SAY)) { - parse->EventPlayer(EVENT_SAY, this, message, language); - } + mus->platinum = m_pp.platinum; + mus->gold = m_pp.gold; + mus->silver = m_pp.silver; + mus->copper = m_pp.copper; - if (sender != this) { - break; - } + FastQueuePacket(&outapp); +} - if (quest_manager.ProximitySayInUse()) { - entity_list.ProcessProximitySay(message, this, language); - } +bool Client::HasMoney(uint64 copper) { - Mob* t = GetTarget(); + if ( + (static_cast(m_pp.copper) + + (static_cast(m_pp.silver) * 10) + + (static_cast(m_pp.gold) * 100) + + (static_cast(m_pp.platinum) * 1000)) >= copper + ) { + return true; + } - if ( - t && - !IsInvisible(t) && - DistanceNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say) - ) { - const bool is_engaged = t->IsEngaged(); + return false; +} - if (is_engaged) { - parse->EventBotMercNPC(EVENT_AGGRO_SAY, t, this, [&]() { return message; }, language); - } else { - parse->EventBotMercNPC(EVENT_SAY, t, this, [&]() { return message; }, language); - } - - if (t->IsNPC() && !is_engaged) { - CheckLDoNHail(t->CastToNPC()); - CheckEmoteHail(t->CastToNPC(), message); - - if (RuleB(TaskSystem, EnableTaskSystem)) { - if (UpdateTasksOnSpeakWith(t->CastToNPC())) { - t->CastToNPC()->DoQuestPause(this); - } - } - } - } - break; - } - case ChatChannel_UCSRelay: - { - // UCS Relay for Underfoot and later. - if(!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - break; - } - case ChatChannel_Emotes: - { - // Emotes for Underfoot and later. - // crash protection -- cheater - message[1023] = '\0'; - size_t msg_len = strlen(message); - if (msg_len > 512) - message[512] = '\0'; - - auto outapp = new EQApplicationPacket(OP_Emote, 4 + msg_len + strlen(GetName()) + 2); - Emote_Struct* es = (Emote_Struct*)outapp->pBuffer; - char *Buffer = (char *)es; - Buffer += 4; - snprintf(Buffer, sizeof(Emote_Struct) - 4, "%s %s", GetName(), message); - entity_list.QueueCloseClients(this, outapp, true, RuleI(Range, Emote), 0, true, FilterSocials); - safe_delete(outapp); - break; - } - default: { - Message(0, "Channel (%i) not implemented", (uint16)chan_num); - } - } -} - -void Client::ChannelMessageSend( - const char *from, - const char *to, - uint8 channel_id, - uint8 language_id, - uint8 language_skill, - const char *message, - ... -) -{ - if ( - (channel_id == ChatChannel_Petition && Admin() < AccountStatus::QuestTroupe) || - (channel_id == ChatChannel_GMSAY && !GetGM()) - ) { - return; - } - - va_list argptr; - char buffer[4096]; - char message_sender[64]; - - va_start(argptr, message); - vsnprintf(buffer, 4096, message, argptr); - va_end(argptr); - - EQApplicationPacket app(OP_ChannelMessage, sizeof(ChannelMessage_Struct) + strlen(buffer) + 1); - - auto* cm = (ChannelMessage_Struct *) app.pBuffer; - - if (from == 0) { - strcpy(cm->sender, "ZServer"); - } else if (from[0] == 0) { - strcpy(cm->sender, "ZServer"); - } else { - CleanMobName(from, message_sender); - strcpy(cm->sender, message_sender); - } - - if (to != 0) { - strcpy((char *) cm->targetname, to); - } else if (channel_id == ChatChannel_Tell) { - strcpy(cm->targetname, m_pp.name); - } else { - cm->targetname[0] = 0; - } - - uint8 listener_skill; - - const bool is_valid_language = EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27); - - if (is_valid_language) { - listener_skill = m_pp.languages[language_id]; - cm->language = listener_skill < 24 ? Language::Unknown27 : language_id; - } else { - listener_skill = m_pp.languages[Language::CommonTongue]; - cm->language = Language::CommonTongue; - } - - // set effective language skill = lower of sender and receiver skills - uint8 effective_skill = (language_skill < listener_skill ? language_skill : listener_skill); - if (effective_skill > Language::MaxValue) { - effective_skill = Language::MaxValue; - } - - cm->skill_in_language = effective_skill; - - cm->chan_num = channel_id; - strcpy(&cm->message[0], buffer); - - QueuePacket(&app); - - const bool can_train_self = RuleB(Client, SelfLanguageLearning); - const bool is_not_sender = strcmp(GetCleanName(), cm->sender); - - if (can_train_self || is_not_sender) { - if ( - channel_id == ChatChannel_Group && - listener_skill < Language::MaxValue - ) { // group message in non-mastered language, check for skill up - if (is_valid_language && m_pp.languages[language_id] <= language_skill) { - CheckLanguageSkillIncrease(language_id, language_skill); - } - } - } -} - -void Client::Message(uint32 type, const char* message, ...) { - if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) - return; - if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... - return; - if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) - return; - - va_list argptr; - auto buffer = new char[4096]; - va_start(argptr, message); - vsnprintf(buffer, 4096, message, argptr); - va_end(argptr); - - SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); - buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); - buf.WriteInt8(static_cast(Journal::Mode::None)); - buf.WriteInt8(0); // language - buf.WriteUInt32(type); - buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here - buf.WriteString(""); // send name, not applicable here - buf.WriteInt32(0); // location, client seems to ignore - buf.WriteInt32(0); - buf.WriteInt32(0); - buf.WriteString(buffer); - - auto app = new EQApplicationPacket(OP_SpecialMesg, buf); - - FastQueuePacket(&app); - - safe_delete_array(buffer); -} - -void Client::FilteredMessage(Mob *sender, uint32 type, eqFilterType filter, const char* message, ...) { - if (!FilteredMessageCheck(sender, filter)) - return; - - va_list argptr; - auto buffer = new char[4096]; - va_start(argptr, message); - vsnprintf(buffer, 4096, message, argptr); - va_end(argptr); - - SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); - buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); - buf.WriteInt8(static_cast(Journal::Mode::None)); - buf.WriteInt8(0); // language - buf.WriteUInt32(type); - buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here - buf.WriteString(""); // send name, not applicable here - buf.WriteInt32(0); // location, client seems to ignore - buf.WriteInt32(0); - buf.WriteInt32(0); - buf.WriteString(buffer); - - auto app = new EQApplicationPacket(OP_SpecialMesg, buf); - - FastQueuePacket(&app); - - safe_delete_array(buffer); -} - -void Client::SetMaxHP() { - if(dead) - return; - SetHP(CalcMaxHP()); - SendHPUpdate(); - Save(); -} - -bool Client::UpdateLDoNPoints(uint32 theme_id, int points) -{ - if (points < 0) { - if (m_pp.ldon_points_available < (0 - points)) { - return false; - } - } - - bool is_loss = false; - - switch (theme_id) { - case LDoNTheme::Unused: { // No theme, so distribute evenly across all - int split_points = (points / 5); - - int guk_points = (split_points + (points % 5)); - int mir_points = split_points; - int mmc_points = split_points; - int ruj_points = split_points; - int tak_points = split_points; - - split_points = 0; - - if (points < 0) { - if (m_pp.ldon_points_available < (0 - points)) { - return false; - } - - is_loss = true; - - if (m_pp.ldon_points_guk < (0 - guk_points)) { - mir_points += (guk_points + m_pp.ldon_points_guk); - guk_points = (0 - m_pp.ldon_points_guk); - } - - if (m_pp.ldon_points_mir < (0 - mir_points)) { - mmc_points += (mir_points + m_pp.ldon_points_mir); - mir_points = (0 - m_pp.ldon_points_mir); - } - - if (m_pp.ldon_points_mmc < (0 - mmc_points)) { - ruj_points += (mmc_points + m_pp.ldon_points_mmc); - mmc_points = (0 - m_pp.ldon_points_mmc); - } - - if (m_pp.ldon_points_ruj < (0 - ruj_points)) { - tak_points += (ruj_points + m_pp.ldon_points_ruj); - ruj_points = (0 - m_pp.ldon_points_ruj); - } - - if (m_pp.ldon_points_tak < (0 - tak_points)) { - split_points = (tak_points + m_pp.ldon_points_tak); - tak_points = (0 - m_pp.ldon_points_tak); - } - } - - m_pp.ldon_points_guk += guk_points; - m_pp.ldon_points_mir += mir_points; - m_pp.ldon_points_mmc += mmc_points; - m_pp.ldon_points_ruj += ruj_points; - m_pp.ldon_points_tak += tak_points; - - points -= split_points; - - if (split_points != 0) { // if anything left, recursively loop thru again - UpdateLDoNPoints(LDoNTheme::Unused, split_points); - } - - break; - } - case LDoNTheme::GUK: { - if (points < 0) { - if (m_pp.ldon_points_guk < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_guk += points; - break; - } - case LDoNTheme::MIR: { - if (points < 0) { - if (m_pp.ldon_points_mir < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_mir += points; - break; - } - case LDoNTheme::MMC: { - if (points < 0) { - if (m_pp.ldon_points_mmc < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_mmc += points; - break; - } - case LDoNTheme::RUJ: { - if (points < 0) { - if (m_pp.ldon_points_ruj < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_ruj += points; - break; - } - case LDoNTheme::TAK: { - if (points < 0) { - if (m_pp.ldon_points_tak < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_tak += points; - break; - } - } - - m_pp.ldon_points_available += points; - - QuestEventID event_id = is_loss ? EVENT_LDON_POINTS_LOSS : EVENT_LDON_POINTS_GAIN; - - if (parse->PlayerHasQuestSub(event_id)) { - const std::string &export_string = fmt::format( - "{} {}", - theme_id, - std::abs(points) - ); - - parse->EventPlayer(event_id, this, export_string, 0); - } - - auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); - auto *apus = (AdventurePoints_Update_Struct *) outapp->pBuffer; - - apus->ldon_available_points = m_pp.ldon_points_available; - apus->ldon_guk_points = m_pp.ldon_points_guk; - apus->ldon_mirugal_points = m_pp.ldon_points_mir; - apus->ldon_mistmoore_points = m_pp.ldon_points_mmc; - apus->ldon_rujarkian_points = m_pp.ldon_points_ruj; - apus->ldon_takish_points = m_pp.ldon_points_tak; - - outapp->priority = 6; - - QueuePacket(outapp); - safe_delete(outapp); - return true; -} - -void Client::SetLDoNPoints(uint32 theme_id, uint32 points) -{ - switch (theme_id) { - case LDoNTheme::GUK: { - m_pp.ldon_points_guk = points; - break; - } - case LDoNTheme::MIR: { - m_pp.ldon_points_mir = points; - break; - } - case LDoNTheme::MMC: { - m_pp.ldon_points_mmc = points; - break; - } - case LDoNTheme::RUJ: { - m_pp.ldon_points_ruj = points; - break; - } - case LDoNTheme::TAK: { - m_pp.ldon_points_tak = points; - break; - } - } - - m_pp.ldon_points_available = ( - m_pp.ldon_points_guk + - m_pp.ldon_points_mir + - m_pp.ldon_points_mmc + - m_pp.ldon_points_ruj + - m_pp.ldon_points_tak - ); - - auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); - - auto a = (AdventurePoints_Update_Struct*) outapp->pBuffer; - - a->ldon_available_points = m_pp.ldon_points_available; - a->ldon_guk_points = m_pp.ldon_points_guk; - a->ldon_mirugal_points = m_pp.ldon_points_mir; - a->ldon_mistmoore_points = m_pp.ldon_points_mmc; - a->ldon_rujarkian_points = m_pp.ldon_points_ruj; - a->ldon_takish_points = m_pp.ldon_points_tak; - - outapp->priority = 6; - - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SetSkill(EQ::skills::SkillType skillid, uint16 value) { - if (skillid > EQ::skills::HIGHEST_SKILL) - return; - m_pp.skills[skillid] = value; // We need to be able to #setskill 254 and 255 to reset skills - - database.SaveCharacterSkill(CharacterID(), skillid, value); - auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); - SkillUpdate_Struct* skill = (SkillUpdate_Struct*)outapp->pBuffer; - skill->skillId=skillid; - skill->value=value; - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::IncreaseLanguageSkill(uint8 language_id, uint8 increase) -{ - if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { - return; - } - - m_pp.languages[language_id] += increase; - - if (m_pp.languages[language_id] > Language::MaxValue) { - m_pp.languages[language_id] = Language::MaxValue; - } - - database.SaveCharacterLanguage(CharacterID(), language_id, m_pp.languages[language_id]); - - auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); - auto* s = (SkillUpdate_Struct*) outapp->pBuffer; - - s->skillId = 100 + language_id; - s->value = m_pp.languages[language_id]; - - QueuePacket(outapp); - safe_delete(outapp); - - MessageString(Chat::Skills, LANG_SKILL_IMPROVED); -} - -void Client::AddSkill(EQ::skills::SkillType skillid, uint16 value) { - if (skillid > EQ::skills::HIGHEST_SKILL) - return; - value = GetRawSkill(skillid) + value; - uint16 max = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); - if (value > max) - value = max; - SetSkill(skillid, value); -} - -void Client::SendSound(){//Makes a sound. - auto outapp = new EQApplicationPacket(OP_Sound, 68); - unsigned char x[68]; - memset(x, 0, 68); - x[0]=0x22; - memset(&x[4],0x8002,sizeof(uint16)); - memset(&x[8],0x8624,sizeof(uint16)); - memset(&x[12],0x4A01,sizeof(uint16)); - x[16]=0x05; - x[28]=0x00;//change this value to give gold to the client - memset(&x[40],0xFFFFFFFF,sizeof(uint32)); - memset(&x[44],0xFFFFFFFF,sizeof(uint32)); - memset(&x[48],0xFFFFFFFF,sizeof(uint32)); - memset(&x[52],0xFFFFFFFF,sizeof(uint32)); - memset(&x[56],0xFFFFFFFF,sizeof(uint32)); - memset(&x[60],0xFFFFFFFF,sizeof(uint32)); - memset(&x[64],0xffffffff,sizeof(uint32)); - memcpy(outapp->pBuffer,x,outapp->size); - QueuePacket(outapp); - safe_delete(outapp); - -} -void Client::UpdateWho(uint8 remove) -{ - if (account_id == 0) { - return; - } - if (!worldserver.Connected()) { - return; - } - - auto pack = new ServerPacket(ServerOP_ClientList, sizeof(ServerClientList_Struct)); - auto *s = (ServerClientList_Struct *) pack->pBuffer; - s->remove = remove; - s->wid = GetWID(); - s->IP = GetIP(); - s->charid = CharacterID(); - strcpy(s->name, GetName()); - - s->gm = GetGM(); - s->Admin = Admin(); - s->AccountID = AccountID(); - strcpy(s->AccountName, AccountName()); - - s->LSAccountID = LSAccountID(); - strn0cpy(s->lskey, lskey, sizeof(s->lskey)); - - s->zone = zone->GetZoneID(); - s->instance_id = zone->GetInstanceID(); - s->race = GetRace(); - s->class_ = GetClass(); - s->level = GetLevel(); - - if (m_pp.anon == 0) { - s->anon = 0; - } - else if (m_pp.anon == 1) { - s->anon = 1; - } - else if (m_pp.anon >= 2) { - s->anon = 2; - } - - s->ClientVersion = static_cast(ClientVersion()); - s->tellsoff = tellsoff; - s->guild_id = guild_id; - s->guild_rank = guildrank; - s->guild_tribute_opt_in = guild_tribute_opt_in; - s->LFG = LFG; - if (LFG) { - s->LFGFromLevel = LFGFromLevel; - s->LFGToLevel = LFGToLevel; - s->LFGMatchFilter = LFGMatchFilter; - memcpy(s->LFGComments, LFGComments, sizeof(s->LFGComments)); - } - - worldserver.SendPacket(pack); - safe_delete(pack); -} - -void Client::WhoAll(Who_All_Struct* whom) { - - if (!worldserver.Connected()) - Message(0, "Error: World server disconnected"); - else { - auto pack = new ServerPacket(ServerOP_Who, sizeof(ServerWhoAll_Struct)); - ServerWhoAll_Struct* whoall = (ServerWhoAll_Struct*) pack->pBuffer; - whoall->admin = Admin(); - whoall->fromid=GetID(); - strcpy(whoall->from, GetName()); - strn0cpy(whoall->whom, whom->whom, 64); - whoall->lvllow = whom->lvllow; - whoall->lvlhigh = whom->lvlhigh; - whoall->gmlookup = whom->gmlookup; - whoall->wclass = whom->wclass; - whoall->wrace = whom->wrace; - worldserver.SendPacket(pack); - safe_delete(pack); - } -} - -void Client::FriendsWho(char *FriendsString) { - - if (!worldserver.Connected()) - Message(0, "Error: World server disconnected"); - else { - auto pack = - new ServerPacket(ServerOP_FriendsWho, sizeof(ServerFriendsWho_Struct) + strlen(FriendsString)); - ServerFriendsWho_Struct* FriendsWho = (ServerFriendsWho_Struct*) pack->pBuffer; - FriendsWho->FromID = GetID(); - strcpy(FriendsWho->FromName, GetName()); - strcpy(FriendsWho->FriendsString, FriendsString); - worldserver.SendPacket(pack); - safe_delete(pack); - } -} - -void Client::UpdateAdmin(bool from_database) { - int16 tmp = admin; - if (from_database) { - admin = database.GetAccountStatus(account_id); - } - - if (tmp == admin && from_database) { - return; - } - - if (m_pp.gm) { - LogInfo("[{}] - [{}] is a GM", __FUNCTION__ , GetName()); - petition_list.UpdateGMQueue(); - } - - UpdateWho(); -} - -void Client::SetStats(uint8 type,int16 set_val){ - if(type>STAT_DISEASE){ - printf("Error in Client::IncStats, received invalid type of: %i\n",type); - return; - } - auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); - IncreaseStat_Struct* iss=(IncreaseStat_Struct*)outapp->pBuffer; - switch(type){ - case STAT_STR: - if(set_val>0) - iss->str=set_val; - if(set_val<0) - m_pp.STR=0; - else if(set_val>255) - m_pp.STR=255; - else - m_pp.STR=set_val; - break; - case STAT_STA: - if(set_val>0) - iss->sta=set_val; - if(set_val<0) - m_pp.STA=0; - else if(set_val>255) - m_pp.STA=255; - else - m_pp.STA=set_val; - break; - case STAT_AGI: - if(set_val>0) - iss->agi=set_val; - if(set_val<0) - m_pp.AGI=0; - else if(set_val>255) - m_pp.AGI=255; - else - m_pp.AGI=set_val; - break; - case STAT_DEX: - if(set_val>0) - iss->dex=set_val; - if(set_val<0) - m_pp.DEX=0; - else if(set_val>255) - m_pp.DEX=255; - else - m_pp.DEX=set_val; - break; - case STAT_INT: - if(set_val>0) - iss->int_=set_val; - if(set_val<0) - m_pp.INT=0; - else if(set_val>255) - m_pp.INT=255; - else - m_pp.INT=set_val; - break; - case STAT_WIS: - if(set_val>0) - iss->wis=set_val; - if(set_val<0) - m_pp.WIS=0; - else if(set_val>255) - m_pp.WIS=255; - else - m_pp.WIS=set_val; - break; - case STAT_CHA: - if(set_val>0) - iss->cha=set_val; - if(set_val<0) - m_pp.CHA=0; - else if(set_val>255) - m_pp.CHA=255; - else - m_pp.CHA=set_val; - break; - } - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::IncStats(uint8 type, int16 increase_val) -{ - if (type > STAT_DISEASE) { - printf("Error in Client::IncStats, received invalid type of: %i\n", type); - return; - } - auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); - IncreaseStat_Struct *iss = (IncreaseStat_Struct *) outapp->pBuffer; - switch (type) { - case STAT_STR: - if (increase_val > 0) { - iss->str = increase_val; - } - - if ((m_pp.STR + increase_val * 2) > 255) { - m_pp.STR = 255; - } else { - m_pp.STR += increase_val * 2; - } - break; - case STAT_STA: - if (increase_val > 0) { - iss->sta = increase_val; - } - - if ((m_pp.STA + increase_val * 2) > 255) { - m_pp.STA = 255; - } else { - m_pp.STA += increase_val * 2; - } - break; - case STAT_AGI: - if (increase_val > 0) { - iss->agi = increase_val; - } - if ((m_pp.AGI + increase_val * 2) > 255) { - m_pp.AGI = 255; - } else { - m_pp.AGI += increase_val * 2; - } - break; - case STAT_DEX: - if (increase_val > 0) { - iss->dex = increase_val; - } - - if ((m_pp.DEX + increase_val * 2) > 255) { - m_pp.DEX = 255; - } else { - m_pp.DEX += increase_val * 2; - } - break; - case STAT_INT: - if (increase_val > 0) { - iss->int_ = increase_val; - } - - if ((m_pp.INT + increase_val * 2) > 255) { - m_pp.INT = 255; - } else { - m_pp.INT += increase_val * 2; - } - break; - case STAT_WIS: - if (increase_val > 0) { - iss->wis = increase_val; - } - - if ((m_pp.WIS + increase_val * 2) > 255) { - m_pp.WIS = 255; - } else { - m_pp.WIS += increase_val * 2; - } - break; - case STAT_CHA: - if (increase_val > 0) { - iss->cha = increase_val; - } - - if ((m_pp.CHA + increase_val * 2) > 255) { - m_pp.CHA = 255; - } else { - m_pp.CHA += increase_val * 2; - } - break; - } - QueuePacket(outapp); - safe_delete(outapp); -} - -const int64& Client::SetMana(int64 amount) { - bool update = false; - if (amount < 0) - amount = 0; - if (amount > GetMaxMana()) - amount = GetMaxMana(); - if (amount != current_mana) - update = true; - current_mana = amount; - if (update) - Mob::SetMana(amount); - CheckManaEndUpdate(); - return current_mana; -} - -void Client::CheckManaEndUpdate() { - if (!Connected()) - return; - - if (last_reported_mana != current_mana || last_reported_endurance != current_endurance) { - - if (ClientVersion() >= EQ::versions::ClientVersion::SoD) { - SendManaUpdate(); - SendEnduranceUpdate(); - } - - auto outapp = new EQApplicationPacket(OP_ManaChange, sizeof(ManaChange_Struct)); - ManaChange_Struct* mana_change = (ManaChange_Struct*)outapp->pBuffer; - mana_change->new_mana = current_mana; - mana_change->stamina = current_endurance; - mana_change->spell_id = casting_spell_id; - mana_change->keepcasting = 1; - mana_change->slot = -1; - outapp->priority = 6; - QueuePacket(outapp); - safe_delete(outapp); - - /* Let others know when our mana percent has changed */ - if (GetManaPercent() != last_reported_mana_percent) { - Group *group = GetGroup(); - Raid *raid = GetRaid(); - - if (raid) { - raid->SendManaPacketFrom(this); - } - else if (group) { - group->SendManaPacketFrom(this); - } - - auto mana_packet = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); - ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_packet->pBuffer; - mana_update->cur_mana = GetMana(); - mana_update->max_mana = GetMaxMana(); - mana_update->spawn_id = GetID(); - if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) - QueuePacket(mana_packet); // do we need this with the OP_ManaChange packet above? - entity_list.QueueClientsByXTarget(this, mana_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); - safe_delete(mana_packet); - - last_reported_mana_percent = GetManaPercent(); - } - - /* Let others know when our endurance percent has changed */ - if (GetEndurancePercent() != last_reported_endurance_percent) { - Group *group = GetGroup(); - Raid *raid = GetRaid(); - - if (raid) { - raid->SendEndurancePacketFrom(this); - } - else if (group) { - group->SendEndurancePacketFrom(this); - } - - auto endurance_packet = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); - EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)endurance_packet->pBuffer; - endurance_update->cur_end = GetEndurance(); - endurance_update->max_end = GetMaxEndurance(); - endurance_update->spawn_id = GetID(); - if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) - QueuePacket(endurance_packet); // do we need this with the OP_ManaChange packet above? - entity_list.QueueClientsByXTarget(this, endurance_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); - safe_delete(endurance_packet); - - last_reported_endurance_percent = GetEndurancePercent(); - } - - last_reported_mana = current_mana; - last_reported_endurance = current_endurance; - } -} - -// sends mana update to self -void Client::SendManaUpdate() -{ - auto mana_app = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); - ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_app->pBuffer; - mana_update->cur_mana = GetMana(); - mana_update->max_mana = GetMaxMana(); - mana_update->spawn_id = GetID(); - QueuePacket(mana_app); - safe_delete(mana_app); -} - -// sends endurance update to self -void Client::SendEnduranceUpdate() -{ - auto end_app = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); - EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)end_app->pBuffer; - endurance_update->cur_end = GetEndurance(); - endurance_update->max_end = GetMaxEndurance(); - endurance_update->spawn_id = GetID(); - QueuePacket(end_app); - safe_delete(end_app); -} - -void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) -{ - Mob::FillSpawnStruct(ns, ForWho); - - // Populate client-specific spawn information - ns->spawn.afk = AFK; - ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live - ns->spawn.anon = m_pp.anon; - ns->spawn.gm = GetGM() ? 1 : 0; - ns->spawn.guildID = GuildID(); - ns->spawn.trader = IsTrader(); - ns->spawn.buyer = IsBuyer(); -// ns->spawn.linkdead = IsLD() ? 1 : 0; -// ns->spawn.pvp = GetPVP(false) ? 1 : 0; - ns->spawn.show_name = true; - - strcpy(ns->spawn.title, m_pp.title); - strcpy(ns->spawn.suffix, m_pp.suffix); - - if (IsBecomeNPC() == true) - ns->spawn.NPC = 1; - else if (ForWho == this) - ns->spawn.NPC = 10; - else - ns->spawn.NPC = 0; - ns->spawn.is_pet = 0; - - if (!IsInAGuild()) { - ns->spawn.guildrank = 0xFF; - } else { - ns->spawn.guildrank = guild_mgr.GetDisplayedRank(GuildID(), GuildRank(), CharacterID()); - ns->spawn.guild_show = guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_DISPLAY_GUILD_NAME); - } - ns->spawn.size = 0; // Changing size works, but then movement stops! (wth?) - ns->spawn.runspeed = (gmspeed == 0) ? runspeed : 3.125f; - ns->spawn.showhelm = m_pp.showhelm ? 1 : 0; - - UpdateEquipmentLight(); - UpdateActiveLight(); - ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive]; -} - -bool Client::GMHideMe(Client* client) { - if (gm_hide_me) { - if (client == 0) - return true; - else if (admin > client->Admin()) - return true; - else - return false; - } - else - return false; -} - -void Client::Duck() { - SetAppearance(eaCrouching, false); -} - -void Client::Stand() { - SetAppearance(eaStanding, false); -} - -void Client::Sit() { - SetAppearance(eaSitting, false); -} - -void Client::ChangeLastName(std::string last_name) { - memset(m_pp.last_name, 0, sizeof(m_pp.last_name)); - strn0cpy(m_pp.last_name, last_name.c_str(), sizeof(m_pp.last_name)); - auto outapp = new EQApplicationPacket(OP_GMLastName, sizeof(GMLastName_Struct)); - auto gmn = (GMLastName_Struct*) outapp->pBuffer; - strn0cpy(gmn->name, name, sizeof(gmn->name)); - strn0cpy(gmn->gmname, name, sizeof(gmn->gmname)); - strn0cpy(gmn->lastname, last_name.c_str(), sizeof(gmn->lastname)); - - gmn->unknown[0] = 1; - gmn->unknown[1] = 1; - gmn->unknown[2] = 1; - gmn->unknown[3] = 1; - - entity_list.QueueClients(this, outapp, false); - - safe_delete(outapp); -} - -bool Client::ChangeFirstName(const char* in_firstname, const char* gmname) -{ - // check duplicate name - bool used_name = database.IsNameUsed((const char*) in_firstname); - if (used_name) { - return false; - } - - // update character_ - if(!database.UpdateName(GetName(), in_firstname)) - return false; - - // update pp - memset(m_pp.name, 0, sizeof(m_pp.name)); - snprintf(m_pp.name, sizeof(m_pp.name), "%s", in_firstname); - strcpy(name, m_pp.name); - Save(); - - // send name update packet - auto outapp = new EQApplicationPacket(OP_GMNameChange, sizeof(GMName_Struct)); - GMName_Struct* gmn=(GMName_Struct*)outapp->pBuffer; - strn0cpy(gmn->gmname,gmname,64); - strn0cpy(gmn->oldname,GetName(),64); - strn0cpy(gmn->newname,in_firstname,64); - gmn->unknown[0] = 1; - gmn->unknown[1] = 1; - gmn->unknown[2] = 1; - entity_list.QueueClients(this, outapp, false); - safe_delete(outapp); - - // finally, update the /who list - UpdateWho(); - - // success - return true; -} - -void Client::SetGM(bool toggle) { - m_pp.gm = toggle ? 1 : 0; - m_inv.SetGMInventory((bool)m_pp.gm); - Message( - Chat::White, - fmt::format( - "You are {} flagged as a GM.", - m_pp.gm ? "now" : "no longer" - ).c_str() - ); - SendAppearancePacket(AppearanceType::GM, m_pp.gm); - Save(); - UpdateWho(); -} - -void Client::ReadBook(BookRequest_Struct* book) -{ - const std::string& text_file = book->txtfile; - - if (text_file.empty()) { - return; - } - - auto b = content_db.GetBook(text_file); - - if (!b.text.empty()) { - auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); - auto inst = const_cast(m_inv[book->invslot]); - - auto t = (BookText_Struct*) outapp->pBuffer; - - t->window = book->window; - t->type = book->type; - t->invslot = book->invslot; - t->target_id = book->target_id; - t->can_cast = 0; // todo: implement - t->can_scribe = false; - - if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END) { - if (inst && inst->GetItem()) { - auto recipe = TradeskillRecipeRepository::GetWhere( - content_db, - fmt::format( - "learned_by_item_id = {} LIMIT 1", - inst->GetItem()->ID - ) - ); - - t->type = inst->GetItem()->Book; - t->can_scribe = !recipe.empty(); - } - } - - memcpy(t->booktext, b.text.c_str(), b.text.size()); - - if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { - if (m_pp.languages[b.language] < Language::MaxValue) { - GarbleMessage(t->booktext, (Language::MaxValue - m_pp.languages[b.language])); - } - } - - // Send only books and scrolls to this event - if (parse->PlayerHasQuestSub(EVENT_READ_ITEM) && t->type != BookType::ItemInfo) { - std::vector args = { - b.text, - t->can_cast, - t->can_scribe, - t->invslot, - t->target_id, - t->type, - inst - }; - - parse->EventPlayer(EVENT_READ_ITEM, this, book->txtfile, inst ? inst->GetID() : 0, &args); - } - - QueuePacket(outapp); - safe_delete(outapp); - } -} - -void Client::QuestReadBook(const char* text, uint8 type) { - std::string booktxt2 = text; - int length = booktxt2.length(); - if (booktxt2[0] != '\0') { - auto outapp = new EQApplicationPacket(OP_ReadBook, length + sizeof(BookText_Struct)); - BookText_Struct *out = (BookText_Struct *) outapp->pBuffer; - out->window = 0xFF; - out->type = type; - out->invslot = 0; - memcpy(out->booktext, booktxt2.c_str(), length); - QueuePacket(outapp); - safe_delete(outapp); - } -} - -uint32 Client::GetCarriedPlatinum() { - return ( - GetMoney(MoneyTypes::Platinum, MoneySubtypes::Personal) + - (GetMoney(MoneyTypes::Gold, MoneySubtypes::Personal) / 10) + - (GetMoney(MoneyTypes::Silver, MoneySubtypes::Personal) / 100) + - (GetMoney(MoneyTypes::Copper, MoneySubtypes::Personal) / 1000) - ); -} - -bool Client::TakePlatinum(uint32 platinum, bool update_client) { - if (GetCarriedPlatinum() >= platinum) { - const auto copper = static_cast(platinum) * 1000; - return TakeMoneyFromPP(copper, update_client); - } - - return false; -} - -bool Client::TakeMoneyFromPP(uint64 copper, bool update_client) { - int64 player_copper, silver, gold, platinum; - player_copper = m_pp.copper; - silver = static_cast(m_pp.silver) * 10; - gold = static_cast(m_pp.gold) * 100; - platinum = static_cast(m_pp.platinum) * 1000; - - int64 client_total = player_copper + silver + gold + platinum; - - client_total -= copper; - if (client_total < 0) { - return false; // Not enough money! - } else { - player_copper -= copper; - if(player_copper <= 0) { - copper = std::abs(player_copper); - m_pp.copper = 0; - } else { - m_pp.copper = player_copper; - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - return true; - } - - silver -= copper; - if (silver <= 0) { - copper = std::abs(silver); - m_pp.silver = 0; - } else { - m_pp.silver = silver / 10; - m_pp.copper += (silver - (m_pp.silver * 10)); - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - return true; - } - - gold -=copper; - - if (gold <= 0) { - copper = std::abs(gold); - m_pp.gold = 0; - } else { - m_pp.gold = gold / 100; - uint64 silver_test = (gold - (static_cast(m_pp.gold) * 100)) / 10; - m_pp.silver += silver_test; - uint64 copper_test = (gold - (static_cast(m_pp.gold) * 100 + silver_test * 10)); - m_pp.copper += copper_test; - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - return true; - } - - platinum -= copper; - - //Impossible for plat to be negative, already checked above - - m_pp.platinum = platinum / 1000; - uint64 gold_test = (platinum - (static_cast(m_pp.platinum) * 1000)) / 100; - m_pp.gold += gold_test; - uint64 silver_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100)) / 10; - m_pp.silver += silver_test; - uint64 copper_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100 + silver_test * 10)); - m_pp.copper = copper_test; - - if (update_client) { - SendMoneyUpdate(); - } - - RecalcWeight(); - SaveCurrency(); - return true; - } -} - -void Client::AddPlatinum(uint32 platinum, bool update_client) { - const auto copper = static_cast(platinum) * 1000; - AddMoneyToPP(copper, update_client); -} - -void Client::AddMoneyToPP(uint64 copper, bool update_client){ - uint64 temporary_copper; - uint64 temporary_copper_two; - temporary_copper = copper; - - /* Add Amount of Platinum */ - temporary_copper_two = temporary_copper / 1000; - int32 new_value = m_pp.platinum + temporary_copper_two; - - if (new_value < 0) { - m_pp.platinum = 0; - } else { - m_pp.platinum = m_pp.platinum + temporary_copper_two; - } - - temporary_copper -= temporary_copper_two * 1000; - - /* Add Amount of Gold */ - temporary_copper_two = temporary_copper / 100; - new_value = m_pp.gold + temporary_copper_two; - - if (new_value < 0) { - m_pp.gold = 0; - } else { - m_pp.gold = m_pp.gold + temporary_copper_two; - } - - temporary_copper -= temporary_copper_two * 100; - - /* Add Amount of Silver */ - temporary_copper_two = temporary_copper / 10; - new_value = m_pp.silver + temporary_copper_two; - - if (new_value < 0) { - m_pp.silver = 0; - } else { - m_pp.silver = m_pp.silver + temporary_copper_two; - } - - temporary_copper -= temporary_copper_two * 10; - - /* Add Amount of Copper */ - temporary_copper_two = temporary_copper; - new_value = m_pp.copper + temporary_copper_two; - - if (new_value < 0) { - m_pp.copper = 0; - } else { - m_pp.copper = m_pp.copper + temporary_copper_two; - } - - //send them all at once, since the above code stopped working. - if (update_client) { - SendMoneyUpdate(); - } - - RecalcWeight(); - - SaveCurrency(); - - LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); -} - -void Client::EVENT_ITEM_ScriptStopReturn(){ - /* Set a timestamp in an entity variable for plugin check_handin.pl in return_items - This will stopgap players from items being returned if global_npc.pl has a catch all return_items - */ - struct timeval read_time; - char buffer[50]; - gettimeofday(&read_time, 0); - sprintf(buffer, "%li.%li \n", read_time.tv_sec, read_time.tv_usec); - SetEntityVariable("Stop_Return", buffer); -} - -void Client::AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 platinum, bool update_client){ - EVENT_ITEM_ScriptStopReturn(); - - int32 new_value = m_pp.platinum + platinum; - if (new_value >= 0 && new_value > m_pp.platinum) { - m_pp.platinum += platinum; - } - - new_value = m_pp.gold + gold; - if (new_value >= 0 && new_value > m_pp.gold) { - m_pp.gold += gold; - } - - new_value = m_pp.silver + silver; - if (new_value >= 0 && new_value > m_pp.silver) { - m_pp.silver += silver; - } - - new_value = m_pp.copper + copper; - if (new_value >= 0 && new_value > m_pp.copper) { - m_pp.copper += copper; - } - - if (update_client) { - SendMoneyUpdate(); - } - - RecalcWeight(); - SaveCurrency(); - -#if (EQDEBUG>=5) - LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", - GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); -#endif -} - -void Client::SendMoneyUpdate() { - auto outapp = new EQApplicationPacket(OP_MoneyUpdate, sizeof(MoneyUpdate_Struct)); - MoneyUpdate_Struct* mus= (MoneyUpdate_Struct*)outapp->pBuffer; - - mus->platinum = m_pp.platinum; - mus->gold = m_pp.gold; - mus->silver = m_pp.silver; - mus->copper = m_pp.copper; - - FastQueuePacket(&outapp); -} - -bool Client::HasMoney(uint64 copper) { - - if ( - (static_cast(m_pp.copper) + - (static_cast(m_pp.silver) * 10) + - (static_cast(m_pp.gold) * 100) + - (static_cast(m_pp.platinum) * 1000)) >= copper - ) { - return true; - } - - return false; -} - -uint64 Client::GetCarriedMoney() { - - return ( - ( - static_cast(m_pp.copper) + - (static_cast(m_pp.silver) * 10) + - (static_cast(m_pp.gold) * 100) + - (static_cast(m_pp.platinum) * 1000) - ) - ); -} - -uint64 Client::GetAllMoney() { - - return ( - ( - static_cast(m_pp.copper) + - (static_cast(m_pp.silver) * 10) + - (static_cast(m_pp.gold) * 100) + - (static_cast(m_pp.platinum) * 1000) + - ( - static_cast(m_pp.copper_bank) + - (static_cast(m_pp.silver_bank) * 10) + - (static_cast(m_pp.gold_bank) * 100) + - (static_cast(m_pp.platinum_bank) * 1000) + - ( - static_cast(m_pp.copper_cursor) + - (static_cast(m_pp.silver_cursor) * 10) + - (static_cast(m_pp.gold_cursor) * 100) + - (static_cast(m_pp.platinum_cursor) * 1000) + - (static_cast(m_pp.platinum_shared) * 1000) - ) - ) - ) - ); -} - -bool Client::CheckIncreaseSkill(EQ::skills::SkillType skillid, Mob *against_who, int chancemodi) { - if (IsDead() || IsUnconscious()) { - return false; - } - - if (IsAIControlled()) { // no skillups while chamred =p - return false; - } - - if (against_who && against_who->IsCorpse()) { // no skillups on corpses - return false; - } - - if (skillid > EQ::skills::HIGHEST_SKILL) { - return false; - } - - auto skillval = GetRawSkill(skillid); - auto maxskill = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); - - if (parse->PlayerHasQuestSub(EVENT_USE_SKILL)) { - const auto& export_string = fmt::format( - "{} {}", - skillid, - skillval - ); - - parse->EventPlayer(EVENT_USE_SKILL, this, export_string, 0); - } - - if (against_who) { - if ( - against_who->GetSpecialAbility(SpecialAbility::AggroImmunity) || - against_who->GetSpecialAbility(SpecialAbility::ClientAggroImmunity) || - against_who->IsClient() || - GetLevelCon(against_who->GetLevel()) == ConsiderColor::Gray - ) { - return false; - } - } - - // Make sure we're not already at skill cap - if (skillval < maxskill) - { - double Chance = 0; - if (RuleI(Character, SkillUpMaximumChancePercentage) + chancemodi - RuleI(Character, SkillUpMinimumChancePercentage) <= RuleI(Character, SkillUpMinimumChancePercentage)) { - Chance = RuleI(Character, SkillUpMinimumChancePercentage); - } - else { - // f(x) = (max - min + modification) * .99^skillval + min - // This results in a exponential decay where as you skill up, you lose a slight chance to skill up, ranging from your modified maximum to approaching your minimum - // This result is increased by the existing SkillUpModifier rule - double working_chance = (((RuleI(Character, SkillUpMaximumChancePercentage) - RuleI(Character, SkillUpMinimumChancePercentage) + chancemodi) * (pow(0.99, skillval))) + RuleI(Character, SkillUpMinimumChancePercentage)); - Chance = (working_chance * RuleI(Character, SkillUpModifier) / 100); - } - - if(zone->random.Real(0, 99) < Chance) - { - SetSkill(skillid, GetRawSkill(skillid) + 1); - - if (player_event_logs.IsEventEnabled(PlayerEvent::SKILL_UP)) { - auto e = PlayerEvent::SkillUpEvent{ - .skill_id = static_cast(skillid), - .value = static_cast((skillval + 1)), - .max_skill = static_cast(maxskill), - .against_who = (against_who) ? against_who->GetCleanName() : GetCleanName(), - }; - RecordPlayerEventLog(PlayerEvent::SKILL_UP, e); - } - - if (parse->PlayerHasQuestSub(EVENT_SKILL_UP)) { - const auto& export_string = fmt::format( - "{} {} {} {}", - skillid, - skillval + 1, - maxskill, - 0 - ); - - parse->EventPlayer(EVENT_SKILL_UP, this, export_string, 0); - } - - LogSkills("Skill [{}] at value [{}] successfully gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); - return true; - } else { - LogSkills("Skill [{}] at value [{}] failed to gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); - } - } else { - LogSkills("Skill [{}] at value [{}] cannot increase due to maxmum [{}]", skillid, skillval, maxskill); - } - return false; -} - -void Client::CheckLanguageSkillIncrease(uint8 language_id, uint8 teacher_skill) { - if (IsDead() || IsUnconscious()) { - return; - } - - if (IsAIControlled()) { - return; - } - - if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { - return; - } - - const uint8 language_skill = m_pp.languages[language_id]; // get current language skill - - if (language_skill < Language::MaxValue) { // if the language isn't already maxed - int chance = 5 + ((teacher_skill - language_skill) / 10); // greater chance to learn if teacher's skill is much higher than yours - chance = (chance * RuleI(Character, SkillUpModifier) / 100); - - if (zone->random.Real(0, 100) < chance) { // if they make the roll - IncreaseLanguageSkill(language_id); - - if (parse->PlayerHasQuestSub(EVENT_LANGUAGE_SKILL_UP)) { - const auto &export_string = fmt::format( - "{} {} {}", - language_id, - language_skill + 1, - Language::MaxValue - ); - - parse->EventPlayer(EVENT_LANGUAGE_SKILL_UP, this, export_string, 0); - } - - LogSkills("Language [{}] at value [{}] successfully gain with [{}] % chance", language_id, language_skill, chance); - } else { - LogSkills("Language [{}] at value [{}] failed to gain with [{}] % chance", language_id, language_skill, chance); - } - } -} - -bool Client::HasSkill(EQ::skills::SkillType skill_id) const -{ - return GetSkill(skill_id) > 0 && CanHaveSkill(skill_id); -} - -bool Client::CanHaveSkill(EQ::skills::SkillType skill_id) const -{ - if ( - ClientVersion() < EQ::versions::ClientVersion::RoF2 && - class_ == Class::Berserker && - skill_id == EQ::skills::Skill1HPiercing - ) { - skill_id = EQ::skills::Skill2HPiercing; - } - - return skill_caps.GetSkillCap(GetClass(), skill_id, RuleI(Character, MaxLevel)).cap > 0; -} - -uint16 Client::MaxSkill(EQ::skills::SkillType skill_id, uint8 class_id, uint8 level) const -{ - if ( - ClientVersion() < EQ::versions::ClientVersion::RoF2 && - class_id == Class::Berserker && - skill_id == EQ::skills::Skill1HPiercing - ) { - skill_id = EQ::skills::Skill2HPiercing; - } - - return skill_caps.GetSkillCap(class_id, skill_id, level).cap; -} - -uint8 Client::GetSkillTrainLevel(EQ::skills::SkillType skill_id, uint8 class_id) -{ - if ( - ClientVersion() < EQ::versions::ClientVersion::RoF2 && - class_id == Class::Berserker && - skill_id == EQ::skills::Skill1HPiercing - ) { - skill_id = EQ::skills::Skill2HPiercing; - } - - return skill_caps.GetSkillTrainLevel(class_id, skill_id, RuleI(Character, MaxLevel)); -} - -uint16 Client::GetMaxSkillAfterSpecializationRules(EQ::skills::SkillType skillid, uint16 maxSkill) -{ - uint16 Result = maxSkill; - - uint16 PrimarySpecialization = 0, SecondaryForte = 0; - - uint16 PrimarySkillValue = 0, SecondarySkillValue = 0; - - uint16 MaxSpecializations = aabonuses.SecondaryForte ? 2 : 1; - - if (skillid >= EQ::skills::SkillSpecializeAbjure && skillid <= EQ::skills::SkillSpecializeEvocation) - { - bool HasPrimarySpecSkill = false; - - int NumberOfPrimarySpecSkills = 0; - - for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) - { - if(m_pp.skills[i] > 50) - { - HasPrimarySpecSkill = true; - NumberOfPrimarySpecSkills++; - } - if(m_pp.skills[i] > PrimarySkillValue) - { - if(PrimarySkillValue > SecondarySkillValue) - { - SecondarySkillValue = PrimarySkillValue; - SecondaryForte = PrimarySpecialization; - } - - PrimarySpecialization = i; - PrimarySkillValue = m_pp.skills[i]; - } - else if(m_pp.skills[i] > SecondarySkillValue) - { - SecondaryForte = i; - SecondarySkillValue = m_pp.skills[i]; - } - } - - if(SecondarySkillValue <=50) - SecondaryForte = 0; - - if(HasPrimarySpecSkill) - { - if(NumberOfPrimarySpecSkills <= MaxSpecializations) - { - if(MaxSpecializations == 1) - { - if(skillid != PrimarySpecialization) - { - Result = 50; - } - } - else - { - if((skillid != PrimarySpecialization) && ((skillid == SecondaryForte) || (SecondaryForte == 0))) - { - if((PrimarySkillValue > 100) || (!SecondaryForte)) - Result = 100; - } - else if(skillid != PrimarySpecialization) - { - Result = 50; - } - } - } - else - { - Message(Chat::Red, "Your spell casting specializations skills have been reset. " - "Only %i primary specialization skill is allowed.", MaxSpecializations); - - for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) - SetSkill((EQ::skills::SkillType)i, 1); - - Save(); - - LogInfo("Reset [{}]'s caster specialization skills to 1" - "Too many specializations skills were above 50.", GetCleanName()); - } - - } - } - - Result += spellbonuses.RaiseSkillCap[skillid] + itembonuses.RaiseSkillCap[skillid] + aabonuses.RaiseSkillCap[skillid]; - - if (skillid == EQ::skills::SkillType::SkillForage) - Result += aabonuses.GrantForage; - - return Result; -} - -void Client::SetPVP(bool toggle, bool message) { - m_pp.pvp = toggle ? 1 : 0; - - if (message) { - if(GetPVP()) { - MessageString(Chat::Shout, PVP_ON); - } else { - Message(Chat::Shout, "You now follow the ways of Order."); - } - } - - SendAppearancePacket(AppearanceType::PVP, GetPVP()); - Save(); -} - -void Client::Kick(const std::string &reason) { - client_state = CLIENT_KICKED; - - LogClientLogin("Client [{}] kicked, reason [{}]", GetCleanName(), reason.c_str()); -} - -void Client::WorldKick() { - auto outapp = new EQApplicationPacket(OP_GMKick, sizeof(GMKick_Struct)); - GMKick_Struct* gmk = (GMKick_Struct *)outapp->pBuffer; - strcpy(gmk->name,GetName()); - QueuePacket(outapp); - safe_delete(outapp); - Kick("World kick issued"); -} - -void Client::GMKill() { - auto outapp = new EQApplicationPacket(OP_GMKill, sizeof(GMKill_Struct)); - GMKill_Struct* gmk = (GMKill_Struct *)outapp->pBuffer; - strcpy(gmk->name,GetName()); - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::MemorizeSpell(uint32 slot, uint32 spell_id, uint32 scribing, uint32 reduction){ - if ( - !EQ::ValueWithin( - slot, - 0, - (EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize - 1) - ) - ) { - return; - } - - if ( - !EQ::ValueWithin( - spell_id, - 3, - EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellIdMax - ) && - spell_id != UINT32_MAX - ) { - return; - } - - auto outapp = new EQApplicationPacket(OP_MemorizeSpell, sizeof(MemorizeSpell_Struct)); - - auto* mss = (MemorizeSpell_Struct*) outapp->pBuffer; - - mss->scribing = scribing; - mss->slot = slot; - mss->spell_id = spell_id; - mss->reduction = reduction; - - outapp->priority = 5; - - if ( - parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) || - parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) || - parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) - ) { - const auto export_string = fmt::format("{} {}", slot, spell_id); - - if ( - scribing == ScribeSpellActions::Memorize && - parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) - ) { - parse->EventPlayer(EVENT_MEMORIZE_SPELL, this, export_string, 0); - } else if ( - scribing == ScribeSpellActions::Unmemorize && - parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) - ) { - parse->EventPlayer(EVENT_UNMEMORIZE_SPELL, this, export_string, 0); - } else if ( - scribing == ScribeSpellActions::Scribe && - parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) - ) { - parse->EventPlayer(EVENT_SCRIBE_SPELL, this, export_string, 0); - } - } - - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::Disarm(Client* disarmer, int chance) { - int16 slot = EQ::invslot::SLOT_INVALID; - const EQ::ItemInstance *inst = GetInv().GetItem(EQ::invslot::slotPrimary); - if (inst && inst->IsWeapon()) { - slot = EQ::invslot::slotPrimary; - } - else { - inst = GetInv().GetItem(EQ::invslot::slotSecondary); - if (inst && inst->IsWeapon()) - slot = EQ::invslot::slotSecondary; - } - if (slot != EQ::invslot::SLOT_INVALID && inst->IsClassCommon()) { - // We have an item that can be disarmed. - if (zone->random.Int(0, 1000) <= chance) { - // Find a free inventory slot - int16 slot_id = EQ::invslot::SLOT_INVALID; - slot_id = m_inv.FindFreeSlot(false, true, inst->GetItem()->Size, (inst->GetItem()->ItemType == EQ::item::ItemTypeArrow)); - if (slot_id != EQ::invslot::SLOT_INVALID) - { - EQ::ItemInstance *InvItem = m_inv.PopItem(slot); - if (InvItem) { // there should be no way it is not there, but check anyway - EQApplicationPacket* outapp = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); - MoveItem_Struct* mi = (MoveItem_Struct*)outapp->pBuffer; - mi->from_slot = slot; - mi->to_slot = 0xFFFFFFFF; - if (inst->IsStackable()) // it should not be stackable - mi->number_in_stack = inst->GetCharges(); - else - mi->number_in_stack = 0; - FastQueuePacket(&outapp); // this deletes item from the weapon slot on the client - if (PutItemInInventory(slot_id, *InvItem, true)) - database.SaveInventory(CharacterID(), NULL, slot); - auto matslot = (slot == EQ::invslot::slotPrimary ? EQ::textures::weaponPrimary : EQ::textures::weaponSecondary); - if (matslot != EQ::textures::materialInvalid) - SendWearChange(matslot); - } - MessageString(Chat::Skills, DISARMED); - if (disarmer != this) - disarmer->MessageString(Chat::Skills, DISARM_SUCCESS, GetCleanName()); - if (chance != 1000) - disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 4); - CalcBonuses(); - // CalcEnduranceWeightFactor(); - return; - } - disarmer->MessageString(Chat::Skills, DISARM_FAILED); - if (chance != 1000) - disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 2); - return; - } - } - disarmer->MessageString(Chat::Skills, DISARM_FAILED); -} - -bool Client::BindWound(Mob *bindmob, bool start, bool fail) -{ - EQApplicationPacket *outapp = nullptr; - if (!fail) { - outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); - BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; - // Start bind - if (!bindwound_timer.Enabled()) { - // make sure we actually have a bandage... and consume it. - int16 bslot = m_inv.HasItemByUse(EQ::item::ItemTypeBandage, 1, invWhereWorn | invWherePersonal); - if (bslot == INVALID_INDEX) { - bind_out->type = 3; - QueuePacket(outapp); - bind_out->type = 7; // this is the wrong message, dont know the right one. - QueuePacket(outapp); - safe_delete(outapp); - return (true); - } - DeleteItemInInventory(bslot, 1, true); // do we need client update? - - // start complete timer - bindwound_timer.Start(10000); - bindwound_target = bindmob; - - // Send client unlock - bind_out->type = 3; - QueuePacket(outapp); - bind_out->type = 0; - // Client Unlocked - if (!bindmob) { - // send "bindmob dead" to client - bind_out->type = 4; - QueuePacket(outapp); - bind_out->type = 0; - bindwound_timer.Disable(); - bindwound_target = 0; - } else { - // send bindmob "stand still" - if (!bindmob->IsAIControlled() && bindmob != this) { - bindmob->CastToClient()->MessageString(Chat::Yellow, - YOU_ARE_BEING_BANDAGED); - } else if (bindmob->IsAIControlled() && bindmob != this) { - ; // Tell IPC to stand still? - } else { - ; // Binding self - } - } - } else if (bindwound_timer.Check()) // Did the timer finish? - { - // finish bind - // disable complete timer - bindwound_timer.Disable(); - bindwound_target = 0; - if (!bindmob) { - // send "bindmob gone" to client - bind_out->type = 5; // not in zone - QueuePacket(outapp); - bind_out->type = 0; - } - - else { - if (!GetFeigned() && (DistanceSquared(bindmob->GetPosition(), m_Position) <= 400)) { - // send bindmob bind done - if (!bindmob->IsAIControlled() && bindmob != this) { - - } else if (bindmob->IsAIControlled() && bindmob != this) { - // Tell IPC to resume?? - } else { - // Binding self - } - // Send client bind done - - bind_out->type = 1; // Done - QueuePacket(outapp); - bind_out->type = 0; - CheckIncreaseSkill(EQ::skills::SkillBindWound, nullptr, 5); - - if (RuleB(Character, UseOldBindWound)) { - int maxHPBonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + - aabonuses.MaxBindWound; - - int max_percent = 50 + maxHPBonus; - - if (GetClass() == Class::Monk && GetSkill(EQ::skills::SkillBindWound) > 200) { - max_percent = 70 + maxHPBonus; - } - - int64 max_hp = bindmob->GetMaxHP() * max_percent / 100; - - // send bindmob new hp's - if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() <= (max_hp)-1) { - // 0.120 per skill point, 0.60 per skill level, minimum 3 max 30 - int bindhps = 3; - - if (GetSkill(EQ::skills::SkillBindWound) > 200) { - bindhps += GetSkill(EQ::skills::SkillBindWound) * 4 / 10; - } - else if (GetSkill(EQ::skills::SkillBindWound) >= 10) { - bindhps += GetSkill(EQ::skills::SkillBindWound) / 4; - } - - // Implementation of aaMithanielsBinding is a guess (the multiplier) - int bindBonus = spellbonuses.BindWound + itembonuses.BindWound + - aabonuses.BindWound; - - bindhps += bindhps * bindBonus / 100; - - // if the bind takes them above the max bindable - // cap it at that value. Dont know if live does it this way - // but it makes sense to me. - int chp = bindmob->GetHP() + bindhps; - if (chp > max_hp) - chp = max_hp; - - bindmob->SetHP(chp); - bindmob->SendHPUpdate(); - } - else { - // I dont have the real, live - Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", - max_percent); - if (bindmob != this && bindmob->IsClient()) - bindmob->CastToClient()->Message( - 15, - "You cannot have your wounds bound above %d%% hitpoints.", - max_percent); - // Too many hp message goes here. - } - } - else { - int percent_base = 50; - if (GetRawSkill(EQ::skills::SkillBindWound) > 200) { - if ((GetClass() == Class::Monk) || (GetClass() == Class::Beastlord)) - percent_base = 70; - else if ((GetLevel() > 50) && ((GetClass() == Class::Warrior) || (GetClass() == Class::Rogue) || (GetClass() == Class::Cleric))) - percent_base = 70; - } - - int percent_bonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + aabonuses.MaxBindWound; - - int max_percent = percent_base + percent_bonus; - if (max_percent < 0) - max_percent = 0; - if (max_percent > 100) - max_percent = 100; - - int max_hp = (bindmob->GetMaxHP() * max_percent) / 100; - if (max_hp > bindmob->GetMaxHP()) - max_hp = bindmob->GetMaxHP(); - - if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() < max_hp) { - int bindhps = 3; // base bind hp - if (percent_base >= 70) - bindhps = (GetSkill(EQ::skills::SkillBindWound) * 4) / 10; // 8:5 skill-to-hp ratio - else if (GetSkill(EQ::skills::SkillBindWound) >= 12) - bindhps = GetSkill(EQ::skills::SkillBindWound) / 4; // 4:1 skill-to-hp ratio - - int bonus_hp_percent = spellbonuses.BindWound + itembonuses.BindWound + aabonuses.BindWound; - - bindhps += (bindhps * bonus_hp_percent) / 100; - - if (bindhps < 3) - bindhps = 3; - - bindhps += bindmob->GetHP(); - if (bindhps > max_hp) - bindhps = max_hp; - - bindmob->SetHP(bindhps); - bindmob->SendHPUpdate(); - } - else { - Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", max_percent); - if (bindmob != this && bindmob->IsClient()) - bindmob->CastToClient()->Message(Chat::Yellow, "You cannot have your wounds bound above %d%% hitpoints.", max_percent); - } - } - } - else { - // Send client bind failed - if (bindmob != this) - bind_out->type = 6; // They moved - else - bind_out->type = 7; // Bandager moved - - QueuePacket(outapp); - bind_out->type = 0; - } - } - } - } else if (bindwound_timer.Enabled()) { - // You moved - outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); - BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; - bindwound_timer.Disable(); - bindwound_target = 0; - bind_out->type = 7; - QueuePacket(outapp); - bind_out->type = 3; - QueuePacket(outapp); - } - safe_delete(outapp); - return true; -} - -void Client::SetMaterial(int16 in_slot, uint32 item_id) -{ - const EQ::ItemData *item = database.GetItem(item_id); - if (item && item->IsClassCommon()) { - uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); - if (matslot != EQ::textures::materialInvalid) { - m_pp.item_material.Slot[matslot].Material = GetEquipmentMaterial(matslot); - } - } -} - -void Client::ServerFilter(SetServerFilter_Struct* filter){ - -/* this code helps figure out the filter IDs in the packet if needed - static SetServerFilter_Struct ssss; - int r; - uint32 *o = (uint32 *) &ssss; - uint32 *n = (uint32 *) filter; - for(r = 0; r < (sizeof(SetServerFilter_Struct)/4); r++) { - if(*o != *n) - LogFile->write(EQEMuLog::Debug, "Filter %d changed from %d to %d", r, *o, *n); - o++; n++; - } - memcpy(&ssss, filter, sizeof(SetServerFilter_Struct)); -*/ -#define Filter0(type) \ - if(filter->filters[type] == 1) \ - SetFilter(type, FilterShow); \ - else \ - SetFilter(type, FilterHide); -#define Filter1(type) \ - if(filter->filters[type] == 0) \ - SetFilter(type, FilterShow); \ - else \ - SetFilter(type, FilterHide); - - Filter0(FilterGuildChat); - Filter0(FilterSocials); - Filter0(FilterGroupChat); - Filter0(FilterShouts); - Filter0(FilterAuctions); - Filter0(FilterOOC); - Filter0(FilterBadWords); - - if (filter->filters[FilterPCSpells] == 0) { - SetFilter(FilterPCSpells, FilterShow); - } else if (filter->filters[FilterPCSpells] == 1) { - SetFilter(FilterPCSpells, FilterHide); - } else { - SetFilter(FilterPCSpells, FilterShowGroupOnly); - } - - Filter1(FilterNPCSpells); - - if (filter->filters[FilterBardSongs] == 0) { - SetFilter(FilterBardSongs, FilterShow); - } else if (filter->filters[FilterBardSongs] == 1) { - SetFilter(FilterBardSongs, FilterShowSelfOnly); - } else if (filter->filters[FilterBardSongs] == 2) { - SetFilter(FilterBardSongs, FilterShowGroupOnly); - } else { - SetFilter(FilterBardSongs, FilterHide); - } - - if (filter->filters[FilterSpellCrits] == 0) { - SetFilter(FilterSpellCrits, FilterShow); - } else if (filter->filters[FilterSpellCrits] == 1) { - SetFilter(FilterSpellCrits, FilterShowSelfOnly); - } else { - SetFilter(FilterSpellCrits, FilterHide); - } - - if (filter->filters[FilterMeleeCrits] == 0) { - SetFilter(FilterMeleeCrits, FilterShow); - } else if (filter->filters[FilterMeleeCrits] == 1) { - SetFilter(FilterMeleeCrits, FilterShowSelfOnly); - } else { - SetFilter(FilterMeleeCrits, FilterHide); - } - - if (filter->filters[FilterSpellDamage] == 0) { - SetFilter(FilterSpellDamage, FilterShow); - } else if (filter->filters[FilterSpellDamage] == 1) { - SetFilter(FilterSpellDamage, FilterShowSelfOnly); - } else { - SetFilter(FilterSpellDamage, FilterHide); - } - - Filter0(FilterMyMisses); - Filter0(FilterOthersMiss); - Filter0(FilterOthersHit); - Filter0(FilterMissedMe); - Filter1(FilterDamageShields); - - if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { - if (filter->filters[FilterDOT] == 0) { - SetFilter(FilterDOT, FilterShow); - } else if (filter->filters[FilterDOT] == 1) { - SetFilter(FilterDOT, FilterShowSelfOnly); - } else if (filter->filters[FilterDOT] == 2) { - SetFilter(FilterDOT, FilterShowGroupOnly); - } else { - SetFilter(FilterDOT, FilterHide); - } - } else { - if (filter->filters[FilterDOT] == 0) { // show functions as self only - SetFilter(FilterDOT, FilterShowSelfOnly); - } else { - SetFilter(FilterDOT, FilterHide); - } - } - - Filter1(FilterPetHits); - Filter1(FilterPetMisses); - Filter1(FilterFocusEffects); - Filter1(FilterPetSpells); - - if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { - if (filter->filters[FilterHealOverTime] == 0) { - SetFilter(FilterHealOverTime, FilterShow); - } else if (filter->filters[FilterHealOverTime] == 1) { - SetFilter(FilterHealOverTime, FilterShowSelfOnly); - } else { - SetFilter(FilterHealOverTime, FilterHide); - } - } else { // these clients don't have a 'self only' filter - Filter1(FilterHealOverTime); - } - - Filter1(FilterItemSpeech); - Filter1(FilterStrikethrough); - Filter1(FilterStuns); - Filter1(FilterBardSongsOnPets); -} - -// this version is for messages with no parameters -void Client::MessageString(uint32 type, uint32 string_id, uint32 distance) -{ - if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) - return; - if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... - return; - if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) - return; - auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); - SimpleMessage_Struct* sms = (SimpleMessage_Struct*)outapp->pBuffer; - sms->color=type; - sms->string_id=string_id; - - sms->unknown8=0; - - if(distance>0) - entity_list.QueueCloseClients(this,outapp,false,distance); - else - QueuePacket(outapp); - safe_delete(outapp); -} - -// -// this list of 9 args isn't how I want to do it, but to use va_arg -// you have to know how many args you're expecting, and to do that we have -// to load the eqstr file and count them in the string. -// This hack sucks but it's gonna work for now. -// -void Client::MessageString(uint32 type, uint32 string_id, const char* message1, - const char* message2,const char* message3,const char* message4, - const char* message5,const char* message6,const char* message7, - const char* message8,const char* message9, uint32 distance) -{ - if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) - return; - if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... - return; - if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) - return; - if (GetFilter(FilterDamageShields) == FilterHide && type == Chat::DamageShield) - return; - if (GetFilter(FilterFocusEffects) == FilterHide && type == Chat::FocusEffect) - return; - - if (type == Chat::Emote) - type = 4; - - if (!message1) { - MessageString(type, string_id); // use the simple message instead - return; - } - - const char *message_arg[] = { - message1, message2, message3, message4, message5, - message6, message7, message8, message9 - }; - - SerializeBuffer buf(20); - buf.WriteInt32(0); // unknown - buf.WriteInt32(string_id); - buf.WriteInt32(type); - for (auto &m : message_arg) { - if (m == nullptr) - break; - buf.WriteString(m); - } - - buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime - - auto outapp = std::make_unique(OP_FormattedMessage, buf); - - if (distance > 0) - entity_list.QueueCloseClients(this, outapp.get(), false, distance); - else - QueuePacket(outapp.get()); -} - -void Client::MessageString(const CZClientMessageString_Struct* msg) -{ - if (msg) - { - if (msg->args_size == 0) - { - MessageString(msg->chat_type, msg->string_id); - } - else - { - uint32_t outsize = sizeof(FormattedMessage_Struct) + msg->args_size; - auto outapp = std::make_unique(OP_FormattedMessage, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->string_id = msg->string_id; - outbuf->type = msg->chat_type; - memcpy(outbuf->message, msg->args, msg->args_size); - QueuePacket(outapp.get()); - } - } -} - -// helper function, returns true if we should see the message -bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) -{ - eqFilterMode mode = GetFilter(filter); - // easy ones first - if (mode == FilterShow) { - return true; - } else if (mode == FilterHide) { - return false; - } - - if (sender != this && mode == FilterShowSelfOnly) { - return false; - } else if (sender) { - if (mode == FilterShowGroupOnly) { - auto g = GetGroup(); - auto r = GetRaid(); - if (g) { - if (g->IsGroupMember(sender)) { - return true; - } - } else if (r && sender->IsClient()) { - auto rgid1 = r->GetGroup(this); - auto rgid2 = r->GetGroup(sender->CastToClient()); - if (rgid1 != RAID_GROUPLESS && rgid1 == rgid2) { - return true; - } - } else { - return false; - } - } - } - - // we passed our checks - return true; -} - -void Client::FilteredMessageString(Mob *sender, uint32 type, - eqFilterType filter, uint32 string_id) -{ - if (!FilteredMessageCheck(sender, filter)) - return; - - auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); - SimpleMessage_Struct *sms = (SimpleMessage_Struct *)outapp->pBuffer; - sms->color = type; - sms->string_id = string_id; - - sms->unknown8 = 0; - - QueuePacket(outapp); - safe_delete(outapp); - - return; -} - -void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id, - const char *message1, const char *message2, const char *message3, - const char *message4, const char *message5, const char *message6, - const char *message7, const char *message8, const char *message9) -{ - if (!FilteredMessageCheck(sender, filter)) - return; - - if (type == Chat::Emote) - type = 4; - - if (!message1) { - FilteredMessageString(sender, type, filter, string_id); // use the simple message instead - return; - } - - const char *message_arg[] = { - message1, message2, message3, message4, message5, - message6, message7, message8, message9 - }; - - SerializeBuffer buf(20); - buf.WriteInt32(0); // unknown - buf.WriteInt32(string_id); - buf.WriteInt32(type); - for (auto &m : message_arg) { - if (m == nullptr) - break; - buf.WriteString(m); - } - - buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime - - auto outapp = std::make_unique(OP_FormattedMessage, buf); - - QueuePacket(outapp.get()); -} - -void Client::Tell_StringID(uint32 string_id, const char *who, const char *message) -{ - char string_id_str[10]; - snprintf(string_id_str, 10, "%d", string_id); - - MessageString(Chat::EchoTell, TELL_QUEUED_MESSAGE, who, string_id_str, message); -} - -void Client::SetTint(int16 in_slot, uint32 color) { - EQ::textures::Tint_Struct new_color; - new_color.Color = color; - SetTint(in_slot, new_color); - database.SaveCharacterMaterialColor(CharacterID(), in_slot, color); -} - -// Still need to reconcile bracer01 versus bracer02 -void Client::SetTint(int16 in_slot, EQ::textures::Tint_Struct& color) { - - uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); - if (matslot != EQ::textures::materialInvalid) - { - m_pp.item_tint.Slot[matslot].Color = color.Color; - database.SaveCharacterMaterialColor(CharacterID(), in_slot, color.Color); - } - -} - -void Client::SetHideMe(bool flag) -{ - EQApplicationPacket app; - - gm_hide_me = flag; - - if (gm_hide_me) { - database.SetHideMe(AccountID(), true); - CreateDespawnPacket(&app, false); - entity_list.RemoveFromTargets(this); - trackable = false; - if (RuleB(Command, HideMeCommandDisablesTells)) { - tellsoff = true; - } - } else { - database.SetHideMe(AccountID(), false); - CreateSpawnPacket(&app); - trackable = true; - tellsoff = false; - } - - entity_list.QueueClientsStatus(this, &app, true, 0, Admin() - 1); - UpdateWho(); -} - -void Client::SetLanguageSkill(uint8 language_id, uint8 language_skill) -{ - if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { - return; - } - - if (language_skill > Language::MaxValue) { - language_skill = Language::MaxValue; - } - - m_pp.languages[language_id] = language_skill; - - database.SaveCharacterLanguage(CharacterID(), language_id, language_skill); - - auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); - auto* s = (SkillUpdate_Struct*) outapp->pBuffer; - - s->skillId = 100 + language_id; - s->value = m_pp.languages[language_id]; - - QueuePacket(outapp); - safe_delete(outapp); - - MessageString(Chat::Skills, LANG_SKILL_IMPROVED); -} - -void Client::LinkDead() -{ - if (GetGroup()) - { - entity_list.MessageGroup(this,true,15,"%s has gone linkdead.",GetName()); - GetGroup()->DelMember(this); - if (GetMerc()) - { - GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); - } - } - Raid *raid = entity_list.GetRaidByClient(this); - if(raid){ - raid->MemberZoned(this); - } - - SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::LinkDead); - -// save_timer.Start(2500); - linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); - SendAppearancePacket(AppearanceType::Linkdead, 1); - client_state = CLIENT_LINKDEAD; - AI_Start(CLIENT_LD_TIMEOUT); -} - -uint8 Client::SlotConvert(uint8 slot,bool bracer){ - uint8 slot2 = 0; // why are we returning MainCharm instead of INVALID_INDEX? (must be a pre-charm segment...) - if(bracer) - return EQ::invslot::slotWrist2; - switch(slot) { - case EQ::textures::armorHead: - slot2 = EQ::invslot::slotHead; - break; - case EQ::textures::armorChest: - slot2 = EQ::invslot::slotChest; - break; - case EQ::textures::armorArms: - slot2 = EQ::invslot::slotArms; - break; - case EQ::textures::armorWrist: - slot2 = EQ::invslot::slotWrist1; - break; - case EQ::textures::armorHands: - slot2 = EQ::invslot::slotHands; - break; - case EQ::textures::armorLegs: - slot2 = EQ::invslot::slotLegs; - break; - case EQ::textures::armorFeet: - slot2 = EQ::invslot::slotFeet; - break; - } - return slot2; -} - -uint8 Client::SlotConvert2(uint8 slot){ - uint8 slot2 = 0; // same as above... - switch(slot){ - case EQ::invslot::slotHead: - slot2 = EQ::textures::armorHead; - break; - case EQ::invslot::slotChest: - slot2 = EQ::textures::armorChest; - break; - case EQ::invslot::slotArms: - slot2 = EQ::textures::armorArms; - break; - case EQ::invslot::slotWrist1: - slot2 = EQ::textures::armorWrist; - break; - case EQ::invslot::slotHands: - slot2 = EQ::textures::armorHands; - break; - case EQ::invslot::slotLegs: - slot2 = EQ::textures::armorLegs; - break; - case EQ::invslot::slotFeet: - slot2 = EQ::textures::armorFeet; - break; - } - return slot2; -} - -void Client::Escape() -{ - entity_list.RemoveFromTargets(this, true); - SetInvisible(Invisibility::Invisible); - MessageString(Chat::Skills, ESCAPE); -} - -float Client::CalcClassicPriceMod(Mob* other, bool reverse) { - float price_multiplier = 0.8f; - - if (other && other->IsNPC()) { - FACTION_VALUE faction_level = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); - int32 cha = GetCHA(); - - if (faction_level <= FACTION_AMIABLY) { - cha += 11; // amiable faction grants a defacto 11 charisma bonus - } - - uint8 greed = other->CastToNPC()->GetGreedPercent(); - - // Sony's precise algorithm is unknown, but this produces output that is virtually identical - if (faction_level <= FACTION_INDIFFERENTLY) { - if (cha > 75) { - if (greed) { - // this is derived from curve fitting to a lot of price data - price_multiplier = -0.2487768 + (1.599635 - -0.2487768) / (1 + pow((cha / 135.1495), 1.001983)); - price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' - price_multiplier = 1.0f / price_multiplier; - } - else { - // non-greedy merchants use a linear scale - price_multiplier = 1.0f - ((115.0f - cha) * 0.004f); - } - } - else if (cha > 60) { - price_multiplier = 1.0f / (1.25f + (greed / 100.0f)); - } - else { - price_multiplier = 1.0f / ((1.0f - (cha - 120.0f) / 220.0f) + (greed / 100.0f)); - } - } - else { // apprehensive - if (cha > 75) { - if (greed) { - // this is derived from curve fitting to a lot of price data - price_multiplier = -0.25f + (1.823662 - -0.25f) / (1 + (cha / 135.0f)); - price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' - price_multiplier = 1.0f / price_multiplier; - } - else { - price_multiplier = (100.0f - (145.0f - cha) / 2.8f) / 100.0f; - } - } - else if (cha > 60) { - price_multiplier = 1.0f / (1.4f + greed / 100.0f); - } - else { - price_multiplier = 1.0f / ((1.0f + (143.574 - cha) / 196.434) + (greed / 100.0f)); - } - } - - float maxResult = 1.0f / 1.05; // price reduction caps at this amount - if (price_multiplier > maxResult) { - price_multiplier = maxResult; - } - - if (!reverse) { - price_multiplier = 1.0f / price_multiplier; - } - } - - LogMerchants( - "[{}] [{}] items at [{}] price multiplier [{}] [{}]", - other->GetName(), - reverse ? "buys" : "sells", - price_multiplier, - reverse ? "from" : "to", - GetName() - ); - - return price_multiplier; -} - -float Client::CalcNewPriceMod(Mob* other, bool reverse) -{ - float chaformula = 0; - if (other) - { - int factionlvl = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetFactionRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); - if (factionlvl >= FACTION_APPREHENSIVELY) // Apprehensive or worse. - { - if (GetCHA() > 103) - { - chaformula = (GetCHA() - 103)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. - if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) - chaformula = -1*(RuleI(Merchant, PriceBonusPct)); - } - else if (GetCHA() < 103) - { - chaformula = (103 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // This will bottom out price penalty. - if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) - chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); - } - } - if (factionlvl <= FACTION_INDIFFERENTLY) // Indifferent or better. - { - if (GetCHA() > 75) - { - chaformula = (GetCHA() - 75)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. - if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) - chaformula = -1*(RuleI(Merchant, PriceBonusPct)); - } - else if (GetCHA() < 75) - { - chaformula = (75 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // Faction modifier keeps up from reaching bottom price penalty. - if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) - chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); - } - } - } - - if (reverse) - chaformula *= -1; //For selling - //Now we have, for example, 10 - chaformula /= 100; //Convert to 0.10 - chaformula += 1; //Convert to 1.10; - return chaformula; //Returns 1.10, expensive stuff! -} - -float Client::CalcPriceMod(Mob* other, bool reverse) -{ - float price_mod = CalcNewPriceMod(other, reverse); - - if (RuleB(Merchant, UseClassicPriceMod)) { - price_mod = CalcClassicPriceMod(other, reverse); - } - - return price_mod; -} - -void Client::GetGroupAAs(GroupLeadershipAA_Struct *into) const { - memcpy(into, &m_pp.leader_abilities.group, sizeof(GroupLeadershipAA_Struct)); -} - -void Client::GetRaidAAs(RaidLeadershipAA_Struct *into) const { - memcpy(into, &m_pp.leader_abilities.raid, sizeof(RaidLeadershipAA_Struct)); -} - -void Client::EnteringMessages(Client* client) -{ - std::string rules = RuleS(World, Rules); - - if (!rules.empty() || database.GetVariable("Rules", rules)) { - const uint8 flag = database.GetAgreementFlag(client->AccountID()); - if (!flag) { - const std::string& rules_link = Saylink::Silent("#serverrules", "rules"); - - client->Message( - Chat::White, - fmt::format( - "You must agree to the {} before you can move.", - rules_link - ).c_str() - ); - - client->SendAppearancePacket(AppearanceType::Animation, Animation::Freeze); - } - } -} - -void Client::SendRules() -{ - std::string rules = RuleS(World, Rules); - - if (rules.empty() && !database.GetVariable("Rules", rules)) { - return; - } - - auto lines = Strings::Split(rules, "|"); - auto line_number = 1; - for (auto&& line : lines) { - Message( - Chat::White, - fmt::format( - "{}. {}", - line_number, - line - ).c_str() - ); - line_number++; - } -} - -void Client::SetEndurance(int32 newEnd) -{ - /*Endurance can't be less than 0 or greater than max*/ - if(newEnd < 0) - newEnd = 0; - else if(newEnd > GetMaxEndurance()){ - newEnd = GetMaxEndurance(); - } - - current_endurance = newEnd; - CheckManaEndUpdate(); -} - -void Client::SacrificeConfirm(Mob *caster) -{ - auto outapp = new EQApplicationPacket(OP_Sacrifice, sizeof(Sacrifice_Struct)); - Sacrifice_Struct *ss = (Sacrifice_Struct *)outapp->pBuffer; - - if (!caster || PendingSacrifice) { - safe_delete(outapp); - return; - } - - if (GetLevel() < RuleI(Spells, SacrificeMinLevel)) { - caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. - safe_delete(outapp); - return; - } - - if (GetLevel() > RuleI(Spells, SacrificeMaxLevel)) { - caster->MessageString(Chat::Red, SAC_TOO_HIGH); - safe_delete(outapp); - return; - } - - ss->CasterID = caster->GetID(); - ss->TargetID = GetID(); - ss->Confirm = 0; - QueuePacket(outapp); - safe_delete(outapp); - // We store the Caster's id, because when the packet comes back, it only has the victim's entityID in it, - // not the caster. - sacrifice_caster_id = caster->GetID(); - PendingSacrifice = true; -} - -//Essentially a special case death function -void Client::Sacrifice(Mob *caster) -{ - if (GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)) { - int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000); - if (exploss < GetEXP()) { - SetEXP(ExpSource::Sacrifice, GetEXP() - exploss, GetAAXP(), false); - SendLogoutPackets(); - - // make our become corpse packet, and queue to ourself before OP_Death. - EQApplicationPacket app2(OP_BecomeCorpse, sizeof(BecomeCorpse_Struct)); - BecomeCorpse_Struct *bc = (BecomeCorpse_Struct *)app2.pBuffer; - bc->spawn_id = GetID(); - bc->x = GetX(); - bc->y = GetY(); - bc->z = GetZ(); - QueuePacket(&app2); - - // make death packet - EQApplicationPacket app(OP_Death, sizeof(Death_Struct)); - Death_Struct *d = (Death_Struct *)app.pBuffer; - d->spawn_id = GetID(); - d->killer_id = caster ? caster->GetID() : 0; - d->bindzoneid = GetPP().binds[0].zone_id; - d->spell_id = SPELL_UNKNOWN; - d->attack_skill = 0xe7; - d->damage = 0; - app.priority = 6; - entity_list.QueueClients(this, &app); - - BuffFadeAll(); - UnmemSpellAll(); - Group *g = GetGroup(); - if (g) { - g->MemberZoned(this); - } - Raid *r = entity_list.GetRaidByClient(this); - if (r) { - r->MemberZoned(this); - } - ClearAllProximities(); - if (RuleB(Character, LeaveCorpses)) { - auto new_corpse = new Corpse(this, 0); - entity_list.AddCorpse(new_corpse, GetID()); - SetID(0); - entity_list.QueueClients(this, &app2, true); - } - Save(); - GoToDeath(); - if (caster && caster->IsClient()) { - caster->CastToClient()->SummonItem(RuleI(Spells, SacrificeItemID)); - } else if (caster && caster->IsNPC()) { - caster->CastToNPC()->AddItem(RuleI(Spells, SacrificeItemID), 1, false); - } - } - } else { - caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. - } -} - -void Client::SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID) { - - if(!Caster || PendingTranslocate) - return; - - const SPDat_Spell_Struct &Spell = spells[SpellID]; - - auto outapp = new EQApplicationPacket(OP_Translocate, sizeof(Translocate_Struct)); - Translocate_Struct *ts = (Translocate_Struct*)outapp->pBuffer; - - strcpy(ts->Caster, Caster->GetName()); - PendingTranslocateData.spell_id = ts->SpellID = SpellID; - - if((SpellID == 1422) || (SpellID == 1334) || (SpellID == 3243)) { - PendingTranslocateData.zone_id = ts->ZoneID = m_pp.binds[0].zone_id; - PendingTranslocateData.instance_id = m_pp.binds[0].instance_id; - PendingTranslocateData.x = ts->x = m_pp.binds[0].x; - PendingTranslocateData.y = ts->y = m_pp.binds[0].y; - PendingTranslocateData.z = ts->z = m_pp.binds[0].z; - PendingTranslocateData.heading = m_pp.binds[0].heading; - } - else { - PendingTranslocateData.zone_id = ts->ZoneID = ZoneID(Spell.teleport_zone); - PendingTranslocateData.instance_id = 0; - PendingTranslocateData.y = ts->y = Spell.base_value[0]; - PendingTranslocateData.x = ts->x = Spell.base_value[1]; - PendingTranslocateData.z = ts->z = Spell.base_value[2]; - PendingTranslocateData.heading = 0.0; - } - - ts->unknown008 = 0; - ts->Complete = 0; - - PendingTranslocate = true; - TranslocateTime = time(nullptr); - - QueuePacket(outapp); - safe_delete(outapp); - - return; -} -void Client::SendPickPocketResponse(Mob *from, uint32 amt, int type, const EQ::ItemData* item){ - auto outapp = new EQApplicationPacket(OP_PickPocket, sizeof(sPickPocket_Struct)); - sPickPocket_Struct *pick_out = (sPickPocket_Struct *)outapp->pBuffer; - pick_out->coin = amt; - pick_out->from = GetID(); - pick_out->to = from->GetID(); - pick_out->myskill = GetSkill(EQ::skills::SkillPickPockets); - - if ((type >= PickPocketPlatinum) && (type <= PickPocketCopper) && (amt == 0)) - type = PickPocketFailed; - - pick_out->type = type; - if (item) - strcpy(pick_out->itemname, item->Name); - else - pick_out->itemname[0] = '\0'; - // if we do not send this packet the client will lock up and require the player to relog. - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SetHoTT(uint32 mobid) { - auto outapp = new EQApplicationPacket(OP_TargetHoTT, sizeof(ClientTarget_Struct)); - ClientTarget_Struct *ct = (ClientTarget_Struct *) outapp->pBuffer; - ct->new_target = mobid; - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SendPopupToClient(const char *Title, const char *Text, uint32 PopupID, uint32 Buttons, uint32 Duration) -{ - auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); - - OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; - - if ((strlen(Title) > (sizeof(olms->Title) - 1)) || (strlen(Text) > (sizeof(olms->Text) - 1))) { - safe_delete(outapp); - return; - } - - strcpy(olms->Title, Title); - strcpy(olms->Text, Text); - - olms->Buttons = Buttons; - - if (Duration > 0) { - olms->Duration = Duration * 1000; - } - else { - olms->Duration = 0xffffffff; - } - - olms->PopupID = PopupID; - olms->NegativeID = 0; - - sprintf(olms->ButtonName0, "%s", "Yes"); - sprintf(olms->ButtonName1, "%s", "No"); - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SendFullPopup( - const char *Title, - const char *Text, - uint32 PopupID, - uint32 NegativeID, - uint32 Buttons, - uint32 Duration, - const char *ButtonName0, - const char *ButtonName1, - uint32 SoundControls -) -{ - auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); - - OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; - - if ((strlen(Text) > (sizeof(olms->Text) - 1)) || (strlen(Title) > (sizeof(olms->Title) - 1))) { - safe_delete(outapp); - return; - } - - if (ButtonName0 && ButtonName1 && ((strlen(ButtonName0) > (sizeof(olms->ButtonName0) - 1)) || - (strlen(ButtonName1) > (sizeof(olms->ButtonName1) - 1)))) { - safe_delete(outapp); - return; - } - - strcpy(olms->Title, Title); - strcpy(olms->Text, Text); - - olms->Buttons = Buttons; - - if (ButtonName0 == nullptr || ButtonName1 == nullptr) { - sprintf(olms->ButtonName0, "%s", "Yes"); - sprintf(olms->ButtonName1, "%s", "No"); - } - else { - strcpy(olms->ButtonName0, ButtonName0); - strcpy(olms->ButtonName1, ButtonName1); - } - - if (Duration > 0) { - olms->Duration = Duration * 1000; - } - else { - olms->Duration = 0xffffffff; - } - - olms->PopupID = PopupID; - olms->NegativeID = NegativeID; - olms->SoundControls = SoundControls; - - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SendWindow( - uint32 button_one_id, - uint32 button_two_id, - uint32 button_type, - const char* button_one_text, - const char* button_two_text, - uint32 duration, - int title_type, - Mob* target, - const char* title, - const char* text, - ... -) -{ - va_list argptr; - char buffer[4096]; - - va_start(argptr, text); - vsnprintf(buffer, sizeof(buffer), text, argptr); - va_end(argptr); - - size_t len = strlen(buffer); - - auto app = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); - auto* olms = (OnLevelMessage_Struct *) app->pBuffer; - - if (strlen(text) > (sizeof(olms->Text) - 1)) { - safe_delete(app); - return; - } - - if (!target) { - title_type = 0; - } - - switch (title_type) { - case 1: { - char name[64] = ""; - strcpy(name, target->GetName()); - - if (strlen(target->GetLastName()) > 0) { - char last_name[64] = ""; - strcpy(last_name, target->GetLastName()); - strcat(name, " "); - strcat(name, last_name); - } - - strcpy(olms->Title, name); - break; - } - case 2: { - if (target->IsClient() && target->CastToClient()->GuildID()) { - auto guild_name = guild_mgr.GetGuildName(target->CastToClient()->GuildID()); - strn0cpy(olms->Title, guild_name, sizeof(olms->Title)); - } else { - strcpy(olms->Title, "No Guild"); - } - break; - } - default: { - strcpy(olms->Title, title); - break; - } - } - - memcpy(olms->Text, buffer, len + 1); - - olms->Buttons = button_type; - - strn0cpy(olms->ButtonName0, button_one_text, sizeof(olms->ButtonName0)); - strn0cpy(olms->ButtonName1, button_two_text, sizeof(olms->ButtonName1)); - - if (duration > 0) { - olms->Duration = duration * 1000; - } else { - olms->Duration = UINT32_MAX; - } - - olms->PopupID = button_one_id; - olms->NegativeID = button_two_id; - - FastQueuePacket(&app); -} - -void Client::KeyRingLoad() -{ - const auto &l = KeyringRepository::GetWhere( - database, - fmt::format( - "`char_id` = {} ORDER BY `item_id`", - character_id - ) - ); - - if (l.empty()) { - return; - } - - - for (const auto &e : l) { - keyring.emplace_back(e.item_id); - } -} - -void Client::KeyRingAdd(uint32 item_id) -{ - if (!item_id) { - return; - } - - const bool found = KeyRingCheck(item_id); - if (found) { - return; - } - - auto e = KeyringRepository::NewEntity(); - - e.char_id = CharacterID(); - e.item_id = item_id; - - e = KeyringRepository::InsertOne(database, e); - - if (!e.id) { - return; - } - - keyring.emplace_back(item_id); - - if (!RuleB(World, UseItemLinksForKeyRing)) { - Message(Chat::LightBlue, "Added to keyring."); - return; - } - - const std::string &item_link = database.CreateItemLink(item_id); - - Message( - Chat::LightBlue, - fmt::format( - "Added {} to keyring.", - item_link - ).c_str() - ); -} - -bool Client::KeyRingCheck(uint32 item_id) -{ - for (const auto &e : keyring) { - if (e == item_id) { - return true; - } - } - - return false; -} - -void Client::KeyRingList() -{ - Message(Chat::LightBlue, "Keys on Keyring:"); - - const EQ::ItemData *item = nullptr; - - for (const auto &e : keyring) { - item = database.GetItem(e); - if (item) { - const std::string &item_string = RuleB(World, UseItemLinksForKeyRing) ? database.CreateItemLink(e) : item->Name; - - Message(Chat::LightBlue, item_string.c_str()); - } - } -} - -bool Client::IsDiscovered(uint32 item_id) { - const auto& l = DiscoveredItemsRepository::GetWhere( - database, - fmt::format( - "item_id = {}", - item_id - ) - ); - if (l.empty()) { - return false; - } - - return true; -} - -void Client::DiscoverItem(uint32 item_id) { - auto e = DiscoveredItemsRepository::NewEntity(); - - e.account_status = Admin(); - e.char_name = GetCleanName(); - e.discovered_date = std::time(nullptr); - e.item_id = item_id; - - auto d = DiscoveredItemsRepository::InsertOne(database, e); - - if (player_event_logs.IsEventEnabled(PlayerEvent::DISCOVER_ITEM)) { - const auto* item = database.GetItem(item_id); - - auto e = PlayerEvent::DiscoverItemEvent{ - .item_id = item_id, - .item_name = item->Name, - }; - RecordPlayerEventLog(PlayerEvent::DISCOVER_ITEM, e); - - } - - if (parse->PlayerHasQuestSub(EVENT_DISCOVER_ITEM)) { - auto* item = database.CreateItem(item_id); - std::vector args = { item }; - - parse->EventPlayer(EVENT_DISCOVER_ITEM, this, "", item_id, &args); - } -} - -void Client::UpdateLFP() { - - Group *g = GetGroup(); - - if(g && !g->IsLeader(this)) { - database.SetLFP(CharacterID(), false); - worldserver.StopLFP(CharacterID()); - LFP = false; - return; - } - - GroupLFPMemberEntry LFPMembers[MAX_GROUP_MEMBERS]; - - for(unsigned int i=0; iGetZoneID(); - - if(g) { - // Fill the LFPMembers array with the rest of the group members, excluding ourself - // We don't fill in the class, level or zone, because we may not be able to determine - // them if the other group members are not in this zone. World will fill in this information - // for us, if it can. - int NextFreeSlot = 1; - for(unsigned int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if((g->membername[i][0] != '\0') && strcasecmp(g->membername[i], LFPMembers[0].Name)) - strcpy(LFPMembers[NextFreeSlot++].Name, g->membername[i]); - } - } - worldserver.UpdateLFP(CharacterID(), LFPMembers); -} - -bool Client::GroupFollow(Client* inviter) { - - if (inviter) { - isgrouped = true; - Raid* raid = entity_list.GetRaidByClient(inviter); - Raid* iraid = entity_list.GetRaidByClient(this); - - //inviter has a raid don't do group stuff instead do raid stuff! - if (raid) { - // Suspend the merc while in a raid (maybe a rule could be added for this) - if (GetMerc()) { - GetMerc()->Suspend(); - } - - uint32 groupToUse = 0xFFFFFFFF; - for (const auto& m : raid->members) { - if (m.member && m.member == inviter) { - groupToUse = m.group_number; - break; - } - } - if (iraid == raid) { - //both in same raid - uint32 ngid = raid->GetGroup(inviter->GetName()); - if (raid->GroupCount(ngid) < MAX_GROUP_MEMBERS) { - raid->MoveMember(GetName(), ngid); - raid->SendGroupDisband(this); - raid->GroupUpdate(ngid); - } - return false; - } - if (raid->RaidCount() < MAX_RAID_MEMBERS) - { - // okay, so we now have a single client (this) joining a group in a raid - // And they're not already in the raid (which is above and doesn't need xtarget shit) - if (!GetXTargetAutoMgr()->empty()) { - raid->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); - GetXTargetAutoMgr()->clear(); - RemoveAutoXTargets(); - } - - SetXTargetAutoMgr(raid->GetXTargetAutoMgr()); - if (!GetXTargetAutoMgr()->empty()) - SetDirtyAutoHaters(); - - if (raid->GroupCount(groupToUse) < MAX_GROUP_MEMBERS) - { - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->AddMember(this, groupToUse); - raid->SendBulkRaid(this); - //raid->SendRaidGroupAdd(GetName(), groupToUse); - //raid->SendGroupUpdate(this); - raid->GroupUpdate(groupToUse); //break - if (raid->IsLocked()) - { - raid->SendRaidLockTo(this); - } - return false; - } - else - { - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->AddMember(this); - raid->SendBulkRaid(this); - if (raid->IsLocked()) - { - raid->SendRaidLockTo(this); - } - return false; - } - } - } - - Group* group = entity_list.GetGroupByClient(inviter); - - if (!group) - { - //Make new group - group = new Group(inviter); - - if (!group) - { - return false; - } - - entity_list.AddGroup(group); - - if (group->GetID() == 0) - { - Message(Chat::Red, "Unable to get new group id. Cannot create group."); - inviter->Message(Chat::Red, "Unable to get new group id. Cannot create group."); - return false; - } - - //now we have a group id, can set inviter's id - group->AddToGroup(inviter); - database.SetGroupLeaderName(group->GetID(), inviter->GetName()); - group->UpdateGroupAAs(); - - //Invite the inviter into the group first.....dont ask - if (inviter->ClientVersion() < EQ::versions::ClientVersion::SoD) - { - auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupJoin_Struct)); - GroupJoin_Struct* outgj = (GroupJoin_Struct*)outapp->pBuffer; - strcpy(outgj->membername, inviter->GetName()); - strcpy(outgj->yourname, inviter->GetName()); - outgj->action = groupActInviteInitial; // 'You have formed the group'. - group->GetGroupAAs(&outgj->leader_aas); - inviter->QueuePacket(outapp); - safe_delete(outapp); - } - else - { - // SoD and later - inviter->SendGroupCreatePacket(); - inviter->SendGroupLeaderChangePacket(inviter->GetName()); - inviter->SendGroupJoinAcknowledge(); - } - group->GetXTargetAutoMgr()->merge(*inviter->GetXTargetAutoMgr()); - inviter->GetXTargetAutoMgr()->clear(); - inviter->SetXTargetAutoMgr(group->GetXTargetAutoMgr()); - } - - if (!group) - { - return false; - } - - // Remove merc from old group before adding client to the new one - if (GetMerc() && GetMerc()->HasGroup()) - { - GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); - } - - if (!group->AddMember(this)) - { - // If failed to add client to new group, regroup with merc - if (GetMerc()) - { - GetMerc()->MercJoinClientGroup(); - } - else - { - isgrouped = false; - } - return false; - } - - if (ClientVersion() >= EQ::versions::ClientVersion::SoD) - { - SendGroupJoinAcknowledge(); - } - - // Temporary hack for SoD, as things seem to work quite differently - if (inviter->IsClient() && inviter->ClientVersion() >= EQ::versions::ClientVersion::SoD) - { - database.RefreshGroupFromDB(inviter); - } - - // Add the merc back into the new group if possible - if (GetMerc()) - { - GetMerc()->MercJoinClientGroup(); - } - - if (inviter->IsLFP()) - { - // If the player who invited us to a group is LFP, have them update world now that we have joined their group. - inviter->UpdateLFP(); - } - - database.RefreshGroupFromDB(this); - group->SendHPManaEndPacketsTo(this); - //send updates to clients out of zone... - group->SendGroupJoinOOZ(this); - return true; - } - return false; -} - -uint16 Client::GetPrimarySkillValue() -{ - EQ::skills::SkillType skill = EQ::skills::HIGHEST_SKILL; //because nullptr == 0, which is 1H Slashing, & we want it to return 0 from GetSkill - bool equipped = m_inv.GetItem(EQ::invslot::slotPrimary); - - if (!equipped) - skill = EQ::skills::SkillHandtoHand; - - else { - - uint8 type = m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->ItemType; //is this the best way to do this? - - switch (type) { - case EQ::item::ItemType1HSlash: // 1H Slashing - skill = EQ::skills::Skill1HSlashing; - break; - case EQ::item::ItemType2HSlash: // 2H Slashing - skill = EQ::skills::Skill2HSlashing; - break; - case EQ::item::ItemType1HPiercing: // Piercing - skill = EQ::skills::Skill1HPiercing; - break; - case EQ::item::ItemType1HBlunt: // 1H Blunt - skill = EQ::skills::Skill1HBlunt; - break; - case EQ::item::ItemType2HBlunt: // 2H Blunt - skill = EQ::skills::Skill2HBlunt; - break; - case EQ::item::ItemType2HPiercing: // 2H Piercing - if (IsClient() && CastToClient()->ClientVersion() < EQ::versions::ClientVersion::RoF2) - skill = EQ::skills::Skill1HPiercing; - else - skill = EQ::skills::Skill2HPiercing; - break; - case EQ::item::ItemTypeMartial: // Hand to Hand - skill = EQ::skills::SkillHandtoHand; - break; - default: // All other types default to Hand to Hand - skill = EQ::skills::SkillHandtoHand; - break; - } - } - - return GetSkill(skill); -} - -uint32 Client::GetTotalATK() -{ - uint32 AttackRating = 0; - uint32 WornCap = itembonuses.ATK; - - if(IsClient()) { - AttackRating = ((WornCap * 1.342) + (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69)); - AttackRating += aabonuses.ATK + GroupLeadershipAAOffenseEnhancement(); - - if (AttackRating < 10) - AttackRating = 10; - } - else - AttackRating = GetATK(); - - AttackRating += spellbonuses.ATK; - - return AttackRating; -} - -uint32 Client::GetATKRating() -{ - uint32 AttackRating = 0; - if(IsClient()) { - AttackRating = (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69); - - if (AttackRating < 10) - AttackRating = 10; - } - return AttackRating; -} - -void Client::VoiceMacroReceived(uint32 Type, char *Target, uint32 MacroNumber) { - - uint32 GroupOrRaidID = 0; - - switch(Type) { - - case VoiceMacroGroup: { - - Group* g = GetGroup(); - - if(g) - GroupOrRaidID = g->GetID(); - else - return; - - break; - } - - case VoiceMacroRaid: { - - Raid* r = GetRaid(); - - if(r) - GroupOrRaidID = r->GetID(); - else - return; - - break; - } - } - - if(!worldserver.SendVoiceMacro(this, Type, Target, MacroNumber, GroupOrRaidID)) - Message(0, "Error: World server disconnected"); -} - -void Client::ClearGroupAAs() { - for(unsigned int i = 0; i < MAX_GROUP_LEADERSHIP_AA_ARRAY; i++) - m_pp.leader_abilities.ranks[i] = 0; - - m_pp.group_leadership_points = 0; - m_pp.raid_leadership_points = 0; - m_pp.group_leadership_exp = 0; - m_pp.raid_leadership_exp = 0; - - Save(); - database.SaveCharacterLeadershipAbilities(CharacterID(), &m_pp); -} - -void Client::UpdateGroupAAs(int32 points, uint32 type) { - switch(type) { - case 0: { m_pp.group_leadership_points += points; break; } - case 1: { m_pp.raid_leadership_points += points; break; } - } - SendLeadershipEXPUpdate(); -} - -bool Client::IsLeadershipEXPOn() { - - if(!m_pp.leadAAActive) - return false; - - Group *g = GetGroup(); - - if (g && g->IsLeader(this) && g->GroupCount() > 2) - return true; - - Raid *r = GetRaid(); - - if (!r) - return false; - - // raid leaders can only gain raid AA XP - if (r->IsLeader(this)) { - if (r->RaidCount() > 17) - return true; - else - return false; - } - - uint32 gid = r->GetGroup(this); - - if (gid > 11) // not in a group - return false; - - if (r->IsGroupLeader(GetName()) && r->GroupCount(gid) > 2) - return true; - - return false; - -} - -uint32 Client::GetAggroCount() { - return AggroCount; -} - -// we pass in for book keeping if RestRegen is enabled -void Client::IncrementAggroCount(bool raid_target) -{ - // This method is called when a client is added to a mob's hate list. It turns the clients aggro flag on so - // rest state regen is stopped, and for SoF, it sends the opcode to show the crossed swords in-combat indicator. - AggroCount++; - - if(!RuleB(Character, RestRegenEnabled)) - return; - - uint32 newtimer = raid_target ? RuleI(Character, RestRegenRaidTimeToActivate) : RuleI(Character, RestRegenTimeToActivate); - - // When our aggro count is 1 here, we are exiting rest state. We need to pause our current timer, if we have time remaining - // We should not actually have to do anything to the Timer object since the AggroCount counter blocks it from being checked - // and will have it's timer changed when we exit combat so let's not do any extra work - if (AggroCount == 1 && rest_timer.GetRemainingTime()) // the Client::rest_timer is never disabled, so don't need to check - m_pp.RestTimer = std::max(1u, rest_timer.GetRemainingTime() / 1000); // I guess round up? - - // save the new timer if it's higher - m_pp.RestTimer = std::max(m_pp.RestTimer, newtimer); - - // If we already had aggro before this method was called, the combat indicator should already be up for SoF clients, - // so we don't need to send it again. - // - if(AggroCount > 1) - return; - - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - auto outapp = new EQApplicationPacket(OP_RestState, 1); - char *Buffer = (char *)outapp->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x01); - QueuePacket(outapp); - safe_delete(outapp); - } - -} - -void Client::DecrementAggroCount() -{ - // This should be called when a client is removed from a mob's hate list (it dies or is memblurred). - // It checks whether any other mob is aggro on the player, and if not, starts the rest timer. - // For SoF, the opcode to start the rest state countdown timer in the UI is sent. - - // If we didn't have aggro before, this method should not have been called. - if(!AggroCount) - return; - - AggroCount--; - - if(!RuleB(Character, RestRegenEnabled)) - return; - - // Something else is still aggro on us, can't rest yet. - if (AggroCount) - return; - - rest_timer.Start(m_pp.RestTimer * 1000); - - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - auto outapp = new EQApplicationPacket(OP_RestState, 5); - char *Buffer = (char *)outapp->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, m_pp.RestTimer); - QueuePacket(outapp); - safe_delete(outapp); - } -} - -// when we cast a beneficial spell we need to steal our targets current timer -// That's what we use this for -void Client::UpdateRestTimer(uint32 new_timer) -{ - // their timer was 0, so we don't do anything - if (new_timer == 0) - return; - - if (!RuleB(Character, RestRegenEnabled)) - return; - - // so if we're currently on aggro, we check our saved timer - if (AggroCount) { - if (m_pp.RestTimer < new_timer) // our timer needs to be updated, don't need to update client here - m_pp.RestTimer = new_timer; - } else { // if we're not aggro, we need to check if current timer needs updating - if (rest_timer.GetRemainingTime() / 1000 < new_timer) { - rest_timer.Start(new_timer * 1000); - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - auto outapp = new EQApplicationPacket(OP_RestState, 5); - char *Buffer = (char *)outapp->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, new_timer); - QueuePacket(outapp); - safe_delete(outapp); - } - } - } -} - -void Client::SendPVPStats() -{ - // This sends the data to the client to populate the PVP Stats Window. - // - // When the PVP Stats window is opened, no opcode is sent. Therefore this method should be called - // from Client::CompleteConnect, and also when the player makes a PVP kill. - // - auto outapp = new EQApplicationPacket(OP_PVPStats, sizeof(PVPStats_Struct)); - PVPStats_Struct *pvps = (PVPStats_Struct *)outapp->pBuffer; - - pvps->Kills = m_pp.PVPKills; - pvps->Deaths = m_pp.PVPDeaths; - pvps->PVPPointsAvailable = m_pp.PVPCurrentPoints; - pvps->TotalPVPPoints = m_pp.PVPCareerPoints; - pvps->BestKillStreak = m_pp.PVPBestKillStreak; - pvps->WorstDeathStreak = m_pp.PVPWorstDeathStreak; - pvps->CurrentKillStreak = m_pp.PVPCurrentKillStreak; - - // TODO: Record and send other PVP Stats - - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SendCrystalCounts() -{ - auto outapp = new EQApplicationPacket(OP_CrystalCountUpdate, sizeof(CrystalCountUpdate_Struct)); - CrystalCountUpdate_Struct *ccus = (CrystalCountUpdate_Struct *)outapp->pBuffer; - - ccus->CurrentRadiantCrystals = GetRadiantCrystals(); - ccus->CurrentEbonCrystals = GetEbonCrystals(); - ccus->CareerRadiantCrystals = m_pp.careerRadCrystals; - ccus->CareerEbonCrystals = m_pp.careerEbonCrystals; - - - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SendDisciplineTimers() -{ - - auto outapp = new EQApplicationPacket(OP_DisciplineTimer, sizeof(DisciplineTimer_Struct)); - DisciplineTimer_Struct *dts = (DisciplineTimer_Struct *)outapp->pBuffer; - - for(unsigned int i = 0; i < MAX_DISCIPLINE_TIMERS; ++i) - { - uint32 RemainingTime = p_timers.GetRemainingTime(pTimerDisciplineReuseStart + i); - - if(RemainingTime > 0) - { - dts->TimerID = i; - dts->Duration = RemainingTime; - QueuePacket(outapp); - } - } - - safe_delete(outapp); -} - -void Client::SendRespawnBinds() -{ - // This sends the data to the client to populate the Respawn from Death Window. - // - // This should be sent after OP_Death for SoF clients - // Client will respond with a 4 byte packet that includes the number of the selection made - // - - //If no options have been given, default to Bind + Rez - if (respawn_options.empty()) - { - BindStruct* b = &m_pp.binds[0]; - RespawnOption opt; - opt.name = "Bind Location"; - opt.zone_id = b->zone_id; - opt.instance_id = b->instance_id; - opt.x = b->x; - opt.y = b->y; - opt.z = b->z; - opt.heading = b->heading; - respawn_options.push_front(opt); - } - //Rez is always added at the end - RespawnOption rez; - rez.name = "Resurrect"; - rez.zone_id = zone->GetZoneID(); - rez.instance_id = zone->GetInstanceID(); - rez.x = GetX(); - rez.y = GetY(); - rez.z = GetZ(); - rez.heading = GetHeading(); - respawn_options.push_back(rez); - - int num_options = respawn_options.size(); - uint32 PacketLength = 17 + (26 * num_options); //Header size + per-option invariant size - - std::list::iterator itr; - RespawnOption* opt = nullptr; - - //Find string size for each option - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - opt = &(*itr); - PacketLength += opt->name.size() + 1; //+1 for cstring - } - - auto outapp = new EQApplicationPacket(OP_RespawnWindow, PacketLength); - char* buffer = (char*)outapp->pBuffer; - - //Packet header - VARSTRUCT_ENCODE_TYPE(uint32, buffer, initial_respawn_selection); //initial selection (from 0) - VARSTRUCT_ENCODE_TYPE(uint32, buffer, RuleI(Character, RespawnFromHoverTimer) * 1000); - VARSTRUCT_ENCODE_TYPE(uint32, buffer, 0); //unknown - VARSTRUCT_ENCODE_TYPE(uint32, buffer, num_options); //number of options to display - - //Individual options - int count = 0; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - opt = &(*itr); - VARSTRUCT_ENCODE_TYPE(uint32, buffer, count++); //option num (from 0) - VARSTRUCT_ENCODE_TYPE(uint32, buffer, opt->zone_id); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->x); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->y); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->z); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->heading); - VARSTRUCT_ENCODE_STRING(buffer, opt->name.c_str()); - VARSTRUCT_ENCODE_TYPE(uint8, buffer, (count == num_options)); //is this one Rez (the last option)? - } - - QueuePacket(outapp); - safe_delete(outapp); - return; -} - -void Client::HandleLDoNOpen(NPC *target) -{ - if(target) - { - if(target->GetClass() != Class::LDoNTreasure) - { - LogDebug("[{}] tried to open [{}] but [{}] was not a treasure chest", - GetName(), target->GetName(), target->GetName()); - return; - } - - if (target->GetSpecialAbility(SpecialAbility::OpenImmunity)) - { - LogDebug("[{}] tried to open [{}] but it was immune", GetName(), target->GetName()); - return; - } - - if(DistanceSquaredNoZ(m_Position, target->GetPosition()) > RuleI(Adventure, LDoNTrapDistanceUse)) - { - LogDebug("[{}] tried to open [{}] but [{}] was out of range", - GetName(), target->GetName(), target->GetName()); - Message(Chat::Red, "Treasure chest out of range."); - return; - } - - if(target->IsLDoNTrapped()) - { - if(target->GetLDoNTrapSpellID() != 0) - { - MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - } - else - { - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - } - } - - if(target->IsLDoNLocked()) - { - MessageString(Chat::Skills, LDON_STILL_LOCKED, target->GetCleanName()); - return; - } - else - { - target->AddToHateList(this, 0, 500000, false, false, false); - if(target->GetLDoNTrapType() != 0) - { - if(GetRaid()) - { - GetRaid()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); - } - else if(GetGroup()) - { - GetGroup()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); - } - else - { - AddEXP(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, GetLevelCon(target->GetLevel())); - } - } - target->Death(this, 0, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); - } - } -} - -void Client::HandleLDoNSenseTraps(NPC *target, uint16 skill, uint8 type) -{ - if(target && target->GetClass() == Class::LDoNTreasure) - { - if(target->IsLDoNTrapped()) - { - if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) - { - MessageString(Chat::Skills, LDON_CANT_DETERMINE_TRAP, target->GetCleanName()); - return; - } +uint64 Client::GetCarriedMoney() { - if(target->IsLDoNTrapDetected()) - { - MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); - } - else - { - int check = LDoNChest_SkillCheck(target, skill); - switch(check) - { - case -1: - case 0: - MessageString(Chat::Skills, LDON_DONT_KNOW_TRAPPED, target->GetCleanName()); - break; - case 1: - MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); - target->SetLDoNTrapDetected(true); - break; - default: - break; - } - } - } - else - { - MessageString(Chat::Skills, LDON_CERTAIN_NOT_TRAP, target->GetCleanName()); - } - } + return ( + ( + static_cast(m_pp.copper) + + (static_cast(m_pp.silver) * 10) + + (static_cast(m_pp.gold) * 100) + + (static_cast(m_pp.platinum) * 1000) + ) + ); } -void Client::HandleLDoNDisarm(NPC *target, uint16 skill, uint8 type) -{ - if(target) - { - if(target->GetClass() == Class::LDoNTreasure) - { - if(!target->IsLDoNTrapped()) - { - MessageString(Chat::Skills, LDON_WAS_NOT_TRAPPED, target->GetCleanName()); - return; - } - - if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) - { - MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); - return; - } +uint64 Client::GetAllMoney() { - int check = 0; - if(target->IsLDoNTrapDetected()) - { - check = LDoNChest_SkillCheck(target, skill); - } - else - { - check = LDoNChest_SkillCheck(target, skill*33/100); - } - switch(check) - { - case 1: - target->SetLDoNTrapDetected(false); - target->SetLDoNTrapped(false); - target->SetLDoNTrapSpellID(0); - MessageString(Chat::Skills, LDON_HAVE_DISARMED, target->GetCleanName()); - break; - case 0: - MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); - break; - case -1: - MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - break; - } - } - } + return ( + ( + static_cast(m_pp.copper) + + (static_cast(m_pp.silver) * 10) + + (static_cast(m_pp.gold) * 100) + + (static_cast(m_pp.platinum) * 1000) + + ( + static_cast(m_pp.copper_bank) + + (static_cast(m_pp.silver_bank) * 10) + + (static_cast(m_pp.gold_bank) * 100) + + (static_cast(m_pp.platinum_bank) * 1000) + + ( + static_cast(m_pp.copper_cursor) + + (static_cast(m_pp.silver_cursor) * 10) + + (static_cast(m_pp.gold_cursor) * 100) + + (static_cast(m_pp.platinum_cursor) * 1000) + + (static_cast(m_pp.platinum_shared) * 1000) + ) + ) + ) + ); } -void Client::HandleLDoNPickLock(NPC *target, uint16 skill, uint8 type) -{ - if(target) - { - if(target->GetClass() == Class::LDoNTreasure) - { - if(target->IsLDoNTrapped()) - { - MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - } - - if(!target->IsLDoNLocked()) - { - MessageString(Chat::Skills, LDON_WAS_NOT_LOCKED, target->GetCleanName()); - return; - } - - if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) - { - Message(Chat::Skills, "You cannot unlock %s with this skill.", target->GetCleanName()); - return; - } - - int check = LDoNChest_SkillCheck(target, skill); - - switch(check) - { - case 0: - case -1: - MessageString(Chat::Skills, LDON_PICKLOCK_FAILURE, target->GetCleanName()); - break; - case 1: - target->SetLDoNLocked(false); - MessageString(Chat::Skills, LDON_PICKLOCK_SUCCESS, target->GetCleanName()); - break; - } - } - } +bool Client::CheckIncreaseSkill(EQ::skills::SkillType skillid, Mob *against_who, int chancemodi) { + if (IsDead() || IsUnconscious()) { + return false; + } + + if (IsAIControlled()) { // no skillups while chamred =p + return false; + } + + if (against_who && against_who->IsCorpse()) { // no skillups on corpses + return false; + } + + if (skillid > EQ::skills::HIGHEST_SKILL) { + return false; + } + + auto skillval = GetRawSkill(skillid); + auto maxskill = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); + + if (parse->PlayerHasQuestSub(EVENT_USE_SKILL)) { + const auto& export_string = fmt::format( + "{} {}", + skillid, + skillval + ); + + parse->EventPlayer(EVENT_USE_SKILL, this, export_string, 0); + } + + if (against_who) { + if ( + against_who->GetSpecialAbility(SpecialAbility::AggroImmunity) || + against_who->GetSpecialAbility(SpecialAbility::ClientAggroImmunity) || + against_who->IsClient() || + GetLevelCon(against_who->GetLevel()) == ConsiderColor::Gray + ) { + return false; + } + } + + // Make sure we're not already at skill cap + if (skillval < maxskill) + { + double Chance = 0; + if (RuleI(Character, SkillUpMaximumChancePercentage) + chancemodi - RuleI(Character, SkillUpMinimumChancePercentage) <= RuleI(Character, SkillUpMinimumChancePercentage)) { + Chance = RuleI(Character, SkillUpMinimumChancePercentage); + } + else { + // f(x) = (max - min + modification) * .99^skillval + min + // This results in a exponential decay where as you skill up, you lose a slight chance to skill up, ranging from your modified maximum to approaching your minimum + // This result is increased by the existing SkillUpModifier rule + double working_chance = (((RuleI(Character, SkillUpMaximumChancePercentage) - RuleI(Character, SkillUpMinimumChancePercentage) + chancemodi) * (pow(0.99, skillval))) + RuleI(Character, SkillUpMinimumChancePercentage)); + Chance = (working_chance * RuleI(Character, SkillUpModifier) / 100); + } + + if(zone->random.Real(0, 99) < Chance) + { + SetSkill(skillid, GetRawSkill(skillid) + 1); + + if (player_event_logs.IsEventEnabled(PlayerEvent::SKILL_UP)) { + auto e = PlayerEvent::SkillUpEvent{ + .skill_id = static_cast(skillid), + .value = static_cast((skillval + 1)), + .max_skill = static_cast(maxskill), + .against_who = (against_who) ? against_who->GetCleanName() : GetCleanName(), + }; + RecordPlayerEventLog(PlayerEvent::SKILL_UP, e); + } + + if (parse->PlayerHasQuestSub(EVENT_SKILL_UP)) { + const auto& export_string = fmt::format( + "{} {} {} {}", + skillid, + skillval + 1, + maxskill, + 0 + ); + + parse->EventPlayer(EVENT_SKILL_UP, this, export_string, 0); + } + + LogSkills("Skill [{}] at value [{}] successfully gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); + return true; + } else { + LogSkills("Skill [{}] at value [{}] failed to gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); + } + } else { + LogSkills("Skill [{}] at value [{}] cannot increase due to maxmum [{}]", skillid, skillval, maxskill); + } + return false; } -int Client::LDoNChest_SkillCheck(NPC *target, int skill) -{ - if(!target) - return -1; +void Client::CheckLanguageSkillIncrease(uint8 language_id, uint8 teacher_skill) { + if (IsDead() || IsUnconscious()) { + return; + } - int chest_difficulty = target->GetLDoNLockedSkill() == 0 ? (target->GetLevel() * 5) : target->GetLDoNLockedSkill(); - float base_difficulty = RuleR(Adventure, LDoNBaseTrapDifficulty); + if (IsAIControlled()) { + return; + } - if(chest_difficulty == 0) - chest_difficulty = 5; + if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { + return; + } - float chance = ((100.0f - base_difficulty) * ((float)skill / (float)chest_difficulty)); + const uint8 language_skill = m_pp.languages[language_id]; // get current language skill - if(chance > (100.0f - base_difficulty)) - { - chance = 100.0f - base_difficulty; - } + if (language_skill < Language::MaxValue) { // if the language isn't already maxed + int chance = 5 + ((teacher_skill - language_skill) / 10); // greater chance to learn if teacher's skill is much higher than yours + chance = (chance * RuleI(Character, SkillUpModifier) / 100); - float d100 = (float)zone->random.Real(0, 100); + if (zone->random.Real(0, 100) < chance) { // if they make the roll + IncreaseLanguageSkill(language_id); - if(d100 <= chance) - return 1; - else - { - if(d100 > (chance + RuleR(Adventure, LDoNCriticalFailTrapThreshold))) - return -1; - } + if (parse->PlayerHasQuestSub(EVENT_LANGUAGE_SKILL_UP)) { + const auto &export_string = fmt::format( + "{} {} {}", + language_id, + language_skill + 1, + Language::MaxValue + ); + + parse->EventPlayer(EVENT_LANGUAGE_SKILL_UP, this, export_string, 0); + } - return 0; + LogSkills("Language [{}] at value [{}] successfully gain with [{}] % chance", language_id, language_skill, chance); + } else { + LogSkills("Language [{}] at value [{}] failed to gain with [{}] % chance", language_id, language_skill, chance); + } + } } -void Client::SummonAndRezzAllCorpses() +bool Client::HasSkill(EQ::skills::SkillType skill_id) const { - PendingRezzXP = -1; - - auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); - - ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; - - sdapcs->CharacterID = CharacterID(); - sdapcs->ZoneID = zone->GetZoneID(); - sdapcs->InstanceID = zone->GetInstanceID(); - - worldserver.SendPacket(Pack); - - safe_delete(Pack); - - entity_list.RemoveAllCorpsesByCharID(CharacterID()); - - int CorpseCount = database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), GetPosition()); - if(CorpseCount <= 0) - { - Message(Chat::Yellow, "You have no corpses to summnon."); - return; - } - - int RezzExp = entity_list.RezzAllCorpsesByCharID(CharacterID()); - - if(RezzExp > 0) - SetEXP(ExpSource::Resurrection, GetEXP() + RezzExp, GetAAXP(), true); - - Message(Chat::Yellow, "All your corpses have been summoned to your feet and have received a 100% resurrection."); + return GetSkill(skill_id) > 0 && CanHaveSkill(skill_id); } -void Client::SummonAllCorpses(const glm::vec4& position) +bool Client::CanHaveSkill(EQ::skills::SkillType skill_id) const { - auto summonLocation = position; - if(IsOrigin(position) && position.w == 0.0f) - summonLocation = GetPosition(); - - auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); - - ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; - - sdapcs->CharacterID = CharacterID(); - sdapcs->ZoneID = zone->GetZoneID(); - sdapcs->InstanceID = zone->GetInstanceID(); - - worldserver.SendPacket(Pack); - - safe_delete(Pack); + if ( + ClientVersion() < EQ::versions::ClientVersion::RoF2 && + class_ == Class::Berserker && + skill_id == EQ::skills::Skill1HPiercing + ) { + skill_id = EQ::skills::Skill2HPiercing; + } - entity_list.RemoveAllCorpsesByCharID(CharacterID()); - - database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), summonLocation); + return skill_caps.GetSkillCap(GetClass(), skill_id, RuleI(Character, MaxLevel)).cap > 0; } -void Client::DepopAllCorpses() +uint16 Client::MaxSkill(EQ::skills::SkillType skill_id, uint8 class_id, uint8 level) const { - auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); - - ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; + if ( + ClientVersion() < EQ::versions::ClientVersion::RoF2 && + class_id == Class::Berserker && + skill_id == EQ::skills::Skill1HPiercing + ) { + skill_id = EQ::skills::Skill2HPiercing; + } - sdapcs->CharacterID = CharacterID(); - sdapcs->ZoneID = zone->GetZoneID(); - sdapcs->InstanceID = zone->GetInstanceID(); - - worldserver.SendPacket(Pack); - - safe_delete(Pack); - - entity_list.RemoveAllCorpsesByCharID(CharacterID()); + return skill_caps.GetSkillCap(class_id, skill_id, level).cap; } -void Client::DepopPlayerCorpse(uint32 dbid) +uint8 Client::GetSkillTrainLevel(EQ::skills::SkillType skill_id, uint8 class_id) { - auto Pack = new ServerPacket(ServerOP_DepopPlayerCorpse, sizeof(ServerDepopPlayerCorpse_Struct)); - - ServerDepopPlayerCorpse_Struct *sdpcs = (ServerDepopPlayerCorpse_Struct*)Pack->pBuffer; - - sdpcs->DBID = dbid; - sdpcs->ZoneID = zone->GetZoneID(); - sdpcs->InstanceID = zone->GetInstanceID(); - - worldserver.SendPacket(Pack); + if ( + ClientVersion() < EQ::versions::ClientVersion::RoF2 && + class_id == Class::Berserker && + skill_id == EQ::skills::Skill1HPiercing + ) { + skill_id = EQ::skills::Skill2HPiercing; + } - safe_delete(Pack); - - entity_list.RemoveCorpseByDBID(dbid); + return skill_caps.GetSkillTrainLevel(class_id, skill_id, RuleI(Character, MaxLevel)); } -void Client::BuryPlayerCorpses() +uint16 Client::GetMaxSkillAfterSpecializationRules(EQ::skills::SkillType skillid, uint16 maxSkill) { - database.BuryAllCharacterCorpses(CharacterID()); + uint16 Result = maxSkill; + + uint16 PrimarySpecialization = 0, SecondaryForte = 0; + + uint16 PrimarySkillValue = 0, SecondarySkillValue = 0; + + uint16 MaxSpecializations = aabonuses.SecondaryForte ? 2 : 1; + + if (skillid >= EQ::skills::SkillSpecializeAbjure && skillid <= EQ::skills::SkillSpecializeEvocation) + { + bool HasPrimarySpecSkill = false; + + int NumberOfPrimarySpecSkills = 0; + + for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) + { + if(m_pp.skills[i] > 50) + { + HasPrimarySpecSkill = true; + NumberOfPrimarySpecSkills++; + } + if(m_pp.skills[i] > PrimarySkillValue) + { + if(PrimarySkillValue > SecondarySkillValue) + { + SecondarySkillValue = PrimarySkillValue; + SecondaryForte = PrimarySpecialization; + } + + PrimarySpecialization = i; + PrimarySkillValue = m_pp.skills[i]; + } + else if(m_pp.skills[i] > SecondarySkillValue) + { + SecondaryForte = i; + SecondarySkillValue = m_pp.skills[i]; + } + } + + if(SecondarySkillValue <=50) + SecondaryForte = 0; + + if(HasPrimarySpecSkill) + { + if(NumberOfPrimarySpecSkills <= MaxSpecializations) + { + if(MaxSpecializations == 1) + { + if(skillid != PrimarySpecialization) + { + Result = 50; + } + } + else + { + if((skillid != PrimarySpecialization) && ((skillid == SecondaryForte) || (SecondaryForte == 0))) + { + if((PrimarySkillValue > 100) || (!SecondaryForte)) + Result = 100; + } + else if(skillid != PrimarySpecialization) + { + Result = 50; + } + } + } + else + { + Message(Chat::Red, "Your spell casting specializations skills have been reset. " + "Only %i primary specialization skill is allowed.", MaxSpecializations); + + for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) + SetSkill((EQ::skills::SkillType)i, 1); + + Save(); + + LogInfo("Reset [{}]'s caster specialization skills to 1" + "Too many specializations skills were above 50.", GetCleanName()); + } + + } + } + + Result += spellbonuses.RaiseSkillCap[skillid] + itembonuses.RaiseSkillCap[skillid] + aabonuses.RaiseSkillCap[skillid]; + + if (skillid == EQ::skills::SkillType::SkillForage) + Result += aabonuses.GrantForage; + + return Result; } -void Client::NotifyNewTitlesAvailable() -{ - auto outapp = new EQApplicationPacket(OP_NewTitlesAvailable, 0); - - QueuePacket(outapp); +void Client::SetPVP(bool toggle, bool message) { + m_pp.pvp = toggle ? 1 : 0; - safe_delete(outapp); + if (message) { + if(GetPVP()) { + MessageString(Chat::Shout, PVP_ON); + } else { + Message(Chat::Shout, "You now follow the ways of Order."); + } + } + SendAppearancePacket(AppearanceType::PVP, GetPVP()); + Save(); } -void Client::SetStartZone(uint32 zoneid, float x, float y, float z, float heading) -{ - // setting city to zero allows the player to use /setstartcity to set the city themselves - if(zoneid == 0) { - m_pp.binds[4].zone_id = 0; - Message(Chat::Yellow,"Your starting city has been reset. Use /setstartcity to choose a new one"); - return; - } - - // check to make sure the zone is valid - const char *target_zone_name = ZoneName(zoneid); - if(target_zone_name == nullptr) - return; - - m_pp.binds[4].zone_id = zoneid; - if(zone->GetInstanceID() != 0 && zone->IsInstancePersistent()) { - m_pp.binds[4].instance_id = zone->GetInstanceID(); - } +void Client::Kick(const std::string &reason) { + client_state = CLIENT_KICKED; - if (x == 0 && y == 0 && z == 0) { - auto zd = GetZone(m_pp.binds[4].zone_id); - if (zd) { - m_pp.binds[4].x = zd->safe_x; - m_pp.binds[4].y = zd->safe_y; - m_pp.binds[4].z = zd->safe_z; - m_pp.binds[4].heading = zd->safe_heading; - } - } - else { - m_pp.binds[4].x = x; - m_pp.binds[4].y = y; - m_pp.binds[4].z = z; - m_pp.binds[4].heading = heading; - } + LogClientLogin("Client [{}] kicked, reason [{}]", GetCleanName(), reason.c_str()); } -uint32 Client::GetStartZone() -{ - return m_pp.binds[4].zone_id; +void Client::WorldKick() { + auto outapp = new EQApplicationPacket(OP_GMKick, sizeof(GMKick_Struct)); + GMKick_Struct* gmk = (GMKick_Struct *)outapp->pBuffer; + strcpy(gmk->name,GetName()); + QueuePacket(outapp); + safe_delete(outapp); + Kick("World kick issued"); } -void Client::ShowSkillsWindow() -{ - std::string popup_text; - std::map skills_map = EQ::skills::GetSkillTypeMap(); - - if (ClientVersion() < EQ::versions::ClientVersion::RoF2) { - skills_map[EQ::skills::Skill1HPiercing] = "Piercing"; - } - - // Table Start - popup_text += ""; - - for (const auto& skill : skills_map) { - auto skill_id = skill.first; - auto skill_name = skill.second; - auto can_have_skill = CanHaveSkill(skill_id); - auto current_skill = GetSkill(skill_id); - auto max_skill = MaxSkill(skill_id); - auto skill_maxed = current_skill >= max_skill; - if ( - skill_id == EQ::skills::Skill2HPiercing && - ClientVersion() < EQ::versions::ClientVersion::RoF2 - ) { - continue; - } - - if ( - !can_have_skill || - !current_skill || - !max_skill - ) { - continue; - } - - // Row Start - popup_text += ""; - - // Skill Name - popup_text += fmt::format( - "", - skill_name - ); - - // Current Skill Level out of Max Skill Level or a Check Mark for Maxed - popup_text += fmt::format( - "", - ( - skill_maxed ? - "Max" : - fmt::format( - "{}/{}", - current_skill, - max_skill - ) - ) - ); - - // Row End - popup_text += ""; - } - - // Table End - popup_text += "
{}{}
"; +void Client::GMKill() { + auto outapp = new EQApplicationPacket(OP_GMKill, sizeof(GMKill_Struct)); + GMKill_Struct* gmk = (GMKill_Struct *)outapp->pBuffer; + strcpy(gmk->name,GetName()); + QueuePacket(outapp); + safe_delete(outapp); +} - SendPopupToClient( - "Skills", - popup_text.c_str() - ); +void Client::MemorizeSpell(uint32 slot, uint32 spell_id, uint32 scribing, uint32 reduction){ + if ( + !EQ::ValueWithin( + slot, + 0, + (EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize - 1) + ) + ) { + return; + } + + if ( + !EQ::ValueWithin( + spell_id, + 3, + EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellIdMax + ) && + spell_id != UINT32_MAX + ) { + return; + } + + auto outapp = new EQApplicationPacket(OP_MemorizeSpell, sizeof(MemorizeSpell_Struct)); + + auto* mss = (MemorizeSpell_Struct*) outapp->pBuffer; + + mss->scribing = scribing; + mss->slot = slot; + mss->spell_id = spell_id; + mss->reduction = reduction; + + outapp->priority = 5; + + if ( + parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) || + parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) || + parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) + ) { + const auto export_string = fmt::format("{} {}", slot, spell_id); + + if ( + scribing == ScribeSpellActions::Memorize && + parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) + ) { + parse->EventPlayer(EVENT_MEMORIZE_SPELL, this, export_string, 0); + } else if ( + scribing == ScribeSpellActions::Unmemorize && + parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) + ) { + parse->EventPlayer(EVENT_UNMEMORIZE_SPELL, this, export_string, 0); + } else if ( + scribing == ScribeSpellActions::Scribe && + parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) + ) { + parse->EventPlayer(EVENT_SCRIBE_SPELL, this, export_string, 0); + } + } + + QueuePacket(outapp); + safe_delete(outapp); } -void Client::Signal(int signal_id) -{ - if (parse->PlayerHasQuestSub(EVENT_SIGNAL)) { - parse->EventPlayer(EVENT_SIGNAL, this, std::to_string(signal_id), 0); - } +void Client::Disarm(Client* disarmer, int chance) { + int16 slot = EQ::invslot::SLOT_INVALID; + const EQ::ItemInstance *inst = GetInv().GetItem(EQ::invslot::slotPrimary); + if (inst && inst->IsWeapon()) { + slot = EQ::invslot::slotPrimary; + } + else { + inst = GetInv().GetItem(EQ::invslot::slotSecondary); + if (inst && inst->IsWeapon()) + slot = EQ::invslot::slotSecondary; + } + if (slot != EQ::invslot::SLOT_INVALID && inst->IsClassCommon()) { + // We have an item that can be disarmed. + if (zone->random.Int(0, 1000) <= chance) { + // Find a free inventory slot + int16 slot_id = EQ::invslot::SLOT_INVALID; + slot_id = m_inv.FindFreeSlot(false, true, inst->GetItem()->Size, (inst->GetItem()->ItemType == EQ::item::ItemTypeArrow)); + if (slot_id != EQ::invslot::SLOT_INVALID) + { + EQ::ItemInstance *InvItem = m_inv.PopItem(slot); + if (InvItem) { // there should be no way it is not there, but check anyway + EQApplicationPacket* outapp = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); + MoveItem_Struct* mi = (MoveItem_Struct*)outapp->pBuffer; + mi->from_slot = slot; + mi->to_slot = 0xFFFFFFFF; + if (inst->IsStackable()) // it should not be stackable + mi->number_in_stack = inst->GetCharges(); + else + mi->number_in_stack = 0; + FastQueuePacket(&outapp); // this deletes item from the weapon slot on the client + if (PutItemInInventory(slot_id, *InvItem, true)) + database.SaveInventory(CharacterID(), NULL, slot); + auto matslot = (slot == EQ::invslot::slotPrimary ? EQ::textures::weaponPrimary : EQ::textures::weaponSecondary); + if (matslot != EQ::textures::materialInvalid) + SendWearChange(matslot); + } + MessageString(Chat::Skills, DISARMED); + if (disarmer != this) + disarmer->MessageString(Chat::Skills, DISARM_SUCCESS, GetCleanName()); + if (chance != 1000) + disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 4); + CalcBonuses(); + // CalcEnduranceWeightFactor(); + return; + } + disarmer->MessageString(Chat::Skills, DISARM_FAILED); + if (chance != 1000) + disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 2); + return; + } + } + disarmer->MessageString(Chat::Skills, DISARM_FAILED); } -void Client::SendPayload(int payload_id, std::string payload_value) +bool Client::BindWound(Mob *bindmob, bool start, bool fail) { - if (parse->PlayerHasQuestSub(EVENT_PAYLOAD)) { - const auto& export_string = fmt::format("{} {}", payload_id, payload_value); - - parse->EventPlayer(EVENT_PAYLOAD, this, export_string, 0); - } + EQApplicationPacket *outapp = nullptr; + if (!fail) { + outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); + BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; + // Start bind + if (!bindwound_timer.Enabled()) { + // make sure we actually have a bandage... and consume it. + int16 bslot = m_inv.HasItemByUse(EQ::item::ItemTypeBandage, 1, invWhereWorn | invWherePersonal); + if (bslot == INVALID_INDEX) { + bind_out->type = 3; + QueuePacket(outapp); + bind_out->type = 7; // this is the wrong message, dont know the right one. + QueuePacket(outapp); + safe_delete(outapp); + return (true); + } + DeleteItemInInventory(bslot, 1, true); // do we need client update? + + // start complete timer + bindwound_timer.Start(10000); + bindwound_target = bindmob; + + // Send client unlock + bind_out->type = 3; + QueuePacket(outapp); + bind_out->type = 0; + // Client Unlocked + if (!bindmob) { + // send "bindmob dead" to client + bind_out->type = 4; + QueuePacket(outapp); + bind_out->type = 0; + bindwound_timer.Disable(); + bindwound_target = 0; + } else { + // send bindmob "stand still" + if (!bindmob->IsAIControlled() && bindmob != this) { + bindmob->CastToClient()->MessageString(Chat::Yellow, + YOU_ARE_BEING_BANDAGED); + } else if (bindmob->IsAIControlled() && bindmob != this) { + ; // Tell IPC to stand still? + } else { + ; // Binding self + } + } + } else if (bindwound_timer.Check()) // Did the timer finish? + { + // finish bind + // disable complete timer + bindwound_timer.Disable(); + bindwound_target = 0; + if (!bindmob) { + // send "bindmob gone" to client + bind_out->type = 5; // not in zone + QueuePacket(outapp); + bind_out->type = 0; + } + + else { + if (!GetFeigned() && (DistanceSquared(bindmob->GetPosition(), m_Position) <= 400)) { + // send bindmob bind done + if (!bindmob->IsAIControlled() && bindmob != this) { + + } else if (bindmob->IsAIControlled() && bindmob != this) { + // Tell IPC to resume?? + } else { + // Binding self + } + // Send client bind done + + bind_out->type = 1; // Done + QueuePacket(outapp); + bind_out->type = 0; + CheckIncreaseSkill(EQ::skills::SkillBindWound, nullptr, 5); + + if (RuleB(Character, UseOldBindWound)) { + int maxHPBonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + + aabonuses.MaxBindWound; + + int max_percent = 50 + maxHPBonus; + + if (GetClass() == Class::Monk && GetSkill(EQ::skills::SkillBindWound) > 200) { + max_percent = 70 + maxHPBonus; + } + + int64 max_hp = bindmob->GetMaxHP() * max_percent / 100; + + // send bindmob new hp's + if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() <= (max_hp)-1) { + // 0.120 per skill point, 0.60 per skill level, minimum 3 max 30 + int bindhps = 3; + + if (GetSkill(EQ::skills::SkillBindWound) > 200) { + bindhps += GetSkill(EQ::skills::SkillBindWound) * 4 / 10; + } + else if (GetSkill(EQ::skills::SkillBindWound) >= 10) { + bindhps += GetSkill(EQ::skills::SkillBindWound) / 4; + } + + // Implementation of aaMithanielsBinding is a guess (the multiplier) + int bindBonus = spellbonuses.BindWound + itembonuses.BindWound + + aabonuses.BindWound; + + bindhps += bindhps * bindBonus / 100; + + // if the bind takes them above the max bindable + // cap it at that value. Dont know if live does it this way + // but it makes sense to me. + int chp = bindmob->GetHP() + bindhps; + if (chp > max_hp) + chp = max_hp; + + bindmob->SetHP(chp); + bindmob->SendHPUpdate(); + } + else { + // I dont have the real, live + Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", + max_percent); + if (bindmob != this && bindmob->IsClient()) + bindmob->CastToClient()->Message( + 15, + "You cannot have your wounds bound above %d%% hitpoints.", + max_percent); + // Too many hp message goes here. + } + } + else { + int percent_base = 50; + if (GetRawSkill(EQ::skills::SkillBindWound) > 200) { + if ((GetClass() == Class::Monk) || (GetClass() == Class::Beastlord)) + percent_base = 70; + else if ((GetLevel() > 50) && ((GetClass() == Class::Warrior) || (GetClass() == Class::Rogue) || (GetClass() == Class::Cleric))) + percent_base = 70; + } + + int percent_bonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + aabonuses.MaxBindWound; + + int max_percent = percent_base + percent_bonus; + if (max_percent < 0) + max_percent = 0; + if (max_percent > 100) + max_percent = 100; + + int max_hp = (bindmob->GetMaxHP() * max_percent) / 100; + if (max_hp > bindmob->GetMaxHP()) + max_hp = bindmob->GetMaxHP(); + + if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() < max_hp) { + int bindhps = 3; // base bind hp + if (percent_base >= 70) + bindhps = (GetSkill(EQ::skills::SkillBindWound) * 4) / 10; // 8:5 skill-to-hp ratio + else if (GetSkill(EQ::skills::SkillBindWound) >= 12) + bindhps = GetSkill(EQ::skills::SkillBindWound) / 4; // 4:1 skill-to-hp ratio + + int bonus_hp_percent = spellbonuses.BindWound + itembonuses.BindWound + aabonuses.BindWound; + + bindhps += (bindhps * bonus_hp_percent) / 100; + + if (bindhps < 3) + bindhps = 3; + + bindhps += bindmob->GetHP(); + if (bindhps > max_hp) + bindhps = max_hp; + + bindmob->SetHP(bindhps); + bindmob->SendHPUpdate(); + } + else { + Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", max_percent); + if (bindmob != this && bindmob->IsClient()) + bindmob->CastToClient()->Message(Chat::Yellow, "You cannot have your wounds bound above %d%% hitpoints.", max_percent); + } + } + } + else { + // Send client bind failed + if (bindmob != this) + bind_out->type = 6; // They moved + else + bind_out->type = 7; // Bandager moved + + QueuePacket(outapp); + bind_out->type = 0; + } + } + } + } else if (bindwound_timer.Enabled()) { + // You moved + outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); + BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; + bindwound_timer.Disable(); + bindwound_target = 0; + bind_out->type = 7; + QueuePacket(outapp); + bind_out->type = 3; + QueuePacket(outapp); + } + safe_delete(outapp); + return true; } -void Client::SendRewards() +void Client::SetMaterial(int16 in_slot, uint32 item_id) { - std::vector rewards; - std::string query = StringFormat("SELECT reward_id, amount " - "FROM account_rewards " - "WHERE account_id = %i " - "ORDER BY reward_id", AccountID()); - auto results = database.QueryDatabase(query); - if (!results.Success()) { - return; - } - - for (auto row = results.begin(); row != results.end(); ++row) { - ClientReward cr; - cr.id = Strings::ToInt(row[0]); - cr.amount = Strings::ToInt(row[1]); - rewards.push_back(cr); - } - - if(rewards.empty()) - return; - - auto vetapp = new EQApplicationPacket(OP_VetRewardsAvaliable, (sizeof(InternalVeteranReward) * rewards.size())); - uchar *data = vetapp->pBuffer; - for(int i = 0; i < rewards.size(); ++i) { - InternalVeteranReward *ivr = (InternalVeteranReward*)data; - ivr->claim_id = rewards[i].id; - ivr->number_available = rewards[i].amount; - auto iter = zone->VeteranRewards.begin(); - for (;iter != zone->VeteranRewards.end(); ++iter) - if((*iter).claim_id == rewards[i].id) - break; - - if(iter != zone->VeteranRewards.end()) { - InternalVeteranReward ivro = (*iter); - ivr->claim_count = ivro.claim_count; - for(int x = 0; x < ivro.claim_count; ++x) { - ivr->items[x].item_id = ivro.items[x].item_id; - ivr->items[x].charges = ivro.items[x].charges; - strcpy(ivr->items[x].item_name, ivro.items[x].item_name); - } - } + const EQ::ItemData *item = database.GetItem(item_id); + if (item && item->IsClassCommon()) { + uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); + if (matslot != EQ::textures::materialInvalid) { + m_pp.item_material.Slot[matslot].Material = GetEquipmentMaterial(matslot); + } + } +} - data += sizeof(InternalVeteranReward); - } +void Client::ServerFilter(SetServerFilter_Struct* filter){ - FastQueuePacket(&vetapp); +/* this code helps figure out the filter IDs in the packet if needed + static SetServerFilter_Struct ssss; + int r; + uint32 *o = (uint32 *) &ssss; + uint32 *n = (uint32 *) filter; + for(r = 0; r < (sizeof(SetServerFilter_Struct)/4); r++) { + if(*o != *n) + LogFile->write(EQEMuLog::Debug, "Filter %d changed from %d to %d", r, *o, *n); + o++; n++; + } + memcpy(&ssss, filter, sizeof(SetServerFilter_Struct)); +*/ +#define Filter0(type) \ + if(filter->filters[type] == 1) \ + SetFilter(type, FilterShow); \ + else \ + SetFilter(type, FilterHide); +#define Filter1(type) \ + if(filter->filters[type] == 0) \ + SetFilter(type, FilterShow); \ + else \ + SetFilter(type, FilterHide); + + Filter0(FilterGuildChat); + Filter0(FilterSocials); + Filter0(FilterGroupChat); + Filter0(FilterShouts); + Filter0(FilterAuctions); + Filter0(FilterOOC); + Filter0(FilterBadWords); + + if (filter->filters[FilterPCSpells] == 0) { + SetFilter(FilterPCSpells, FilterShow); + } else if (filter->filters[FilterPCSpells] == 1) { + SetFilter(FilterPCSpells, FilterHide); + } else { + SetFilter(FilterPCSpells, FilterShowGroupOnly); + } + + Filter1(FilterNPCSpells); + + if (filter->filters[FilterBardSongs] == 0) { + SetFilter(FilterBardSongs, FilterShow); + } else if (filter->filters[FilterBardSongs] == 1) { + SetFilter(FilterBardSongs, FilterShowSelfOnly); + } else if (filter->filters[FilterBardSongs] == 2) { + SetFilter(FilterBardSongs, FilterShowGroupOnly); + } else { + SetFilter(FilterBardSongs, FilterHide); + } + + if (filter->filters[FilterSpellCrits] == 0) { + SetFilter(FilterSpellCrits, FilterShow); + } else if (filter->filters[FilterSpellCrits] == 1) { + SetFilter(FilterSpellCrits, FilterShowSelfOnly); + } else { + SetFilter(FilterSpellCrits, FilterHide); + } + + if (filter->filters[FilterMeleeCrits] == 0) { + SetFilter(FilterMeleeCrits, FilterShow); + } else if (filter->filters[FilterMeleeCrits] == 1) { + SetFilter(FilterMeleeCrits, FilterShowSelfOnly); + } else { + SetFilter(FilterMeleeCrits, FilterHide); + } + + if (filter->filters[FilterSpellDamage] == 0) { + SetFilter(FilterSpellDamage, FilterShow); + } else if (filter->filters[FilterSpellDamage] == 1) { + SetFilter(FilterSpellDamage, FilterShowSelfOnly); + } else { + SetFilter(FilterSpellDamage, FilterHide); + } + + Filter0(FilterMyMisses); + Filter0(FilterOthersMiss); + Filter0(FilterOthersHit); + Filter0(FilterMissedMe); + Filter1(FilterDamageShields); + + if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { + if (filter->filters[FilterDOT] == 0) { + SetFilter(FilterDOT, FilterShow); + } else if (filter->filters[FilterDOT] == 1) { + SetFilter(FilterDOT, FilterShowSelfOnly); + } else if (filter->filters[FilterDOT] == 2) { + SetFilter(FilterDOT, FilterShowGroupOnly); + } else { + SetFilter(FilterDOT, FilterHide); + } + } else { + if (filter->filters[FilterDOT] == 0) { // show functions as self only + SetFilter(FilterDOT, FilterShowSelfOnly); + } else { + SetFilter(FilterDOT, FilterHide); + } + } + + Filter1(FilterPetHits); + Filter1(FilterPetMisses); + Filter1(FilterFocusEffects); + Filter1(FilterPetSpells); + + if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { + if (filter->filters[FilterHealOverTime] == 0) { + SetFilter(FilterHealOverTime, FilterShow); + } else if (filter->filters[FilterHealOverTime] == 1) { + SetFilter(FilterHealOverTime, FilterShowSelfOnly); + } else { + SetFilter(FilterHealOverTime, FilterHide); + } + } else { // these clients don't have a 'self only' filter + Filter1(FilterHealOverTime); + } + + Filter1(FilterItemSpeech); + Filter1(FilterStrikethrough); + Filter1(FilterStuns); + Filter1(FilterBardSongsOnPets); } -bool Client::TryReward(uint32 claim_id) +// this version is for messages with no parameters +void Client::MessageString(uint32 type, uint32 string_id, uint32 distance) { - // Make sure we have an open spot - // Make sure we have it in our acct and count > 0 - // Make sure the entry was found - // If we meet all the criteria: - // Decrement our count by 1 if it > 1 delete if it == 1 - // Create our item in bag if necessary at the free inv slot - // save - uint32 free_slot = 0xFFFFFFFF; - - for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; ++i) { - EQ::ItemInstance *item = GetInv().GetItem(i); - if (!item) { - free_slot = i; - break; - } - } - - if (free_slot == 0xFFFFFFFF) - return false; - - std::string query = StringFormat("SELECT amount FROM account_rewards " - "WHERE account_id = %i AND reward_id = %i", - AccountID(), claim_id); - auto results = database.QueryDatabase(query); - if (!results.Success()) - return false; - - if (results.RowCount() == 0) - return false; - - auto row = results.begin(); - - uint32 amt = Strings::ToInt(row[0]); - if (amt == 0) - return false; - - auto iter = std::find_if(zone->VeteranRewards.begin(), zone->VeteranRewards.end(), - [claim_id](const InternalVeteranReward &a) { return a.claim_id == claim_id; }); - - if (iter == zone->VeteranRewards.end()) - return false; - - if (amt == 1) { - query = StringFormat("DELETE FROM account_rewards " - "WHERE account_id = %i AND reward_id = %i", - AccountID(), claim_id); - auto results = database.QueryDatabase(query); - } else { - query = StringFormat("UPDATE account_rewards SET amount = (amount-1) " - "WHERE account_id = %i AND reward_id = %i", - AccountID(), claim_id); - auto results = database.QueryDatabase(query); - } - - auto &ivr = (*iter); - EQ::ItemInstance *claim = database.CreateItem(ivr.items[0].item_id, ivr.items[0].charges); - if (!claim) { - Save(); - return true; - } - - bool lore_conflict = CheckLoreConflict(claim->GetItem()); - - for (int y = 1; y < 8; y++) - if (ivr.items[y].item_id && claim->GetItem()->ItemClass == 1) { - EQ::ItemInstance *item_temp = database.CreateItem(ivr.items[y].item_id, ivr.items[y].charges); - if (item_temp) { - if (CheckLoreConflict(item_temp->GetItem())) { - lore_conflict = true; - DuplicateLoreMessage(ivr.items[y].item_id); - } - claim->PutItem(y - 1, *item_temp); - safe_delete(item_temp); - } - } - - if (lore_conflict) { - safe_delete(claim); - return true; - } + if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) + return; + if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... + return; + if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) + return; + auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); + SimpleMessage_Struct* sms = (SimpleMessage_Struct*)outapp->pBuffer; + sms->color=type; + sms->string_id=string_id; - PutItemInInventory(free_slot, *claim); - SendItemPacket(free_slot, claim, ItemPacketTrade); - safe_delete(claim); + sms->unknown8=0; - Save(); - return true; + if(distance>0) + entity_list.QueueCloseClients(this,outapp,false,distance); + else + QueuePacket(outapp); + safe_delete(outapp); } -uint32 Client::GetLDoNPointsTheme(uint32 t) -{ - switch(t) - { - case LDoNTheme::GUK: - return m_pp.ldon_points_guk; - case LDoNTheme::MIR: - return m_pp.ldon_points_mir; - case LDoNTheme::MMC: - return m_pp.ldon_points_mmc; - case LDoNTheme::RUJ: - return m_pp.ldon_points_ruj; - case LDoNTheme::TAK: - return m_pp.ldon_points_tak; - default: - return 0; - } +// +// this list of 9 args isn't how I want to do it, but to use va_arg +// you have to know how many args you're expecting, and to do that we have +// to load the eqstr file and count them in the string. +// This hack sucks but it's gonna work for now. +// +void Client::MessageString(uint32 type, uint32 string_id, const char* message1, + const char* message2,const char* message3,const char* message4, + const char* message5,const char* message6,const char* message7, + const char* message8,const char* message9, uint32 distance) +{ + if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) + return; + if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... + return; + if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) + return; + if (GetFilter(FilterDamageShields) == FilterHide && type == Chat::DamageShield) + return; + if (GetFilter(FilterFocusEffects) == FilterHide && type == Chat::FocusEffect) + return; + + if (type == Chat::Emote) + type = 4; + + if (!message1) { + MessageString(type, string_id); // use the simple message instead + return; + } + + const char *message_arg[] = { + message1, message2, message3, message4, message5, + message6, message7, message8, message9 + }; + + SerializeBuffer buf(20); + buf.WriteInt32(0); // unknown + buf.WriteInt32(string_id); + buf.WriteInt32(type); + for (auto &m : message_arg) { + if (m == nullptr) + break; + buf.WriteString(m); + } + + buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime + + auto outapp = std::make_unique(OP_FormattedMessage, buf); + + if (distance > 0) + entity_list.QueueCloseClients(this, outapp.get(), false, distance); + else + QueuePacket(outapp.get()); } -uint32 Client::GetLDoNWinsTheme(uint32 t) +void Client::MessageString(const CZClientMessageString_Struct* msg) { - switch(t) - { - case LDoNTheme::GUK: - return m_pp.ldon_wins_guk; - case LDoNTheme::MIR: - return m_pp.ldon_wins_mir; - case LDoNTheme::MMC: - return m_pp.ldon_wins_mmc; - case LDoNTheme::RUJ: - return m_pp.ldon_wins_ruj; - case LDoNTheme::TAK: - return m_pp.ldon_wins_tak; - default: - return 0; - } + if (msg) + { + if (msg->args_size == 0) + { + MessageString(msg->chat_type, msg->string_id); + } + else + { + uint32_t outsize = sizeof(FormattedMessage_Struct) + msg->args_size; + auto outapp = std::make_unique(OP_FormattedMessage, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->string_id = msg->string_id; + outbuf->type = msg->chat_type; + memcpy(outbuf->message, msg->args, msg->args_size); + QueuePacket(outapp.get()); + } + } } -uint32 Client::GetLDoNLossesTheme(uint32 t) +// helper function, returns true if we should see the message +bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) { - switch(t) - { - case LDoNTheme::GUK: - return m_pp.ldon_losses_guk; - case LDoNTheme::MIR: - return m_pp.ldon_losses_mir; - case LDoNTheme::MMC: - return m_pp.ldon_losses_mmc; - case LDoNTheme::RUJ: - return m_pp.ldon_losses_ruj; - case LDoNTheme::TAK: - return m_pp.ldon_losses_tak; - default: - return 0; - } -} - -void Client::UpdateLDoNWinLoss(uint32 theme_id, bool win, bool remove) { - switch (theme_id) { - case LDoNTheme::GUK: - if (win) { - m_pp.ldon_wins_guk += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_guk += (remove ? -1 : 1); - } - break; - case LDoNTheme::MIR: - if (win) { - m_pp.ldon_wins_mir += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_mir += (remove ? -1 : 1); - } - break; - case LDoNTheme::MMC: - if (win) { - m_pp.ldon_wins_mmc += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_mmc += (remove ? -1 : 1); - } - break; - case LDoNTheme::RUJ: - if (win) { - m_pp.ldon_wins_ruj += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_ruj += (remove ? -1 : 1); - } - break; - case LDoNTheme::TAK: - if (win) { - m_pp.ldon_wins_tak += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_tak += (remove ? -1 : 1); - } - break; - default: - return; - } - database.UpdateAdventureStatsEntry(CharacterID(), theme_id, win, remove); + eqFilterMode mode = GetFilter(filter); + // easy ones first + if (mode == FilterShow) { + return true; + } else if (mode == FilterHide) { + return false; + } + + if (sender != this && mode == FilterShowSelfOnly) { + return false; + } else if (sender) { + if (mode == FilterShowGroupOnly) { + auto g = GetGroup(); + auto r = GetRaid(); + if (g) { + if (g->IsGroupMember(sender)) { + return true; + } + } else if (r && sender->IsClient()) { + auto rgid1 = r->GetGroup(this); + auto rgid2 = r->GetGroup(sender->CastToClient()); + if (rgid1 != RAID_GROUPLESS && rgid1 == rgid2) { + return true; + } + } else { + return false; + } + } + } + + // we passed our checks + return true; } - -void Client::SuspendMinion(int value) +void Client::FilteredMessageString(Mob *sender, uint32 type, + eqFilterType filter, uint32 string_id) { - /* - SPA 151 Allows an extra pet to be saved and resummoned later. - Casting with a pet but without a suspended pet will suspend the pet - Casting without a pet and with a suspended pet will unsuspend the pet - effect value 0 = save pet with no buffs or equipment - effect value 1 = save pet with buffs and equipment - effect value 2 = unknown - Note: SPA 308 allows for suspended pets to be resummoned after zoning. - */ - - NPC *CurrentPet = GetPet()->CastToNPC(); - - if(!CurrentPet) - { - if(m_suspendedminion.SpellID > 0) - { - if (m_suspendedminion.SpellID >= SPDAT_RECORDS) { - Message(Chat::Red, "Invalid suspended minion spell id (%u).", m_suspendedminion.SpellID); - memset(&m_suspendedminion, 0, sizeof(PetInfo)); - return; - } - - MakePoweredPet(m_suspendedminion.SpellID, spells[m_suspendedminion.SpellID].teleport_zone, - m_suspendedminion.petpower, m_suspendedminion.Name, m_suspendedminion.size); - - CurrentPet = GetPet()->CastToNPC(); - - if(!CurrentPet) - { - Message(Chat::Red, "Failed to recall suspended minion."); - return; - } - - if(value >= 1) - { - CurrentPet->SetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items); - - CurrentPet->SendPetBuffsToClient(); - } - CurrentPet->CalcBonuses(); - - CurrentPet->SetHP(m_suspendedminion.HP); - - CurrentPet->SetMana(m_suspendedminion.Mana); - - CurrentPet->SetTaunting(m_suspendedminion.taunting); - - MessageString(Chat::Magenta, SUSPEND_MINION_UNSUSPEND, CurrentPet->GetCleanName()); - - memset(&m_suspendedminion, 0, sizeof(struct PetInfo)); - // TODO: These pet command states need to be synced ... - // Will just fix them for now - if (m_ClientVersionBit & EQ::versions::maskUFAndLater) { - SetPetCommandState(PET_BUTTON_SIT, 0); - SetPetCommandState(PET_BUTTON_STOP, 0); - SetPetCommandState(PET_BUTTON_REGROUP, 0); - SetPetCommandState(PET_BUTTON_FOLLOW, 1); - SetPetCommandState(PET_BUTTON_GUARD, 0); - // Taunt saved on client side for logging on with pet - // In our db for when we zone. - SetPetCommandState(PET_BUTTON_HOLD, 0); - SetPetCommandState(PET_BUTTON_GHOLD, 0); - SetPetCommandState(PET_BUTTON_FOCUS, 0); - SetPetCommandState(PET_BUTTON_SPELLHOLD, 0); - } - } - else - return; - - } - else - { - uint16 SpellID = CurrentPet->GetPetSpellID(); - - if(SpellID) - { - if(m_suspendedminion.SpellID > 0) - { - MessageString(Chat::Red,ONLY_ONE_PET); - - return; - } - else if(CurrentPet->IsEngaged()) - { - MessageString(Chat::Red,SUSPEND_MINION_FIGHTING); - - return; - } - else if(entity_list.Fighting(CurrentPet)) - { - MessageString(Chat::Blue,SUSPEND_MINION_HAS_AGGRO); - } - else - { - m_suspendedminion.SpellID = SpellID; - - m_suspendedminion.HP = CurrentPet->GetHP();; - - m_suspendedminion.Mana = CurrentPet->GetMana(); - m_suspendedminion.petpower = CurrentPet->GetPetPower(); - m_suspendedminion.size = CurrentPet->GetSize(); + if (!FilteredMessageCheck(sender, filter)) + return; - if(value >= 1) - CurrentPet->GetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items, m_suspendedminion.Name); - else - strn0cpy(m_suspendedminion.Name, CurrentPet->GetName(), 64); // Name stays even at rank 1 + auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); + SimpleMessage_Struct *sms = (SimpleMessage_Struct *)outapp->pBuffer; + sms->color = type; + sms->string_id = string_id; - MessageString(Chat::Magenta, SUSPEND_MINION_SUSPEND, CurrentPet->GetCleanName()); + sms->unknown8 = 0; - CurrentPet->Depop(false); + QueuePacket(outapp); + safe_delete(outapp); - SetPetID(0); - } - } - else - { - MessageString(Chat::Red, ONLY_SUMMONED_PETS); - - return; - } - } + return; } -void Client::AddPVPPoints(uint32 Points) +void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id, + const char *message1, const char *message2, const char *message3, + const char *message4, const char *message5, const char *message6, + const char *message7, const char *message8, const char *message9) { - m_pp.PVPCurrentPoints += Points; - m_pp.PVPCareerPoints += Points; - - Save(); - - SendPVPStats(); -} - -void Client::AddEbonCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentEbonCrystals += amount; - m_pp.careerEbonCrystals += amount; - - SaveCurrency(); - SendCrystalCounts(); - - MessageString( - Chat::Yellow, - YOU_RECEIVE, - fmt::format( - "{} {}", - amount, - database.CreateItemLink(RuleI(Zone, EbonCrystalItemID)) - ).c_str() - ); - - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { - const std::string &export_string = fmt::format( - "{} 0 {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); - } -} - -void Client::AddRadiantCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentRadCrystals += amount; - m_pp.careerRadCrystals += amount; - - SaveCurrency(); - SendCrystalCounts(); - - MessageString( - Chat::Yellow, - YOU_RECEIVE, - fmt::format( - "{} {}", - amount, - database.CreateItemLink(RuleI(Zone, RadiantCrystalItemID)) - ).c_str() - ); + if (!FilteredMessageCheck(sender, filter)) + return; - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { - const std::string &export_string = fmt::format( - "0 {} {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); - } -} - -void Client::RemoveEbonCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentEbonCrystals -= amount; - - SaveCurrency(); - SendCrystalCounts(); + if (type == Chat::Emote) + type = 4; - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { - const std::string &export_string = fmt::format( - "{} 0 {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); - } -} + if (!message1) { + FilteredMessageString(sender, type, filter, string_id); // use the simple message instead + return; + } -void Client::RemoveRadiantCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentRadCrystals -= amount; + const char *message_arg[] = { + message1, message2, message3, message4, message5, + message6, message7, message8, message9 + }; - SaveCurrency(); - SendCrystalCounts(); + SerializeBuffer buf(20); + buf.WriteInt32(0); // unknown + buf.WriteInt32(string_id); + buf.WriteInt32(type); + for (auto &m : message_arg) { + if (m == nullptr) + break; + buf.WriteString(m); + } - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { - const std::string &export_string = fmt::format( - "0 {} {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); - } -} + buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime -void Client::SetEbonCrystals(uint32 value) { - m_pp.currentEbonCrystals = value; - SaveCurrency(); - SendCrystalCounts(); -} + auto outapp = std::make_unique(OP_FormattedMessage, buf); -void Client::SetRadiantCrystals(uint32 value) { - m_pp.currentRadCrystals = value; - SaveCurrency(); - SendCrystalCounts(); + QueuePacket(outapp.get()); } -// Processes a client request to inspect a SoF+ client's equipment. -void Client::ProcessInspectRequest(Client *requestee, Client *requester) +void Client::Tell_StringID(uint32 string_id, const char *who, const char *message) { - if (requestee && requester) { - auto outapp = new EQApplicationPacket(OP_InspectAnswer, sizeof(InspectResponse_Struct)); - auto insr = (InspectResponse_Struct *) outapp->pBuffer; - - insr->TargetID = requester->GetID(); - insr->playerid = requestee->GetID(); - - const EQ::ItemData *item = nullptr; - const EQ::ItemInstance *inst = nullptr; - - for (int16 L = EQ::invslot::EQUIPMENT_BEGIN; L <= EQ::invslot::EQUIPMENT_END; L++) { - inst = requestee->GetInv().GetItem(L); - - if (inst) { - item = inst->GetItem(); - if (item) { - strcpy(insr->itemnames[L], item->Name); - - const EQ::ItemData *augment_item = nullptr; - const auto augment = inst->GetOrnamentationAugment(); - - if (augment) { - augment_item = augment->GetItem(); - } - - if (augment_item) { - insr->itemicons[L] = augment_item->Icon; - } else if (inst->GetOrnamentationIcon()) { - insr->itemicons[L] = inst->GetOrnamentationIcon(); - } else { - insr->itemicons[L] = item->Icon; - } - } else { - insr->itemnames[L][0] = '\0'; - insr->itemicons[L] = 0xFFFFFFFF; - } - } else { - insr->itemnames[L][0] = '\0'; - insr->itemicons[L] = 0xFFFFFFFF; - } - } - - strcpy(insr->text, requestee->GetInspectMessage().text); - - // There could be an OP for this..or not... (Ti clients are not processed here..this message is generated client-side) - if (requestee->IsClient() && requestee != requester) { - requestee->Message( - Chat::White, - fmt::format( - "{} is looking at your equipment...", - requester->GetName() - ).c_str() - ); - } + char string_id_str[10]; + snprintf(string_id_str, 10, "%d", string_id); - requester->QueuePacket(outapp); // Send answer to requester - safe_delete(outapp); - } + MessageString(Chat::EchoTell, TELL_QUEUED_MESSAGE, who, string_id_str, message); } -void Client::GuildBankAck() -{ - auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankAck_Struct)); - - GuildBankAck_Struct *gbas = (GuildBankAck_Struct*) outapp->pBuffer; - - gbas->Action = GuildBankAcknowledge; - - FastQueuePacket(&outapp); +void Client::SetTint(int16 in_slot, uint32 color) { + EQ::textures::Tint_Struct new_color; + new_color.Color = color; + SetTint(in_slot, new_color); + database.SaveCharacterMaterialColor(CharacterID(), in_slot, color); } -void Client::GuildBankDepositAck(bool Fail, int8 action) -{ - - auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankDepositAck_Struct)); - - GuildBankDepositAck_Struct *gbdas = (GuildBankDepositAck_Struct*) outapp->pBuffer; - - gbdas->Action = action; +// Still need to reconcile bracer01 versus bracer02 +void Client::SetTint(int16 in_slot, EQ::textures::Tint_Struct& color) { - gbdas->Fail = Fail ? 1 : 0; + uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); + if (matslot != EQ::textures::materialInvalid) + { + m_pp.item_tint.Slot[matslot].Color = color.Color; + database.SaveCharacterMaterialColor(CharacterID(), in_slot, color.Color); + } - FastQueuePacket(&outapp); } -void Client::ClearGuildBank() +void Client::SetHideMe(bool flag) { - auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankClear_Struct)); + EQApplicationPacket app; - GuildBankClear_Struct *gbcs = (GuildBankClear_Struct*) outapp->pBuffer; + gm_hide_me = flag; - gbcs->Action = GuildBankBulkItems; - gbcs->DepositAreaCount = 0; - gbcs->MainAreaCount = 0; + if (gm_hide_me) { + database.SetHideMe(AccountID(), true); + CreateDespawnPacket(&app, false); + entity_list.RemoveFromTargets(this); + trackable = false; + if (RuleB(Command, HideMeCommandDisablesTells)) { + tellsoff = true; + } + } else { + database.SetHideMe(AccountID(), false); + CreateSpawnPacket(&app); + trackable = true; + tellsoff = false; + } - FastQueuePacket(&outapp); + entity_list.QueueClientsStatus(this, &app, true, 0, Admin() - 1); + UpdateWho(); } -void Client::SendGroupCreatePacket() +void Client::SetLanguageSkill(uint8 language_id, uint8 language_skill) { - // For SoD and later clients, this is sent the Group Leader upon initial creation of the group - // - auto outapp = new EQApplicationPacket(OP_GroupUpdateB, 32 + strlen(GetName())); + if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { + return; + } - char *Buffer = (char *)outapp->pBuffer; - // Header - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // group ID probably - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 1); // count of members in packet - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // Null Leader name, shouldn't be null besides this case + if (language_skill > Language::MaxValue) { + language_skill = Language::MaxValue; + } - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // Member 0, index - VARSTRUCT_ENCODE_STRING(Buffer, GetName()); // group member name - VARSTRUCT_ENCODE_TYPE(uint16, Buffer, 0); // merc flag - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // owner name (if merc) - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, GetLevel()); // level - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group tank flag - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group assist flag - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group puller flag - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // offline flag - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // timestamp + m_pp.languages[language_id] = language_skill; - FastQueuePacket(&outapp); -} - -void Client::SendGroupLeaderChangePacket(const char *LeaderName) -{ - // For SoD and later, send name of Group Leader to this client + database.SaveCharacterLanguage(CharacterID(), language_id, language_skill); - auto outapp = new EQApplicationPacket(OP_GroupLeaderChange, sizeof(GroupLeaderChange_Struct)); + auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); + auto* s = (SkillUpdate_Struct*) outapp->pBuffer; - GroupLeaderChange_Struct *glcs = (GroupLeaderChange_Struct*)outapp->pBuffer; + s->skillId = 100 + language_id; + s->value = m_pp.languages[language_id]; - strn0cpy(glcs->LeaderName, LeaderName, sizeof(glcs->LeaderName)); + QueuePacket(outapp); + safe_delete(outapp); - FastQueuePacket(&outapp); + MessageString(Chat::Skills, LANG_SKILL_IMPROVED); } -void Client::SendGroupJoinAcknowledge() +void Client::LinkDead() { - // For SoD and later, This produces the 'You have joined the group' message. - auto outapp = new EQApplicationPacket(OP_GroupAcknowledge, 4); - FastQueuePacket(&outapp); -} + if (GetGroup()) + { + entity_list.MessageGroup(this,true,15,"%s has gone linkdead.",GetName()); + GetGroup()->DelMember(this); + if (GetMerc()) + { + GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); + } + } + Raid *raid = entity_list.GetRaidByClient(this); + if(raid){ + raid->MemberZoned(this); + } + + SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::LinkDead); -void Client::SendAdventureError(const char *error) -{ - size_t error_size = strlen(error); - auto outapp = new EQApplicationPacket(OP_AdventureInfo, (error_size + 2)); - strn0cpy((char*)outapp->pBuffer, error, error_size); - FastQueuePacket(&outapp); +// save_timer.Start(2500); + linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); + SendAppearancePacket(AppearanceType::Linkdead, 1); + client_state = CLIENT_LINKDEAD; + AI_Start(CLIENT_LD_TIMEOUT); } -void Client::SendAdventureDetails() -{ - if(adv_data) - { - ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; - auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); - AdventureRequestResponse_Struct *arr = (AdventureRequestResponse_Struct*)outapp->pBuffer; - arr->unknown000 = 0xBFC40100; - arr->unknown2080 = 0x0A; - arr->risk = ad->risk; - strcpy(arr->text, ad->text); - - if(ad->time_to_enter != 0) - { - arr->timetoenter = ad->time_to_enter; - } - else - { - arr->timeleft = ad->time_left; - } - - if(ad->zone_in_id == zone->GetZoneID()) - { - arr->y = ad->x; - arr->x = ad->y; - arr->showcompass = 1; - } - FastQueuePacket(&outapp); - - SendAdventureCount(ad->count, ad->total); - } - else - { - ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; - auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); - FastQueuePacket(&outapp); - } +uint8 Client::SlotConvert(uint8 slot,bool bracer){ + uint8 slot2 = 0; // why are we returning MainCharm instead of INVALID_INDEX? (must be a pre-charm segment...) + if(bracer) + return EQ::invslot::slotWrist2; + switch(slot) { + case EQ::textures::armorHead: + slot2 = EQ::invslot::slotHead; + break; + case EQ::textures::armorChest: + slot2 = EQ::invslot::slotChest; + break; + case EQ::textures::armorArms: + slot2 = EQ::invslot::slotArms; + break; + case EQ::textures::armorWrist: + slot2 = EQ::invslot::slotWrist1; + break; + case EQ::textures::armorHands: + slot2 = EQ::invslot::slotHands; + break; + case EQ::textures::armorLegs: + slot2 = EQ::invslot::slotLegs; + break; + case EQ::textures::armorFeet: + slot2 = EQ::invslot::slotFeet; + break; + } + return slot2; } -void Client::SendAdventureCount(uint32 count, uint32 total) -{ - auto outapp = new EQApplicationPacket(OP_AdventureUpdate, sizeof(AdventureCountUpdate_Struct)); - AdventureCountUpdate_Struct *acu = (AdventureCountUpdate_Struct*)outapp->pBuffer; - acu->current = count; - acu->total = total; - FastQueuePacket(&outapp); +uint8 Client::SlotConvert2(uint8 slot){ + uint8 slot2 = 0; // same as above... + switch(slot){ + case EQ::invslot::slotHead: + slot2 = EQ::textures::armorHead; + break; + case EQ::invslot::slotChest: + slot2 = EQ::textures::armorChest; + break; + case EQ::invslot::slotArms: + slot2 = EQ::textures::armorArms; + break; + case EQ::invslot::slotWrist1: + slot2 = EQ::textures::armorWrist; + break; + case EQ::invslot::slotHands: + slot2 = EQ::textures::armorHands; + break; + case EQ::invslot::slotLegs: + slot2 = EQ::textures::armorLegs; + break; + case EQ::invslot::slotFeet: + slot2 = EQ::textures::armorFeet; + break; + } + return slot2; } -void Client::NewAdventure(int id, int theme, const char *text, int member_count, const char *members) +void Client::Escape() { - size_t text_size = strlen(text); - auto outapp = new EQApplicationPacket(OP_AdventureDetails, text_size + 2); - strn0cpy((char*)outapp->pBuffer, text, text_size); - FastQueuePacket(&outapp); - - adv_requested_id = id; - adv_requested_theme = theme; - safe_delete_array(adv_requested_data); - adv_requested_member_count = member_count; - adv_requested_data = new char[64 * member_count]; - memcpy(adv_requested_data, members, (64 * member_count)); + entity_list.RemoveFromTargets(this, true); + SetInvisible(Invisibility::Invisible); + MessageString(Chat::Skills, ESCAPE); } -void Client::ClearPendingAdventureData() -{ - adv_requested_id = 0; - adv_requested_theme = LDoNTheme::Unused; - safe_delete_array(adv_requested_data); - adv_requested_member_count = 0; +float Client::CalcClassicPriceMod(Mob* other, bool reverse) { + float price_multiplier = 0.8f; + + if (other && other->IsNPC()) { + FACTION_VALUE faction_level = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); + int32 cha = GetCHA(); + + if (faction_level <= FACTION_AMIABLY) { + cha += 11; // amiable faction grants a defacto 11 charisma bonus + } + + uint8 greed = other->CastToNPC()->GetGreedPercent(); + + // Sony's precise algorithm is unknown, but this produces output that is virtually identical + if (faction_level <= FACTION_INDIFFERENTLY) { + if (cha > 75) { + if (greed) { + // this is derived from curve fitting to a lot of price data + price_multiplier = -0.2487768 + (1.599635 - -0.2487768) / (1 + pow((cha / 135.1495), 1.001983)); + price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' + price_multiplier = 1.0f / price_multiplier; + } + else { + // non-greedy merchants use a linear scale + price_multiplier = 1.0f - ((115.0f - cha) * 0.004f); + } + } + else if (cha > 60) { + price_multiplier = 1.0f / (1.25f + (greed / 100.0f)); + } + else { + price_multiplier = 1.0f / ((1.0f - (cha - 120.0f) / 220.0f) + (greed / 100.0f)); + } + } + else { // apprehensive + if (cha > 75) { + if (greed) { + // this is derived from curve fitting to a lot of price data + price_multiplier = -0.25f + (1.823662 - -0.25f) / (1 + (cha / 135.0f)); + price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' + price_multiplier = 1.0f / price_multiplier; + } + else { + price_multiplier = (100.0f - (145.0f - cha) / 2.8f) / 100.0f; + } + } + else if (cha > 60) { + price_multiplier = 1.0f / (1.4f + greed / 100.0f); + } + else { + price_multiplier = 1.0f / ((1.0f + (143.574 - cha) / 196.434) + (greed / 100.0f)); + } + } + + float maxResult = 1.0f / 1.05; // price reduction caps at this amount + if (price_multiplier > maxResult) { + price_multiplier = maxResult; + } + + if (!reverse) { + price_multiplier = 1.0f / price_multiplier; + } + } + + LogMerchants( + "[{}] [{}] items at [{}] price multiplier [{}] [{}]", + other->GetName(), + reverse ? "buys" : "sells", + price_multiplier, + reverse ? "from" : "to", + GetName() + ); + + return price_multiplier; } -bool Client::IsOnAdventure() +float Client::CalcNewPriceMod(Mob* other, bool reverse) { - if(adv_data) - { - ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; - if(ad->zone_in_id == 0) - { - return false; - } - else - { - return true; - } - } - return false; + float chaformula = 0; + if (other) + { + int factionlvl = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetFactionRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); + if (factionlvl >= FACTION_APPREHENSIVELY) // Apprehensive or worse. + { + if (GetCHA() > 103) + { + chaformula = (GetCHA() - 103)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. + if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) + chaformula = -1*(RuleI(Merchant, PriceBonusPct)); + } + else if (GetCHA() < 103) + { + chaformula = (103 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // This will bottom out price penalty. + if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) + chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); + } + } + if (factionlvl <= FACTION_INDIFFERENTLY) // Indifferent or better. + { + if (GetCHA() > 75) + { + chaformula = (GetCHA() - 75)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. + if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) + chaformula = -1*(RuleI(Merchant, PriceBonusPct)); + } + else if (GetCHA() < 75) + { + chaformula = (75 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // Faction modifier keeps up from reaching bottom price penalty. + if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) + chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); + } + } + } + + if (reverse) + chaformula *= -1; //For selling + //Now we have, for example, 10 + chaformula /= 100; //Convert to 0.10 + chaformula += 1; //Convert to 1.10; + return chaformula; //Returns 1.10, expensive stuff! } -void Client::LeaveAdventure() +float Client::CalcPriceMod(Mob* other, bool reverse) { - if(!GetPendingAdventureLeave()) - { - PendingAdventureLeave(); - auto pack = new ServerPacket(ServerOP_AdventureLeave, 64); - strcpy((char*)pack->pBuffer, GetName()); - worldserver.SendPacket(pack); - delete pack; - } -} + float price_mod = CalcNewPriceMod(other, reverse); -void Client::ClearCurrentAdventure() -{ - if(adv_data) - { - ServerSendAdventureData_Struct* ds = (ServerSendAdventureData_Struct*)adv_data; - if(ds->finished_adventures > 0) - { - ds->instance_id = 0; - ds->risk = 0; - memset(ds->text, 0, 512); - ds->time_left = 0; - ds->time_to_enter = 0; - ds->x = 0; - ds->y = 0; - ds->zone_in_id = 0; - ds->zone_in_object = 0; - } - else - { - safe_delete(adv_data); - } + if (RuleB(Merchant, UseClassicPriceMod)) { + price_mod = CalcClassicPriceMod(other, reverse); + } - SendAdventureError("You are not currently assigned to an adventure."); - } + return price_mod; } -void Client::AdventureFinish(bool win, int theme, int points) -{ - UpdateLDoNPoints(theme, points); - auto outapp = new EQApplicationPacket(OP_AdventureFinish, sizeof(AdventureFinish_Struct)); - AdventureFinish_Struct *af = (AdventureFinish_Struct*)outapp->pBuffer; - af->win_lose = win ? 1 : 0; - af->points = points; - FastQueuePacket(&outapp); +void Client::GetGroupAAs(GroupLeadershipAA_Struct *into) const { + memcpy(into, &m_pp.leader_abilities.group, sizeof(GroupLeadershipAA_Struct)); } -void Client::CheckLDoNHail(NPC* n) -{ - if (!zone->adv_data || !n || n->GetOwnerID()) { - return; - } +void Client::GetRaidAAs(RaidLeadershipAA_Struct *into) const { + memcpy(into, &m_pp.leader_abilities.raid, sizeof(RaidLeadershipAA_Struct)); +} - auto* ds = (ServerZoneAdventureDataReply_Struct*) zone->adv_data; - if (ds->type != Adventure_Rescue || ds->data_id != n->GetNPCTypeID()) { - return; - } +void Client::EnteringMessages(Client* client) +{ + std::string rules = RuleS(World, Rules); - if (entity_list.CheckNPCsClose(n)) { - n->Say( - "You're here to save me? I couldn't possibly risk leaving yet. There are " - "far too many of those horrid things out there waiting to recapture me! Please get " - "rid of some more of those vermin and then we can try to leave." - ); - return; - } + if (!rules.empty() || database.GetVariable("Rules", rules)) { + const uint8 flag = database.GetAgreementFlag(client->AccountID()); + if (!flag) { + const std::string& rules_link = Saylink::Silent("#serverrules", "rules"); - auto pet = GetPet(); - if (pet) { - if (pet->GetPetType() == petCharmed) { - pet->BuffFadeByEffect(SE_Charm); - } else if (pet->GetPetType() == petNPCFollow) { - pet->SetOwnerID(0); - } else { - pet->Depop(); - } - } + client->Message( + Chat::White, + fmt::format( + "You must agree to the {} before you can move.", + rules_link + ).c_str() + ); - SetPet(n); - n->SetOwnerID(GetID()); - n->Say( - "Wonderful! Someone to set me free! I feared for my life for so long, " - "never knowing when they might choose to end my life. Now that you're here though " - "I can rest easy. Please help me find my way out of here as soon as you can " - "I'll stay close behind you!" - ); + client->SendAppearancePacket(AppearanceType::Animation, Animation::Freeze); + } + } } -void Client::CheckEmoteHail(NPC* n, const char* message) +void Client::SendRules() { - if (!Strings::BeginsWith(Strings::ToLower(message), "hail")) { - return; - } + std::string rules = RuleS(World, Rules); - if (!n || n->GetOwnerID()) { - return; - } + if (rules.empty() && !database.GetVariable("Rules", rules)) { + return; + } - const uint32 emote_id = n->GetEmoteID(); - if (emote_id) { - n->DoNPCEmote(EQ::constants::EmoteEventTypes::Hailed, emote_id, this); - } + auto lines = Strings::Split(rules, "|"); + auto line_number = 1; + for (auto&& line : lines) { + Message( + Chat::White, + fmt::format( + "{}. {}", + line_number, + line + ).c_str() + ); + line_number++; + } } -void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count) +void Client::SetEndurance(int32 newEnd) { - m_has_quest_compass = (count != 0); - m_quest_compass.x = in_x; - m_quest_compass.y = in_y; - m_quest_compass.z = in_z; + /*Endurance can't be less than 0 or greater than max*/ + if(newEnd < 0) + newEnd = 0; + else if(newEnd > GetMaxEndurance()){ + newEnd = GetMaxEndurance(); + } - SendDzCompassUpdate(); + current_endurance = newEnd; + CheckManaEndUpdate(); } -void Client::SendZonePoints() +void Client::SacrificeConfirm(Mob *caster) { - int count = 0; - LinkedListIterator iterator(zone->zone_point_list); - iterator.Reset(); - while (iterator.MoreElements()) { - ZonePoint *data = iterator.GetData(); - - if (ClientVersionBit() & data->client_version_mask) { - count++; - } - - iterator.Advance(); - } - - uint32 zpsize = sizeof(ZonePoints) + ((count + 1) * sizeof(ZonePoint_Entry)); - auto outapp = new EQApplicationPacket(OP_SendZonepoints, zpsize); - ZonePoints* zp = (ZonePoints*)outapp->pBuffer; - zp->count = count; - - int i = 0; - iterator.Reset(); - while(iterator.MoreElements()) - { - ZonePoint* data = iterator.GetData(); - - LogZonePoints( - "Sending zone point to client [{}] mask [{}] x [{}] y [{}] z [{}] number [{}]", - GetCleanName(), - ClientVersionBit() & data->client_version_mask ? "true" : "false", - data->x, - data->y, - data->z, - data->number - ); - - if(ClientVersionBit() & data->client_version_mask) - { - zp->zpe[i].iterator = data->number; - zp->zpe[i].x = data->target_x; - zp->zpe[i].y = data->target_y; - zp->zpe[i].z = data->target_z; - zp->zpe[i].heading = data->target_heading; - zp->zpe[i].zoneid = data->target_zone_id; - - // if the target zone is the same as the current zone, use the instance of the current zone - // if we don't use the same instance_id that the client was sent, the client will forcefully - // issue a zone change request when they should be simply moving to a different point in the same zone - // because the client will think the zone point target is different from the current instance - auto target_instance = data->target_zone_instance; - if (data->target_zone_id == zone->GetZoneID() && data->target_zone_instance == 0) { - target_instance = zone->GetInstanceID(); - } - - zp->zpe[i].zoneinstance = target_instance; - i++; - } - iterator.Advance(); - } - - FastQueuePacket(&outapp); + auto outapp = new EQApplicationPacket(OP_Sacrifice, sizeof(Sacrifice_Struct)); + Sacrifice_Struct *ss = (Sacrifice_Struct *)outapp->pBuffer; + + if (!caster || PendingSacrifice) { + safe_delete(outapp); + return; + } + + if (GetLevel() < RuleI(Spells, SacrificeMinLevel)) { + caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. + safe_delete(outapp); + return; + } + + if (GetLevel() > RuleI(Spells, SacrificeMaxLevel)) { + caster->MessageString(Chat::Red, SAC_TOO_HIGH); + safe_delete(outapp); + return; + } + + ss->CasterID = caster->GetID(); + ss->TargetID = GetID(); + ss->Confirm = 0; + QueuePacket(outapp); + safe_delete(outapp); + // We store the Caster's id, because when the packet comes back, it only has the victim's entityID in it, + // not the caster. + sacrifice_caster_id = caster->GetID(); + PendingSacrifice = true; } -void Client::SendTargetCommand(uint32 EntityID) +//Essentially a special case death function +void Client::Sacrifice(Mob *caster) { - auto outapp = new EQApplicationPacket(OP_TargetCommand, sizeof(ClientTarget_Struct)); - ClientTarget_Struct *cts = (ClientTarget_Struct*)outapp->pBuffer; - cts->new_target = EntityID; - FastQueuePacket(&outapp); + if (GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)) { + int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000); + if (exploss < GetEXP()) { + SetEXP(ExpSource::Sacrifice, GetEXP() - exploss, GetAAXP(), false); + SendLogoutPackets(); + + // make our become corpse packet, and queue to ourself before OP_Death. + EQApplicationPacket app2(OP_BecomeCorpse, sizeof(BecomeCorpse_Struct)); + BecomeCorpse_Struct *bc = (BecomeCorpse_Struct *)app2.pBuffer; + bc->spawn_id = GetID(); + bc->x = GetX(); + bc->y = GetY(); + bc->z = GetZ(); + QueuePacket(&app2); + + // make death packet + EQApplicationPacket app(OP_Death, sizeof(Death_Struct)); + Death_Struct *d = (Death_Struct *)app.pBuffer; + d->spawn_id = GetID(); + d->killer_id = caster ? caster->GetID() : 0; + d->bindzoneid = GetPP().binds[0].zone_id; + d->spell_id = SPELL_UNKNOWN; + d->attack_skill = 0xe7; + d->damage = 0; + app.priority = 6; + entity_list.QueueClients(this, &app); + + BuffFadeAll(); + UnmemSpellAll(); + Group *g = GetGroup(); + if (g) { + g->MemberZoned(this); + } + Raid *r = entity_list.GetRaidByClient(this); + if (r) { + r->MemberZoned(this); + } + ClearAllProximities(); + if (RuleB(Character, LeaveCorpses)) { + auto new_corpse = new Corpse(this, 0); + entity_list.AddCorpse(new_corpse, GetID()); + SetID(0); + entity_list.QueueClients(this, &app2, true); + } + Save(); + GoToDeath(); + if (caster && caster->IsClient()) { + caster->CastToClient()->SummonItem(RuleI(Spells, SacrificeItemID)); + } else if (caster && caster->IsNPC()) { + caster->CastToNPC()->AddItem(RuleI(Spells, SacrificeItemID), 1, false); + } + } + } else { + caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. + } } -void Client::LocateCorpse() -{ - Corpse *ClosestCorpse = nullptr; - if(!GetTarget()) - ClosestCorpse = entity_list.GetClosestCorpse(this, nullptr); - else if(GetTarget()->IsCorpse()) - ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->CastToCorpse()->GetOwnerName()); - else - ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->GetCleanName()); - - if(ClosestCorpse) - { - MessageString(Chat::Spells, SENSE_CORPSE_DIRECTION); - SetHeading(CalculateHeadingToTarget(ClosestCorpse->GetX(), ClosestCorpse->GetY())); - SetTarget(ClosestCorpse); - SendTargetCommand(ClosestCorpse->GetID()); - SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0, true); - } - else if(!GetTarget()) - MessageString(Chat::Red, SENSE_CORPSE_NONE); - else - MessageString(Chat::Red, SENSE_CORPSE_NOT_NAME); -} +void Client::SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID) { -void Client::NPCSpawn(NPC *target_npc, const char *identifier, uint32 extra) -{ - if (!target_npc || !identifier) { - return; - } + if(!Caster || PendingTranslocate) + return; - std::string spawn_type = Strings::ToLower(identifier); - bool is_add = spawn_type.find("add") != std::string::npos; - bool is_create = spawn_type.find("create") != std::string::npos; - bool is_delete = spawn_type.find("delete") != std::string::npos; - bool is_remove = spawn_type.find("remove") != std::string::npos; - bool is_update = spawn_type.find("update") != std::string::npos; - bool is_clone = spawn_type.find("clone") != std::string::npos; - if (is_add || is_create) { - // extra sets the Respawn Timer for add/create - content_db.NPCSpawnDB( - is_add ? NPCSpawnTypes::AddNewSpawngroup : NPCSpawnTypes::CreateNewSpawn, - zone->GetShortName(), - zone->GetInstanceVersion(), - this, - target_npc->CastToNPC(), - extra - ); - } else if (is_delete || is_remove || is_update) { - uint8 spawn_update_type = ( - is_delete ? - NPCSpawnTypes::DeleteSpawn : - ( - is_remove ? - NPCSpawnTypes::RemoveSpawn : - NPCSpawnTypes::UpdateAppearance - ) - ); - content_db.NPCSpawnDB( - spawn_update_type, - zone->GetShortName(), - zone->GetInstanceVersion(), - this, - target_npc->CastToNPC(), - extra - ); - } else if (is_clone) { - content_db.NPCSpawnDB( - NPCSpawnTypes::AddSpawnFromSpawngroup, - zone->GetShortName(), - zone->GetInstanceVersion(), - this, - target_npc->CastToNPC(), - extra - ); - } -} + const SPDat_Spell_Struct &Spell = spells[SpellID]; -bool Client::IsDraggingCorpse(uint16 CorpseID) -{ - for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { - if (It->second == CorpseID) - return true; - } + auto outapp = new EQApplicationPacket(OP_Translocate, sizeof(Translocate_Struct)); + Translocate_Struct *ts = (Translocate_Struct*)outapp->pBuffer; - return false; -} + strcpy(ts->Caster, Caster->GetName()); + PendingTranslocateData.spell_id = ts->SpellID = SpellID; -void Client::DragCorpses() -{ - for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { - Mob *corpse = entity_list.GetMob(It->second); + if((SpellID == 1422) || (SpellID == 1334) || (SpellID == 3243)) { + PendingTranslocateData.zone_id = ts->ZoneID = m_pp.binds[0].zone_id; + PendingTranslocateData.instance_id = m_pp.binds[0].instance_id; + PendingTranslocateData.x = ts->x = m_pp.binds[0].x; + PendingTranslocateData.y = ts->y = m_pp.binds[0].y; + PendingTranslocateData.z = ts->z = m_pp.binds[0].z; + PendingTranslocateData.heading = m_pp.binds[0].heading; + } + else { + PendingTranslocateData.zone_id = ts->ZoneID = ZoneID(Spell.teleport_zone); + PendingTranslocateData.instance_id = 0; + PendingTranslocateData.y = ts->y = Spell.base_value[0]; + PendingTranslocateData.x = ts->x = Spell.base_value[1]; + PendingTranslocateData.z = ts->z = Spell.base_value[2]; + PendingTranslocateData.heading = 0.0; + } - if (corpse && corpse->IsPlayerCorpse() && - (DistanceSquaredNoZ(m_Position, corpse->GetPosition()) <= RuleR(Character, DragCorpseDistance))) - continue; + ts->unknown008 = 0; + ts->Complete = 0; - if (!corpse || !corpse->IsPlayerCorpse() || - corpse->CastToCorpse()->IsBeingLooted() || - !corpse->CastToCorpse()->Summon(this, false, false)) { - MessageString(Chat::DefaultText, CORPSEDRAG_STOP); - It = DraggedCorpses.erase(It); - if (It == DraggedCorpses.end()) - break; - } - } + PendingTranslocate = true; + TranslocateTime = time(nullptr); + + QueuePacket(outapp); + safe_delete(outapp); + + return; +} +void Client::SendPickPocketResponse(Mob *from, uint32 amt, int type, const EQ::ItemData* item){ + auto outapp = new EQApplicationPacket(OP_PickPocket, sizeof(sPickPocket_Struct)); + sPickPocket_Struct *pick_out = (sPickPocket_Struct *)outapp->pBuffer; + pick_out->coin = amt; + pick_out->from = GetID(); + pick_out->to = from->GetID(); + pick_out->myskill = GetSkill(EQ::skills::SkillPickPockets); + + if ((type >= PickPocketPlatinum) && (type <= PickPocketCopper) && (amt == 0)) + type = PickPocketFailed; + + pick_out->type = type; + if (item) + strcpy(pick_out->itemname, item->Name); + else + pick_out->itemname[0] = '\0'; + // if we do not send this packet the client will lock up and require the player to relog. + QueuePacket(outapp); + safe_delete(outapp); } -void Client::ConsentCorpses(std::string consent_name, bool deny) -{ - if (strcasecmp(consent_name.c_str(), GetName()) == 0) { - MessageString(Chat::Red, CONSENT_YOURSELF); - } - else if (!consent_throttle_timer.Check()) { - MessageString(Chat::Red, CONSENT_WAIT); - } - else { - auto pack = new ServerPacket(ServerOP_Consent, sizeof(ServerOP_Consent_Struct)); - ServerOP_Consent_Struct* scs = (ServerOP_Consent_Struct*)pack->pBuffer; - strn0cpy(scs->grantname, consent_name.c_str(), sizeof(scs->grantname)); - strn0cpy(scs->ownername, GetName(), sizeof(scs->ownername)); - strn0cpy(scs->zonename, "Unknown", sizeof(scs->zonename)); - scs->permission = deny ? 0 : 1; - scs->zone_id = zone->GetZoneID(); - scs->instance_id = zone->GetInstanceID(); - scs->consent_type = EQ::consent::Normal; - scs->consent_id = 0; - if (strcasecmp(scs->grantname, "group") == 0) { - if (!deny) { - Group* grp = GetGroup(); - scs->consent_id = grp ? grp->GetID() : 0; - } - scs->consent_type = EQ::consent::Group; - } - else if (strcasecmp(scs->grantname, "raid") == 0) { - if (!deny) { - Raid* raid = GetRaid(); - scs->consent_id = raid ? raid->GetID() : 0; - } - scs->consent_type = EQ::consent::Raid; - } - else if (strcasecmp(scs->grantname, "guild") == 0) { - if (!deny) { - scs->consent_id = GuildID(); - } - scs->consent_type = EQ::consent::Guild; - // update all corpses in db so buried/unloaded corpses see new consent id - database.UpdateCharacterCorpseConsent(CharacterID(), scs->consent_id); - } - worldserver.SendPacket(pack); - safe_delete(pack); - } +void Client::SetHoTT(uint32 mobid) { + auto outapp = new EQApplicationPacket(OP_TargetHoTT, sizeof(ClientTarget_Struct)); + ClientTarget_Struct *ct = (ClientTarget_Struct *) outapp->pBuffer; + ct->new_target = mobid; + QueuePacket(outapp); + safe_delete(outapp); } -void Client::Doppelganger(uint16 spell_id, Mob *target, const char *name_override, int pet_count, int pet_duration) +void Client::SendPopupToClient(const char *Title, const char *Text, uint32 PopupID, uint32 Buttons, uint32 Duration) { - if(!target || !IsValidSpell(spell_id) || GetID() == target->GetID()) - return; + auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); - PetRecord record; - if(!database.GetPetEntry(spells[spell_id].teleport_zone, &record)) - { - LogError("Unknown doppelganger spell id: [{}], check pets table", spell_id); - Message(Chat::Red, "Unable to find data for pet %s", spells[spell_id].teleport_zone); - return; - } + OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; - SwarmPet_Struct pet; - pet.count = pet_count; - pet.duration = pet_duration; - pet.npc_id = record.npc_type; + if ((strlen(Title) > (sizeof(olms->Title) - 1)) || (strlen(Text) > (sizeof(olms->Text) - 1))) { + safe_delete(outapp); + return; + } - NPCType *made_npc = nullptr; + strcpy(olms->Title, Title); + strcpy(olms->Text, Text); - const NPCType *npc_type = content_db.LoadNPCTypesData(pet.npc_id); - if(npc_type == nullptr) { - LogError("Unknown npc type for doppelganger spell id: [{}]", spell_id); - Message(0,"Unable to find pet!"); - return; - } - // make a custom NPC type for this - made_npc = new NPCType; - memcpy(made_npc, npc_type, sizeof(NPCType)); - - strcpy(made_npc->name, name_override); - made_npc->level = GetLevel(); - made_npc->race = GetRace(); - made_npc->gender = GetGender(); - made_npc->size = GetSize(); - made_npc->AC = GetAC(); - made_npc->STR = GetSTR(); - made_npc->STA = GetSTA(); - made_npc->DEX = GetDEX(); - made_npc->AGI = GetAGI(); - made_npc->MR = GetMR(); - made_npc->FR = GetFR(); - made_npc->CR = GetCR(); - made_npc->DR = GetDR(); - made_npc->PR = GetPR(); - made_npc->Corrup = GetCorrup(); - made_npc->PhR = GetPhR(); - // looks - made_npc->texture = GetEquipmentMaterial(EQ::textures::armorChest); - made_npc->helmtexture = GetEquipmentMaterial(EQ::textures::armorHead); - made_npc->haircolor = GetHairColor(); - made_npc->beardcolor = GetBeardColor(); - made_npc->eyecolor1 = GetEyeColor1(); - made_npc->eyecolor2 = GetEyeColor2(); - made_npc->hairstyle = GetHairStyle(); - made_npc->luclinface = GetLuclinFace(); - made_npc->beard = GetBeard(); - made_npc->drakkin_heritage = GetDrakkinHeritage(); - made_npc->drakkin_tattoo = GetDrakkinTattoo(); - made_npc->drakkin_details = GetDrakkinDetails(); - made_npc->d_melee_texture1 = GetEquipmentMaterial(EQ::textures::weaponPrimary); - made_npc->d_melee_texture2 = GetEquipmentMaterial(EQ::textures::weaponSecondary); - for (int i = EQ::textures::textureBegin; i <= EQ::textures::LastTexture; i++) { - made_npc->armor_tint.Slot[i].Color = GetEquipmentColor(i); - } - made_npc->loottable_id = 0; + olms->Buttons = Buttons; - int summon_count = pet.count; + if (Duration > 0) { + olms->Duration = Duration * 1000; + } + else { + olms->Duration = 0xffffffff; + } - if(summon_count > MAX_SWARM_PETS) - summon_count = MAX_SWARM_PETS; + olms->PopupID = PopupID; + olms->NegativeID = 0; - static const glm::vec2 swarmPetLocations[MAX_SWARM_PETS] = { - glm::vec2(5, 5), glm::vec2(-5, 5), glm::vec2(5, -5), glm::vec2(-5, -5), - glm::vec2(10, 10), glm::vec2(-10, 10), glm::vec2(10, -10), glm::vec2(-10, -10), - glm::vec2(8, 8), glm::vec2(-8, 8), glm::vec2(8, -8), glm::vec2(-8, -8) - }; + sprintf(olms->ButtonName0, "%s", "Yes"); + sprintf(olms->ButtonName1, "%s", "No"); + QueuePacket(outapp); + safe_delete(outapp); +} - while(summon_count > 0) { - auto npc_type_copy = new NPCType; - memcpy(npc_type_copy, made_npc, sizeof(NPCType)); +void Client::SendFullPopup( + const char *Title, + const char *Text, + uint32 PopupID, + uint32 NegativeID, + uint32 Buttons, + uint32 Duration, + const char *ButtonName0, + const char *ButtonName1, + uint32 SoundControls +) +{ + auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); - NPC* swarm_pet_npc = new NPC( - npc_type_copy, - 0, - GetPosition() + glm::vec4(swarmPetLocations[summon_count - 1], 0.0f, 0.0f), - GravityBehavior::Water); + OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; - if(!swarm_pet_npc->GetSwarmInfo()){ - auto nSI = new SwarmPet; - swarm_pet_npc->SetSwarmInfo(nSI); - swarm_pet_npc->GetSwarmInfo()->duration = new Timer(pet_duration*1000); - } - else{ - swarm_pet_npc->GetSwarmInfo()->duration->Start(pet_duration*1000); - } + if ((strlen(Text) > (sizeof(olms->Text) - 1)) || (strlen(Title) > (sizeof(olms->Title) - 1))) { + safe_delete(outapp); + return; + } - swarm_pet_npc->StartSwarmTimer(pet_duration * 1000); + if (ButtonName0 && ButtonName1 && ((strlen(ButtonName0) > (sizeof(olms->ButtonName0) - 1)) || + (strlen(ButtonName1) > (sizeof(olms->ButtonName1) - 1)))) { + safe_delete(outapp); + return; + } - swarm_pet_npc->GetSwarmInfo()->owner_id = GetID(); - swarm_pet_npc->SetFollowID(GetID()); + strcpy(olms->Title, Title); + strcpy(olms->Text, Text); - // Give the pets alittle more agro than the caster and then agro them on the target - target->AddToHateList(swarm_pet_npc, (target->GetHateAmount(this) + 100), (target->GetDamageAmount(this) + 100)); - swarm_pet_npc->AddToHateList(target, 1000, 1000); - swarm_pet_npc->GetSwarmInfo()->target = 0; + olms->Buttons = Buttons; - //we allocated a new NPC type object, give the NPC ownership of that memory - swarm_pet_npc->GiveNPCTypeData(npc_type_copy); + if (ButtonName0 == nullptr || ButtonName1 == nullptr) { + sprintf(olms->ButtonName0, "%s", "Yes"); + sprintf(olms->ButtonName1, "%s", "No"); + } + else { + strcpy(olms->ButtonName0, ButtonName0); + strcpy(olms->ButtonName1, ButtonName1); + } - entity_list.AddNPC(swarm_pet_npc); - summon_count--; - } + if (Duration > 0) { + olms->Duration = Duration * 1000; + } + else { + olms->Duration = 0xffffffff; + } - safe_delete(made_npc); -} + olms->PopupID = PopupID; + olms->NegativeID = NegativeID; + olms->SoundControls = SoundControls; -void Client::AssignToInstance(uint16 instance_id) -{ - database.AddClientToInstance(instance_id, CharacterID()); + QueuePacket(outapp); + safe_delete(outapp); } -void Client::RemoveFromInstance(uint16 instance_id) +void Client::SendWindow( + uint32 button_one_id, + uint32 button_two_id, + uint32 button_type, + const char* button_one_text, + const char* button_two_text, + uint32 duration, + int title_type, + Mob* target, + const char* title, + const char* text, + ... +) { - database.RemoveClientFromInstance(instance_id, CharacterID()); -} - -void Client::SendAltCurrencies() { - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - const uint32 currency_count = zone->AlternateCurrencies.size(); - if (!currency_count) { - return; - } + va_list argptr; + char buffer[4096]; - auto outapp = new EQApplicationPacket( - OP_AltCurrency, - sizeof(AltCurrencyPopulate_Struct) + - sizeof(AltCurrencyPopulateEntry_Struct) * currency_count - ); + va_start(argptr, text); + vsnprintf(buffer, sizeof(buffer), text, argptr); + va_end(argptr); - auto a = (AltCurrencyPopulate_Struct*) outapp->pBuffer; + size_t len = strlen(buffer); - a->opcode = AlternateCurrencyMode::Populate; - a->count = currency_count; + auto app = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); + auto* olms = (OnLevelMessage_Struct *) app->pBuffer; - uint32 currency_id = 0; - for (const auto& c : zone->AlternateCurrencies) { - const auto* item = database.GetItem(c.item_id); + if (strlen(text) > (sizeof(olms->Text) - 1)) { + safe_delete(app); + return; + } - a->entries[currency_id].currency_number = c.id; - a->entries[currency_id].unknown00 = 1; - a->entries[currency_id].currency_number2 = c.id; - a->entries[currency_id].item_id = c.item_id; - a->entries[currency_id].item_icon = item ? item->Icon : 1000; - a->entries[currency_id].stack_size = item ? item->StackSize : 1000; + if (!target) { + title_type = 0; + } - currency_id++; - } + switch (title_type) { + case 1: { + char name[64] = ""; + strcpy(name, target->GetName()); - FastQueuePacket(&outapp); - } -} + if (strlen(target->GetLastName()) > 0) { + char last_name[64] = ""; + strcpy(last_name, target->GetLastName()); + strcat(name, " "); + strcat(name, last_name); + } -void Client::SetAlternateCurrencyValue(uint32 currency_id, uint32 new_amount) -{ - if (!zone->DoesAlternateCurrencyExist(currency_id)) { - return; - } + strcpy(olms->Title, name); + break; + } + case 2: { + if (target->IsClient() && target->CastToClient()->GuildID()) { + auto guild_name = guild_mgr.GetGuildName(target->CastToClient()->GuildID()); + strn0cpy(olms->Title, guild_name, sizeof(olms->Title)); + } else { + strcpy(olms->Title, "No Guild"); + } + break; + } + default: { + strcpy(olms->Title, title); + break; + } + } - const uint32 current_amount = alternate_currency[currency_id]; + memcpy(olms->Text, buffer, len + 1); - const bool is_gain = new_amount > current_amount; + olms->Buttons = button_type; - const uint32 change_amount = is_gain ? (new_amount - current_amount) : (current_amount - new_amount); + strn0cpy(olms->ButtonName0, button_one_text, sizeof(olms->ButtonName0)); + strn0cpy(olms->ButtonName1, button_two_text, sizeof(olms->ButtonName1)); - if (!change_amount) { - return; - } + if (duration > 0) { + olms->Duration = duration * 1000; + } else { + olms->Duration = UINT32_MAX; + } - alternate_currency[currency_id] = new_amount; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); - SendAlternateCurrencyValue(currency_id); - - QuestEventID event_id = is_gain ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; - if (parse->PlayerHasQuestSub(event_id)) { - const std::string &export_string = fmt::format( - "{} {} {}", - currency_id, - change_amount, - new_amount - ); + olms->PopupID = button_one_id; + olms->NegativeID = button_two_id; - parse->EventPlayer(event_id, this, export_string, 0); - } + FastQueuePacket(&app); } -bool Client::RemoveAlternateCurrencyValue(uint32 currency_id, uint32 amount) +void Client::KeyRingLoad() { - if (!amount || !zone->DoesAlternateCurrencyExist(currency_id)) { - return false; - } - - const uint32 current_amount = alternate_currency[currency_id]; - if (current_amount < amount) { - return false; - } - - const uint32 new_amount = (current_amount - amount); + const auto &l = KeyringRepository::GetWhere( + database, + fmt::format( + "`char_id` = {} ORDER BY `item_id`", + character_id + ) + ); - alternate_currency[currency_id] = new_amount; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); - SendAlternateCurrencyValue(currency_id); - - if (parse->PlayerHasQuestSub(EVENT_ALT_CURRENCY_LOSS)) { - const std::string &export_string = fmt::format( - "{} {} {}", - currency_id, - amount, - new_amount - ); + if (l.empty()) { + return; + } - parse->EventPlayer(EVENT_ALT_CURRENCY_LOSS, this, export_string, 0); - } - return true; + for (const auto &e : l) { + keyring.emplace_back(e.item_id); + } } -int Client::AddAlternateCurrencyValue(uint32 currency_id, int amount, bool is_scripted) +void Client::KeyRingAdd(uint32 item_id) { - if (!zone->DoesAlternateCurrencyExist(currency_id)) { - return 0; - } + if (!item_id) { + return; + } - /* Added via Quest, rest of the logging methods may be done inline due to information available in that area of the code */ - if (is_scripted) { - /* QS: PlayerLogAlternateCurrencyTransactions :: Cursor to Item Storage */ - if (RuleB(QueryServ, PlayerLogAlternateCurrencyTransactions)){ - std::string event_desc = StringFormat("Added via Quest :: Cursor to Item :: alt_currency_id:%i amount:%i in zoneid:%i instid:%i", currency_id, GetZoneID(), GetInstanceID()); - QServ->PlayerLogEvent(Player_Log_Alternate_Currency_Transactions, CharacterID(), event_desc); - } - } - - if (!amount) { - return 0; - } + const bool found = KeyRingCheck(item_id); + if (found) { + return; + } - if (!alternate_currency_loaded) { - alternate_currency_queued_operations.push(std::make_pair(currency_id, amount)); - return 0; - } + auto e = KeyringRepository::NewEntity(); - int new_value = 0; - auto iter = alternate_currency.find(currency_id); - if (iter == alternate_currency.end()) { - new_value = amount; - } else { - new_value = (*iter).second + amount; - } + e.char_id = CharacterID(); + e.item_id = item_id; - if (new_value < 0) { - new_value = 0; - alternate_currency[currency_id] = 0; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, 0); - } else { - alternate_currency[currency_id] = new_value; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_value); - } + e = KeyringRepository::InsertOne(database, e); - SendAlternateCurrencyValue(currency_id); + if (!e.id) { + return; + } - QuestEventID event_id = amount > 0 ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; - if (parse->PlayerHasQuestSub(event_id)) { - const std::string &export_string = fmt::format( - "{} {} {}", - currency_id, - std::abs(amount), - new_value - ); + keyring.emplace_back(item_id); - parse->EventPlayer(event_id, this, export_string, 0); - } + if (!RuleB(World, UseItemLinksForKeyRing)) { + Message(Chat::LightBlue, "Added to keyring."); + return; + } - return new_value; -} + const std::string &item_link = database.CreateItemLink(item_id); -void Client::SendAlternateCurrencyValues() -{ - for (const auto& alternate_currency : zone->AlternateCurrencies) { - SendAlternateCurrencyValue(alternate_currency.id, false); - } + Message( + Chat::LightBlue, + fmt::format( + "Added {} to keyring.", + item_link + ).c_str() + ); } -void Client::SendAlternateCurrencyValue(uint32 currency_id, bool send_if_null) +bool Client::KeyRingCheck(uint32 item_id) { - const auto value = GetAlternateCurrencyValue(currency_id); - if (value > 0 || send_if_null) { - auto outapp = new EQApplicationPacket(OP_AltCurrency, sizeof(AltCurrencyUpdate_Struct)); - auto update = (AltCurrencyUpdate_Struct *) outapp->pBuffer; - update->opcode = 7; - update->currency_number = currency_id; - update->amount = value; - update->unknown072 = 1; - - strn0cpy(update->name, GetName(), sizeof(update->name)); + for (const auto &e : keyring) { + if (e == item_id) { + return true; + } + } - FastQueuePacket(&outapp); - } + return false; } -uint32 Client::GetAlternateCurrencyValue(uint32 currency_id) const +void Client::KeyRingList() { - if (!zone->DoesAlternateCurrencyExist(currency_id)) { - return 0; - } + Message(Chat::LightBlue, "Keys on Keyring:"); - auto iter = alternate_currency.find(currency_id); + const EQ::ItemData *item = nullptr; - return iter == alternate_currency.end() ? 0 : (*iter).second; + for (const auto &e : keyring) { + item = database.GetItem(e); + if (item) { + const std::string &item_string = RuleB(World, UseItemLinksForKeyRing) ? database.CreateItemLink(e) : item->Name; + + Message(Chat::LightBlue, item_string.c_str()); + } + } } -void Client::ProcessAlternateCurrencyQueue() { - while(!alternate_currency_queued_operations.empty()) { - std::pair op = alternate_currency_queued_operations.front(); +bool Client::IsPetNameChangeAllowed() { + DataBucketKey k = GetScopedBucketKeys(); + k.key = "PetNameChangesAllowed"; - AddAlternateCurrencyValue(op.first, op.second); + auto b = DataBucket::GetData(k); + if (!b.value.empty()) { + return true; + } - alternate_currency_queued_operations.pop(); - } + return false; } -void Client::OpenLFGuildWindow() -{ - auto outapp = new EQApplicationPacket(OP_LFGuild, 8); +void Client::InvokeChangePetName(bool immediate) { + if (!IsPetNameChangeAllowed()) { + return; + } - outapp->WriteUInt32(6); + auto packet_op = immediate ? OP_InvokeChangePetNameImmediate : OP_InvokeChangePetName; - FastQueuePacket(&outapp); + auto outapp = new EQApplicationPacket(packet_op, 0); + QueuePacket(outapp); + safe_delete(outapp); } -bool Client::IsXTarget(const Mob *m) const -{ - if(!XTargettingAvailable() || !m || (m->GetID() == 0)) - return false; +void Client::GrantPetNameChange() { + DataBucketKey k = GetScopedBucketKeys(); + k.key = "PetNameChangesAllowed"; + k.value = "true"; + DataBucket::SetData(k); - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(XTargets[i].ID == m->GetID()) - return true; - } - return false; + InvokeChangePetName(true); } -bool Client::IsClientXTarget(const Client *c) const -{ - if(!XTargettingAvailable() || !c) - return false; +void Client::ClearPetNameChange() { + DataBucketKey k = GetScopedBucketKeys(); + k.key = "PetNameChangesAllowed"; - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(!strcasecmp(XTargets[i].Name, c->GetName())) - return true; - } - return false; + DataBucket::DeleteData(k); } +bool Client::ChangePetName(std::string new_name) { + if (new_name.empty()) { + return false; + } -void Client::UpdateClientXTarget(Client *c) -{ - if(!XTargettingAvailable() || !c) - return; - - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(!strcasecmp(XTargets[i].Name, c->GetName())) - { - XTargets[i].ID = c->GetID(); - SendXTargetPacket(i, c); - } - } -} + if (!IsPetNameChangeAllowed()) { + return false; + } -// IT IS NOT SAFE TO CALL THIS IF IT'S NOT INITIAL AGGRO -void Client::AddAutoXTarget(Mob *m, bool send) -{ - if (m->IsBot() || ((m->IsPet() || m->IsTempPet()) && m->IsPetOwnerBot())) { - return; - } + if (GetPet()) { + std::string cur_name = GetPet()->GetName(); - m_activeautohatermgr->increment_count(m); + if (new_name == cur_name) { + return false; + } + } - if (!XTargettingAvailable() || !XTargetAutoAddHaters || IsXTarget(m)) { - return; - } + if (!database.CheckNameFilter(new_name) || database.IsNameUsed(new_name)) { + return false; + } - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type == Auto && XTargets[i].ID == 0) { - XTargets[i].ID = m->GetID(); + CharacterPetNameRepository::ReplaceOne(database, { + .char_id = static_cast(CharacterID()), + .name = new_name + }); - if (send) { // if we don't send we're bulk sending updates later on - SendXTargetPacket(i, m); - } else { - XTargets[i].dirty = true; - } - break; - } - } + if (GetPet()) { + GetPet()->TempName(new_name.c_str()); + } - LogXTargets( - "Adding [{}] to [{}] ({}) XTargets", - m->GetCleanName(), - GetCleanName(), - GetID() - ); + ClearPetNameChange(); + return true; } -void Client::RemoveXTarget(Mob *m, bool OnlyAutoSlots) -{ - if (!XTargettingAvailable() || !m || !m_activeautohatermgr) { - return; - } - - m_activeautohatermgr->decrement_count(m); - // now we may need to clean up our CurrentTargetNPC entries - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type == CurrentTargetNPC && XTargets[i].ID == m->GetID()) { - XTargets[i].Type = Auto; - XTargets[i].ID = 0; - XTargets[i].dirty = true; - } - } - - auto r = GetRaid(); - if (r) { - r->UpdateRaidXTargets(); - } +bool Client::IsDiscovered(uint32 item_id) { + const auto& l = DiscoveredItemsRepository::GetWhere( + database, + fmt::format( + "item_id = {}", + item_id + ) + ); + if (l.empty()) { + return false; + } - LogXTargets( - "Removing [{}] from [{}] ({}) XTargets", - m->GetCleanName(), - GetCleanName(), - GetID() - ); + return true; } -void Client::UpdateXTargetType(XTargetType Type, Mob *m, const char *Name) -{ - if (!XTargettingAvailable()) { - return; - } +void Client::DiscoverItem(uint32 item_id) { + auto e = DiscoveredItemsRepository::NewEntity(); - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type == Type) { - if (m) { - XTargets[i].ID = m->GetID(); - } - else { - XTargets[i].ID = 0; - } + e.account_status = Admin(); + e.char_name = GetCleanName(); + e.discovered_date = std::time(nullptr); + e.item_id = item_id; - if (Name) { - strncpy(XTargets[i].Name, Name, 64); - } + auto d = DiscoveredItemsRepository::InsertOne(database, e); - SendXTargetPacket(i, m); - } - } -} + if (player_event_logs.IsEventEnabled(PlayerEvent::DISCOVER_ITEM)) { + const auto* item = database.GetItem(item_id); -void Client::SendXTargetPacket(uint32 Slot, Mob *m) -{ - if(!XTargettingAvailable()) - return; + auto e = PlayerEvent::DiscoverItemEvent{ + .item_id = item_id, + .item_name = item->Name, + }; + RecordPlayerEventLog(PlayerEvent::DISCOVER_ITEM, e); - uint32 PacketSize = 18; + } - if(m) - PacketSize += strlen(m->GetCleanName()); - else - { - PacketSize += strlen(XTargets[Slot].Name); - } + if (parse->PlayerHasQuestSub(EVENT_DISCOVER_ITEM)) { + auto* item = database.CreateItem(item_id); + std::vector args = { item }; - auto outapp = new EQApplicationPacket(OP_XTargetResponse, PacketSize); - outapp->WriteUInt32(GetMaxXTargets()); - outapp->WriteUInt32(1); - outapp->WriteUInt32(Slot); - if(m) - { - outapp->WriteUInt8(1); - } - else - { - if (strlen(XTargets[Slot].Name) && ((XTargets[Slot].Type == CurrentTargetPC) || - (XTargets[Slot].Type == GroupTank) || - (XTargets[Slot].Type == GroupAssist) || - (XTargets[Slot].Type == Puller))) - { - outapp->WriteUInt8(2); - } - else - { - outapp->WriteUInt8(0); - } - } - outapp->WriteUInt32(XTargets[Slot].ID); - outapp->WriteString(m ? m->GetCleanName() : XTargets[Slot].Name); - FastQueuePacket(&outapp); + parse->EventPlayer(EVENT_DISCOVER_ITEM, this, "", item_id, &args); + } } -// This is a bulk packet, we use it when we remove something since we need to reorder the xtargets and maybe -// add new mobs! Currently doesn't check if there is a dirty flag set, so it should only be called when there is -void Client::SendXTargetUpdates() -{ - if (!XTargettingAvailable()) - return; - - int count = 0; - // header is 4 bytes max xtargets, 4 bytes count - // entry is 4 bytes slot, 1 byte unknown, 4 bytes ID, 65 char name - auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8 + 74 * GetMaxXTargets()); // fuck it max size - outapp->WriteUInt32(GetMaxXTargets()); - outapp->WriteUInt32(1); // we will correct this later - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].dirty) { - outapp->WriteUInt32(i); - // MQ2 checks this for valid mobs, so 0 is bad here at least ... - outapp->WriteUInt8(XTargets[i].ID ? 1 : 0); - outapp->WriteUInt32(XTargets[i].ID); - outapp->WriteString(XTargets[i].Name); - count++; - XTargets[i].dirty = false; - } - } +void Client::UpdateLFP() { - // RemoveXTarget probably got called with a mob not on our xtargets - if (count == 0) { - safe_delete(outapp); - return; - } + Group *g = GetGroup(); + + if(g && !g->IsLeader(this)) { + database.SetLFP(CharacterID(), false); + worldserver.StopLFP(CharacterID()); + LFP = false; + return; + } + + GroupLFPMemberEntry LFPMembers[MAX_GROUP_MEMBERS]; + + for(unsigned int i=0; iGetZoneID(); + + if(g) { + // Fill the LFPMembers array with the rest of the group members, excluding ourself + // We don't fill in the class, level or zone, because we may not be able to determine + // them if the other group members are not in this zone. World will fill in this information + // for us, if it can. + int NextFreeSlot = 1; + for(unsigned int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if((g->membername[i][0] != '\0') && strcasecmp(g->membername[i], LFPMembers[0].Name)) + strcpy(LFPMembers[NextFreeSlot++].Name, g->membername[i]); + } + } + worldserver.UpdateLFP(CharacterID(), LFPMembers); +} + +bool Client::GroupFollow(Client* inviter) { - auto newbuff = new uchar[outapp->GetWritePosition()]; - memcpy(newbuff, outapp->pBuffer, outapp->GetWritePosition()); - safe_delete_array(outapp->pBuffer); - outapp->pBuffer = newbuff; - outapp->size = outapp->GetWritePosition(); - outapp->SetWritePosition(4); - outapp->WriteUInt32(count); - FastQueuePacket(&outapp); + if (inviter) { + isgrouped = true; + Raid* raid = entity_list.GetRaidByClient(inviter); + Raid* iraid = entity_list.GetRaidByClient(this); + + //inviter has a raid don't do group stuff instead do raid stuff! + if (raid) { + // Suspend the merc while in a raid (maybe a rule could be added for this) + if (GetMerc()) { + GetMerc()->Suspend(); + } + + uint32 groupToUse = 0xFFFFFFFF; + for (const auto& m : raid->members) { + if (m.member && m.member == inviter) { + groupToUse = m.group_number; + break; + } + } + if (iraid == raid) { + //both in same raid + uint32 ngid = raid->GetGroup(inviter->GetName()); + if (raid->GroupCount(ngid) < MAX_GROUP_MEMBERS) { + raid->MoveMember(GetName(), ngid); + raid->SendGroupDisband(this); + raid->GroupUpdate(ngid); + } + return false; + } + if (raid->RaidCount() < MAX_RAID_MEMBERS) + { + // okay, so we now have a single client (this) joining a group in a raid + // And they're not already in the raid (which is above and doesn't need xtarget shit) + if (!GetXTargetAutoMgr()->empty()) { + raid->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); + GetXTargetAutoMgr()->clear(); + RemoveAutoXTargets(); + } + + SetXTargetAutoMgr(raid->GetXTargetAutoMgr()); + if (!GetXTargetAutoMgr()->empty()) + SetDirtyAutoHaters(); + + if (raid->GroupCount(groupToUse) < MAX_GROUP_MEMBERS) + { + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->AddMember(this, groupToUse); + raid->SendBulkRaid(this); + //raid->SendRaidGroupAdd(GetName(), groupToUse); + //raid->SendGroupUpdate(this); + raid->GroupUpdate(groupToUse); //break + if (raid->IsLocked()) + { + raid->SendRaidLockTo(this); + } + return false; + } + else + { + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->AddMember(this); + raid->SendBulkRaid(this); + if (raid->IsLocked()) + { + raid->SendRaidLockTo(this); + } + return false; + } + } + } + + Group* group = entity_list.GetGroupByClient(inviter); + + if (!group) + { + //Make new group + group = new Group(inviter); + + if (!group) + { + return false; + } + + entity_list.AddGroup(group); + + if (group->GetID() == 0) + { + Message(Chat::Red, "Unable to get new group id. Cannot create group."); + inviter->Message(Chat::Red, "Unable to get new group id. Cannot create group."); + return false; + } + + //now we have a group id, can set inviter's id + group->AddToGroup(inviter); + database.SetGroupLeaderName(group->GetID(), inviter->GetName()); + group->UpdateGroupAAs(); + + //Invite the inviter into the group first.....dont ask + if (inviter->ClientVersion() < EQ::versions::ClientVersion::SoD) + { + auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupJoin_Struct)); + GroupJoin_Struct* outgj = (GroupJoin_Struct*)outapp->pBuffer; + strcpy(outgj->membername, inviter->GetName()); + strcpy(outgj->yourname, inviter->GetName()); + outgj->action = groupActInviteInitial; // 'You have formed the group'. + group->GetGroupAAs(&outgj->leader_aas); + inviter->QueuePacket(outapp); + safe_delete(outapp); + } + else + { + // SoD and later + inviter->SendGroupCreatePacket(); + inviter->SendGroupLeaderChangePacket(inviter->GetName()); + inviter->SendGroupJoinAcknowledge(); + } + group->GetXTargetAutoMgr()->merge(*inviter->GetXTargetAutoMgr()); + inviter->GetXTargetAutoMgr()->clear(); + inviter->SetXTargetAutoMgr(group->GetXTargetAutoMgr()); + } + + if (!group) + { + return false; + } + + // Remove merc from old group before adding client to the new one + if (GetMerc() && GetMerc()->HasGroup()) + { + GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); + } + + if (!group->AddMember(this)) + { + // If failed to add client to new group, regroup with merc + if (GetMerc()) + { + GetMerc()->MercJoinClientGroup(); + } + else + { + isgrouped = false; + } + return false; + } + + if (ClientVersion() >= EQ::versions::ClientVersion::SoD) + { + SendGroupJoinAcknowledge(); + } + + // Temporary hack for SoD, as things seem to work quite differently + if (inviter->IsClient() && inviter->ClientVersion() >= EQ::versions::ClientVersion::SoD) + { + database.RefreshGroupFromDB(inviter); + } + + // Add the merc back into the new group if possible + if (GetMerc()) + { + GetMerc()->MercJoinClientGroup(); + } + + if (inviter->IsLFP()) + { + // If the player who invited us to a group is LFP, have them update world now that we have joined their group. + inviter->UpdateLFP(); + } + + database.RefreshGroupFromDB(this); + group->SendHPManaEndPacketsTo(this); + //send updates to clients out of zone... + group->SendGroupJoinOOZ(this); + return true; + } + return false; } -void Client::RemoveGroupXTargets() +uint16 Client::GetPrimarySkillValue() { - if(!XTargettingAvailable()) - return; - - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if ((XTargets[i].Type == GroupTank) || - (XTargets[i].Type == GroupAssist) || - (XTargets[i].Type == Puller)) - { - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - SendXTargetPacket(i, nullptr); - } - } + EQ::skills::SkillType skill = EQ::skills::HIGHEST_SKILL; //because nullptr == 0, which is 1H Slashing, & we want it to return 0 from GetSkill + bool equipped = m_inv.GetItem(EQ::invslot::slotPrimary); + + if (!equipped) + skill = EQ::skills::SkillHandtoHand; + + else { + + uint8 type = m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->ItemType; //is this the best way to do this? + + switch (type) { + case EQ::item::ItemType1HSlash: // 1H Slashing + skill = EQ::skills::Skill1HSlashing; + break; + case EQ::item::ItemType2HSlash: // 2H Slashing + skill = EQ::skills::Skill2HSlashing; + break; + case EQ::item::ItemType1HPiercing: // Piercing + skill = EQ::skills::Skill1HPiercing; + break; + case EQ::item::ItemType1HBlunt: // 1H Blunt + skill = EQ::skills::Skill1HBlunt; + break; + case EQ::item::ItemType2HBlunt: // 2H Blunt + skill = EQ::skills::Skill2HBlunt; + break; + case EQ::item::ItemType2HPiercing: // 2H Piercing + if (IsClient() && CastToClient()->ClientVersion() < EQ::versions::ClientVersion::RoF2) + skill = EQ::skills::Skill1HPiercing; + else + skill = EQ::skills::Skill2HPiercing; + break; + case EQ::item::ItemTypeMartial: // Hand to Hand + skill = EQ::skills::SkillHandtoHand; + break; + default: // All other types default to Hand to Hand + skill = EQ::skills::SkillHandtoHand; + break; + } + } + + return GetSkill(skill); } -void Client::RemoveAutoXTargets() +uint32 Client::GetTotalATK() { - if(!XTargettingAvailable()) - return; + uint32 AttackRating = 0; + uint32 WornCap = itembonuses.ATK; - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(XTargets[i].Type == Auto) - { - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - SendXTargetPacket(i, nullptr); - } - } + if(IsClient()) { + AttackRating = ((WornCap * 1.342) + (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69)); + AttackRating += aabonuses.ATK + GroupLeadershipAAOffenseEnhancement(); + + if (AttackRating < 10) + AttackRating = 10; + } + else + AttackRating = GetATK(); + + AttackRating += spellbonuses.ATK; + + return AttackRating; } -void Client::ShowXTargets(Client *c) +uint32 Client::GetATKRating() { - if (!c) { - return; - } + uint32 AttackRating = 0; + if(IsClient()) { + AttackRating = (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69); - auto xtarget_count = 0; - - for (int i = 0; i < GetMaxXTargets(); ++i) { - c->Message( - Chat::White, - fmt::format( - "xtarget slot [{}] type [{}] ID [{}] name [{}]", - i, - static_cast(XTargets[i].Type), - XTargets[i].ID, - strlen(XTargets[i].Name) ? XTargets[i].Name : "No Name" - ).c_str() - ); + if (AttackRating < 10) + AttackRating = 10; + } + return AttackRating; +} - xtarget_count++; - } +void Client::VoiceMacroReceived(uint32 Type, char *Target, uint32 MacroNumber) { - auto &list = GetXTargetAutoMgr()->get_list(); - // yeah, I kept having to do something for debugging to tell if managers were the same object or not :P - // so lets use the address as an "ID" - c->Message( - Chat::White, - fmt::format( - "XTargetAutoMgr ID [{}] size [{}]", - fmt::ptr(GetXTargetAutoMgr()), - list.size() - ).c_str() - ); + uint32 GroupOrRaidID = 0; - int count = 0; - for (auto &e : list) { - c->Message( - Chat::White, - fmt::format( - "Spawn ID: {} Count: {}", - e.spawn_id, - e.count - ).c_str() - ); + switch(Type) { - count++; + case VoiceMacroGroup: { - if (count == 20) { - break; - } - } -} + Group* g = GetGroup(); -void Client::ProcessXTargetAutoHaters() -{ - if (!XTargettingAvailable()) - return; + if(g) + GroupOrRaidID = g->GetID(); + else + return; - // move shit up! If the removed NPC was in a CurrentTargetNPC slot it becomes Auto - // and we need to potentially fill it - std::queue empty_slots; - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type != Auto) - continue; + break; + } - if (XTargets[i].ID != 0 && !GetXTargetAutoMgr()->contains_mob(XTargets[i].ID)) { - XTargets[i].ID = 0; - XTargets[i].dirty = true; - } + case VoiceMacroRaid: { - if (XTargets[i].ID == 0) { - empty_slots.push(i); - continue; - } + Raid* r = GetRaid(); - if (XTargets[i].ID != 0 && !empty_slots.empty()) { - int temp = empty_slots.front(); - std::swap(XTargets[i], XTargets[temp]); - XTargets[i].dirty = XTargets[temp].dirty = true; - empty_slots.pop(); - empty_slots.push(i); - } - } - // okay, now we need to check if we have any empty slots and if we have aggro - // We make the assumption that if we shuffled the NPCs up that they're still on the aggro - // list in the same order. We could probably do this better and try to calc if - // there are new NPCs for our empty slots on the manager, but ahhh fuck it. - if (!empty_slots.empty() && !GetXTargetAutoMgr()->empty() && XTargetAutoAddHaters) { - auto &haters = GetXTargetAutoMgr()->get_list(); - for (auto &e : haters) { - auto *mob = entity_list.GetMob(e.spawn_id); - if (mob && !IsXTarget(mob)) { - auto slot = empty_slots.front(); - empty_slots.pop(); - XTargets[slot].dirty = true; - XTargets[slot].ID = mob->GetID(); - strn0cpy(XTargets[slot].Name, mob->GetCleanName(), 64); - } - if (empty_slots.empty()) - break; - } - } + if(r) + GroupOrRaidID = r->GetID(); + else + return; - m_dirtyautohaters = false; - SendXTargetUpdates(); + break; + } + } + + if(!worldserver.SendVoiceMacro(this, Type, Target, MacroNumber, GroupOrRaidID)) + Message(0, "Error: World server disconnected"); } -// This function is called when a client is added to a group -// Group leader joining isn't handled by this function -void Client::JoinGroupXTargets(Group *g) -{ - if (!g) - return; +void Client::ClearGroupAAs() { + for(unsigned int i = 0; i < MAX_GROUP_LEADERSHIP_AA_ARRAY; i++) + m_pp.leader_abilities.ranks[i] = 0; - if (!GetXTargetAutoMgr()->empty()) { - g->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); - GetXTargetAutoMgr()->clear(); - RemoveAutoXTargets(); - } + m_pp.group_leadership_points = 0; + m_pp.raid_leadership_points = 0; + m_pp.group_leadership_exp = 0; + m_pp.raid_leadership_exp = 0; - SetXTargetAutoMgr(g->GetXTargetAutoMgr()); + Save(); + database.SaveCharacterLeadershipAbilities(CharacterID(), &m_pp); +} - if (!GetXTargetAutoMgr()->empty()) - SetDirtyAutoHaters(); +void Client::UpdateGroupAAs(int32 points, uint32 type) { + switch(type) { + case 0: { m_pp.group_leadership_points += points; break; } + case 1: { m_pp.raid_leadership_points += points; break; } + } + SendLeadershipEXPUpdate(); } -// This function is called when a client leaves a group -void Client::LeaveGroupXTargets(Group *g) -{ - if (!g) - return; +bool Client::IsLeadershipEXPOn() { - SetXTargetAutoMgr(nullptr); // this will set it back to our manager - RemoveAutoXTargets(); - entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever - // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups - if (!GetXTargetAutoMgr()->empty()) { - GetXTargetAutoMgr()->demerge(*g->GetXTargetAutoMgr()); // this will remove entries where we only had aggro - SetDirtyAutoHaters(); - } -} + if(!m_pp.leadAAActive) + return false; -// This function is called when a client leaves a group -void Client::LeaveRaidXTargets(Raid *r) -{ - if (!r) - return; + Group *g = GetGroup(); - SetXTargetAutoMgr(nullptr); // this will set it back to our manager - RemoveAutoXTargets(); - entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever - // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups - if (!GetXTargetAutoMgr()->empty()) { - GetXTargetAutoMgr()->demerge(*r->GetXTargetAutoMgr()); // this will remove entries where we only had aggro - SetDirtyAutoHaters(); - } -} + if (g && g->IsLeader(this) && g->GroupCount() > 2) + return true; -void Client::SetMaxXTargets(uint8 NewMax) -{ - if(!XTargettingAvailable()) - return; + Raid *r = GetRaid(); - if(NewMax > XTARGET_HARDCAP) - return; + if (!r) + return false; - MaxXTargets = NewMax; + // raid leaders can only gain raid AA XP + if (r->IsLeader(this)) { + if (r->RaidCount() > 17) + return true; + else + return false; + } - Save(0); + uint32 gid = r->GetGroup(this); - for(int i = MaxXTargets; i < XTARGET_HARDCAP; ++i) - { - XTargets[i].Type = Auto; - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - } + if (gid > 11) // not in a group + return false; + + if (r->IsGroupLeader(GetName()) && r->GroupCount(gid) > 2) + return true; + + return false; - auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8); - outapp->WriteUInt32(GetMaxXTargets()); - outapp->WriteUInt32(0); - FastQueuePacket(&outapp); } -void Client::SendWebLink(const char *website) +uint32 Client::GetAggroCount() { + return AggroCount; +} + +// we pass in for book keeping if RestRegen is enabled +void Client::IncrementAggroCount(bool raid_target) { - if (website) { - size_t len = strlen(website) + 1; - if (len > 1) - { - auto outapp = new EQApplicationPacket(OP_Weblink, sizeof(Weblink_Struct) + len); - Weblink_Struct* wl = (Weblink_Struct*)outapp->pBuffer; - memcpy(wl->weblink, website, len); - wl->weblink[len] = '\0'; + // This method is called when a client is added to a mob's hate list. It turns the clients aggro flag on so + // rest state regen is stopped, and for SoF, it sends the opcode to show the crossed swords in-combat indicator. + AggroCount++; + + if(!RuleB(Character, RestRegenEnabled)) + return; + + uint32 newtimer = raid_target ? RuleI(Character, RestRegenRaidTimeToActivate) : RuleI(Character, RestRegenTimeToActivate); + + // When our aggro count is 1 here, we are exiting rest state. We need to pause our current timer, if we have time remaining + // We should not actually have to do anything to the Timer object since the AggroCount counter blocks it from being checked + // and will have it's timer changed when we exit combat so let's not do any extra work + if (AggroCount == 1 && rest_timer.GetRemainingTime()) // the Client::rest_timer is never disabled, so don't need to check + m_pp.RestTimer = std::max(1u, rest_timer.GetRemainingTime() / 1000); // I guess round up? + + // save the new timer if it's higher + m_pp.RestTimer = std::max(m_pp.RestTimer, newtimer); + + // If we already had aggro before this method was called, the combat indicator should already be up for SoF clients, + // so we don't need to send it again. + // + if(AggroCount > 1) + return; + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + auto outapp = new EQApplicationPacket(OP_RestState, 1); + char *Buffer = (char *)outapp->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x01); + QueuePacket(outapp); + safe_delete(outapp); + } - FastQueuePacket(&outapp); - } - } } -void Client::SendMercPersonalInfo() +void Client::DecrementAggroCount() { - uint32 mercTypeCount = 1; - uint32 mercCount = 1; //TODO: Un-hardcode this and support multiple mercs like in later clients than SoD. - uint32 i = 0; - uint32 altCurrentType = 19; //TODO: Implement alternate currency purchases involving mercs! + // This should be called when a client is removed from a mob's hate list (it dies or is memblurred). + // It checks whether any other mob is aggro on the player, and if not, starts the rest timer. + // For SoF, the opcode to start the rest state countdown timer in the UI is sent. - MercTemplate *mercData = &zone->merc_templates[GetMercInfo().MercTemplateID]; + // If we didn't have aggro before, this method should not have been called. + if(!AggroCount) + return; - int stancecount = 0; - stancecount += zone->merc_stance_list[GetMercInfo().MercTemplateID].size(); - if(stancecount > MAX_MERC_STANCES || mercCount > MAX_MERC || mercTypeCount > MAX_MERC_GRADES) - { - Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo canceled: (%i) (%i) (%i) for %s", stancecount, mercCount, mercTypeCount, GetName()); - SendMercMerchantResponsePacket(0); - return; - } + AggroCount--; - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { - auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(MercenaryDataUpdate_Struct)); - auto mdus = (MercenaryDataUpdate_Struct *) outapp->pBuffer; - - mdus->MercStatus = 0; - mdus->MercCount = mercCount; - mdus->MercData[i].MercID = mercData->MercTemplateID; - mdus->MercData[i].MercType = mercData->MercType; - mdus->MercData[i].MercSubType = mercData->MercSubType; - mdus->MercData[i].PurchaseCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0); - mdus->MercData[i].UpkeepCost = Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0); - mdus->MercData[i].Status = 0; - mdus->MercData[i].AltCurrencyCost = Merc::CalcPurchaseCost( - mercData->MercTemplateID, - GetLevel(), - altCurrentType - ); - mdus->MercData[i].AltCurrencyUpkeep = Merc::CalcPurchaseCost( - mercData->MercTemplateID, - GetLevel(), - altCurrentType - ); - mdus->MercData[i].AltCurrencyType = altCurrentType; - mdus->MercData[i].MercUnk01 = 0; - mdus->MercData[i].TimeLeft = GetMercInfo().MercTimerRemaining; //GetMercTimer().GetRemainingTime(); - mdus->MercData[i].MerchantSlot = i + 1; - mdus->MercData[i].MercUnk02 = 1; - mdus->MercData[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); - mdus->MercData[i].MercUnk03 = 0; - mdus->MercData[i].MercUnk04 = 1; - - strn0cpy(mdus->MercData[i].MercName, GetMercInfo().merc_name, sizeof(mdus->MercData[i].MercName)); - - uint32 stanceindex = 0; - if (mdus->MercData[i].StanceCount != 0) { - auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); - while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { - mdus->MercData[i].Stances[stanceindex].StanceIndex = stanceindex; - mdus->MercData[i].Stances[stanceindex].Stance = (iter->StanceID); - stanceindex++; - ++iter; - } - } + if(!RuleB(Character, RestRegenEnabled)) + return; - mdus->MercData[i].MercUnk05 = 1; - FastQueuePacket(&outapp); - safe_delete(outapp); - return; - } else { - auto outapp = new EQApplicationPacket(OP_MercenaryDataResponse, sizeof(MercenaryMerchantList_Struct)); - auto mml = (MercenaryMerchantList_Struct *) outapp->pBuffer; - - mml->MercTypeCount = mercTypeCount; //We should only have one merc entry. - mml->MercGrades[i] = 1; - - mml->MercCount = mercCount; - mml->Mercs[i].MercID = mercData->MercTemplateID; - mml->Mercs[i].MercType = mercData->MercType; - mml->Mercs[i].MercSubType = mercData->MercSubType; - mml->Mercs[i].PurchaseCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0) : 0; - mml->Mercs[i].UpkeepCost = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0) : 0; - mml->Mercs[i].Status = 0; - mml->Mercs[i].AltCurrencyCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; - mml->Mercs[i].AltCurrencyUpkeep = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; - mml->Mercs[i].AltCurrencyType = altCurrentType; - mml->Mercs[i].MercUnk01 = 0; - mml->Mercs[i].TimeLeft = GetMercInfo().MercTimerRemaining; - mml->Mercs[i].MerchantSlot = i + 1; - mml->Mercs[i].MercUnk02 = 1; - mml->Mercs[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); - mml->Mercs[i].MercUnk03 = 0; - mml->Mercs[i].MercUnk04 = 1; - - strn0cpy(mml->Mercs[i].MercName, GetMercInfo().merc_name, sizeof(mml->Mercs[i].MercName)); - - int stanceindex = 0; - if (mml->Mercs[i].StanceCount != 0) { - auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); - while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { - mml->Mercs[i].Stances[stanceindex].StanceIndex = stanceindex; - mml->Mercs[i].Stances[stanceindex].Stance = (iter->StanceID); - stanceindex++; - ++iter; - } - } + // Something else is still aggro on us, can't rest yet. + if (AggroCount) + return; - FastQueuePacket(&outapp); - safe_delete(outapp); - return; - } + rest_timer.Start(m_pp.RestTimer * 1000); + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + auto outapp = new EQApplicationPacket(OP_RestState, 5); + char *Buffer = (char *)outapp->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, m_pp.RestTimer); + QueuePacket(outapp); + safe_delete(outapp); + } } -void Client::SendClearMercInfo() +// when we cast a beneficial spell we need to steal our targets current timer +// That's what we use this for +void Client::UpdateRestTimer(uint32 new_timer) { - auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(NoMercenaryHired_Struct)); - NoMercenaryHired_Struct *nmhs = (NoMercenaryHired_Struct*)outapp->pBuffer; - nmhs->MercStatus = -1; - nmhs->MercCount = 0; - nmhs->MercID = 1; - FastQueuePacket(&outapp); + // their timer was 0, so we don't do anything + if (new_timer == 0) + return; + + if (!RuleB(Character, RestRegenEnabled)) + return; + + // so if we're currently on aggro, we check our saved timer + if (AggroCount) { + if (m_pp.RestTimer < new_timer) // our timer needs to be updated, don't need to update client here + m_pp.RestTimer = new_timer; + } else { // if we're not aggro, we need to check if current timer needs updating + if (rest_timer.GetRemainingTime() / 1000 < new_timer) { + rest_timer.Start(new_timer * 1000); + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + auto outapp = new EQApplicationPacket(OP_RestState, 5); + char *Buffer = (char *)outapp->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, new_timer); + QueuePacket(outapp); + safe_delete(outapp); + } + } + } } - -void Client::DuplicateLoreMessage(uint32 ItemID) +void Client::SendPVPStats() { - if (!(m_ClientVersionBit & EQ::versions::maskRoFAndLater)) - { - MessageString(Chat::White, PICK_LORE); - return; - } + // This sends the data to the client to populate the PVP Stats Window. + // + // When the PVP Stats window is opened, no opcode is sent. Therefore this method should be called + // from Client::CompleteConnect, and also when the player makes a PVP kill. + // + auto outapp = new EQApplicationPacket(OP_PVPStats, sizeof(PVPStats_Struct)); + PVPStats_Struct *pvps = (PVPStats_Struct *)outapp->pBuffer; - const EQ::ItemData *item = database.GetItem(ItemID); + pvps->Kills = m_pp.PVPKills; + pvps->Deaths = m_pp.PVPDeaths; + pvps->PVPPointsAvailable = m_pp.PVPCurrentPoints; + pvps->TotalPVPPoints = m_pp.PVPCareerPoints; + pvps->BestKillStreak = m_pp.PVPBestKillStreak; + pvps->WorstDeathStreak = m_pp.PVPWorstDeathStreak; + pvps->CurrentKillStreak = m_pp.PVPCurrentKillStreak; - if(!item) - return; + // TODO: Record and send other PVP Stats - MessageString(Chat::White, PICK_LORE, item->Name); + QueuePacket(outapp); + safe_delete(outapp); } -void Client::GarbleMessage(char *message, uint8 variance) +void Client::SendCrystalCounts() { - // Garble message by variance% - const char alpha_list[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // only change alpha characters for now - const char delimiter = 0x12; - int delimiter_count = 0; + auto outapp = new EQApplicationPacket(OP_CrystalCountUpdate, sizeof(CrystalCountUpdate_Struct)); + CrystalCountUpdate_Struct *ccus = (CrystalCountUpdate_Struct *)outapp->pBuffer; - // Don't garble # commands - if (message[0] == COMMAND_CHAR || message[0] == BOT_COMMAND_CHAR) { - return; - } + ccus->CurrentRadiantCrystals = GetRadiantCrystals(); + ccus->CurrentEbonCrystals = GetEbonCrystals(); + ccus->CareerRadiantCrystals = m_pp.careerRadCrystals; + ccus->CareerEbonCrystals = m_pp.careerEbonCrystals; - for (size_t i = 0; i < strlen(message); i++) { - // Client expects hex values inside of a text link body - if (message[i] == delimiter) { - if (!(delimiter_count & 1)) { i += EQ::constants::SAY_LINK_BODY_SIZE; } - ++delimiter_count; - continue; - } - uint8 chance = (uint8)zone->random.Int(0, 115); // variation just over worst possible scrambling - if (isalpha((unsigned char)message[i]) && (chance <= variance)) { - uint8 rand_char = (uint8)zone->random.Int(0,51); // choose a random character from the alpha list - message[i] = alpha_list[rand_char]; - } - } + QueuePacket(outapp); + safe_delete(outapp); } -// returns what Other thinks of this -FACTION_VALUE Client::GetReverseFactionCon(Mob* iOther) { - if (GetOwnerID()) { - return GetOwnerOrSelf()->GetReverseFactionCon(iOther); - } +void Client::SendDisciplineTimers() +{ - iOther = iOther->GetOwnerOrSelf(); + auto outapp = new EQApplicationPacket(OP_DisciplineTimer, sizeof(DisciplineTimer_Struct)); + DisciplineTimer_Struct *dts = (DisciplineTimer_Struct *)outapp->pBuffer; - if (iOther->GetPrimaryFaction() < 0) - return GetSpecialFactionCon(iOther); + for(unsigned int i = 0; i < MAX_DISCIPLINE_TIMERS; ++i) + { + uint32 RemainingTime = p_timers.GetRemainingTime(pTimerDisciplineReuseStart + i); - if (iOther->GetPrimaryFaction() == 0) - return FACTION_INDIFFERENTLY; + if(RemainingTime > 0) + { + dts->TimerID = i; + dts->Duration = RemainingTime; + QueuePacket(outapp); + } + } - return GetFactionLevel(CharacterID(), 0, GetFactionRace(), GetClass(), GetDeity(), iOther->GetPrimaryFaction(), iOther); + safe_delete(outapp); } -bool Client::ReloadCharacterFaction(Client *c, uint32 facid, uint32 charid) +void Client::SendRespawnBinds() { - if (database.SetCharacterFactionLevel(charid, facid, 0, 0, factionvalues)) - return true; - else - return false; + // This sends the data to the client to populate the Respawn from Death Window. + // + // This should be sent after OP_Death for SoF clients + // Client will respond with a 4 byte packet that includes the number of the selection made + // + + //If no options have been given, default to Bind + Rez + if (respawn_options.empty()) + { + BindStruct* b = &m_pp.binds[0]; + RespawnOption opt; + opt.name = "Bind Location"; + opt.zone_id = b->zone_id; + opt.instance_id = b->instance_id; + opt.x = b->x; + opt.y = b->y; + opt.z = b->z; + opt.heading = b->heading; + respawn_options.push_front(opt); + } + //Rez is always added at the end + RespawnOption rez; + rez.name = "Resurrect"; + rez.zone_id = zone->GetZoneID(); + rez.instance_id = zone->GetInstanceID(); + rez.x = GetX(); + rez.y = GetY(); + rez.z = GetZ(); + rez.heading = GetHeading(); + respawn_options.push_back(rez); + + int num_options = respawn_options.size(); + uint32 PacketLength = 17 + (26 * num_options); //Header size + per-option invariant size + + std::list::iterator itr; + RespawnOption* opt = nullptr; + + //Find string size for each option + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + opt = &(*itr); + PacketLength += opt->name.size() + 1; //+1 for cstring + } + + auto outapp = new EQApplicationPacket(OP_RespawnWindow, PacketLength); + char* buffer = (char*)outapp->pBuffer; + + //Packet header + VARSTRUCT_ENCODE_TYPE(uint32, buffer, initial_respawn_selection); //initial selection (from 0) + VARSTRUCT_ENCODE_TYPE(uint32, buffer, RuleI(Character, RespawnFromHoverTimer) * 1000); + VARSTRUCT_ENCODE_TYPE(uint32, buffer, 0); //unknown + VARSTRUCT_ENCODE_TYPE(uint32, buffer, num_options); //number of options to display + + //Individual options + int count = 0; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + opt = &(*itr); + VARSTRUCT_ENCODE_TYPE(uint32, buffer, count++); //option num (from 0) + VARSTRUCT_ENCODE_TYPE(uint32, buffer, opt->zone_id); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->x); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->y); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->z); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->heading); + VARSTRUCT_ENCODE_STRING(buffer, opt->name.c_str()); + VARSTRUCT_ENCODE_TYPE(uint8, buffer, (count == num_options)); //is this one Rez (the last option)? + } + + QueuePacket(outapp); + safe_delete(outapp); + return; } -//o-------------------------------------------------------------- -//| Name: GetFactionLevel; Dec. 16, 2001 -//o-------------------------------------------------------------- -//| Notes: Gets the characters faction standing with the specified NPC. -//| Will return Indifferent on failure. -//o-------------------------------------------------------------- -FACTION_VALUE Client::GetFactionLevel(uint32 char_id, uint32 npc_id, uint32 p_race, uint32 p_class, uint32 p_deity, int32 pFaction, Mob* tnpc) +void Client::HandleLDoNOpen(NPC *target) { - if (pFaction < 0) - return GetSpecialFactionCon(tnpc); - FACTION_VALUE fac = FACTION_INDIFFERENTLY; - int32 tmpFactionValue; - FactionMods fmods; - - // few optimizations - if (GetFeigned()) - return FACTION_INDIFFERENTLY; - if(!zone->CanDoCombat()) - return FACTION_INDIFFERENTLY; - if (invisible_undead && tnpc && !tnpc->SeeInvisibleUndead()) - return FACTION_INDIFFERENTLY; - if (IsInvisible(tnpc)) - return FACTION_INDIFFERENTLY; - if (tnpc && tnpc->GetOwnerID() != 0) // pets con amiably to owner and indiff to rest - { - if (char_id == tnpc->GetOwner()->CastToClient()->CharacterID()) - return FACTION_AMIABLY; - else - return FACTION_INDIFFERENTLY; - } - - //First get the NPC's Primary faction - if(pFaction > 0) - { - //Get the faction data from the database - if(content_db.GetFactionData(&fmods, p_class, p_race, p_deity, pFaction)) - { - //Get the players current faction with pFaction - tmpFactionValue = GetCharacterFactionLevel(pFaction); - //Tack on any bonuses from Alliance type spell effects - tmpFactionValue += GetFactionBonus(pFaction); - tmpFactionValue += GetItemFactionBonus(pFaction); - //Return the faction to the client - fac = CalculateFaction(&fmods, tmpFactionValue); - } - } - else - { - return(FACTION_INDIFFERENTLY); - } - - // merchant fix - if (tnpc && tnpc->IsNPC() && tnpc->CastToNPC()->MerchantType && (fac == FACTION_THREATENINGLY || fac == FACTION_SCOWLS)) - fac = FACTION_DUBIOUSLY; - - if (tnpc != 0 && fac != FACTION_SCOWLS && tnpc->CastToNPC()->CheckAggro(this)) - fac = FACTION_THREATENINGLY; - - return fac; + if(target) + { + if(target->GetClass() != Class::LDoNTreasure) + { + LogDebug("[{}] tried to open [{}] but [{}] was not a treasure chest", + GetName(), target->GetName(), target->GetName()); + return; + } + + if (target->GetSpecialAbility(SpecialAbility::OpenImmunity)) + { + LogDebug("[{}] tried to open [{}] but it was immune", GetName(), target->GetName()); + return; + } + + if(DistanceSquaredNoZ(m_Position, target->GetPosition()) > RuleI(Adventure, LDoNTrapDistanceUse)) + { + LogDebug("[{}] tried to open [{}] but [{}] was out of range", + GetName(), target->GetName(), target->GetName()); + Message(Chat::Red, "Treasure chest out of range."); + return; + } + + if(target->IsLDoNTrapped()) + { + if(target->GetLDoNTrapSpellID() != 0) + { + MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + } + else + { + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + } + } + + if(target->IsLDoNLocked()) + { + MessageString(Chat::Skills, LDON_STILL_LOCKED, target->GetCleanName()); + return; + } + else + { + target->AddToHateList(this, 0, 500000, false, false, false); + if(target->GetLDoNTrapType() != 0) + { + if(GetRaid()) + { + GetRaid()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); + } + else if(GetGroup()) + { + GetGroup()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); + } + else + { + AddEXP(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, GetLevelCon(target->GetLevel())); + } + } + target->Death(this, 0, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); + } + } } -//Sets the characters faction standing with the specified NPC. -void Client::SetFactionLevel( - uint32 character_id, - uint32 npc_faction_id, - uint8 class_id, - uint8 race_id, - uint8 deity_id, - bool is_quest -) +void Client::HandleLDoNSenseTraps(NPC *target, uint16 skill, uint8 type) { - auto l = zone->GetNPCFactionEntries(npc_faction_id); + if(target && target->GetClass() == Class::LDoNTreasure) + { + if(target->IsLDoNTrapped()) + { + if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) + { + MessageString(Chat::Skills, LDON_CANT_DETERMINE_TRAP, target->GetCleanName()); + return; + } + + if(target->IsLDoNTrapDetected()) + { + MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); + } + else + { + int check = LDoNChest_SkillCheck(target, skill); + switch(check) + { + case -1: + case 0: + MessageString(Chat::Skills, LDON_DONT_KNOW_TRAPPED, target->GetCleanName()); + break; + case 1: + MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); + target->SetLDoNTrapDetected(true); + break; + default: + break; + } + } + } + else + { + MessageString(Chat::Skills, LDON_CERTAIN_NOT_TRAP, target->GetCleanName()); + } + } +} - if (l.empty()) { - return; - } +void Client::HandleLDoNDisarm(NPC *target, uint16 skill, uint8 type) +{ + if(target) + { + if(target->GetClass() == Class::LDoNTreasure) + { + if(!target->IsLDoNTrapped()) + { + MessageString(Chat::Skills, LDON_WAS_NOT_TRAPPED, target->GetCleanName()); + return; + } + + if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) + { + MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); + return; + } + + int check = 0; + if(target->IsLDoNTrapDetected()) + { + check = LDoNChest_SkillCheck(target, skill); + } + else + { + check = LDoNChest_SkillCheck(target, skill*33/100); + } + switch(check) + { + case 1: + target->SetLDoNTrapDetected(false); + target->SetLDoNTrapped(false); + target->SetLDoNTrapSpellID(0); + MessageString(Chat::Skills, LDON_HAVE_DISARMED, target->GetCleanName()); + break; + case 0: + MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); + break; + case -1: + MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + break; + } + } + } +} - int current_value; +void Client::HandleLDoNPickLock(NPC *target, uint16 skill, uint8 type) +{ + if(target) + { + if(target->GetClass() == Class::LDoNTreasure) + { + if(target->IsLDoNTrapped()) + { + MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + } + + if(!target->IsLDoNLocked()) + { + MessageString(Chat::Skills, LDON_WAS_NOT_LOCKED, target->GetCleanName()); + return; + } + + if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) + { + Message(Chat::Skills, "You cannot unlock %s with this skill.", target->GetCleanName()); + return; + } + + int check = LDoNChest_SkillCheck(target, skill); + + switch(check) + { + case 0: + case -1: + MessageString(Chat::Skills, LDON_PICKLOCK_FAILURE, target->GetCleanName()); + break; + case 1: + target->SetLDoNLocked(false); + MessageString(Chat::Skills, LDON_PICKLOCK_SUCCESS, target->GetCleanName()); + break; + } + } + } +} - for (auto& e : l) { - if (e.faction_id <= 0 || e.value == 0) { - continue; - } +int Client::LDoNChest_SkillCheck(NPC *target, int skill) +{ + if(!target) + return -1; - int faction_before; - int faction_minimum; - int faction_maximum; + int chest_difficulty = target->GetLDoNLockedSkill() == 0 ? (target->GetLevel() * 5) : target->GetLDoNLockedSkill(); + float base_difficulty = RuleR(Adventure, LDoNBaseTrapDifficulty); - FactionMods faction_modifiers; + if(chest_difficulty == 0) + chest_difficulty = 5; - content_db.GetFactionData(&faction_modifiers, class_id, race_id, deity_id, e.faction_id); + float chance = ((100.0f - base_difficulty) * ((float)skill / (float)chest_difficulty)); - if (is_quest) { - if (e.value > 0) { - e.value = -std::abs(e.value); - } else if (e.value < 0) { - e.value = std::abs(e.value); - } - } + if(chance > (100.0f - base_difficulty)) + { + chance = 100.0f - base_difficulty; + } - // Adjust the amount you can go up or down so the resulting range - // is PERSONAL_MAX - PERSONAL_MIN - // - // Adjust these values for cases where starting faction is below - // min or above max by not allowing any earn in those directions. - faction_minimum = faction_modifiers.min - faction_modifiers.base; - faction_minimum = std::min(0, faction_minimum); + float d100 = (float)zone->random.Real(0, 100); - faction_maximum = faction_modifiers.max - faction_modifiers.base; - faction_maximum = std::max(0, faction_maximum); + if(d100 <= chance) + return 1; + else + { + if(d100 > (chance + RuleR(Adventure, LDoNCriticalFailTrapThreshold))) + return -1; + } - // Get the characters current value with that faction - current_value = GetCharacterFactionLevel(e.faction_id); - faction_before = current_value; + return 0; +} -#ifdef LUA_EQEMU - int32 lua_ret = 0; - bool ignore_default = false; - lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, e.value, e.faction_id, current_value, e.temp, faction_minimum, faction_maximum, ignore_default); +void Client::SummonAndRezzAllCorpses() +{ + PendingRezzXP = -1; - if (ignore_default) { - e.value = lua_ret; - } -#endif + auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); - UpdatePersonalFaction( - character_id, - e.value, - e.faction_id, - ¤t_value, - e.temp, - faction_minimum, - faction_maximum - ); + ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; - SendFactionMessage( - e.value, - e.faction_id, - faction_before, - current_value, - e.temp, - faction_minimum, - faction_maximum - ); - } -} + sdapcs->CharacterID = CharacterID(); + sdapcs->ZoneID = zone->GetZoneID(); + sdapcs->InstanceID = zone->GetInstanceID(); -void Client::SetFactionLevel2(uint32 char_id, int32 faction_id, uint8 char_class, uint8 char_race, uint8 char_deity, int32 value, uint8 temp) -{ - int32 current_value; - - //Get the npc faction list - if(faction_id > 0 && value != 0) { - int32 faction_before_hit; - FactionMods fm; - int32 this_faction_max; - int32 this_faction_min; - - // Find out starting faction for this faction - // It needs to be used to adj max and min personal - // The range is still the same, 1200-3000(4200), but adjusted for base - content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), - faction_id); - - // Adjust the amount you can go up or down so the resulting range - // is PERSONAL_MAX - PERSONAL_MIN - // - // Adjust these values for cases where starting faction is below - // min or above max by not allowing any earn/loss in those directions. - // At least one faction starts out way below min, so we don't want - // to allow loses in those cases, just massive gains. - this_faction_min = fm.min - fm.base; - this_faction_min = std::min(0, this_faction_min); - this_faction_max = fm.max - fm.base; - this_faction_max = std::max(0, this_faction_max); - - //Get the faction modifiers - current_value = GetCharacterFactionLevel(faction_id); - faction_before_hit = current_value; + worldserver.SendPacket(Pack); -#ifdef LUA_EQEMU - int32 lua_ret = 0; - bool ignore_default = false; - lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, value, faction_id, current_value, temp, this_faction_min, this_faction_max, ignore_default); + safe_delete(Pack); - if (ignore_default) { - value = lua_ret; - } -#endif + entity_list.RemoveAllCorpsesByCharID(CharacterID()); - UpdatePersonalFaction(char_id, value, faction_id, ¤t_value, temp, this_faction_min, this_faction_max); + int CorpseCount = database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), GetPosition()); + if(CorpseCount <= 0) + { + Message(Chat::Yellow, "You have no corpses to summnon."); + return; + } - //Message(Chat::Lime, "Min(%d) Max(%d) Before(%d), After(%d)\n", this_faction_min, this_faction_max, faction_before_hit, current_value); + int RezzExp = entity_list.RezzAllCorpsesByCharID(CharacterID()); - SendFactionMessage(value, faction_id, faction_before_hit, current_value, temp, this_faction_min, this_faction_max); - } + if(RezzExp > 0) + SetEXP(ExpSource::Resurrection, GetEXP() + RezzExp, GetAAXP(), true); - return; + Message(Chat::Yellow, "All your corpses have been summoned to your feet and have received a 100% resurrection."); } -int32 Client::GetCharacterFactionLevel(int32 faction_id) +void Client::SummonAllCorpses(const glm::vec4& position) { - if (faction_id <= 0) - return 0; - faction_map::iterator res; - res = factionvalues.find(faction_id); - if (res == factionvalues.end()) - return 0; - return res->second; -} + auto summonLocation = position; + if(IsOrigin(position) && position.w == 0.0f) + summonLocation = GetPosition(); -// Common code to set faction level. -// Applies HeroicCHA is it applies -// Checks for bottom out and max faction and old faction db entries -// Updates the faction if we are not minned, maxed or we need to repair + auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); -void Client::UpdatePersonalFaction(int32 char_id, int32 npc_value, int32 faction_id, int32 *current_value, int32 temp, int32 this_faction_min, int32 this_faction_max) -{ - bool repair = false; - bool change = false; - - if (itembonuses.HeroicCHA) - { - int faction_mod = itembonuses.HeroicCHA / 5; - // If our result isn't truncated, then just do that - if (npc_value * faction_mod / 100 != 0) - npc_value += npc_value * faction_mod / 100; - // If our result is truncated, then double a mob's value every once and a while to equal what they would have got - else - { - if (zone->random.Int(0, 100) < faction_mod) - npc_value *= 2; - } - } + ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; - // Set flag when to update db - // Repair needed, as db changes could modify a base value for a faction - // and we need to auto correct when that happens. - if (*current_value > this_faction_max) - { - *current_value = this_faction_max; - repair = true; - } - else if (*current_value < this_faction_min) - { - *current_value = this_faction_min; - repair = true; - } - else if ((m_pp.gm != 1) && (npc_value != 0) && - ((npc_value > 0 && *current_value != this_faction_max) || - ((npc_value < 0 && *current_value != this_faction_min)))) - change = true; + sdapcs->CharacterID = CharacterID(); + sdapcs->ZoneID = zone->GetZoneID(); + sdapcs->InstanceID = zone->GetInstanceID(); - if (change || repair) - { - *current_value += npc_value; + worldserver.SendPacket(Pack); - if (*current_value > this_faction_max) - *current_value = this_faction_max; - else if (*current_value < this_faction_min) - *current_value = this_faction_min; + safe_delete(Pack); - database.SetCharacterFactionLevel(char_id, faction_id, *current_value, temp, factionvalues); - } + entity_list.RemoveAllCorpsesByCharID(CharacterID()); -return; + database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), summonLocation); } -// returns the character's faction level, adjusted for racial, class, and deity modifiers -int32 Client::GetModCharacterFactionLevel(int32 faction_id) { - int32 Modded = GetCharacterFactionLevel(faction_id); - FactionMods fm; - if (content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), faction_id)) - { - Modded += fm.base + fm.class_mod + fm.race_mod + fm.deity_mod; - - //Tack on any bonuses from Alliance type spell effects - Modded += GetFactionBonus(faction_id); - Modded += GetItemFactionBonus(faction_id); - } +void Client::DepopAllCorpses() +{ + auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); - return Modded; -} + ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; -void Client::MerchantRejectMessage(Mob *merchant, int primaryfaction) -{ - int messageid = 0; - int32 tmpFactionValue = 0; - int32 lowestvalue = 0; - FactionMods fmod; - - // If a faction is involved, get the data. - if (primaryfaction > 0) { - if (content_db.GetFactionData(&fmod, GetClass(), GetFactionRace(), GetDeity(), primaryfaction)) { - tmpFactionValue = GetCharacterFactionLevel(primaryfaction); - lowestvalue = std::min(std::min(tmpFactionValue, fmod.deity_mod), - std::min(fmod.class_mod, fmod.race_mod)); - } - } - // If no primary faction or biggest influence is your faction hit - if (primaryfaction <= 0 || lowestvalue == tmpFactionValue) { - merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS6)); - } else if (lowestvalue == fmod.race_mod) { // race biggest - // Non-standard race (ex. illusioned to wolf) - if (GetRace() > PLAYER_RACE_COUNT) { - messageid = zone->random.Int(1, 3); // these aren't sequential StringIDs :( - switch (messageid) { - case 1: - messageid = WONT_SELL_NONSTDRACE1; - break; - case 2: - messageid = WONT_SELL_NONSTDRACE2; - break; - case 3: - messageid = WONT_SELL_NONSTDRACE3; - break; - default: // w/e should never happen - messageid = WONT_SELL_NONSTDRACE1; - break; - } - merchant->SayString(messageid); - } else { // normal player races - messageid = zone->random.Int(1, 4); - switch (messageid) { - case 1: - messageid = WONT_SELL_RACE1; - break; - case 2: - messageid = WONT_SELL_RACE2; - break; - case 3: - messageid = WONT_SELL_RACE3; - break; - case 4: - messageid = WONT_SELL_RACE4; - break; - default: // w/e should never happen - messageid = WONT_SELL_RACE1; - break; - } - merchant->SayString(messageid, itoa(GetRace())); - } - } else if (lowestvalue == fmod.class_mod) { - merchant->SayString(zone->random.Int(WONT_SELL_CLASS1, WONT_SELL_CLASS5), itoa(GetClass())); - } else { - // Must be deity - these two sound the best for that. - // Can't use a message with a field, GUI wants class/race names. - // for those message IDs. These are straight text. - merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS2)); - } - return; -} + sdapcs->CharacterID = CharacterID(); + sdapcs->ZoneID = zone->GetZoneID(); + sdapcs->InstanceID = zone->GetInstanceID(); -//o-------------------------------------------------------------- -//| Name: SendFactionMessage -//o-------------------------------------------------------------- -//| Purpose: Send faction change message to client -//o-------------------------------------------------------------- -void Client::SendFactionMessage(int32 tmpvalue, int32 faction_id, int32 faction_before_hit, int32 totalvalue, uint8 temp, int32 this_faction_min, int32 this_faction_max) -{ - char name[50]; - int32 faction_value; - - // If we're dropping from MAX or raising from MIN or repairing, - // we should base the message on the new updated value so we don't show - // a min MAX message - // - // If we're changing any other place, we use the value before the - // hit. For example, if we go from 1199 to 1200 which is the MAX - // we still want to say faction got better this time around. - - if (!EQ::ValueWithin(faction_before_hit, this_faction_min, this_faction_max)) { - faction_value = totalvalue; - } else { - faction_value = faction_before_hit; - } + worldserver.SendPacket(Pack); - // default to Faction# if we couldn't get the name from the ID - if (!content_db.GetFactionName(faction_id, name, sizeof(name))) { - snprintf(name, sizeof(name), "Faction%i", faction_id); - } + safe_delete(Pack); - if (tmpvalue == 0 || temp == 1 || temp == 2) { - return; - } else if (faction_value >= this_faction_max) { - MessageString(Chat::Yellow, FACTION_BEST, name); - } else if (faction_value <= this_faction_min) { - MessageString(Chat::Yellow, FACTION_WORST, name); - } else if (tmpvalue > 0 && !RuleB(Client, UseLiveFactionMessage)) { - MessageString(Chat::Yellow, FACTION_BETTER, name); - } else if (tmpvalue < 0 && !RuleB(Client, UseLiveFactionMessage)) { - MessageString(Chat::Yellow, FACTION_WORSE, name); - } else if (RuleB(Client, UseLiveFactionMessage)) { - Message( - Chat::Yellow, - fmt::format( - "Your faction standing with {} has been adjusted by {}.", - name, - tmpvalue - ).c_str() - ); - } //New Live faction message (14261) + entity_list.RemoveAllCorpsesByCharID(CharacterID()); } -void Client::LoadAccountFlags() +void Client::DepopPlayerCorpse(uint32 dbid) { - accountflags.clear(); + auto Pack = new ServerPacket(ServerOP_DepopPlayerCorpse, sizeof(ServerDepopPlayerCorpse_Struct)); - const auto& l = AccountFlagsRepository::GetWhere(database, fmt::format("p_accid = {}", account_id)); - if (l.empty()) { - return; - } + ServerDepopPlayerCorpse_Struct *sdpcs = (ServerDepopPlayerCorpse_Struct*)Pack->pBuffer; - for (const auto& e : l) { - accountflags[e.p_flag] = e.p_value; - } -} + sdpcs->DBID = dbid; + sdpcs->ZoneID = zone->GetZoneID(); + sdpcs->InstanceID = zone->GetInstanceID(); -void Client::ClearAccountFlag(const std::string& flag) -{ - auto e = AccountFlagsRepository::NewEntity(); + worldserver.SendPacket(Pack); - e.p_accid = account_id; - e.p_flag = flag; + safe_delete(Pack); - AccountFlagsRepository::ClearFlag(database, e); + entity_list.RemoveCorpseByDBID(dbid); } -void Client::SetAccountFlag(const std::string& flag, const std::string& value) +void Client::BuryPlayerCorpses() +{ + database.BuryAllCharacterCorpses(CharacterID()); +} + +void Client::NotifyNewTitlesAvailable() { - auto e = AccountFlagsRepository::NewEntity(); + auto outapp = new EQApplicationPacket(OP_NewTitlesAvailable, 0); - e.p_accid = account_id; - e.p_flag = flag; - e.p_value = value; + QueuePacket(outapp); - AccountFlagsRepository::ReplaceFlag(database, e); + safe_delete(outapp); - accountflags[flag] = value; } -std::string Client::GetAccountFlag(const std::string& flag) +void Client::SetStartZone(uint32 zoneid, float x, float y, float z, float heading) { - return accountflags[flag]; + // setting city to zero allows the player to use /setstartcity to set the city themselves + if(zoneid == 0) { + m_pp.binds[4].zone_id = 0; + Message(Chat::Yellow,"Your starting city has been reset. Use /setstartcity to choose a new one"); + return; + } + + // check to make sure the zone is valid + const char *target_zone_name = ZoneName(zoneid); + if(target_zone_name == nullptr) + return; + + m_pp.binds[4].zone_id = zoneid; + if(zone->GetInstanceID() != 0 && zone->IsInstancePersistent()) { + m_pp.binds[4].instance_id = zone->GetInstanceID(); + } + + if (x == 0 && y == 0 && z == 0) { + auto zd = GetZone(m_pp.binds[4].zone_id); + if (zd) { + m_pp.binds[4].x = zd->safe_x; + m_pp.binds[4].y = zd->safe_y; + m_pp.binds[4].z = zd->safe_z; + m_pp.binds[4].heading = zd->safe_heading; + } + } + else { + m_pp.binds[4].x = x; + m_pp.binds[4].y = y; + m_pp.binds[4].z = z; + m_pp.binds[4].heading = heading; + } } -std::vector Client::GetAccountFlags() +uint32 Client::GetStartZone() { - std::vector l; - - l.reserve(accountflags.size()); - - for (const auto& e : accountflags) { - l.emplace_back(e.first); - } + return m_pp.binds[4].zone_id; +} - return l; +void Client::ShowSkillsWindow() +{ + std::string popup_text; + std::map skills_map = EQ::skills::GetSkillTypeMap(); + + if (ClientVersion() < EQ::versions::ClientVersion::RoF2) { + skills_map[EQ::skills::Skill1HPiercing] = "Piercing"; + } + + // Table Start + popup_text += ""; + + for (const auto& skill : skills_map) { + auto skill_id = skill.first; + auto skill_name = skill.second; + auto can_have_skill = CanHaveSkill(skill_id); + auto current_skill = GetSkill(skill_id); + auto max_skill = MaxSkill(skill_id); + auto skill_maxed = current_skill >= max_skill; + if ( + skill_id == EQ::skills::Skill2HPiercing && + ClientVersion() < EQ::versions::ClientVersion::RoF2 + ) { + continue; + } + + if ( + !can_have_skill || + !current_skill || + !max_skill + ) { + continue; + } + + // Row Start + popup_text += ""; + + // Skill Name + popup_text += fmt::format( + "", + skill_name + ); + + // Current Skill Level out of Max Skill Level or a Check Mark for Maxed + popup_text += fmt::format( + "", + ( + skill_maxed ? + "Max" : + fmt::format( + "{}/{}", + current_skill, + max_skill + ) + ) + ); + + // Row End + popup_text += ""; + } + + // Table End + popup_text += "
{}{}
"; + + SendPopupToClient( + "Skills", + popup_text.c_str() + ); } -void Client::ItemTimerCheck() +void Client::Signal(int signal_id) { - int i; - for (i = EQ::invslot::POSSESSIONS_BEGIN; i <= EQ::invslot::POSSESSIONS_END; i++) - { - TryItemTimer(i); - } - for (i = EQ::invbag::GENERAL_BAGS_BEGIN; i <= EQ::invbag::CURSOR_BAG_END; i++) - { - TryItemTimer(i); - } + if (parse->PlayerHasQuestSub(EVENT_SIGNAL)) { + parse->EventPlayer(EVENT_SIGNAL, this, std::to_string(signal_id), 0); + } } -void Client::TryItemTimer(int slot) +void Client::SendPayload(int payload_id, std::string payload_value) { - EQ::ItemInstance* inst = m_inv.GetItem(slot); - if(!inst) { - return; - } + if (parse->PlayerHasQuestSub(EVENT_PAYLOAD)) { + const auto& export_string = fmt::format("{} {}", payload_id, payload_value); - auto item_timers = inst->GetTimers(); - auto it_iter = item_timers.begin(); - while(it_iter != item_timers.end()) { - if(it_iter->second.Check()) { - if (parse->ItemHasQuestSub(inst, EVENT_TIMER)) { - parse->EventItem(EVENT_TIMER, this, inst, nullptr, it_iter->first, 0); - } - } - ++it_iter; - } + parse->EventPlayer(EVENT_PAYLOAD, this, export_string, 0); + } +} - if (slot > EQ::invslot::EQUIPMENT_END) { - return; - } +void Client::SendRewards() +{ + std::vector rewards; + std::string query = StringFormat("SELECT reward_id, amount " + "FROM account_rewards " + "WHERE account_id = %i " + "ORDER BY reward_id", AccountID()); + auto results = database.QueryDatabase(query); + if (!results.Success()) { + return; + } + + for (auto row = results.begin(); row != results.end(); ++row) { + ClientReward cr; + cr.id = Strings::ToInt(row[0]); + cr.amount = Strings::ToInt(row[1]); + rewards.push_back(cr); + } + + if(rewards.empty()) + return; + + auto vetapp = new EQApplicationPacket(OP_VetRewardsAvaliable, (sizeof(InternalVeteranReward) * rewards.size())); + uchar *data = vetapp->pBuffer; + for(int i = 0; i < rewards.size(); ++i) { + InternalVeteranReward *ivr = (InternalVeteranReward*)data; + ivr->claim_id = rewards[i].id; + ivr->number_available = rewards[i].amount; + auto iter = zone->VeteranRewards.begin(); + for (;iter != zone->VeteranRewards.end(); ++iter) + if((*iter).claim_id == rewards[i].id) + break; + + if(iter != zone->VeteranRewards.end()) { + InternalVeteranReward ivro = (*iter); + ivr->claim_count = ivro.claim_count; + for(int x = 0; x < ivro.claim_count; ++x) { + ivr->items[x].item_id = ivro.items[x].item_id; + ivr->items[x].charges = ivro.items[x].charges; + strcpy(ivr->items[x].item_name, ivro.items[x].item_name); + } + } + + data += sizeof(InternalVeteranReward); + } + + FastQueuePacket(&vetapp); +} - for (int x = EQ::invaug::SOCKET_BEGIN; x <= EQ::invaug::SOCKET_END; ++x) - { - EQ::ItemInstance * a_inst = inst->GetAugment(x); - if(!a_inst) { - continue; - } +bool Client::TryReward(uint32 claim_id) +{ + // Make sure we have an open spot + // Make sure we have it in our acct and count > 0 + // Make sure the entry was found + // If we meet all the criteria: + // Decrement our count by 1 if it > 1 delete if it == 1 + // Create our item in bag if necessary at the free inv slot + // save + uint32 free_slot = 0xFFFFFFFF; + + for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; ++i) { + EQ::ItemInstance *item = GetInv().GetItem(i); + if (!item) { + free_slot = i; + break; + } + } + + if (free_slot == 0xFFFFFFFF) + return false; + + std::string query = StringFormat("SELECT amount FROM account_rewards " + "WHERE account_id = %i AND reward_id = %i", + AccountID(), claim_id); + auto results = database.QueryDatabase(query); + if (!results.Success()) + return false; + + if (results.RowCount() == 0) + return false; + + auto row = results.begin(); + + uint32 amt = Strings::ToInt(row[0]); + if (amt == 0) + return false; + + auto iter = std::find_if(zone->VeteranRewards.begin(), zone->VeteranRewards.end(), + [claim_id](const InternalVeteranReward &a) { return a.claim_id == claim_id; }); + + if (iter == zone->VeteranRewards.end()) + return false; + + if (amt == 1) { + query = StringFormat("DELETE FROM account_rewards " + "WHERE account_id = %i AND reward_id = %i", + AccountID(), claim_id); + auto results = database.QueryDatabase(query); + } else { + query = StringFormat("UPDATE account_rewards SET amount = (amount-1) " + "WHERE account_id = %i AND reward_id = %i", + AccountID(), claim_id); + auto results = database.QueryDatabase(query); + } + + auto &ivr = (*iter); + EQ::ItemInstance *claim = database.CreateItem(ivr.items[0].item_id, ivr.items[0].charges); + if (!claim) { + Save(); + return true; + } + + bool lore_conflict = CheckLoreConflict(claim->GetItem()); + + for (int y = 1; y < 8; y++) + if (ivr.items[y].item_id && claim->GetItem()->ItemClass == 1) { + EQ::ItemInstance *item_temp = database.CreateItem(ivr.items[y].item_id, ivr.items[y].charges); + if (item_temp) { + if (CheckLoreConflict(item_temp->GetItem())) { + lore_conflict = true; + DuplicateLoreMessage(ivr.items[y].item_id); + } + claim->PutItem(y - 1, *item_temp); + safe_delete(item_temp); + } + } + + if (lore_conflict) { + safe_delete(claim); + return true; + } + + PutItemInInventory(free_slot, *claim); + SendItemPacket(free_slot, claim, ItemPacketTrade); + safe_delete(claim); + + Save(); + return true; +} - auto& item_timers = a_inst->GetTimers(); - auto it_iter = item_timers.begin(); - while(it_iter != item_timers.end()) { - if(it_iter->second.Check()) { - if (parse->ItemHasQuestSub(a_inst, EVENT_TIMER)) { - parse->EventItem(EVENT_TIMER, this, a_inst, nullptr, it_iter->first, 0); - } - } - ++it_iter; - } - } +uint32 Client::GetLDoNPointsTheme(uint32 t) +{ + switch(t) + { + case LDoNTheme::GUK: + return m_pp.ldon_points_guk; + case LDoNTheme::MIR: + return m_pp.ldon_points_mir; + case LDoNTheme::MMC: + return m_pp.ldon_points_mmc; + case LDoNTheme::RUJ: + return m_pp.ldon_points_ruj; + case LDoNTheme::TAK: + return m_pp.ldon_points_tak; + default: + return 0; + } } -void Client::SendItemScale(EQ::ItemInstance *inst) { - int slot = m_inv.GetSlotByItemInst(inst); - if(slot != -1) { - inst->ScaleItem(); - SendItemPacket(slot, inst, ItemPacketCharmUpdate); - CalcBonuses(); - } +uint32 Client::GetLDoNWinsTheme(uint32 t) +{ + switch(t) + { + case LDoNTheme::GUK: + return m_pp.ldon_wins_guk; + case LDoNTheme::MIR: + return m_pp.ldon_wins_mir; + case LDoNTheme::MMC: + return m_pp.ldon_wins_mmc; + case LDoNTheme::RUJ: + return m_pp.ldon_wins_ruj; + case LDoNTheme::TAK: + return m_pp.ldon_wins_tak; + default: + return 0; + } } -void Client::AddRespawnOption(std::string option_name, uint32 zoneid, uint16 instance_id, float x, float y, float z, float heading, bool initial_selection, int8 position) +uint32 Client::GetLDoNLossesTheme(uint32 t) { - //If respawn window is already open, any changes would create an inconsistency with the client - if (IsHoveringForRespawn()) { return; } - - if (zoneid == 0) - zoneid = zone->GetZoneID(); - - //Create respawn option - RespawnOption res_opt; - res_opt.name = option_name; - res_opt.zone_id = zoneid; - res_opt.instance_id = instance_id; - res_opt.x = x; - res_opt.y = y; - res_opt.z = z; - res_opt.heading = heading; - - if (position == -1 || position >= respawn_options.size()) - { - //No position specified, or specified beyond the end, simply append - respawn_options.push_back(res_opt); - //Make this option the initial selection for the window if desired - if (initial_selection) - initial_respawn_selection = static_cast(respawn_options.size()) - 1; - } - else if (position == 0) - { - respawn_options.push_front(res_opt); - if (initial_selection) - initial_respawn_selection = 0; - } - else - { - //Insert new option between existing options - std::list::iterator itr; - uint8 pos = 0; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - if (pos++ == position) - { - respawn_options.insert(itr,res_opt); - //Make this option the initial selection for the window if desired - if (initial_selection) - initial_respawn_selection = pos; - return; - } - } - } + switch(t) + { + case LDoNTheme::GUK: + return m_pp.ldon_losses_guk; + case LDoNTheme::MIR: + return m_pp.ldon_losses_mir; + case LDoNTheme::MMC: + return m_pp.ldon_losses_mmc; + case LDoNTheme::RUJ: + return m_pp.ldon_losses_ruj; + case LDoNTheme::TAK: + return m_pp.ldon_losses_tak; + default: + return 0; + } } -bool Client::RemoveRespawnOption(std::string option_name) +void Client::UpdateLDoNWinLoss(uint32 theme_id, bool win, bool remove) { + switch (theme_id) { + case LDoNTheme::GUK: + if (win) { + m_pp.ldon_wins_guk += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_guk += (remove ? -1 : 1); + } + break; + case LDoNTheme::MIR: + if (win) { + m_pp.ldon_wins_mir += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_mir += (remove ? -1 : 1); + } + break; + case LDoNTheme::MMC: + if (win) { + m_pp.ldon_wins_mmc += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_mmc += (remove ? -1 : 1); + } + break; + case LDoNTheme::RUJ: + if (win) { + m_pp.ldon_wins_ruj += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_ruj += (remove ? -1 : 1); + } + break; + case LDoNTheme::TAK: + if (win) { + m_pp.ldon_wins_tak += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_tak += (remove ? -1 : 1); + } + break; + default: + return; + } + database.UpdateAdventureStatsEntry(CharacterID(), theme_id, win, remove); +} + + +void Client::SuspendMinion(int value) { - //If respawn window is already open, any changes would create an inconsistency with the client - if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } - - bool had = false; - RespawnOption* opt = nullptr; - std::list::iterator itr; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - opt = &(*itr); - if (opt->name.compare(option_name) == 0) - { - itr = respawn_options.erase(itr); - had = true; - //could be more with the same name, so keep going... - } - } - return had; + /* + SPA 151 Allows an extra pet to be saved and resummoned later. + Casting with a pet but without a suspended pet will suspend the pet + Casting without a pet and with a suspended pet will unsuspend the pet + effect value 0 = save pet with no buffs or equipment + effect value 1 = save pet with buffs and equipment + effect value 2 = unknown + Note: SPA 308 allows for suspended pets to be resummoned after zoning. + */ + + NPC *CurrentPet = GetPet()->CastToNPC(); + + if(!CurrentPet) + { + if(m_suspendedminion.SpellID > 0) + { + if (m_suspendedminion.SpellID >= SPDAT_RECORDS) { + Message(Chat::Red, "Invalid suspended minion spell id (%u).", m_suspendedminion.SpellID); + memset(&m_suspendedminion, 0, sizeof(PetInfo)); + return; + } + + MakePoweredPet(m_suspendedminion.SpellID, spells[m_suspendedminion.SpellID].teleport_zone, + m_suspendedminion.petpower, m_suspendedminion.Name, m_suspendedminion.size); + + CurrentPet = GetPet()->CastToNPC(); + + if(!CurrentPet) + { + Message(Chat::Red, "Failed to recall suspended minion."); + return; + } + + if(value >= 1) + { + CurrentPet->SetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items); + + CurrentPet->SendPetBuffsToClient(); + } + CurrentPet->CalcBonuses(); + + CurrentPet->SetHP(m_suspendedminion.HP); + + CurrentPet->SetMana(m_suspendedminion.Mana); + + CurrentPet->SetTaunting(m_suspendedminion.taunting); + + MessageString(Chat::Magenta, SUSPEND_MINION_UNSUSPEND, CurrentPet->GetCleanName()); + + memset(&m_suspendedminion, 0, sizeof(struct PetInfo)); + // TODO: These pet command states need to be synced ... + // Will just fix them for now + if (m_ClientVersionBit & EQ::versions::maskUFAndLater) { + SetPetCommandState(PET_BUTTON_SIT, 0); + SetPetCommandState(PET_BUTTON_STOP, 0); + SetPetCommandState(PET_BUTTON_REGROUP, 0); + SetPetCommandState(PET_BUTTON_FOLLOW, 1); + SetPetCommandState(PET_BUTTON_GUARD, 0); + // Taunt saved on client side for logging on with pet + // In our db for when we zone. + SetPetCommandState(PET_BUTTON_HOLD, 0); + SetPetCommandState(PET_BUTTON_GHOLD, 0); + SetPetCommandState(PET_BUTTON_FOCUS, 0); + SetPetCommandState(PET_BUTTON_SPELLHOLD, 0); + } + } + else + return; + + } + else + { + uint16 SpellID = CurrentPet->GetPetSpellID(); + + if(SpellID) + { + if(m_suspendedminion.SpellID > 0) + { + MessageString(Chat::Red,ONLY_ONE_PET); + + return; + } + else if(CurrentPet->IsEngaged()) + { + MessageString(Chat::Red,SUSPEND_MINION_FIGHTING); + + return; + } + else if(entity_list.Fighting(CurrentPet)) + { + MessageString(Chat::Blue,SUSPEND_MINION_HAS_AGGRO); + } + else + { + m_suspendedminion.SpellID = SpellID; + + m_suspendedminion.HP = CurrentPet->GetHP();; + + m_suspendedminion.Mana = CurrentPet->GetMana(); + m_suspendedminion.petpower = CurrentPet->GetPetPower(); + m_suspendedminion.size = CurrentPet->GetSize(); + + if(value >= 1) + CurrentPet->GetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items, m_suspendedminion.Name); + else + strn0cpy(m_suspendedminion.Name, CurrentPet->GetName(), 64); // Name stays even at rank 1 + + MessageString(Chat::Magenta, SUSPEND_MINION_SUSPEND, CurrentPet->GetCleanName()); + + CurrentPet->Depop(false); + + SetPetID(0); + } + } + else + { + MessageString(Chat::Red, ONLY_SUMMONED_PETS); + + return; + } + } } -bool Client::RemoveRespawnOption(uint8 position) +void Client::AddPVPPoints(uint32 Points) { - //If respawn window is already open, any changes would create an inconsistency with the client - if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } + m_pp.PVPCurrentPoints += Points; + m_pp.PVPCareerPoints += Points; - //Easy cases first... - if (position == 0) - { - respawn_options.pop_front(); - return true; - } - else if (position == (respawn_options.size() - 1)) - { - respawn_options.pop_back(); - return true; - } + Save(); - std::list::iterator itr; - uint8 pos = 0; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - if (pos++ == position) - { - respawn_options.erase(itr); - return true; - } - } - return false; + SendPVPStats(); } -void Client::SetHunger(int32 in_hunger) -{ - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); - Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; - sta->food = in_hunger; - sta->water = m_pp.thirst_level > 6000 ? 6000 : m_pp.thirst_level; - - m_pp.hunger_level = in_hunger; +void Client::AddEbonCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentEbonCrystals += amount; + m_pp.careerEbonCrystals += amount; + + SaveCurrency(); + SendCrystalCounts(); + + MessageString( + Chat::Yellow, + YOU_RECEIVE, + fmt::format( + "{} {}", + amount, + database.CreateItemLink(RuleI(Zone, EbonCrystalItemID)) + ).c_str() + ); + + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { + const std::string &export_string = fmt::format( + "{} 0 {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); + } +} - QueuePacket(outapp); - safe_delete(outapp); +void Client::AddRadiantCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentRadCrystals += amount; + m_pp.careerRadCrystals += amount; + + SaveCurrency(); + SendCrystalCounts(); + + MessageString( + Chat::Yellow, + YOU_RECEIVE, + fmt::format( + "{} {}", + amount, + database.CreateItemLink(RuleI(Zone, RadiantCrystalItemID)) + ).c_str() + ); + + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { + const std::string &export_string = fmt::format( + "0 {} {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); + } } -void Client::SetThirst(int32 in_thirst) -{ - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); - Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; - sta->food = m_pp.hunger_level > 6000 ? 6000 : m_pp.hunger_level; - sta->water = in_thirst; +void Client::RemoveEbonCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentEbonCrystals -= amount; - m_pp.thirst_level = in_thirst; + SaveCurrency(); + SendCrystalCounts(); - QueuePacket(outapp); - safe_delete(outapp); + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { + const std::string &export_string = fmt::format( + "{} 0 {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); + } } -void Client::SetIntoxication(int32 in_intoxication) -{ - m_pp.intoxication = EQ::Clamp(in_intoxication, 0, 200); +void Client::RemoveRadiantCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentRadCrystals -= amount; + + SaveCurrency(); + SendCrystalCounts(); + + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { + const std::string &export_string = fmt::format( + "0 {} {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); + } } -void Client::SetConsumption(int32 in_hunger, int32 in_thirst) -{ - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); - Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; - sta->food = in_hunger; - sta->water = in_thirst; +void Client::SetEbonCrystals(uint32 value) { + m_pp.currentEbonCrystals = value; + SaveCurrency(); + SendCrystalCounts(); +} - m_pp.hunger_level = in_hunger; - m_pp.thirst_level = in_thirst; +void Client::SetRadiantCrystals(uint32 value) { + m_pp.currentRadCrystals = value; + SaveCurrency(); + SendCrystalCounts(); +} - QueuePacket(outapp); - safe_delete(outapp); +// Processes a client request to inspect a SoF+ client's equipment. +void Client::ProcessInspectRequest(Client *requestee, Client *requester) +{ + if (requestee && requester) { + auto outapp = new EQApplicationPacket(OP_InspectAnswer, sizeof(InspectResponse_Struct)); + auto insr = (InspectResponse_Struct *) outapp->pBuffer; + + insr->TargetID = requester->GetID(); + insr->playerid = requestee->GetID(); + + const EQ::ItemData *item = nullptr; + const EQ::ItemInstance *inst = nullptr; + + for (int16 L = EQ::invslot::EQUIPMENT_BEGIN; L <= EQ::invslot::EQUIPMENT_END; L++) { + inst = requestee->GetInv().GetItem(L); + + if (inst) { + item = inst->GetItem(); + if (item) { + strcpy(insr->itemnames[L], item->Name); + + const EQ::ItemData *augment_item = nullptr; + const auto augment = inst->GetOrnamentationAugment(); + + if (augment) { + augment_item = augment->GetItem(); + } + + if (augment_item) { + insr->itemicons[L] = augment_item->Icon; + } else if (inst->GetOrnamentationIcon()) { + insr->itemicons[L] = inst->GetOrnamentationIcon(); + } else { + insr->itemicons[L] = item->Icon; + } + } else { + insr->itemnames[L][0] = '\0'; + insr->itemicons[L] = 0xFFFFFFFF; + } + } else { + insr->itemnames[L][0] = '\0'; + insr->itemicons[L] = 0xFFFFFFFF; + } + } + + strcpy(insr->text, requestee->GetInspectMessage().text); + + // There could be an OP for this..or not... (Ti clients are not processed here..this message is generated client-side) + if (requestee->IsClient() && requestee != requester) { + requestee->Message( + Chat::White, + fmt::format( + "{} is looking at your equipment...", + requester->GetName() + ).c_str() + ); + } + + requester->QueuePacket(outapp); // Send answer to requester + safe_delete(outapp); + } } -void Client::Consume(const EQ::ItemData *item, uint8 type, int16 slot, bool auto_consume) +void Client::GuildBankAck() { - if (!item) - return; + auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankAck_Struct)); - int increase = item->CastTime_ * 100; - if (!auto_consume) // force feeding is half as effective - increase /= 2; + GuildBankAck_Struct *gbas = (GuildBankAck_Struct*) outapp->pBuffer; - if (increase < 0) // wasn't food? oh well - return; + gbas->Action = GuildBankAcknowledge; + + FastQueuePacket(&outapp); +} - if (type == EQ::item::ItemTypeFood) { - m_pp.hunger_level += increase; +void Client::GuildBankDepositAck(bool Fail, int8 action) +{ - LogFood("Consuming food, points added to hunger_level: [{}] - current_hunger: [{}]", increase, m_pp.hunger_level); + auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankDepositAck_Struct)); - DeleteItemInInventory(slot, 1); + GuildBankDepositAck_Struct *gbdas = (GuildBankDepositAck_Struct*) outapp->pBuffer; - if (!auto_consume) // no message if the client consumed for us - entity_list.MessageCloseString(this, true, 50, 0, EATING_MESSAGE, GetName(), item->Name); + gbdas->Action = action; - LogFood("Eating from slot: [{}]", (int)slot); + gbdas->Fail = Fail ? 1 : 0; - } else { - m_pp.thirst_level += increase; + FastQueuePacket(&outapp); +} - DeleteItemInInventory(slot, 1); +void Client::ClearGuildBank() +{ + auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankClear_Struct)); - LogFood("Consuming drink, points added to thirst_level: [{}] current_thirst: [{}]", increase, m_pp.thirst_level); + GuildBankClear_Struct *gbcs = (GuildBankClear_Struct*) outapp->pBuffer; - if (!auto_consume) // no message if the client consumed for us - entity_list.MessageCloseString(this, true, 50, 0, DRINKING_MESSAGE, GetName(), item->Name); + gbcs->Action = GuildBankBulkItems; + gbcs->DepositAreaCount = 0; + gbcs->MainAreaCount = 0; - LogFood("Drinking from slot: [{}]", (int)slot); - } + FastQueuePacket(&outapp); } -void Client::SendMarqueeMessage(uint32 type, std::string message, uint32 duration) +void Client::SendGroupCreatePacket() { - if (!duration || !message.length()) { - return; - } - - EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); - ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; + // For SoD and later clients, this is sent the Group Leader upon initial creation of the group + // + auto outapp = new EQApplicationPacket(OP_GroupUpdateB, 32 + strlen(GetName())); - cms->type = type; - cms->unk04 = 10; - cms->priority = 510; - cms->fade_in_time = 0; - cms->fade_out_time = 3000; - cms->duration = duration; + char *Buffer = (char *)outapp->pBuffer; + // Header + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // group ID probably + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 1); // count of members in packet + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // Null Leader name, shouldn't be null besides this case - strcpy(cms->msg, message.c_str()); + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // Member 0, index + VARSTRUCT_ENCODE_STRING(Buffer, GetName()); // group member name + VARSTRUCT_ENCODE_TYPE(uint16, Buffer, 0); // merc flag + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // owner name (if merc) + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, GetLevel()); // level + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group tank flag + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group assist flag + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group puller flag + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // offline flag + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // timestamp - QueuePacket(&outapp); + FastQueuePacket(&outapp); } -void Client::SendMarqueeMessage(uint32 type, uint32 priority, uint32 fade_in, uint32 fade_out, uint32 duration, std::string message) +void Client::SendGroupLeaderChangePacket(const char *LeaderName) { - if (!duration || !message.length()) { - return; - } + // For SoD and later, send name of Group Leader to this client - EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); - ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; + auto outapp = new EQApplicationPacket(OP_GroupLeaderChange, sizeof(GroupLeaderChange_Struct)); - cms->type = type; - cms->unk04 = 10; - cms->priority = priority; - cms->fade_in_time = fade_in; - cms->fade_out_time = fade_out; - cms->duration = duration; + GroupLeaderChange_Struct *glcs = (GroupLeaderChange_Struct*)outapp->pBuffer; - strcpy(cms->msg, message.c_str()); + strn0cpy(glcs->LeaderName, LeaderName, sizeof(glcs->LeaderName)); - QueuePacket(&outapp); + FastQueuePacket(&outapp); } -void Client::PlayMP3(const char* fname) +void Client::SendGroupJoinAcknowledge() { - std::string filename = fname; - auto outapp = new EQApplicationPacket(OP_PlayMP3, filename.length() + 1); - PlayMP3_Struct* buf = (PlayMP3_Struct*)outapp->pBuffer; - strncpy(buf->filename, fname, filename.length()); - QueuePacket(outapp); - safe_delete(outapp); + // For SoD and later, This produces the 'You have joined the group' message. + auto outapp = new EQApplicationPacket(OP_GroupAcknowledge, 4); + FastQueuePacket(&outapp); } -void Client::ExpeditionSay(const char *str, int ExpID) { - - std::string query = StringFormat("SELECT `player_name` FROM `cust_inst_players` " - "WHERE `inst_id` = %i", ExpID); - auto results = database.QueryDatabase(query); - if (!results.Success()) - return; +void Client::SendAdventureError(const char *error) +{ + size_t error_size = strlen(error); + auto outapp = new EQApplicationPacket(OP_AdventureInfo, (error_size + 2)); + strn0cpy((char*)outapp->pBuffer, error, error_size); + FastQueuePacket(&outapp); +} - if(results.RowCount() == 0) { - Message(Chat::Lime, "You say to the expedition, '%s'", str); - return; - } +void Client::SendAdventureDetails() +{ + if(adv_data) + { + ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; + auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); + AdventureRequestResponse_Struct *arr = (AdventureRequestResponse_Struct*)outapp->pBuffer; + arr->unknown000 = 0xBFC40100; + arr->unknown2080 = 0x0A; + arr->risk = ad->risk; + strcpy(arr->text, ad->text); + + if(ad->time_to_enter != 0) + { + arr->timetoenter = ad->time_to_enter; + } + else + { + arr->timeleft = ad->time_left; + } + + if(ad->zone_in_id == zone->GetZoneID()) + { + arr->y = ad->x; + arr->x = ad->y; + arr->showcompass = 1; + } + FastQueuePacket(&outapp); + + SendAdventureCount(ad->count, ad->total); + } + else + { + ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; + auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); + FastQueuePacket(&outapp); + } +} - for(auto row = results.begin(); row != results.end(); ++row) { - const char* charName = row[0]; - if(strcmp(charName, GetCleanName()) != 0) { - worldserver.SendEmoteMessage( - charName, - 0, - AccountStatus::Player, - Chat::Lime, - fmt::format( - "{} says to the expedition, '{}'", - GetCleanName(), - str - ).c_str() - ); - } - // ChannelList->CreateChannel(ChannelName, ChannelOwner, ChannelPassword, true, Strings::ToInt(row[3])); - } +void Client::SendAdventureCount(uint32 count, uint32 total) +{ + auto outapp = new EQApplicationPacket(OP_AdventureUpdate, sizeof(AdventureCountUpdate_Struct)); + AdventureCountUpdate_Struct *acu = (AdventureCountUpdate_Struct*)outapp->pBuffer; + acu->current = count; + acu->total = total; + FastQueuePacket(&outapp); +} +void Client::NewAdventure(int id, int theme, const char *text, int member_count, const char *members) +{ + size_t text_size = strlen(text); + auto outapp = new EQApplicationPacket(OP_AdventureDetails, text_size + 2); + strn0cpy((char*)outapp->pBuffer, text, text_size); + FastQueuePacket(&outapp); + adv_requested_id = id; + adv_requested_theme = theme; + safe_delete_array(adv_requested_data); + adv_requested_member_count = member_count; + adv_requested_data = new char[64 * member_count]; + memcpy(adv_requested_data, members, (64 * member_count)); } -int Client::GetQuiverHaste(int delay) +void Client::ClearPendingAdventureData() { - const EQ::ItemInstance *pi = nullptr; - for (int r = EQ::invslot::GENERAL_BEGIN; r <= EQ::invslot::GENERAL_END; r++) { - pi = GetInv().GetItem(r); - if (pi && pi->IsClassBag() && pi->GetItem()->BagType == EQ::item::BagTypeQuiver && - pi->GetItem()->BagWR > 0) - break; - if (r == EQ::invslot::GENERAL_END) - // we will get here if we don't find a valid quiver - return 0; - } - return (pi->GetItem()->BagWR * 0.0025f * delay) + 1; + adv_requested_id = 0; + adv_requested_theme = LDoNTheme::Unused; + safe_delete_array(adv_requested_data); + adv_requested_member_count = 0; } -void Client::SendColoredText(uint32 color, std::string message) +bool Client::IsOnAdventure() { - // arbitrary size limit - if (message.size() > 512) // live does send this with empty strings sometimes ... - return; - auto outapp = new EQApplicationPacket(OP_ColoredText, sizeof(ColoredText_Struct) + message.size()); - ColoredText_Struct *cts = (ColoredText_Struct *)outapp->pBuffer; - cts->color = color; - strcpy(cts->msg, message.c_str()); - QueuePacket(outapp); - safe_delete(outapp); + if(adv_data) + { + ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; + if(ad->zone_in_id == 0) + { + return false; + } + else + { + return true; + } + } + return false; } +void Client::LeaveAdventure() +{ + if(!GetPendingAdventureLeave()) + { + PendingAdventureLeave(); + auto pack = new ServerPacket(ServerOP_AdventureLeave, 64); + strcpy((char*)pack->pBuffer, GetName()); + worldserver.SendPacket(pack); + delete pack; + } +} -void Client::QuestReward(Mob* target, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, uint32 itemid, uint32 exp, bool faction) +void Client::ClearCurrentAdventure() { + if(adv_data) + { + ServerSendAdventureData_Struct* ds = (ServerSendAdventureData_Struct*)adv_data; + if(ds->finished_adventures > 0) + { + ds->instance_id = 0; + ds->risk = 0; + memset(ds->text, 0, 512); + ds->time_left = 0; + ds->time_to_enter = 0; + ds->x = 0; + ds->y = 0; + ds->zone_in_id = 0; + ds->zone_in_object = 0; + } + else + { + safe_delete(adv_data); + } + + SendAdventureError("You are not currently assigned to an adventure."); + } +} - auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); - memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); - QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; +void Client::AdventureFinish(bool win, int theme, int points) +{ + UpdateLDoNPoints(theme, points); + auto outapp = new EQApplicationPacket(OP_AdventureFinish, sizeof(AdventureFinish_Struct)); + AdventureFinish_Struct *af = (AdventureFinish_Struct*)outapp->pBuffer; + af->win_lose = win ? 1 : 0; + af->points = points; + FastQueuePacket(&outapp); +} - qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name - qr->target_id = GetID(); // The Client ID (this) - qr->copper = copper; - qr->silver = silver; - qr->gold = gold; - qr->platinum = platinum; - qr->item_id[0] = itemid; - qr->exp_reward = exp; +void Client::CheckLDoNHail(NPC* n) +{ + if (!zone->adv_data || !n || n->GetOwnerID()) { + return; + } + + auto* ds = (ServerZoneAdventureDataReply_Struct*) zone->adv_data; + if (ds->type != Adventure_Rescue || ds->data_id != n->GetNPCTypeID()) { + return; + } + + if (entity_list.CheckNPCsClose(n)) { + n->Say( + "You're here to save me? I couldn't possibly risk leaving yet. There are " + "far too many of those horrid things out there waiting to recapture me! Please get " + "rid of some more of those vermin and then we can try to leave." + ); + return; + } + + auto pet = GetPet(); + if (pet) { + if (pet->GetPetType() == petCharmed) { + pet->BuffFadeByEffect(SE_Charm); + } else if (pet->GetPetType() == petNPCFollow) { + pet->SetOwnerID(0); + } else { + pet->Depop(); + } + } + + SetPet(n); + n->SetOwnerID(GetID()); + n->Say( + "Wonderful! Someone to set me free! I feared for my life for so long, " + "never knowing when they might choose to end my life. Now that you're here though " + "I can rest easy. Please help me find my way out of here as soon as you can " + "I'll stay close behind you!" + ); +} - if (copper > 0 || silver > 0 || gold > 0 || platinum > 0) { - AddMoneyToPP(copper, silver, gold, platinum); - } +void Client::CheckEmoteHail(NPC* n, const char* message) +{ + if (!Strings::BeginsWith(Strings::ToLower(message), "hail")) { + return; + } - if (itemid > 0) { - SummonItemIntoInventory(itemid, -1, 0, 0, 0, 0, 0, 0, false); - } + if (!n || n->GetOwnerID()) { + return; + } - if (faction) { - if (target && target->IsNPC() && !target->IsCharmed()) { - int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); - SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); - qr->faction = target->CastToNPC()->GetPrimaryFaction(); - qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. - } - } + const uint32 emote_id = n->GetEmoteID(); + if (emote_id) { + n->DoNPCEmote(EQ::constants::EmoteEventTypes::Hailed, emote_id, this); + } +} - if (exp > 0) { - AddEXP(ExpSource::Quest, exp); - } +void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count) +{ + m_has_quest_compass = (count != 0); + m_quest_compass.x = in_x; + m_quest_compass.y = in_y; + m_quest_compass.z = in_z; - QueuePacket(outapp, true, Client::CLIENT_CONNECTED); - safe_delete(outapp); + SendDzCompassUpdate(); } -void Client::QuestReward(Mob* target, const QuestReward_Struct &reward, bool faction) +void Client::SendZonePoints() { - auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); - memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); - QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; + int count = 0; + LinkedListIterator iterator(zone->zone_point_list); + iterator.Reset(); + while (iterator.MoreElements()) { + ZonePoint *data = iterator.GetData(); + + if (ClientVersionBit() & data->client_version_mask) { + count++; + } + + iterator.Advance(); + } + + uint32 zpsize = sizeof(ZonePoints) + ((count + 1) * sizeof(ZonePoint_Entry)); + auto outapp = new EQApplicationPacket(OP_SendZonepoints, zpsize); + ZonePoints* zp = (ZonePoints*)outapp->pBuffer; + zp->count = count; + + int i = 0; + iterator.Reset(); + while(iterator.MoreElements()) + { + ZonePoint* data = iterator.GetData(); + + LogZonePoints( + "Sending zone point to client [{}] mask [{}] x [{}] y [{}] z [{}] number [{}]", + GetCleanName(), + ClientVersionBit() & data->client_version_mask ? "true" : "false", + data->x, + data->y, + data->z, + data->number + ); + + if(ClientVersionBit() & data->client_version_mask) + { + zp->zpe[i].iterator = data->number; + zp->zpe[i].x = data->target_x; + zp->zpe[i].y = data->target_y; + zp->zpe[i].z = data->target_z; + zp->zpe[i].heading = data->target_heading; + zp->zpe[i].zoneid = data->target_zone_id; + + // if the target zone is the same as the current zone, use the instance of the current zone + // if we don't use the same instance_id that the client was sent, the client will forcefully + // issue a zone change request when they should be simply moving to a different point in the same zone + // because the client will think the zone point target is different from the current instance + auto target_instance = data->target_zone_instance; + if (data->target_zone_id == zone->GetZoneID() && data->target_zone_instance == 0) { + target_instance = zone->GetInstanceID(); + } + + zp->zpe[i].zoneinstance = target_instance; + i++; + } + iterator.Advance(); + } + + FastQueuePacket(&outapp); +} - memcpy(qr, &reward, sizeof(QuestReward_Struct)); +void Client::SendTargetCommand(uint32 EntityID) +{ + auto outapp = new EQApplicationPacket(OP_TargetCommand, sizeof(ClientTarget_Struct)); + ClientTarget_Struct *cts = (ClientTarget_Struct*)outapp->pBuffer; + cts->new_target = EntityID; + FastQueuePacket(&outapp); +} - // not set in caller because reasons - qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name, tasks won't set this +void Client::LocateCorpse() +{ + Corpse *ClosestCorpse = nullptr; + if(!GetTarget()) + ClosestCorpse = entity_list.GetClosestCorpse(this, nullptr); + else if(GetTarget()->IsCorpse()) + ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->CastToCorpse()->GetOwnerName()); + else + ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->GetCleanName()); + + if(ClosestCorpse) + { + MessageString(Chat::Spells, SENSE_CORPSE_DIRECTION); + SetHeading(CalculateHeadingToTarget(ClosestCorpse->GetX(), ClosestCorpse->GetY())); + SetTarget(ClosestCorpse); + SendTargetCommand(ClosestCorpse->GetID()); + SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0, true); + } + else if(!GetTarget()) + MessageString(Chat::Red, SENSE_CORPSE_NONE); + else + MessageString(Chat::Red, SENSE_CORPSE_NOT_NAME); +} - if (reward.copper > 0 || reward.silver > 0 || reward.gold > 0 || reward.platinum > 0) { - AddMoneyToPP(reward.copper, reward.silver, reward.gold, reward.platinum); - } +void Client::NPCSpawn(NPC *target_npc, const char *identifier, uint32 extra) +{ + if (!target_npc || !identifier) { + return; + } + + std::string spawn_type = Strings::ToLower(identifier); + bool is_add = spawn_type.find("add") != std::string::npos; + bool is_create = spawn_type.find("create") != std::string::npos; + bool is_delete = spawn_type.find("delete") != std::string::npos; + bool is_remove = spawn_type.find("remove") != std::string::npos; + bool is_update = spawn_type.find("update") != std::string::npos; + bool is_clone = spawn_type.find("clone") != std::string::npos; + if (is_add || is_create) { + // extra sets the Respawn Timer for add/create + content_db.NPCSpawnDB( + is_add ? NPCSpawnTypes::AddNewSpawngroup : NPCSpawnTypes::CreateNewSpawn, + zone->GetShortName(), + zone->GetInstanceVersion(), + this, + target_npc->CastToNPC(), + extra + ); + } else if (is_delete || is_remove || is_update) { + uint8 spawn_update_type = ( + is_delete ? + NPCSpawnTypes::DeleteSpawn : + ( + is_remove ? + NPCSpawnTypes::RemoveSpawn : + NPCSpawnTypes::UpdateAppearance + ) + ); + content_db.NPCSpawnDB( + spawn_update_type, + zone->GetShortName(), + zone->GetInstanceVersion(), + this, + target_npc->CastToNPC(), + extra + ); + } else if (is_clone) { + content_db.NPCSpawnDB( + NPCSpawnTypes::AddSpawnFromSpawngroup, + zone->GetShortName(), + zone->GetInstanceVersion(), + this, + target_npc->CastToNPC(), + extra + ); + } +} - for (int i = 0; i < QUESTREWARD_COUNT; ++i) { - if (reward.item_id[i] > 0) { - SummonItemIntoInventory(reward.item_id[i], -1, 0, 0, 0, 0, 0, 0, false); - } - } +bool Client::IsDraggingCorpse(uint16 CorpseID) +{ + for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { + if (It->second == CorpseID) + return true; + } - // only process if both are valid - // if we don't have a target here, we want to just reward, but if there is a target, need to check charm - if (reward.faction && reward.faction_mod && (target == nullptr || !target->IsCharmed())) { - RewardFaction(reward.faction, reward.faction_mod); - } + return false; +} - // legacy support - if (faction) { - if (target && target->IsNPC() && !target->IsCharmed()) { - int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); - SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); - qr->faction = target->CastToNPC()->GetPrimaryFaction(); - qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. - } - } +void Client::DragCorpses() +{ + for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { + Mob *corpse = entity_list.GetMob(It->second); - if (reward.exp_reward > 0) { - AddEXP(ExpSource::Quest, reward.exp_reward); - } + if (corpse && corpse->IsPlayerCorpse() && + (DistanceSquaredNoZ(m_Position, corpse->GetPosition()) <= RuleR(Character, DragCorpseDistance))) + continue; - QueuePacket(outapp, true, Client::CLIENT_CONNECTED); - safe_delete(outapp); + if (!corpse || !corpse->IsPlayerCorpse() || + corpse->CastToCorpse()->IsBeingLooted() || + !corpse->CastToCorpse()->Summon(this, false, false)) { + MessageString(Chat::DefaultText, CORPSEDRAG_STOP); + It = DraggedCorpses.erase(It); + if (It == DraggedCorpses.end()) + break; + } + } } -void Client::CashReward(uint32 copper, uint32 silver, uint32 gold, uint32 platinum) +void Client::ConsentCorpses(std::string consent_name, bool deny) { - auto outapp = std::make_unique(OP_CashReward, sizeof(CashReward_Struct)); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->copper = copper; - outbuf->silver = silver; - outbuf->gold = gold; - outbuf->platinum = platinum; + if (strcasecmp(consent_name.c_str(), GetName()) == 0) { + MessageString(Chat::Red, CONSENT_YOURSELF); + } + else if (!consent_throttle_timer.Check()) { + MessageString(Chat::Red, CONSENT_WAIT); + } + else { + auto pack = new ServerPacket(ServerOP_Consent, sizeof(ServerOP_Consent_Struct)); + ServerOP_Consent_Struct* scs = (ServerOP_Consent_Struct*)pack->pBuffer; + strn0cpy(scs->grantname, consent_name.c_str(), sizeof(scs->grantname)); + strn0cpy(scs->ownername, GetName(), sizeof(scs->ownername)); + strn0cpy(scs->zonename, "Unknown", sizeof(scs->zonename)); + scs->permission = deny ? 0 : 1; + scs->zone_id = zone->GetZoneID(); + scs->instance_id = zone->GetInstanceID(); + scs->consent_type = EQ::consent::Normal; + scs->consent_id = 0; + if (strcasecmp(scs->grantname, "group") == 0) { + if (!deny) { + Group* grp = GetGroup(); + scs->consent_id = grp ? grp->GetID() : 0; + } + scs->consent_type = EQ::consent::Group; + } + else if (strcasecmp(scs->grantname, "raid") == 0) { + if (!deny) { + Raid* raid = GetRaid(); + scs->consent_id = raid ? raid->GetID() : 0; + } + scs->consent_type = EQ::consent::Raid; + } + else if (strcasecmp(scs->grantname, "guild") == 0) { + if (!deny) { + scs->consent_id = GuildID(); + } + scs->consent_type = EQ::consent::Guild; + // update all corpses in db so buried/unloaded corpses see new consent id + database.UpdateCharacterCorpseConsent(CharacterID(), scs->consent_id); + } + worldserver.SendPacket(pack); + safe_delete(pack); + } +} - AddMoneyToPP(copper, silver, gold, platinum); +void Client::Doppelganger(uint16 spell_id, Mob *target, const char *name_override, int pet_count, int pet_duration) +{ + if(!target || !IsValidSpell(spell_id) || GetID() == target->GetID()) + return; + + PetRecord record; + if(!database.GetPetEntry(spells[spell_id].teleport_zone, &record)) + { + LogError("Unknown doppelganger spell id: [{}], check pets table", spell_id); + Message(Chat::Red, "Unable to find data for pet %s", spells[spell_id].teleport_zone); + return; + } + + SwarmPet_Struct pet; + pet.count = pet_count; + pet.duration = pet_duration; + pet.npc_id = record.npc_type; + + NPCType *made_npc = nullptr; + + const NPCType *npc_type = content_db.LoadNPCTypesData(pet.npc_id); + if(npc_type == nullptr) { + LogError("Unknown npc type for doppelganger spell id: [{}]", spell_id); + Message(0,"Unable to find pet!"); + return; + } + // make a custom NPC type for this + made_npc = new NPCType; + memcpy(made_npc, npc_type, sizeof(NPCType)); + + strcpy(made_npc->name, name_override); + made_npc->level = GetLevel(); + made_npc->race = GetRace(); + made_npc->gender = GetGender(); + made_npc->size = GetSize(); + made_npc->AC = GetAC(); + made_npc->STR = GetSTR(); + made_npc->STA = GetSTA(); + made_npc->DEX = GetDEX(); + made_npc->AGI = GetAGI(); + made_npc->MR = GetMR(); + made_npc->FR = GetFR(); + made_npc->CR = GetCR(); + made_npc->DR = GetDR(); + made_npc->PR = GetPR(); + made_npc->Corrup = GetCorrup(); + made_npc->PhR = GetPhR(); + // looks + made_npc->texture = GetEquipmentMaterial(EQ::textures::armorChest); + made_npc->helmtexture = GetEquipmentMaterial(EQ::textures::armorHead); + made_npc->haircolor = GetHairColor(); + made_npc->beardcolor = GetBeardColor(); + made_npc->eyecolor1 = GetEyeColor1(); + made_npc->eyecolor2 = GetEyeColor2(); + made_npc->hairstyle = GetHairStyle(); + made_npc->luclinface = GetLuclinFace(); + made_npc->beard = GetBeard(); + made_npc->drakkin_heritage = GetDrakkinHeritage(); + made_npc->drakkin_tattoo = GetDrakkinTattoo(); + made_npc->drakkin_details = GetDrakkinDetails(); + made_npc->d_melee_texture1 = GetEquipmentMaterial(EQ::textures::weaponPrimary); + made_npc->d_melee_texture2 = GetEquipmentMaterial(EQ::textures::weaponSecondary); + for (int i = EQ::textures::textureBegin; i <= EQ::textures::LastTexture; i++) { + made_npc->armor_tint.Slot[i].Color = GetEquipmentColor(i); + } + made_npc->loottable_id = 0; + + int summon_count = pet.count; + + if(summon_count > MAX_SWARM_PETS) + summon_count = MAX_SWARM_PETS; + + static const glm::vec2 swarmPetLocations[MAX_SWARM_PETS] = { + glm::vec2(5, 5), glm::vec2(-5, 5), glm::vec2(5, -5), glm::vec2(-5, -5), + glm::vec2(10, 10), glm::vec2(-10, 10), glm::vec2(10, -10), glm::vec2(-10, -10), + glm::vec2(8, 8), glm::vec2(-8, 8), glm::vec2(8, -8), glm::vec2(-8, -8) + }; + + while(summon_count > 0) { + auto npc_type_copy = new NPCType; + memcpy(npc_type_copy, made_npc, sizeof(NPCType)); + + NPC* swarm_pet_npc = new NPC( + npc_type_copy, + 0, + GetPosition() + glm::vec4(swarmPetLocations[summon_count - 1], 0.0f, 0.0f), + GravityBehavior::Water); + + if(!swarm_pet_npc->GetSwarmInfo()){ + auto nSI = new SwarmPet; + swarm_pet_npc->SetSwarmInfo(nSI); + swarm_pet_npc->GetSwarmInfo()->duration = new Timer(pet_duration*1000); + } + else{ + swarm_pet_npc->GetSwarmInfo()->duration->Start(pet_duration*1000); + } + + swarm_pet_npc->StartSwarmTimer(pet_duration * 1000); + + swarm_pet_npc->GetSwarmInfo()->owner_id = GetID(); + swarm_pet_npc->SetFollowID(GetID()); + + // Give the pets alittle more agro than the caster and then agro them on the target + target->AddToHateList(swarm_pet_npc, (target->GetHateAmount(this) + 100), (target->GetDamageAmount(this) + 100)); + swarm_pet_npc->AddToHateList(target, 1000, 1000); + swarm_pet_npc->GetSwarmInfo()->target = 0; + + //we allocated a new NPC type object, give the NPC ownership of that memory + swarm_pet_npc->GiveNPCTypeData(npc_type_copy); + + entity_list.AddNPC(swarm_pet_npc); + summon_count--; + } + + safe_delete(made_npc); +} - QueuePacket(outapp.get()); +void Client::AssignToInstance(uint16 instance_id) +{ + database.AddClientToInstance(instance_id, CharacterID()); } -void Client::RewardFaction(int faction_id, int amount) +void Client::RemoveFromInstance(uint16 instance_id) { - SetFactionLevel2(CharacterID(), faction_id, GetClass(), GetBaseRace(), GetDeity(), amount, false); + database.RemoveClientFromInstance(instance_id, CharacterID()); +} - auto f = zone->GetFactionAssociation(faction_id); - if (!f) { - return; - } +void Client::SendAltCurrencies() { + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + const uint32 currency_count = zone->AlternateCurrencies.size(); + if (!currency_count) { + return; + } - std::vector faction_ids = { - f->id_1, - f->id_2, - f->id_3, - f->id_4, - f->id_5, - f->id_6, - f->id_7, - f->id_8, - f->id_9, - f->id_10 - }; - - std::vector faction_modifiers = { - f->mod_1, - f->mod_2, - f->mod_3, - f->mod_4, - f->mod_5, - f->mod_6, - f->mod_7, - f->mod_8, - f->mod_9, - f->mod_10 - }; - - std::vector temporary_values = { - static_cast(faction_modifiers[0] * amount), - static_cast(faction_modifiers[1] * amount), - static_cast(faction_modifiers[2] * amount), - static_cast(faction_modifiers[3] * amount), - static_cast(faction_modifiers[4] * amount), - static_cast(faction_modifiers[5] * amount), - static_cast(faction_modifiers[6] * amount), - static_cast(faction_modifiers[7] * amount), - static_cast(faction_modifiers[8] * amount), - static_cast(faction_modifiers[9] * amount) - }; - - std::vector signs = { - temporary_values[0] < 0.0f ? -1 : 1, - temporary_values[1] < 0.0f ? -1 : 1, - temporary_values[2] < 0.0f ? -1 : 1, - temporary_values[3] < 0.0f ? -1 : 1, - temporary_values[4] < 0.0f ? -1 : 1, - temporary_values[5] < 0.0f ? -1 : 1, - temporary_values[6] < 0.0f ? -1 : 1, - temporary_values[7] < 0.0f ? -1 : 1, - temporary_values[8] < 0.0f ? -1 : 1, - temporary_values[9] < 0.0f ? -1 : 1 - }; - - std::vector new_values = { - std::max(1, static_cast(std::abs(temporary_values[0]))) * signs[0], - std::max(1, static_cast(std::abs(temporary_values[1]))) * signs[1], - std::max(1, static_cast(std::abs(temporary_values[2]))) * signs[2], - std::max(1, static_cast(std::abs(temporary_values[3]))) * signs[3], - std::max(1, static_cast(std::abs(temporary_values[4]))) * signs[4], - std::max(1, static_cast(std::abs(temporary_values[5]))) * signs[5], - std::max(1, static_cast(std::abs(temporary_values[6]))) * signs[6], - std::max(1, static_cast(std::abs(temporary_values[7]))) * signs[7], - std::max(1, static_cast(std::abs(temporary_values[8]))) * signs[8], - std::max(1, static_cast(std::abs(temporary_values[9]))) * signs[9] - }; - - for (uint16 slot_id = 0; slot_id < faction_ids.size(); slot_id++) { - if (faction_ids[slot_id] > 0) { - SetFactionLevel2( - CharacterID(), - faction_ids[slot_id], - GetClass(), - GetBaseRace(), - GetDeity(), - new_values[slot_id], - false - ); - } - } -} + auto outapp = new EQApplicationPacket( + OP_AltCurrency, + sizeof(AltCurrencyPopulate_Struct) + + sizeof(AltCurrencyPopulateEntry_Struct) * currency_count + ); -void Client::SendHPUpdateMarquee(){ - if (!IsClient() || !current_hp || !max_hp) { - return; - } + auto a = (AltCurrencyPopulate_Struct*) outapp->pBuffer; - /* Health Update Marquee Display: Custom*/ - const auto health_percentage = static_cast(current_hp * 100 / max_hp); - if (health_percentage >= 100) { - return; - } + a->opcode = AlternateCurrencyMode::Populate; + a->count = currency_count; - const auto health_update_notification = fmt::format("Health: {}%%", health_percentage); - SendMarqueeMessage(Chat::Yellow, 510, 0, 3000, 3000, health_update_notification); -} + uint32 currency_id = 0; + for (const auto& c : zone->AlternateCurrencies) { + const auto* item = database.GetItem(c.item_id); -uint32 Client::GetMoney(uint8 type, uint8 subtype) { - uint32 value = 0; - - switch (type) { - case MoneyTypes::Copper: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.copper); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.copper_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.copper_cursor); - break; - default: - break; - } - break; - } - case MoneyTypes::Silver: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.silver); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.silver_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.silver_cursor); - break; - default: - break; - } - break; - } - case MoneyTypes::Gold: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.gold); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.gold_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.gold_cursor); - break; - default: - break; - } - break; - } - case MoneyTypes::Platinum: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.platinum); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.platinum_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.platinum_cursor); - break; - case MoneySubtypes::SharedBank: - value = static_cast(m_pp.platinum_shared); - break; - default: - break; - } - break; - } - default: - break; - } + a->entries[currency_id].currency_number = c.id; + a->entries[currency_id].unknown00 = 1; + a->entries[currency_id].currency_number2 = c.id; + a->entries[currency_id].item_id = c.item_id; + a->entries[currency_id].item_icon = item ? item->Icon : 1000; + a->entries[currency_id].stack_size = item ? item->StackSize : 1000; - return value; -} + currency_id++; + } -int Client::GetAccountAge() { - return (time(nullptr) - GetAccountCreation()); + FastQueuePacket(&outapp); + } } -void Client::CheckRegionTypeChanges() +void Client::SetAlternateCurrencyValue(uint32 currency_id, uint32 new_amount) { - if (!zone->HasWaterMap()) { - return; - } + if (!zone->DoesAlternateCurrencyExist(currency_id)) { + return; + } - auto new_region = zone->watermap->ReturnRegionType(glm::vec3(m_Position)); + const uint32 current_amount = alternate_currency[currency_id]; - // still same region, do nothing - if (last_region_type == new_region) { - return; - } + const bool is_gain = new_amount > current_amount; - // If we got out of water clear any water aggro for water only npcs - if (last_region_type == RegionTypeWater) { - entity_list.ClearWaterAggro(this); - } + const uint32 change_amount = is_gain ? (new_amount - current_amount) : (current_amount - new_amount); - // region type changed - last_region_type = new_region; + if (!change_amount) { + return; + } - // PVP is the only state we need to keep track of, so we can just return now for PVP servers - if (RuleI(World, PVPSettings) > 0) { - return; - } + alternate_currency[currency_id] = new_amount; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); + SendAlternateCurrencyValue(currency_id); - if (last_region_type == RegionTypePVP && RuleB(World, EnablePVPRegions)) { - temp_pvp = true; - } else if (temp_pvp) { - temp_pvp = false; - } + QuestEventID event_id = is_gain ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; + if (parse->PlayerHasQuestSub(event_id)) { + const std::string &export_string = fmt::format( + "{} {} {}", + currency_id, + change_amount, + new_amount + ); + + parse->EventPlayer(event_id, this, export_string, 0); + } } -void Client::ProcessAggroMeter() +bool Client::RemoveAlternateCurrencyValue(uint32 currency_id, uint32 amount) { - if (!AggroMeterAvailable()) { - aggro_meter_timer.Disable(); - return; - } - - // we need to decide if we need to send OP_AggroMeterTargetInfo now - // This packet sends the current lock target ID and the current target ID - // target ID will be either our target or our target of target when we're targeting a PC - bool send_targetinfo = false; - auto cur_tar = GetTarget(); - - // probably should have PVP rules ... - if (cur_tar && cur_tar != this) { - if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerClient() && cur_tar->GetID() != m_aggrometer.get_target_id()) { - m_aggrometer.set_target_id(cur_tar->GetID()); - send_targetinfo = true; - } else if ((cur_tar->IsPetOwnerClient() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { - m_aggrometer.set_target_id(cur_tar->GetTarget()->GetID()); - send_targetinfo = true; - } - } else if (m_aggrometer.get_target_id()) { - m_aggrometer.set_target_id(0); - send_targetinfo = true; - } + if (!amount || !zone->DoesAlternateCurrencyExist(currency_id)) { + return false; + } - if (m_aggrometer.update_lock()) - send_targetinfo = true; + const uint32 current_amount = alternate_currency[currency_id]; + if (current_amount < amount) { + return false; + } - if (send_targetinfo) { - auto app = new EQApplicationPacket(OP_AggroMeterTargetInfo, sizeof(uint32) * 2); - app->WriteUInt32(m_aggrometer.get_lock_id()); - app->WriteUInt32(m_aggrometer.get_target_id()); - FastQueuePacket(&app); - } + const uint32 new_amount = (current_amount - amount); - // we could just calculate how big the packet would need to be ... but it's easier this way :P should be 87 bytes - auto app = new EQApplicationPacket(OP_AggroMeterUpdate, m_aggrometer.max_packet_size()); - - cur_tar = entity_list.GetMob(m_aggrometer.get_target_id()); - - // first we must check the secondary - // TODO: lock target should affect secondary as well - bool send = false; - Mob *secondary = nullptr; - bool has_aggro = false; - if (cur_tar) { - if (cur_tar->GetTarget() == this) {// we got aggro - secondary = cur_tar->GetSecondaryHate(this); - has_aggro = true; - } else { - secondary = cur_tar->CheckAggro(cur_tar->GetTarget()) ? cur_tar->GetTarget() : nullptr; // make sure they are targeting for aggro reasons - } - } + alternate_currency[currency_id] = new_amount; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); + SendAlternateCurrencyValue(currency_id); - if (secondary && secondary->GetID() != m_aggrometer.get_secondary_id()) { - m_aggrometer.set_secondary_id(secondary->GetID()); - app->WriteUInt8(1); - app->WriteUInt32(m_aggrometer.get_secondary_id()); - send = true; - } else if (!secondary && m_aggrometer.get_secondary_id()) { - m_aggrometer.set_secondary_id(0); - app->WriteUInt8(1); - app->WriteUInt32(0); - send = true; - } else { // might not need to send in this case - app->WriteUInt8(0); - } + if (parse->PlayerHasQuestSub(EVENT_ALT_CURRENCY_LOSS)) { + const std::string &export_string = fmt::format( + "{} {} {}", + currency_id, + amount, + new_amount + ); - auto count_offset = app->GetWritePosition(); - app->WriteUInt8(0); - - int count = 0; - auto add_entry = [&app, &count, this](AggroMeter::AggroTypes i) { - count++; - app->WriteUInt8(i); - app->WriteUInt16(m_aggrometer.get_pct(i)); - }; - // TODO: Player entry should either be lock or yourself, ignoring lock for now - // player, secondary, and group depend on your target/lock - if (cur_tar) { - if (m_aggrometer.set_pct(AggroMeter::AT_Player, cur_tar->GetHateRatio(cur_tar->GetTarget(), this))) - add_entry(AggroMeter::AT_Player); - - if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, has_aggro ? cur_tar->GetHateRatio(this, secondary) : secondary ? 100 : 0)) - add_entry(AggroMeter::AT_Secondary); - - if (IsRaidGrouped()) { - auto raid = GetRaid(); - if (raid) { - auto gid = raid->GetGroup(this); - if (gid < MAX_RAID_GROUPS) { - int at_id = AggroMeter::AT_Group1; - for (const auto& m : raid->members) { - if (m.member && m.member != this && m.group_number == gid) { - if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), m.member))) - add_entry(static_cast(at_id)); - at_id++; - if (at_id > AggroMeter::AT_Group5) - break; - } - } - } - } - } else if (IsGrouped()) { - auto group = GetGroup(); - if (group) { - int at_id = AggroMeter::AT_Group1; - for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { - if (group->members[i] && group->members[i] != this) { - if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), group->members[i]))) - add_entry(static_cast(at_id)); - at_id++; - } - } - } - } - } else { // we might need to clear out some data now - if (m_aggrometer.set_pct(AggroMeter::AT_Player, 0)) - add_entry(AggroMeter::AT_Player); - if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, 0)) - add_entry(AggroMeter::AT_Secondary); - if (m_aggrometer.set_pct(AggroMeter::AT_Group1, 0)) - add_entry(AggroMeter::AT_Group1); - if (m_aggrometer.set_pct(AggroMeter::AT_Group2, 0)) - add_entry(AggroMeter::AT_Group2); - if (m_aggrometer.set_pct(AggroMeter::AT_Group3, 0)) - add_entry(AggroMeter::AT_Group3); - if (m_aggrometer.set_pct(AggroMeter::AT_Group4, 0)) - add_entry(AggroMeter::AT_Group4); - if (m_aggrometer.set_pct(AggroMeter::AT_Group5, 0)) - add_entry(AggroMeter::AT_Group5); - } + parse->EventPlayer(EVENT_ALT_CURRENCY_LOSS, this, export_string, 0); + } - // now to go over our xtargets - // if the entry is an NPC it's our hate relative to the NPCs current tank - // if it's a PC, it's their hate relative to our current target - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].ID) { - auto mob = entity_list.GetMob(XTargets[i].ID); - if (mob) { - int ratio = 0; - if (mob->IsNPC()) - ratio = mob->GetHateRatio(mob->GetTarget(), this); - else if (cur_tar) - ratio = cur_tar->GetHateRatio(cur_tar->GetTarget(), mob); - if (m_aggrometer.set_pct(static_cast(AggroMeter::AT_XTarget1 + i), ratio)) - add_entry(static_cast(AggroMeter::AT_XTarget1 + i)); - } - } - } + return true; +} - if (send || count) { - app->size = app->GetWritePosition(); // this should be safe, although not recommended - // but this way we can have a smaller buffer created for the packet dispatched to the client w/o resizing this one - app->SetWritePosition(count_offset); - app->WriteUInt8(count); - FastQueuePacket(&app); - } else { - safe_delete(app); - } +int Client::AddAlternateCurrencyValue(uint32 currency_id, int amount, bool is_scripted) +{ + if (!zone->DoesAlternateCurrencyExist(currency_id)) { + return 0; + } + + /* Added via Quest, rest of the logging methods may be done inline due to information available in that area of the code */ + if (is_scripted) { + /* QS: PlayerLogAlternateCurrencyTransactions :: Cursor to Item Storage */ + if (RuleB(QueryServ, PlayerLogAlternateCurrencyTransactions)){ + std::string event_desc = StringFormat("Added via Quest :: Cursor to Item :: alt_currency_id:%i amount:%i in zoneid:%i instid:%i", currency_id, GetZoneID(), GetInstanceID()); + QServ->PlayerLogEvent(Player_Log_Alternate_Currency_Transactions, CharacterID(), event_desc); + } + } + + if (!amount) { + return 0; + } + + if (!alternate_currency_loaded) { + alternate_currency_queued_operations.push(std::make_pair(currency_id, amount)); + return 0; + } + + int new_value = 0; + auto iter = alternate_currency.find(currency_id); + if (iter == alternate_currency.end()) { + new_value = amount; + } else { + new_value = (*iter).second + amount; + } + + if (new_value < 0) { + new_value = 0; + alternate_currency[currency_id] = 0; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, 0); + } else { + alternate_currency[currency_id] = new_value; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_value); + } + + SendAlternateCurrencyValue(currency_id); + + QuestEventID event_id = amount > 0 ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; + if (parse->PlayerHasQuestSub(event_id)) { + const std::string &export_string = fmt::format( + "{} {} {}", + currency_id, + std::abs(amount), + new_value + ); + + parse->EventPlayer(event_id, this, export_string, 0); + } + + return new_value; } -void Client::SetPetCommandState(int button, int state) +void Client::SendAlternateCurrencyValues() { - auto app = new EQApplicationPacket(OP_PetCommandState, sizeof(PetCommandState_Struct)); - auto pcs = (PetCommandState_Struct *)app->pBuffer; - pcs->button_id = button; - pcs->state = state; - FastQueuePacket(&app); + for (const auto& alternate_currency : zone->AlternateCurrencies) { + SendAlternateCurrencyValue(alternate_currency.id, false); + } } -bool Client::CanMedOnHorse() +void Client::SendAlternateCurrencyValue(uint32 currency_id, bool send_if_null) { - // no horse is false - if (GetHorseId() == 0) - return false; + const auto value = GetAlternateCurrencyValue(currency_id); + if (value > 0 || send_if_null) { + auto outapp = new EQApplicationPacket(OP_AltCurrency, sizeof(AltCurrencyUpdate_Struct)); + auto update = (AltCurrencyUpdate_Struct *) outapp->pBuffer; + update->opcode = 7; + update->currency_number = currency_id; + update->amount = value; + update->unknown072 = 1; - // can't med while attacking - if (auto_attack) - return false; + strn0cpy(update->name, GetName(), sizeof(update->name)); - return animation == 0 && m_Delta.x == 0.0f && m_Delta.y == 0.0f; // TODO: animation is SpeedRun + FastQueuePacket(&outapp); + } } -void Client::EnableAreaHPRegen(int value) +uint32 Client::GetAlternateCurrencyValue(uint32 currency_id) const { - AreaHPRegen = value * 0.001f; - SendAppearancePacket(AppearanceType::AreaHealthRegen, value, false); -} + if (!zone->DoesAlternateCurrencyExist(currency_id)) { + return 0; + } -void Client::DisableAreaHPRegen() -{ - AreaHPRegen = 1.0f; - SendAppearancePacket(AppearanceType::AreaHealthRegen, 1000, false); -} + auto iter = alternate_currency.find(currency_id); -void Client::EnableAreaManaRegen(int value) -{ - AreaManaRegen = value * 0.001f; - SendAppearancePacket(AppearanceType::AreaManaRegen, value, false); + return iter == alternate_currency.end() ? 0 : (*iter).second; } -void Client::DisableAreaManaRegen() -{ - AreaManaRegen = 1.0f; - SendAppearancePacket(AppearanceType::AreaManaRegen, 1000, false); +void Client::ProcessAlternateCurrencyQueue() { + while(!alternate_currency_queued_operations.empty()) { + std::pair op = alternate_currency_queued_operations.front(); + + AddAlternateCurrencyValue(op.first, op.second); + + alternate_currency_queued_operations.pop(); + } } -void Client::EnableAreaEndRegen(int value) +void Client::OpenLFGuildWindow() { - AreaEndRegen = value * 0.001f; - SendAppearancePacket(AppearanceType::AreaEnduranceRegen, value, false); + auto outapp = new EQApplicationPacket(OP_LFGuild, 8); + + outapp->WriteUInt32(6); + + FastQueuePacket(&outapp); } -void Client::DisableAreaEndRegen() +bool Client::IsXTarget(const Mob *m) const { - AreaEndRegen = 1.0f; - SendAppearancePacket(AppearanceType::AreaEnduranceRegen, 1000, false); + if(!XTargettingAvailable() || !m || (m->GetID() == 0)) + return false; + + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(XTargets[i].ID == m->GetID()) + return true; + } + return false; } -void Client::EnableAreaRegens(int value) +bool Client::IsClientXTarget(const Client *c) const { - EnableAreaHPRegen(value); - EnableAreaManaRegen(value); - EnableAreaEndRegen(value); + if(!XTargettingAvailable() || !c) + return false; + + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(!strcasecmp(XTargets[i].Name, c->GetName())) + return true; + } + return false; } -void Client::DisableAreaRegens() + +void Client::UpdateClientXTarget(Client *c) { - DisableAreaHPRegen(); - DisableAreaManaRegen(); - DisableAreaEndRegen(); + if(!XTargettingAvailable() || !c) + return; + + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(!strcasecmp(XTargets[i].Name, c->GetName())) + { + XTargets[i].ID = c->GetID(); + SendXTargetPacket(i, c); + } + } } -void Client::InitInnates() +// IT IS NOT SAFE TO CALL THIS IF IT'S NOT INITIAL AGGRO +void Client::AddAutoXTarget(Mob *m, bool send) { - // this function on the client also inits the level one innate skills (like swimming, hide, etc) - // we won't do that here, lets just do the InnateSkills for now. Basically translation of what the client is doing - // A lot of these we could probably have ignored because they have no known use or are 100% client side - // but I figured just in case we'll do them all out - // - // The client calls this in a few places. When you remove a vision buff and in SetHeights, which is called in - // illusions, mounts, and a bunch of other cases. All of the calls to InitInnates are wrapped in restoring regen - // besides the call initializing the first time - auto race = GetRace(); - auto class_ = GetClass(); - - for (int i = 0; i < InnateSkillMax; ++i) { - m_pp.InnateSkills[i] = InnateDisabled; - } + if (m->IsBot() || ((m->IsPet() || m->IsTempPet()) && m->IsPetOwnerBot())) { + return; + } - m_pp.InnateSkills[InnateInspect] = InnateEnabled; - m_pp.InnateSkills[InnateOpen] = InnateEnabled; + m_activeautohatermgr->increment_count(m); - if (race >= Race::Froglok2) { - if (race == Race::Skeleton2 || race == Race::Froglok2) { - m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; - } else { - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - } - } + if (!XTargettingAvailable() || !XTargetAutoAddHaters || IsXTarget(m)) { + return; + } - switch (race) { - case Race::Barbarian: - case Race::HalasCitizen: - m_pp.InnateSkills[InnateSlam] = InnateEnabled; - break; - case Race::Erudite: - case Race::EruditeCitizen: - m_pp.InnateSkills[InnateLore] = InnateEnabled; - break; - case Race::WoodElf: - case Race::Fayguard: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Gnome: - case Race::HighElf: - case Race::Felguard: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - m_pp.InnateSkills[InnateLore] = InnateEnabled; - break; - case Race::Troll: - case Race::GrobbCitizen: - m_pp.InnateSkills[InnateRegen] = InnateEnabled; - m_pp.InnateSkills[InnateSlam] = InnateEnabled; - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Dwarf: - case Race::KaladimCitizen: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Ogre: - case Race::OggokCitizen: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - m_pp.InnateSkills[InnateSlam] = InnateEnabled; - m_pp.InnateSkills[InnateNoBash] = InnateEnabled; - m_pp.InnateSkills[InnateBashDoor] = InnateEnabled; - break; - case Race::Halfling: - case Race::RivervaleCitizen: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Iksar: - m_pp.InnateSkills[InnateRegen] = InnateEnabled; - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::VahShir: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::DarkElf: - case Race::NeriakCitizen: - case Race::ElfVampire: - case Race::FroglokGhoul: - case Race::Ghost: - case Race::Ghoul: - case Race::Skeleton: - case Race::Vampire: - case Race::Wisp: - case Race::Zombie: - case Race::Spectre: - case Race::DwarfGhost: - case Race::EruditeGhost: - case Race::DragonSkeleton: - case Race::Innoruuk: - m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; - break; - case Race::Human: - case Race::FreeportGuard: - case Race::HumanBeggar: - case Race::HighpassCitizen: - case Race::QeynosCitizen: - case Race::Froglok2: // client does froglok weird, but this should work out fine - break; - default: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - } + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type == Auto && XTargets[i].ID == 0) { + XTargets[i].ID = m->GetID(); - switch (class_) { - case Class::Druid: - m_pp.InnateSkills[InnateHarmony] = InnateEnabled; - break; - case Class::Bard: - m_pp.InnateSkills[InnateReveal] = InnateEnabled; - break; - case Class::Rogue: - m_pp.InnateSkills[InnateSurprise] = InnateEnabled; - m_pp.InnateSkills[InnateReveal] = InnateEnabled; - break; - case Class::Ranger: - m_pp.InnateSkills[InnateAwareness] = InnateEnabled; - break; - case Class::Monk: - m_pp.InnateSkills[InnateSurprise] = InnateEnabled; - m_pp.InnateSkills[InnateAwareness] = InnateEnabled; - default: - break; - } -} + if (send) { // if we don't send we're bulk sending updates later on + SendXTargetPacket(i, m); + } else { + XTargets[i].dirty = true; + } -bool Client::GetDisplayMobInfoWindow() const -{ - return display_mob_info_window; -} + break; + } + } -void Client::SetDisplayMobInfoWindow(bool display_mob_info_window) -{ - Client::display_mob_info_window = display_mob_info_window; + LogXTargets( + "Adding [{}] to [{}] ({}) XTargets", + m->GetCleanName(), + GetCleanName(), + GetID() + ); } -bool Client::IsDevToolsEnabled() const +void Client::RemoveXTarget(Mob *m, bool OnlyAutoSlots) { - return dev_tools_enabled && GetGM() && RuleB(World, EnableDevTools); + if (!XTargettingAvailable() || !m || !m_activeautohatermgr) { + return; + } + + m_activeautohatermgr->decrement_count(m); + // now we may need to clean up our CurrentTargetNPC entries + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type == CurrentTargetNPC && XTargets[i].ID == m->GetID()) { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].dirty = true; + } + } + + auto r = GetRaid(); + if (r) { + r->UpdateRaidXTargets(); + } + + LogXTargets( + "Removing [{}] from [{}] ({}) XTargets", + m->GetCleanName(), + GetCleanName(), + GetID() + ); } -void Client::SetDevToolsEnabled(bool in_dev_tools_enabled) +void Client::UpdateXTargetType(XTargetType Type, Mob *m, const char *Name) { - const auto dev_tools_key = fmt::format("{}-dev-tools-disabled", AccountID()); + if (!XTargettingAvailable()) { + return; + } - if (in_dev_tools_enabled) { - DataBucket::DeleteData(dev_tools_key); - } else { - DataBucket::SetData(dev_tools_key, "true"); - } + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type == Type) { + if (m) { + XTargets[i].ID = m->GetID(); + } + else { + XTargets[i].ID = 0; + } - Client::dev_tools_enabled = in_dev_tools_enabled; -} + if (Name) { + strncpy(XTargets[i].Name, Name, 64); + } -bool Client::IsEXPEnabled() const { - return m_exp_enabled; + SendXTargetPacket(i, m); + } + } } -void Client::SetEXPEnabled(bool is_exp_enabled) +void Client::SendXTargetPacket(uint32 Slot, Mob *m) { - auto c = CharacterDataRepository::FindOne(database, CharacterID()); - - c.exp_enabled = is_exp_enabled; - - auto updated = CharacterDataRepository::UpdateOne(database, c); - - if (!updated) { - return; - } - - m_exp_enabled = is_exp_enabled; + if(!XTargettingAvailable()) + return; + + uint32 PacketSize = 18; + + if(m) + PacketSize += strlen(m->GetCleanName()); + else + { + PacketSize += strlen(XTargets[Slot].Name); + } + + auto outapp = new EQApplicationPacket(OP_XTargetResponse, PacketSize); + outapp->WriteUInt32(GetMaxXTargets()); + outapp->WriteUInt32(1); + outapp->WriteUInt32(Slot); + if(m) + { + outapp->WriteUInt8(1); + } + else + { + if (strlen(XTargets[Slot].Name) && ((XTargets[Slot].Type == CurrentTargetPC) || + (XTargets[Slot].Type == GroupTank) || + (XTargets[Slot].Type == GroupAssist) || + (XTargets[Slot].Type == Puller))) + { + outapp->WriteUInt8(2); + } + else + { + outapp->WriteUInt8(0); + } + } + outapp->WriteUInt32(XTargets[Slot].ID); + outapp->WriteString(m ? m->GetCleanName() : XTargets[Slot].Name); + FastQueuePacket(&outapp); } -void Client::SetPrimaryWeaponOrnamentation(uint32 model_id) +// This is a bulk packet, we use it when we remove something since we need to reorder the xtargets and maybe +// add new mobs! Currently doesn't check if there is a dirty flag set, so it should only be called when there is +void Client::SendXTargetUpdates() { - auto primary_item = m_inv.GetItem(EQ::invslot::slotPrimary); - if (primary_item) { - auto l = InventoryRepository::GetWhere( - database, - fmt::format( - "`charid` = {} AND `slotid` = {}", - character_id, - EQ::invslot::slotPrimary - ) - ); - - if (l.empty()) { - return; - } - - auto e = l.front(); - - e.ornamentidfile = model_id; - - const int updated = InventoryRepository::UpdateOne(database, e); - - if (updated) { - primary_item->SetOrnamentationIDFile(model_id); - SendItemPacket(EQ::invslot::slotPrimary, primary_item, ItemPacketTrade); - WearChange(EQ::textures::weaponPrimary, model_id, 0); - - Message(Chat::Yellow, "Your primary weapon appearance has been modified."); - } - } + if (!XTargettingAvailable()) + return; + + int count = 0; + // header is 4 bytes max xtargets, 4 bytes count + // entry is 4 bytes slot, 1 byte unknown, 4 bytes ID, 65 char name + auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8 + 74 * GetMaxXTargets()); // fuck it max size + outapp->WriteUInt32(GetMaxXTargets()); + outapp->WriteUInt32(1); // we will correct this later + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].dirty) { + outapp->WriteUInt32(i); + // MQ2 checks this for valid mobs, so 0 is bad here at least ... + outapp->WriteUInt8(XTargets[i].ID ? 1 : 0); + outapp->WriteUInt32(XTargets[i].ID); + outapp->WriteString(XTargets[i].Name); + count++; + XTargets[i].dirty = false; + } + } + + // RemoveXTarget probably got called with a mob not on our xtargets + if (count == 0) { + safe_delete(outapp); + return; + } + + auto newbuff = new uchar[outapp->GetWritePosition()]; + memcpy(newbuff, outapp->pBuffer, outapp->GetWritePosition()); + safe_delete_array(outapp->pBuffer); + outapp->pBuffer = newbuff; + outapp->size = outapp->GetWritePosition(); + outapp->SetWritePosition(4); + outapp->WriteUInt32(count); + FastQueuePacket(&outapp); } -void Client::SetSecondaryWeaponOrnamentation(uint32 model_id) +void Client::RemoveGroupXTargets() { - auto secondary_item = m_inv.GetItem(EQ::invslot::slotSecondary); - if (secondary_item) { - auto l = InventoryRepository::GetWhere( - database, - fmt::format( - "`charid` = {} AND `slotid` = {}", - character_id, - EQ::invslot::slotSecondary - ) - ); - - if (l.empty()) { - return; - } - - auto e = l.front(); + if(!XTargettingAvailable()) + return; - e.ornamentidfile = model_id; - - const int updated = InventoryRepository::UpdateOne(database, e); + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if ((XTargets[i].Type == GroupTank) || + (XTargets[i].Type == GroupAssist) || + (XTargets[i].Type == Puller)) + { + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + SendXTargetPacket(i, nullptr); + } + } +} - if (updated) { - secondary_item->SetOrnamentationIDFile(model_id); - SendItemPacket(EQ::invslot::slotSecondary, secondary_item, ItemPacketTrade); - WearChange(EQ::textures::weaponSecondary, model_id, 0); +void Client::RemoveAutoXTargets() +{ + if(!XTargettingAvailable()) + return; - Message(Chat::Yellow, "Your secondary weapon appearance has been modified."); - } - } + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(XTargets[i].Type == Auto) + { + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + SendXTargetPacket(i, nullptr); + } + } } -/** - * Used in #goto - * - * @param player_name - */ -bool Client::GotoPlayer(const std::string& player_name) +void Client::ShowXTargets(Client *c) { - const auto& l = CharacterDataRepository::GetWhere( - database, - fmt::format( - "name = '{}' AND last_login > (UNIX_TIMESTAMP() - 600) LIMIT 1", - Strings::Escape(player_name) - ) - ); - - if (l.empty()) { - return false; - } + if (!c) { + return; + } + + auto xtarget_count = 0; + + for (int i = 0; i < GetMaxXTargets(); ++i) { + c->Message( + Chat::White, + fmt::format( + "xtarget slot [{}] type [{}] ID [{}] name [{}]", + i, + static_cast(XTargets[i].Type), + XTargets[i].ID, + strlen(XTargets[i].Name) ? XTargets[i].Name : "No Name" + ).c_str() + ); + + xtarget_count++; + } + + auto &list = GetXTargetAutoMgr()->get_list(); + // yeah, I kept having to do something for debugging to tell if managers were the same object or not :P + // so lets use the address as an "ID" + c->Message( + Chat::White, + fmt::format( + "XTargetAutoMgr ID [{}] size [{}]", + fmt::ptr(GetXTargetAutoMgr()), + list.size() + ).c_str() + ); + + int count = 0; + for (auto &e : list) { + c->Message( + Chat::White, + fmt::format( + "Spawn ID: {} Count: {}", + e.spawn_id, + e.count + ).c_str() + ); + + count++; + + if (count == 20) { + break; + } + } +} - const auto& e = l.front(); +void Client::ProcessXTargetAutoHaters() +{ + if (!XTargettingAvailable()) + return; + + // move shit up! If the removed NPC was in a CurrentTargetNPC slot it becomes Auto + // and we need to potentially fill it + std::queue empty_slots; + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type != Auto) + continue; + + if (XTargets[i].ID != 0 && !GetXTargetAutoMgr()->contains_mob(XTargets[i].ID)) { + XTargets[i].ID = 0; + XTargets[i].dirty = true; + } + + if (XTargets[i].ID == 0) { + empty_slots.push(i); + continue; + } + + if (XTargets[i].ID != 0 && !empty_slots.empty()) { + int temp = empty_slots.front(); + std::swap(XTargets[i], XTargets[temp]); + XTargets[i].dirty = XTargets[temp].dirty = true; + empty_slots.pop(); + empty_slots.push(i); + } + } + // okay, now we need to check if we have any empty slots and if we have aggro + // We make the assumption that if we shuffled the NPCs up that they're still on the aggro + // list in the same order. We could probably do this better and try to calc if + // there are new NPCs for our empty slots on the manager, but ahhh fuck it. + if (!empty_slots.empty() && !GetXTargetAutoMgr()->empty() && XTargetAutoAddHaters) { + auto &haters = GetXTargetAutoMgr()->get_list(); + for (auto &e : haters) { + auto *mob = entity_list.GetMob(e.spawn_id); + if (mob && !IsXTarget(mob)) { + auto slot = empty_slots.front(); + empty_slots.pop(); + XTargets[slot].dirty = true; + XTargets[slot].ID = mob->GetID(); + strn0cpy(XTargets[slot].Name, mob->GetCleanName(), 64); + } + if (empty_slots.empty()) + break; + } + } + + m_dirtyautohaters = false; + SendXTargetUpdates(); +} - if (e.zone_instance > 0 && !database.CheckInstanceExists(e.zone_instance)) { - Message(Chat::Yellow, "Instance no longer exists..."); - return false; - } +// This function is called when a client is added to a group +// Group leader joining isn't handled by this function +void Client::JoinGroupXTargets(Group *g) +{ + if (!g) + return; - if (e.zone_instance > 0) { - database.AddClientToInstance(e.zone_instance, CharacterID()); - } + if (!GetXTargetAutoMgr()->empty()) { + g->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); + GetXTargetAutoMgr()->clear(); + RemoveAutoXTargets(); + } - MovePC(e.zone_id, e.zone_instance, e.x, e.y, e.z, e.heading); + SetXTargetAutoMgr(g->GetXTargetAutoMgr()); - return true; + if (!GetXTargetAutoMgr()->empty()) + SetDirtyAutoHaters(); } -bool Client::GotoPlayerGroup(const std::string& player_name) +// This function is called when a client leaves a group +void Client::LeaveGroupXTargets(Group *g) { - if (!GetGroup()) { - return GotoPlayer(player_name); - } - - for (auto &m: GetGroup()->members) { - if (m && m->IsClient()) { - auto c = m->CastToClient(); - if (!c->GotoPlayer(player_name)) { - return false; - } - } - } + if (!g) + return; - return true; + SetXTargetAutoMgr(nullptr); // this will set it back to our manager + RemoveAutoXTargets(); + entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever + // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups + if (!GetXTargetAutoMgr()->empty()) { + GetXTargetAutoMgr()->demerge(*g->GetXTargetAutoMgr()); // this will remove entries where we only had aggro + SetDirtyAutoHaters(); + } } -bool Client::GotoPlayerRaid(const std::string& player_name) +// This function is called when a client leaves a group +void Client::LeaveRaidXTargets(Raid *r) { - if (!GetRaid()) { - return GotoPlayer(player_name); - } - - for (auto &m: GetRaid()->members) { - if (m.member && m.member->IsClient()) { - auto c = m.member->CastToClient(); - if (!c->GotoPlayer(player_name)) { - return false; - } - } - } + if (!r) + return; - return true; + SetXTargetAutoMgr(nullptr); // this will set it back to our manager + RemoveAutoXTargets(); + entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever + // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups + if (!GetXTargetAutoMgr()->empty()) { + GetXTargetAutoMgr()->demerge(*r->GetXTargetAutoMgr()); // this will remove entries where we only had aggro + SetDirtyAutoHaters(); + } } -void Client::SendToGuildHall() +void Client::SetMaxXTargets(uint8 NewMax) { - std::string zone_short_name = "guildhall"; - uint32 zone_id = ZoneID(zone_short_name.c_str()); - if (zone_id == 0) { - return; - } + if(!XTargettingAvailable()) + return; - uint32 expiration_time = (RuleI(Instances, GuildHallExpirationDays) * 86400); - uint16 instance_id = 0; - std::string guild_hall_instance_key = fmt::format("guild-hall-instance-{}", GuildID()); - std::string instance_data = DataBucket::GetData(guild_hall_instance_key); - if (!instance_data.empty() && Strings::ToInt(instance_data) > 0) { - instance_id = Strings::ToInt(instance_data); - } + if(NewMax > XTARGET_HARDCAP) + return; - if (instance_id <= 0) { - if (!database.GetUnusedInstanceID(instance_id)) { - Message(Chat::Red, "Server was unable to find a free instance id."); - return; - } + MaxXTargets = NewMax; - if (!database.CreateInstance(instance_id, zone_id, 1, expiration_time)) { - Message(Chat::Red, "Server was unable to create a new instance."); - return; - } + Save(0); - DataBucket::SetData( - guild_hall_instance_key, - std::to_string(instance_id), - std::to_string(expiration_time) - ); - } + for(int i = MaxXTargets; i < XTARGET_HARDCAP; ++i) + { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + } - AssignToInstance(instance_id); - MovePC(345, instance_id, -1.00, -1.00, 3.34, 0, 1); + auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8); + outapp->WriteUInt32(GetMaxXTargets()); + outapp->WriteUInt32(0); + FastQueuePacket(&outapp); } -void Client::CheckVirtualZoneLines() +void Client::SendWebLink(const char *website) { - for (auto &virtual_zone_point : zone->virtual_zone_point_list) { - float half_width = ((float) virtual_zone_point.width / 2); - - if ( - GetX() > (virtual_zone_point.x - half_width) && - GetX() < (virtual_zone_point.x + half_width) && - GetY() > (virtual_zone_point.y - half_width) && - GetY() < (virtual_zone_point.y + half_width) && - GetZ() >= (virtual_zone_point.z - 10) && - GetZ() < (virtual_zone_point.z + (float) virtual_zone_point.height) - ) { - - MovePC( - virtual_zone_point.target_zone_id, - virtual_zone_point.target_instance, - virtual_zone_point.target_x, - virtual_zone_point.target_y, - virtual_zone_point.target_z, - virtual_zone_point.target_heading - ); + if (website) { + size_t len = strlen(website) + 1; + if (len > 1) + { + auto outapp = new EQApplicationPacket(OP_Weblink, sizeof(Weblink_Struct) + len); + Weblink_Struct* wl = (Weblink_Struct*)outapp->pBuffer; + memcpy(wl->weblink, website, len); + wl->weblink[len] = '\0'; - LogZonePoints( - "Virtual Zone Box Sending player [{}] to [{}]", - GetCleanName(), - ZoneLongName(virtual_zone_point.target_zone_id) - ); - } - } + FastQueuePacket(&outapp); + } + } } -void Client::ShowDevToolsMenu() +void Client::SendMercPersonalInfo() { - std::string menu_search; - std::string menu_show; - std::string menu_reload_one; - std::string menu_reload_two; - std::string menu_reload_three; - std::string menu_reload_four; - std::string menu_reload_five; - std::string menu_reload_six; - std::string menu_reload_seven; - std::string menu_reload_eight; - std::string menu_reload_nine; - std::string menu_toggle; - std::string window_toggle; - - /** - * Search entity commands - */ - menu_search += Saylink::Silent("#list corpses", "Corpses"); - menu_search += " | " + Saylink::Silent("#list doors", "Doors"); - menu_search += " | " + Saylink::Silent("#finditem", "Items"); - menu_search += " | " + Saylink::Silent("#list npcs", "NPC"); - menu_search += " | " + Saylink::Silent("#list objects", "Objects"); - menu_search += " | " + Saylink::Silent("#list players", "Players"); - menu_search += " | " + Saylink::Silent("#findzone", "Zones"); - - /** - * Show - */ - menu_show += Saylink::Silent("#showzonepoints", "Zone Points"); - menu_show += " | " + Saylink::Silent("#showzonegloballoot", "Zone Global Loot"); - menu_show += " | " + Saylink::Silent("#show content_flags", "Content Flags"); - - /** - * Reload - */ - menu_reload_one += Saylink::Silent("#reload aa", "AAs"); - menu_reload_one += " | " + Saylink::Silent("#reload alternate_currencies", "Alternate Currencies"); - menu_reload_one += " | " + Saylink::Silent("#reload base_data", "Base Data"); - menu_reload_one += " | " + Saylink::Silent("#reload blocked_spells", "Blocked Spells"); - - menu_reload_two += Saylink::Silent("#reload commands", "Commands"); - menu_reload_two += " | " + Saylink::Silent("#reload content_flags", "Content Flags"); - - menu_reload_three += Saylink::Silent("#reload data_buckets_cache", "Databuckets"); - menu_reload_three += " | " + Saylink::Silent("#reload doors", "Doors"); - menu_reload_three += " | " + Saylink::Silent("#reload factions", "Factions"); - menu_reload_three += " | " + Saylink::Silent("#reload ground_spawns", "Ground Spawns"); - - menu_reload_four += Saylink::Silent("#reload logs", "Level Based Experience Modifiers"); - menu_reload_four += " | " + Saylink::Silent("#reload logs", "Log Settings"); - menu_reload_four += " | " + Saylink::Silent("#reload Loot", "Loot"); - - menu_reload_five += Saylink::Silent("#reload merchants", "Merchants"); - menu_reload_five += " | " + Saylink::Silent("#reload npc_emotes", "NPC Emotes"); - menu_reload_five += " | " + Saylink::Silent("#reload npc_spells", "NPC Spells"); - menu_reload_five += " | " + Saylink::Silent("#reload objects", "Objects"); - menu_reload_five += " | " + Saylink::Silent("#reload opcodes", "Opcodes"); - - menu_reload_six += Saylink::Silent("#reload perl_export", "Perl Event Export Settings"); - menu_reload_six += " | " + Saylink::Silent("#reload quest", "Quests"); - - menu_reload_seven += Saylink::Silent("#reload rules", "Rules"); - menu_reload_seven += " | " + Saylink::Silent("#reload skill_caps", "Skill Caps"); - menu_reload_seven += " | " + Saylink::Silent("#reload static", "Static Zone Data"); - menu_reload_seven += " | " + Saylink::Silent("#reload tasks", "Tasks"); - - menu_reload_eight += Saylink::Silent("#reload titles", "Titles"); - menu_reload_eight += " | " + Saylink::Silent("#reload traps 1", "Traps"); - menu_reload_eight += " | " + Saylink::Silent("#reload variables", "Variables"); - menu_reload_eight += " | " + Saylink::Silent("#reload veteran_rewards", "Veteran Rewards"); - - menu_reload_nine += Saylink::Silent("#reload world", "World"); - menu_reload_nine += " | " + Saylink::Silent("#reload zone", "Zone"); - menu_reload_nine += " | " + Saylink::Silent("#reload zone_points", "Zone Points"); - - /** - * Show window status - */ - menu_toggle = Saylink::Silent("#devtools menu enable", "Enable"); - if (IsDevToolsEnabled()) { - menu_toggle = Saylink::Silent("#devtools menu disable", "Disable"); - } + uint32 mercTypeCount = 1; + uint32 mercCount = 1; //TODO: Un-hardcode this and support multiple mercs like in later clients than SoD. + uint32 i = 0; + uint32 altCurrentType = 19; //TODO: Implement alternate currency purchases involving mercs! + + MercTemplate *mercData = &zone->merc_templates[GetMercInfo().MercTemplateID]; + + int stancecount = 0; + stancecount += zone->merc_stance_list[GetMercInfo().MercTemplateID].size(); + if(stancecount > MAX_MERC_STANCES || mercCount > MAX_MERC || mercTypeCount > MAX_MERC_GRADES) + { + Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo canceled: (%i) (%i) (%i) for %s", stancecount, mercCount, mercTypeCount, GetName()); + SendMercMerchantResponsePacket(0); + return; + } + + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(MercenaryDataUpdate_Struct)); + auto mdus = (MercenaryDataUpdate_Struct *) outapp->pBuffer; + + mdus->MercStatus = 0; + mdus->MercCount = mercCount; + mdus->MercData[i].MercID = mercData->MercTemplateID; + mdus->MercData[i].MercType = mercData->MercType; + mdus->MercData[i].MercSubType = mercData->MercSubType; + mdus->MercData[i].PurchaseCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0); + mdus->MercData[i].UpkeepCost = Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0); + mdus->MercData[i].Status = 0; + mdus->MercData[i].AltCurrencyCost = Merc::CalcPurchaseCost( + mercData->MercTemplateID, + GetLevel(), + altCurrentType + ); + mdus->MercData[i].AltCurrencyUpkeep = Merc::CalcPurchaseCost( + mercData->MercTemplateID, + GetLevel(), + altCurrentType + ); + mdus->MercData[i].AltCurrencyType = altCurrentType; + mdus->MercData[i].MercUnk01 = 0; + mdus->MercData[i].TimeLeft = GetMercInfo().MercTimerRemaining; //GetMercTimer().GetRemainingTime(); + mdus->MercData[i].MerchantSlot = i + 1; + mdus->MercData[i].MercUnk02 = 1; + mdus->MercData[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); + mdus->MercData[i].MercUnk03 = 0; + mdus->MercData[i].MercUnk04 = 1; + + strn0cpy(mdus->MercData[i].MercName, GetMercInfo().merc_name, sizeof(mdus->MercData[i].MercName)); + + uint32 stanceindex = 0; + if (mdus->MercData[i].StanceCount != 0) { + auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); + while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { + mdus->MercData[i].Stances[stanceindex].StanceIndex = stanceindex; + mdus->MercData[i].Stances[stanceindex].Stance = (iter->StanceID); + stanceindex++; + ++iter; + } + } + + mdus->MercData[i].MercUnk05 = 1; + FastQueuePacket(&outapp); + safe_delete(outapp); + return; + } else { + auto outapp = new EQApplicationPacket(OP_MercenaryDataResponse, sizeof(MercenaryMerchantList_Struct)); + auto mml = (MercenaryMerchantList_Struct *) outapp->pBuffer; + + mml->MercTypeCount = mercTypeCount; //We should only have one merc entry. + mml->MercGrades[i] = 1; + + mml->MercCount = mercCount; + mml->Mercs[i].MercID = mercData->MercTemplateID; + mml->Mercs[i].MercType = mercData->MercType; + mml->Mercs[i].MercSubType = mercData->MercSubType; + mml->Mercs[i].PurchaseCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0) : 0; + mml->Mercs[i].UpkeepCost = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0) : 0; + mml->Mercs[i].Status = 0; + mml->Mercs[i].AltCurrencyCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; + mml->Mercs[i].AltCurrencyUpkeep = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; + mml->Mercs[i].AltCurrencyType = altCurrentType; + mml->Mercs[i].MercUnk01 = 0; + mml->Mercs[i].TimeLeft = GetMercInfo().MercTimerRemaining; + mml->Mercs[i].MerchantSlot = i + 1; + mml->Mercs[i].MercUnk02 = 1; + mml->Mercs[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); + mml->Mercs[i].MercUnk03 = 0; + mml->Mercs[i].MercUnk04 = 1; + + strn0cpy(mml->Mercs[i].MercName, GetMercInfo().merc_name, sizeof(mml->Mercs[i].MercName)); + + int stanceindex = 0; + if (mml->Mercs[i].StanceCount != 0) { + auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); + while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { + mml->Mercs[i].Stances[stanceindex].StanceIndex = stanceindex; + mml->Mercs[i].Stances[stanceindex].Stance = (iter->StanceID); + stanceindex++; + ++iter; + } + } + + FastQueuePacket(&outapp); + safe_delete(outapp); + return; + } +} - window_toggle = Saylink::Silent("#devtools window enable", "Enable"); - if (GetDisplayMobInfoWindow()) { - window_toggle = Saylink::Silent("#devtools window disable", "Disable"); - } +void Client::SendClearMercInfo() +{ + auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(NoMercenaryHired_Struct)); + NoMercenaryHired_Struct *nmhs = (NoMercenaryHired_Struct*)outapp->pBuffer; + nmhs->MercStatus = -1; + nmhs->MercCount = 0; + nmhs->MercID = 1; + FastQueuePacket(&outapp); +} - /** - * Print menu - */ - SendChatLineBreak(); - Message(Chat::White, "Developer Tools Menu"); +void Client::DuplicateLoreMessage(uint32 ItemID) +{ + if (!(m_ClientVersionBit & EQ::versions::maskRoFAndLater)) + { + MessageString(Chat::White, PICK_LORE); + return; + } - Message( - Chat::White, - fmt::format( - "Show Menu | {}", - Saylink::Silent("#dev") - ).c_str() - ); + const EQ::ItemData *item = database.GetItem(ItemID); - Message( - Chat::White, - fmt::format( - "Toggle Menu | {}", - menu_toggle - ).c_str() - ); + if(!item) + return; - Message( - Chat::White, - fmt::format( - "Toggle Window | {}", - window_toggle - ).c_str() - ); + MessageString(Chat::White, PICK_LORE, item->Name); +} - Message( - Chat::White, - fmt::format( - "Search | {}", - menu_search - ).c_str() - ); +void Client::GarbleMessage(char *message, uint8 variance) +{ + // Garble message by variance% + const char alpha_list[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // only change alpha characters for now + const char delimiter = 0x12; + int delimiter_count = 0; - Message( - Chat::White, - fmt::format( - "Show | {}", - menu_show - ).c_str() - ); + // Don't garble # commands + if (message[0] == COMMAND_CHAR || message[0] == BOT_COMMAND_CHAR) { + return; + } - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_one - ).c_str() - ); + for (size_t i = 0; i < strlen(message); i++) { + // Client expects hex values inside of a text link body + if (message[i] == delimiter) { + if (!(delimiter_count & 1)) { i += EQ::constants::SAY_LINK_BODY_SIZE; } + ++delimiter_count; + continue; + } - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_two - ).c_str() - ); + uint8 chance = (uint8)zone->random.Int(0, 115); // variation just over worst possible scrambling + if (isalpha((unsigned char)message[i]) && (chance <= variance)) { + uint8 rand_char = (uint8)zone->random.Int(0,51); // choose a random character from the alpha list + message[i] = alpha_list[rand_char]; + } + } +} - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_three - ).c_str() - ); +// returns what Other thinks of this +FACTION_VALUE Client::GetReverseFactionCon(Mob* iOther) { + if (GetOwnerID()) { + return GetOwnerOrSelf()->GetReverseFactionCon(iOther); + } - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_four - ).c_str() - ); + iOther = iOther->GetOwnerOrSelf(); - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_five - ).c_str() - ); + if (iOther->GetPrimaryFaction() < 0) + return GetSpecialFactionCon(iOther); - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_six - ).c_str() - ); + if (iOther->GetPrimaryFaction() == 0) + return FACTION_INDIFFERENTLY; - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_seven - ).c_str() - ); + return GetFactionLevel(CharacterID(), 0, GetFactionRace(), GetClass(), GetDeity(), iOther->GetPrimaryFaction(), iOther); +} - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_eight - ).c_str() - ); +bool Client::ReloadCharacterFaction(Client *c, uint32 facid, uint32 charid) +{ + if (database.SetCharacterFactionLevel(charid, facid, 0, 0, factionvalues)) + return true; + else + return false; +} - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_nine - ).c_str() - ); +//o-------------------------------------------------------------- +//| Name: GetFactionLevel; Dec. 16, 2001 +//o-------------------------------------------------------------- +//| Notes: Gets the characters faction standing with the specified NPC. +//| Will return Indifferent on failure. +//o-------------------------------------------------------------- +FACTION_VALUE Client::GetFactionLevel(uint32 char_id, uint32 npc_id, uint32 p_race, uint32 p_class, uint32 p_deity, int32 pFaction, Mob* tnpc) +{ + if (pFaction < 0) + return GetSpecialFactionCon(tnpc); + FACTION_VALUE fac = FACTION_INDIFFERENTLY; + int32 tmpFactionValue; + FactionMods fmods; + + // few optimizations + if (GetFeigned()) + return FACTION_INDIFFERENTLY; + if(!zone->CanDoCombat()) + return FACTION_INDIFFERENTLY; + if (invisible_undead && tnpc && !tnpc->SeeInvisibleUndead()) + return FACTION_INDIFFERENTLY; + if (IsInvisible(tnpc)) + return FACTION_INDIFFERENTLY; + if (tnpc && tnpc->GetOwnerID() != 0) // pets con amiably to owner and indiff to rest + { + if (char_id == tnpc->GetOwner()->CastToClient()->CharacterID()) + return FACTION_AMIABLY; + else + return FACTION_INDIFFERENTLY; + } + + //First get the NPC's Primary faction + if(pFaction > 0) + { + //Get the faction data from the database + if(content_db.GetFactionData(&fmods, p_class, p_race, p_deity, pFaction)) + { + //Get the players current faction with pFaction + tmpFactionValue = GetCharacterFactionLevel(pFaction); + //Tack on any bonuses from Alliance type spell effects + tmpFactionValue += GetFactionBonus(pFaction); + tmpFactionValue += GetItemFactionBonus(pFaction); + //Return the faction to the client + fac = CalculateFaction(&fmods, tmpFactionValue); + } + } + else + { + return(FACTION_INDIFFERENTLY); + } + + // merchant fix + if (tnpc && tnpc->IsNPC() && tnpc->CastToNPC()->MerchantType && (fac == FACTION_THREATENINGLY || fac == FACTION_SCOWLS)) + fac = FACTION_DUBIOUSLY; + + if (tnpc != 0 && fac != FACTION_SCOWLS && tnpc->CastToNPC()->CheckAggro(this)) + fac = FACTION_THREATENINGLY; + + return fac; +} - auto help_link = Saylink::Silent("#help"); +//Sets the characters faction standing with the specified NPC. +void Client::SetFactionLevel( + uint32 character_id, + uint32 npc_faction_id, + uint8 class_id, + uint8 race_id, + uint8 deity_id, + bool is_quest +) +{ + auto l = zone->GetNPCFactionEntries(npc_faction_id); - Message( - Chat::White, - fmt::format( - "Note: You can search for commands with {} [Search String]", - help_link - ).c_str() - ); + if (l.empty()) { + return; + } - SendChatLineBreak(); + int current_value; - Message( - Chat::White, - fmt::format( - "Current Expansion | {} ({})", - content_service.GetCurrentExpansionName(), - content_service.GetCurrentExpansion() - ).c_str() - ); + for (auto& e : l) { + if (e.faction_id <= 0 || e.value == 0) { + continue; + } + int faction_before; + int faction_minimum; + int faction_maximum; - auto z = GetZoneVersionWithFallback(zone->GetZoneID(), zone->GetInstanceVersion()); - - if (z) { - Message( - Chat::White, - fmt::format( - "Current Zone | [{}] ({}) version [{}] instance_id [{}] min/max expansion ({}/{}) content_flags [{}]", - z->short_name, - z->long_name, - z->version, - zone->GetInstanceID(), - z->min_expansion, - z->max_expansion, - z->content_flags - ).c_str() - ); - } + FactionMods faction_modifiers; - SendChatLineBreak(); -} + content_db.GetFactionData(&faction_modifiers, class_id, race_id, deity_id, e.faction_id); -void Client::SendChatLineBreak(uint16 color) { - Message(color, "------------------------------------------------"); -} + if (is_quest) { + if (e.value > 0) { + e.value = -std::abs(e.value); + } else if (e.value < 0) { + e.value = std::abs(e.value); + } + } -void Client::SendCrossZoneMessage( - Client* client, const std::string& character_name, uint16_t chat_type, const std::string& message) -{ - // if client is null, falls back to sending a cross zone message by name - if (!client && !character_name.empty()) - { - client = entity_list.GetClientByName(character_name.c_str()); - } + // Adjust the amount you can go up or down so the resulting range + // is PERSONAL_MAX - PERSONAL_MIN + // + // Adjust these values for cases where starting faction is below + // min or above max by not allowing any earn in those directions. + faction_minimum = faction_modifiers.min - faction_modifiers.base; + faction_minimum = std::min(0, faction_minimum); - if (client) - { - client->Message(chat_type, message.c_str()); - } - else if (!character_name.empty() && !message.empty()) - { - uint32_t pack_size = sizeof(CZMessage_Struct); - auto pack = std::make_unique(ServerOP_CZMessage, pack_size); - auto buf = reinterpret_cast(pack->pBuffer); - uint8 update_type = CZUpdateType_Character; - int update_identifier = 0; - buf->update_type = update_type; - buf->update_identifier = update_identifier; - buf->type = chat_type; - strn0cpy(buf->message, message.c_str(), sizeof(buf->message)); - strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); - - worldserver.SendPacket(pack.get()); - } -} + faction_maximum = faction_modifiers.max - faction_modifiers.base; + faction_maximum = std::max(0, faction_maximum); -void Client::SendCrossZoneMessageString( - Client* client, const std::string& character_name, uint16_t chat_type, - uint32_t string_id, const std::initializer_list& arguments) -{ - // if client is null, falls back to sending a cross zone message by name - if (!client && !character_name.empty()) // double check client isn't in this zone - { - client = entity_list.GetClientByName(character_name.c_str()); - } + // Get the characters current value with that faction + current_value = GetCharacterFactionLevel(e.faction_id); + faction_before = current_value; - if (!client && character_name.empty()) - { - return; - } +#ifdef LUA_EQEMU + int32 lua_ret = 0; + bool ignore_default = false; + lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, e.value, e.faction_id, current_value, e.temp, faction_minimum, faction_maximum, ignore_default); - SerializeBuffer argument_buffer; - for (const auto& argument : arguments) - { - argument_buffer.WriteString(argument); - } + if (ignore_default) { + e.value = lua_ret; + } +#endif - uint32_t args_size = static_cast(argument_buffer.size()); - uint32_t pack_size = sizeof(CZClientMessageString_Struct) + args_size; - auto pack = std::make_unique(ServerOP_CZClientMessageString, pack_size); - auto buf = reinterpret_cast(pack->pBuffer); - buf->string_id = string_id; - buf->chat_type = chat_type; - strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); - buf->args_size = args_size; - memcpy(buf->args, argument_buffer.buffer(), argument_buffer.size()); - - if (client) - { - client->MessageString(buf); - } - else - { - worldserver.SendPacket(pack.get()); - } + UpdatePersonalFaction( + character_id, + e.value, + e.faction_id, + ¤t_value, + e.temp, + faction_minimum, + faction_maximum + ); + + SendFactionMessage( + e.value, + e.faction_id, + faction_before, + current_value, + e.temp, + faction_minimum, + faction_maximum + ); + } } -void Client::SendDynamicZoneUpdates() +void Client::SetFactionLevel2(uint32 char_id, int32 faction_id, uint8 char_class, uint8 char_race, uint8 char_deity, int32 value, uint8 temp) { - // bit inefficient since each do lookups but it avoids duplicating code here - SendDzCompassUpdate(); - SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Online); + int32 current_value; + + //Get the npc faction list + if(faction_id > 0 && value != 0) { + int32 faction_before_hit; + FactionMods fm; + int32 this_faction_max; + int32 this_faction_min; + + // Find out starting faction for this faction + // It needs to be used to adj max and min personal + // The range is still the same, 1200-3000(4200), but adjusted for base + content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), + faction_id); + + // Adjust the amount you can go up or down so the resulting range + // is PERSONAL_MAX - PERSONAL_MIN + // + // Adjust these values for cases where starting faction is below + // min or above max by not allowing any earn/loss in those directions. + // At least one faction starts out way below min, so we don't want + // to allow loses in those cases, just massive gains. + this_faction_min = fm.min - fm.base; + this_faction_min = std::min(0, this_faction_min); + this_faction_max = fm.max - fm.base; + this_faction_max = std::max(0, this_faction_max); + + //Get the faction modifiers + current_value = GetCharacterFactionLevel(faction_id); + faction_before_hit = current_value; - m_expedition_lockouts = ExpeditionDatabase::LoadCharacterLockouts(CharacterID()); +#ifdef LUA_EQEMU + int32 lua_ret = 0; + bool ignore_default = false; + lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, value, faction_id, current_value, temp, this_faction_min, this_faction_max, ignore_default); - // expeditions are the only dz type that keep the window updated - auto expedition = GetExpedition(); - if (expedition) - { - expedition->GetDynamicZone()->SendClientWindowUpdate(this); + if (ignore_default) { + value = lua_ret; + } +#endif - // live synchronizes lockouts obtained during the active expedition to - // members once they zone into the expedition's dynamic zone instance - if (expedition->GetDynamicZone()->IsCurrentZoneDzInstance()) - { - expedition->SyncCharacterLockouts(CharacterID(), m_expedition_lockouts); - } - } + UpdatePersonalFaction(char_id, value, faction_id, ¤t_value, temp, this_faction_min, this_faction_max); - SendExpeditionLockoutTimers(); + //Message(Chat::Lime, "Min(%d) Max(%d) Before(%d), After(%d)\n", this_faction_min, this_faction_max, faction_before_hit, current_value); - // ask world for any pending invite we saved from a previous zone - RequestPendingExpeditionInvite(); -} + SendFactionMessage(value, faction_id, faction_before_hit, current_value, temp, this_faction_min, this_faction_max); + } -Expedition* Client::CreateExpedition(DynamicZone& dz, bool disable_messages) -{ - return Expedition::TryCreate(this, dz, disable_messages); + return; } -Expedition* Client::CreateExpedition( - const std::string& zone_name, uint32 version, uint32 duration, const std::string& expedition_name, - uint32 min_players, uint32 max_players, bool disable_messages) +int32 Client::GetCharacterFactionLevel(int32 faction_id) { - DynamicZone dz{ ZoneID(zone_name), version, duration, DynamicZoneType::Expedition }; - dz.SetName(expedition_name); - dz.SetMinPlayers(min_players); - dz.SetMaxPlayers(max_players); - - return Expedition::TryCreate(this, dz, disable_messages); + if (faction_id <= 0) + return 0; + faction_map::iterator res; + res = factionvalues.find(faction_id); + if (res == factionvalues.end()) + return 0; + return res->second; } -Expedition* Client::CreateExpeditionFromTemplate(uint32_t dz_template_id) -{ - Expedition* expedition = nullptr; - auto it = zone->dz_template_cache.find(dz_template_id); - if (it != zone->dz_template_cache.end()) - { - DynamicZone dz(DynamicZoneType::Expedition); - dz.LoadTemplate(it->second); - expedition = Expedition::TryCreate(this, dz, false); - } - return expedition; -} +// Common code to set faction level. +// Applies HeroicCHA is it applies +// Checks for bottom out and max faction and old faction db entries +// Updates the faction if we are not minned, maxed or we need to repair -void Client::CreateTaskDynamicZone(int task_id, DynamicZone& dz_request) +void Client::UpdatePersonalFaction(int32 char_id, int32 npc_value, int32 faction_id, int32 *current_value, int32 temp, int32 this_faction_min, int32 this_faction_max) { - if (task_state) - { - task_state->CreateTaskDynamicZone(this, task_id, dz_request); - } -} + bool repair = false; + bool change = false; + + if (itembonuses.HeroicCHA) + { + int faction_mod = itembonuses.HeroicCHA / 5; + // If our result isn't truncated, then just do that + if (npc_value * faction_mod / 100 != 0) + npc_value += npc_value * faction_mod / 100; + // If our result is truncated, then double a mob's value every once and a while to equal what they would have got + else + { + if (zone->random.Int(0, 100) < faction_mod) + npc_value *= 2; + } + } + + // Set flag when to update db + // Repair needed, as db changes could modify a base value for a faction + // and we need to auto correct when that happens. + if (*current_value > this_faction_max) + { + *current_value = this_faction_max; + repair = true; + } + else if (*current_value < this_faction_min) + { + *current_value = this_faction_min; + repair = true; + } + else if ((m_pp.gm != 1) && (npc_value != 0) && + ((npc_value > 0 && *current_value != this_faction_max) || + ((npc_value < 0 && *current_value != this_faction_min)))) + change = true; + + if (change || repair) + { + *current_value += npc_value; + + if (*current_value > this_faction_max) + *current_value = this_faction_max; + else if (*current_value < this_faction_min) + *current_value = this_faction_min; + + database.SetCharacterFactionLevel(char_id, faction_id, *current_value, temp, factionvalues); + } -Expedition* Client::GetExpedition() const -{ - if (zone && m_expedition_id) - { - auto expedition_cache_iter = zone->expedition_cache.find(m_expedition_id); - if (expedition_cache_iter != zone->expedition_cache.end()) - { - return expedition_cache_iter->second.get(); - } - } - return nullptr; +return; } -void Client::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db) -{ - // todo: support for account based lockouts like live AoC expeditions - - // if client already has this lockout, we're replacing it with the new one - m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& existing_lockout) { - return existing_lockout.IsSameLockout(lockout); - } - ), m_expedition_lockouts.end()); - - m_expedition_lockouts.emplace_back(lockout); +// returns the character's faction level, adjusted for racial, class, and deity modifiers +int32 Client::GetModCharacterFactionLevel(int32 faction_id) { + int32 Modded = GetCharacterFactionLevel(faction_id); + FactionMods fm; + if (content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), faction_id)) + { + Modded += fm.base + fm.class_mod + fm.race_mod + fm.deity_mod; - if (update_db) // for quest api - { - ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { lockout }); - } + //Tack on any bonuses from Alliance type spell effects + Modded += GetFactionBonus(faction_id); + Modded += GetItemFactionBonus(faction_id); + } - SendExpeditionLockoutTimers(); + return Modded; } -void Client::AddNewExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, uint32_t seconds, std::string uuid) +void Client::MerchantRejectMessage(Mob *merchant, int primaryfaction) { - auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); - AddExpeditionLockout(lockout, true); + int messageid = 0; + int32 tmpFactionValue = 0; + int32 lowestvalue = 0; + FactionMods fmod; + + // If a faction is involved, get the data. + if (primaryfaction > 0) { + if (content_db.GetFactionData(&fmod, GetClass(), GetFactionRace(), GetDeity(), primaryfaction)) { + tmpFactionValue = GetCharacterFactionLevel(primaryfaction); + lowestvalue = std::min(std::min(tmpFactionValue, fmod.deity_mod), + std::min(fmod.class_mod, fmod.race_mod)); + } + } + // If no primary faction or biggest influence is your faction hit + if (primaryfaction <= 0 || lowestvalue == tmpFactionValue) { + merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS6)); + } else if (lowestvalue == fmod.race_mod) { // race biggest + // Non-standard race (ex. illusioned to wolf) + if (GetRace() > PLAYER_RACE_COUNT) { + messageid = zone->random.Int(1, 3); // these aren't sequential StringIDs :( + switch (messageid) { + case 1: + messageid = WONT_SELL_NONSTDRACE1; + break; + case 2: + messageid = WONT_SELL_NONSTDRACE2; + break; + case 3: + messageid = WONT_SELL_NONSTDRACE3; + break; + default: // w/e should never happen + messageid = WONT_SELL_NONSTDRACE1; + break; + } + merchant->SayString(messageid); + } else { // normal player races + messageid = zone->random.Int(1, 4); + switch (messageid) { + case 1: + messageid = WONT_SELL_RACE1; + break; + case 2: + messageid = WONT_SELL_RACE2; + break; + case 3: + messageid = WONT_SELL_RACE3; + break; + case 4: + messageid = WONT_SELL_RACE4; + break; + default: // w/e should never happen + messageid = WONT_SELL_RACE1; + break; + } + merchant->SayString(messageid, itoa(GetRace())); + } + } else if (lowestvalue == fmod.class_mod) { + merchant->SayString(zone->random.Int(WONT_SELL_CLASS1, WONT_SELL_CLASS5), itoa(GetClass())); + } else { + // Must be deity - these two sound the best for that. + // Can't use a message with a field, GUI wants class/race names. + // for those message IDs. These are straight text. + merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS2)); + } + return; } -void Client::AddExpeditionLockoutDuration( - const std::string& expedition_name, const std::string& event_name, int seconds, - const std::string& uuid, bool update_db) +//o-------------------------------------------------------------- +//| Name: SendFactionMessage +//o-------------------------------------------------------------- +//| Purpose: Send faction change message to client +//o-------------------------------------------------------------- +void Client::SendFactionMessage(int32 tmpvalue, int32 faction_id, int32 faction_before_hit, int32 totalvalue, uint8 temp, int32 this_faction_min, int32 this_faction_max) { - auto it = std::find_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& lockout) { - return lockout.IsSameLockout(expedition_name, event_name); - }); - - if (it != m_expedition_lockouts.end()) - { - it->AddLockoutTime(seconds); + char name[50]; + int32 faction_value; + + // If we're dropping from MAX or raising from MIN or repairing, + // we should base the message on the new updated value so we don't show + // a min MAX message + // + // If we're changing any other place, we use the value before the + // hit. For example, if we go from 1199 to 1200 which is the MAX + // we still want to say faction got better this time around. + + if (!EQ::ValueWithin(faction_before_hit, this_faction_min, this_faction_max)) { + faction_value = totalvalue; + } else { + faction_value = faction_before_hit; + } + + // default to Faction# if we couldn't get the name from the ID + if (!content_db.GetFactionName(faction_id, name, sizeof(name))) { + snprintf(name, sizeof(name), "Faction%i", faction_id); + } + + if (tmpvalue == 0 || temp == 1 || temp == 2) { + return; + } else if (faction_value >= this_faction_max) { + MessageString(Chat::Yellow, FACTION_BEST, name); + } else if (faction_value <= this_faction_min) { + MessageString(Chat::Yellow, FACTION_WORST, name); + } else if (tmpvalue > 0 && !RuleB(Client, UseLiveFactionMessage)) { + MessageString(Chat::Yellow, FACTION_BETTER, name); + } else if (tmpvalue < 0 && !RuleB(Client, UseLiveFactionMessage)) { + MessageString(Chat::Yellow, FACTION_WORSE, name); + } else if (RuleB(Client, UseLiveFactionMessage)) { + Message( + Chat::Yellow, + fmt::format( + "Your faction standing with {} has been adjusted by {}.", + name, + tmpvalue + ).c_str() + ); + } //New Live faction message (14261) +} - if (!uuid.empty()) - { - it->SetUUID(uuid); - } +void Client::LoadAccountFlags() +{ + accountflags.clear(); - if (update_db) - { - ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { *it }); - } + const auto& l = AccountFlagsRepository::GetWhere(database, fmt::format("p_accid = {}", account_id)); + if (l.empty()) { + return; + } - SendExpeditionLockoutTimers(); - } - else if (seconds > 0) // missing lockouts inserted for reductions would be instantly expired - { - auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); - AddExpeditionLockout(lockout, update_db); - } + for (const auto& e : l) { + accountflags[e.p_flag] = e.p_value; + } } -void Client::RemoveExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, bool update_db) +void Client::ClearAccountFlag(const std::string& flag) { - m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& lockout) { - return lockout.IsSameLockout(expedition_name, event_name); - } - ), m_expedition_lockouts.end()); + auto e = AccountFlagsRepository::NewEntity(); - if (update_db) // for quest api - { - ExpeditionDatabase::DeleteCharacterLockout(CharacterID(), expedition_name, event_name); - } + e.p_accid = account_id; + e.p_flag = flag; - SendExpeditionLockoutTimers(); + AccountFlagsRepository::ClearFlag(database, e); } -void Client::RemoveAllExpeditionLockouts(const std::string& expedition_name, bool update_db) +void Client::SetAccountFlag(const std::string& flag, const std::string& value) { - if (expedition_name.empty()) - { - if (update_db) - { - ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID()); - } - m_expedition_lockouts.clear(); - } - else - { - if (update_db) - { - ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID(), expedition_name); - } - - m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& lockout) { - return lockout.GetExpeditionName() == expedition_name; - } - ), m_expedition_lockouts.end()); - } + auto e = AccountFlagsRepository::NewEntity(); - SendExpeditionLockoutTimers(); -} + e.p_accid = account_id; + e.p_flag = flag; + e.p_value = value; -const ExpeditionLockoutTimer* Client::GetExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, bool include_expired) const -{ - for (const auto& expedition_lockout : m_expedition_lockouts) - { - if ((include_expired || !expedition_lockout.IsExpired()) && - expedition_lockout.IsSameLockout(expedition_name, event_name)) - { - return &expedition_lockout; - } - } - return nullptr; -} + AccountFlagsRepository::ReplaceFlag(database, e); -std::vector Client::GetExpeditionLockouts( - const std::string& expedition_name, bool include_expired) -{ - std::vector lockouts; - for (const auto& lockout : m_expedition_lockouts) - { - if ((include_expired || !lockout.IsExpired()) && - lockout.GetExpeditionName() == expedition_name) - { - lockouts.emplace_back(lockout); - } - } - return lockouts; + accountflags[flag] = value; } -bool Client::HasExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, bool include_expired) +std::string Client::GetAccountFlag(const std::string& flag) { - return (GetExpeditionLockout(expedition_name, event_name, include_expired) != nullptr); + return accountflags[flag]; } -void Client::SendExpeditionLockoutTimers() +std::vector Client::GetAccountFlags() { - std::vector lockout_entries; + std::vector l; - // client displays lockouts rounded down to nearest minute, send lockouts - // with 60s offset added to compensate (live does this too) - constexpr uint32_t rounding_seconds = 60; + l.reserve(accountflags.size()); - // erases expired lockouts while building lockout timer list - for (auto it = m_expedition_lockouts.begin(); it != m_expedition_lockouts.end();) - { - uint32_t seconds_remaining = it->GetSecondsRemaining(); - if (seconds_remaining == 0) - { - it = m_expedition_lockouts.erase(it); - } - else - { - ExpeditionLockoutTimerEntry_Struct lockout; - strn0cpy(lockout.expedition_name, it->GetExpeditionName().c_str(), sizeof(lockout.expedition_name)); - lockout.seconds_remaining = seconds_remaining + rounding_seconds; - lockout.event_type = it->IsReplayTimer() ? Expedition::REPLAY_TIMER_ID : Expedition::EVENT_TIMER_ID; - strn0cpy(lockout.event_name, it->GetEventName().c_str(), sizeof(lockout.event_name)); - - lockout_entries.emplace_back(lockout); - ++it; - } - } + for (const auto& e : accountflags) { + l.emplace_back(e.first); + } - uint32_t lockout_count = static_cast(lockout_entries.size()); - uint32_t lockout_entries_size = sizeof(ExpeditionLockoutTimerEntry_Struct) * lockout_count; - uint32_t outsize = sizeof(ExpeditionLockoutTimers_Struct) + lockout_entries_size; - auto outapp = std::make_unique(OP_DzExpeditionLockoutTimers, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->count = lockout_count; - if (!lockout_entries.empty()) - { - memcpy(outbuf->timers, lockout_entries.data(), lockout_entries_size); - } - QueuePacket(outapp.get()); + return l; } -void Client::RequestPendingExpeditionInvite() +void Client::ItemTimerCheck() { - uint32_t packsize = sizeof(ServerExpeditionCharacterID_Struct); - auto pack = std::make_unique(ServerOP_ExpeditionRequestInvite, packsize); - auto packbuf = reinterpret_cast(pack->pBuffer); - packbuf->character_id = CharacterID(); - worldserver.SendPacket(pack.get()); + int i; + for (i = EQ::invslot::POSSESSIONS_BEGIN; i <= EQ::invslot::POSSESSIONS_END; i++) + { + TryItemTimer(i); + } + for (i = EQ::invbag::GENERAL_BAGS_BEGIN; i <= EQ::invbag::CURSOR_BAG_END; i++) + { + TryItemTimer(i); + } } -void Client::DzListTimers() +void Client::TryItemTimer(int slot) { - // only lists player's current replay timer lockouts, not all event lockouts - bool found = false; - for (const auto& lockout : m_expedition_lockouts) - { - if (lockout.IsReplayTimer()) - { - found = true; - auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); - MessageString( - Chat::Yellow, DZLIST_REPLAY_TIMER, - time_remaining.days.c_str(), - time_remaining.hours.c_str(), - time_remaining.mins.c_str(), - lockout.GetExpeditionName().c_str() - ); - } - } - - if (!found) - { - MessageString(Chat::Yellow, EXPEDITION_NO_TIMERS); - } + EQ::ItemInstance* inst = m_inv.GetItem(slot); + if(!inst) { + return; + } + + auto item_timers = inst->GetTimers(); + auto it_iter = item_timers.begin(); + while(it_iter != item_timers.end()) { + if(it_iter->second.Check()) { + if (parse->ItemHasQuestSub(inst, EVENT_TIMER)) { + parse->EventItem(EVENT_TIMER, this, inst, nullptr, it_iter->first, 0); + } + } + ++it_iter; + } + + if (slot > EQ::invslot::EQUIPMENT_END) { + return; + } + + for (int x = EQ::invaug::SOCKET_BEGIN; x <= EQ::invaug::SOCKET_END; ++x) + { + EQ::ItemInstance * a_inst = inst->GetAugment(x); + if(!a_inst) { + continue; + } + + auto& item_timers = a_inst->GetTimers(); + auto it_iter = item_timers.begin(); + while(it_iter != item_timers.end()) { + if(it_iter->second.Check()) { + if (parse->ItemHasQuestSub(a_inst, EVENT_TIMER)) { + parse->EventItem(EVENT_TIMER, this, a_inst, nullptr, it_iter->first, 0); + } + } + ++it_iter; + } + } } -void Client::SetDzRemovalTimer(bool enable_timer) -{ - uint32_t timer_ms = RuleI(DynamicZone, ClientRemovalDelayMS); - - LogDynamicZones( - "Character [{}] instance [{}] removal timer enabled: [{}] delay (ms): [{}]", - CharacterID(), zone ? zone->GetInstanceID() : 0, enable_timer, timer_ms - ); - - if (enable_timer) - { - dynamiczone_removal_timer.Start(timer_ms); - } - else - { - dynamiczone_removal_timer.Disable(); - } +void Client::SendItemScale(EQ::ItemInstance *inst) { + int slot = m_inv.GetSlotByItemInst(inst); + if(slot != -1) { + inst->ScaleItem(); + SendItemPacket(slot, inst, ItemPacketCharmUpdate); + CalcBonuses(); + } } -void Client::SendDzCompassUpdate() +void Client::AddRespawnOption(std::string option_name, uint32 zoneid, uint16 instance_id, float x, float y, float z, float heading, bool initial_selection, int8 position) { - // client may be associated with multiple dynamic zone compasses in this zone - std::vector compass_entries; - - // need to sort by local doorid in case multiple have same dz switch id (live only sends first) - // todo: just store zone's door list ordered and ditch this - std::vector switches; - switches.reserve(entity_list.GetDoorsList().size()); - for (const auto& door_pair : entity_list.GetDoorsList()) - { - switches.push_back(door_pair.second); - } - std::sort(switches.begin(), switches.end(), - [](Doors* lhs, Doors* rhs) { return lhs->GetDoorID() < rhs->GetDoorID(); }); - - for (const auto& client_dz : GetDynamicZones()) - { - auto& compass = client_dz->GetCompassLocation(); - if (zone && zone->IsZone(compass.zone_id, 0)) - { - DynamicZoneCompassEntry_Struct entry{}; - entry.dz_zone_id = client_dz->GetZoneID(); - entry.dz_instance_id = client_dz->GetInstanceID(); - entry.dz_type = static_cast(client_dz->GetType()); - entry.x = compass.x; - entry.y = compass.y; - entry.z = compass.z; - - compass_entries.emplace_back(entry); - } - - // if client has a dz with a switch id add compass to any switch locs that share it - if (client_dz->GetSwitchID() != 0) - { - // live only sends one if multiple in zone have the same switch id - auto it = std::find_if(switches.begin(), switches.end(), - [&](const auto& eqswitch) { - return eqswitch->GetDzSwitchID() == client_dz->GetSwitchID(); - }); - - if (it != switches.end()) - { - DynamicZoneCompassEntry_Struct entry{}; - entry.dz_zone_id = client_dz->GetZoneID(); - entry.dz_instance_id = client_dz->GetInstanceID(); - entry.dz_type = static_cast(client_dz->GetType()); - entry.dz_switch_id = client_dz->GetSwitchID(); - entry.x = (*it)->GetX(); - entry.y = (*it)->GetY(); - entry.z = (*it)->GetZ(); - - compass_entries.emplace_back(entry); - } - } - } - - // compass set via MarkSingleCompassLocation() - if (m_has_quest_compass) - { - DynamicZoneCompassEntry_Struct entry{}; - entry.dz_zone_id = 0; - entry.dz_instance_id = 0; - entry.dz_type = 0; - entry.x = m_quest_compass.x; - entry.y = m_quest_compass.y; - entry.z = m_quest_compass.z; - - compass_entries.emplace_back(entry); - } - - QueuePacket(CreateCompassPacket(compass_entries).get()); + //If respawn window is already open, any changes would create an inconsistency with the client + if (IsHoveringForRespawn()) { return; } + + if (zoneid == 0) + zoneid = zone->GetZoneID(); + + //Create respawn option + RespawnOption res_opt; + res_opt.name = option_name; + res_opt.zone_id = zoneid; + res_opt.instance_id = instance_id; + res_opt.x = x; + res_opt.y = y; + res_opt.z = z; + res_opt.heading = heading; + + if (position == -1 || position >= respawn_options.size()) + { + //No position specified, or specified beyond the end, simply append + respawn_options.push_back(res_opt); + //Make this option the initial selection for the window if desired + if (initial_selection) + initial_respawn_selection = static_cast(respawn_options.size()) - 1; + } + else if (position == 0) + { + respawn_options.push_front(res_opt); + if (initial_selection) + initial_respawn_selection = 0; + } + else + { + //Insert new option between existing options + std::list::iterator itr; + uint8 pos = 0; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + if (pos++ == position) + { + respawn_options.insert(itr,res_opt); + //Make this option the initial selection for the window if desired + if (initial_selection) + initial_respawn_selection = pos; + return; + } + } + } } -std::unique_ptr Client::CreateCompassPacket( - const std::vector& compass_entries) +bool Client::RemoveRespawnOption(std::string option_name) { - uint32 count = static_cast(compass_entries.size()); - uint32 entries_size = sizeof(DynamicZoneCompassEntry_Struct) * count; - uint32 outsize = sizeof(DynamicZoneCompass_Struct) + entries_size; - auto outapp = std::make_unique(OP_DzCompass, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->count = count; - memcpy(outbuf->entries, compass_entries.data(), entries_size); - - return outapp; + //If respawn window is already open, any changes would create an inconsistency with the client + if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } + + bool had = false; + RespawnOption* opt = nullptr; + std::list::iterator itr; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + opt = &(*itr); + if (opt->name.compare(option_name) == 0) + { + itr = respawn_options.erase(itr); + had = true; + //could be more with the same name, so keep going... + } + } + return had; } -void Client::GoToDzSafeReturnOrBind(const DynamicZone* dynamic_zone) +bool Client::RemoveRespawnOption(uint8 position) { - if (dynamic_zone) - { - auto safereturn = dynamic_zone->GetSafeReturnLocation(); - if (safereturn.zone_id != 0) - { - LogDynamicZonesDetail("Sending [{}] to safereturn zone [{}]", CharacterID(), safereturn.zone_id); - MovePC(safereturn.zone_id, 0, safereturn.x, safereturn.y, safereturn.z, safereturn.heading); - return; - } - } - - GoToBind(); + //If respawn window is already open, any changes would create an inconsistency with the client + if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } + + //Easy cases first... + if (position == 0) + { + respawn_options.pop_front(); + return true; + } + else if (position == (respawn_options.size() - 1)) + { + respawn_options.pop_back(); + return true; + } + + std::list::iterator itr; + uint8 pos = 0; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + if (pos++ == position) + { + respawn_options.erase(itr); + return true; + } + } + return false; } -void Client::AddDynamicZoneID(uint32_t dz_id) +void Client::SetHunger(int32 in_hunger) { - auto it = std::find_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), - [&](uint32_t current_dz_id) { return current_dz_id == dz_id; }); + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); + Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; + sta->food = in_hunger; + sta->water = m_pp.thirst_level > 6000 ? 6000 : m_pp.thirst_level; - if (it == m_dynamic_zone_ids.end()) - { - LogDynamicZonesDetail("Adding dz [{}] to client [{}]", dz_id, GetName()); - m_dynamic_zone_ids.push_back(dz_id); - } -} + m_pp.hunger_level = in_hunger; -void Client::RemoveDynamicZoneID(uint32_t dz_id) -{ - LogDynamicZonesDetail("Removing dz [{}] from client [{}]", dz_id, GetName()); - m_dynamic_zone_ids.erase(std::remove_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), - [&](uint32_t current_dz_id) { return current_dz_id == dz_id; } - ), m_dynamic_zone_ids.end()); + QueuePacket(outapp); + safe_delete(outapp); } -std::vector Client::GetDynamicZones(uint32_t zone_id, int zone_version) +void Client::SetThirst(int32 in_thirst) { - std::vector client_dzs; + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); + Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; + sta->food = m_pp.hunger_level > 6000 ? 6000 : m_pp.hunger_level; + sta->water = in_thirst; - for (uint32_t dz_id : m_dynamic_zone_ids) - { - auto dz = DynamicZone::FindDynamicZoneByID(dz_id); - if (dz && - (zone_id == 0 || dz->GetZoneID() == zone_id) && - (zone_version < 0 || dz->GetZoneVersion() == zone_version)) - { - client_dzs.emplace_back(dz); - } - } + m_pp.thirst_level = in_thirst; - return client_dzs; + QueuePacket(outapp); + safe_delete(outapp); } -void Client::SetDynamicZoneMemberStatus(DynamicZoneMemberStatus status) +void Client::SetIntoxication(int32 in_intoxication) { - // sets status on all associated dzs client may have. if client is online - // inside a dz, only that dz has the "In Dynamic Zone" status set - for (auto& dz : GetDynamicZones()) - { - // the rule to disable this status is handled internally by the dz - if (status == DynamicZoneMemberStatus::Online && dz->IsCurrentZoneDzInstance()) - { - status = DynamicZoneMemberStatus::InDynamicZone; - } - dz->SetMemberStatus(CharacterID(), status); - } + m_pp.intoxication = EQ::Clamp(in_intoxication, 0, 200); } -void Client::MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid) +void Client::SetConsumption(int32 in_hunger, int32 in_thirst) { - if (zone_id == 0) - { - return; - } - - auto client_dzs = GetDynamicZones(zone_id, zone_version); + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); + Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; + sta->food = in_hunger; + sta->water = in_thirst; - if (client_dzs.empty()) - { - if (msg_if_invalid) - { - MessageString(Chat::Red, DYNAMICZONE_WAY_IS_BLOCKED); // unconfirmed message - } - } - else if (client_dzs.size() == 1) - { - auto dz = client_dzs.front(); - DynamicZoneLocation zonein = dz->GetZoneInLocation(); - ZoneMode zone_mode = dz->HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords; - MovePC(zone_id, dz->GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode); - } - else - { - LogDynamicZonesDetail("Sending DzSwitchListWnd to [{}] for zone [{}] with [{}] dynamic zone(s)", - CharacterID(), zone_id, client_dzs.size()); + m_pp.hunger_level = in_hunger; + m_pp.thirst_level = in_thirst; - // client has more than one dz for this zone, send out the switchlist window - QueuePacket(CreateDzSwitchListPacket(client_dzs).get()); - } + QueuePacket(outapp); + safe_delete(outapp); } -bool Client::TryMovePCDynamicZoneSwitch(int dz_switch_id) +void Client::Consume(const EQ::ItemData *item, uint8 type, int16 slot, bool auto_consume) { - auto client_dzs = GetDynamicZones(); + if (!item) + return; - std::vector switch_dzs; - auto it = std::copy_if(client_dzs.begin(), client_dzs.end(), std::back_inserter(switch_dzs), - [&](const DynamicZone* dz) { return dz->GetSwitchID() == dz_switch_id; }); + int increase = item->CastTime_ * 100; + if (!auto_consume) // force feeding is half as effective + increase /= 2; - if (switch_dzs.size() == 1) - { - LogDynamicZonesDetail("Moving client [{}] to dz with switch id [{}]", GetName(), dz_switch_id); - switch_dzs.front()->MovePCInto(this, true); - } - else if (switch_dzs.size() > 1) - { - QueuePacket(CreateDzSwitchListPacket(switch_dzs).get()); - } + if (increase < 0) // wasn't food? oh well + return; - return !switch_dzs.empty(); -} + if (type == EQ::item::ItemTypeFood) { + m_pp.hunger_level += increase; -std::unique_ptr Client::CreateDzSwitchListPacket( - const std::vector& client_dzs) -{ - uint32 count = static_cast(client_dzs.size()); - uint32 entries_size = sizeof(DynamicZoneChooseZoneEntry_Struct) * count; - uint32 outsize = sizeof(DynamicZoneChooseZone_Struct) + entries_size; - auto outapp = std::make_unique(OP_DzChooseZone, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->count = count; - for (int i = 0; i < client_dzs.size(); ++i) - { - outbuf->choices[i].dz_zone_id = client_dzs[i]->GetZoneID(); - outbuf->choices[i].dz_instance_id = client_dzs[i]->GetInstanceID(); - outbuf->choices[i].dz_type = static_cast(client_dzs[i]->GetType()); - strn0cpy(outbuf->choices[i].description, client_dzs[i]->GetName().c_str(), sizeof(outbuf->choices[i].description)); - strn0cpy(outbuf->choices[i].leader_name, client_dzs[i]->GetLeaderName().c_str(), sizeof(outbuf->choices[i].leader_name)); - } - return outapp; -} + LogFood("Consuming food, points added to hunger_level: [{}] - current_hunger: [{}]", increase, m_pp.hunger_level); -void Client::MovePCDynamicZone(const std::string& zone_name, int zone_version, bool msg_if_invalid) -{ - auto zone_id = ZoneID(zone_name.c_str()); - MovePCDynamicZone(zone_id, zone_version, msg_if_invalid); -} + DeleteItemInInventory(slot, 1); -void Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clip_through_walls, bool calculate_speed) { - BuffFadeByEffect(SE_Levitate); - if (CheckLosFN(target_x, target_y, target_z, 6.0f) || ignore_los) { - auto p = new EQApplicationPacket(OP_Fling, sizeof(fling_struct)); - auto* f = (fling_struct*) p->pBuffer; - - if (!calculate_speed) { - f->speed_z = value; - } else { - auto speed = 1.0f; - const auto distance = CalculateDistance(target_x, target_y, target_z); - - auto z_diff = target_z - GetZ(); - if (z_diff != 0.0f) { - speed += std::abs(z_diff) / 12.0f; - } + if (!auto_consume) // no message if the client consumed for us + entity_list.MessageCloseString(this, true, 50, 0, EATING_MESSAGE, GetName(), item->Name); - speed += distance / 200.0f; + LogFood("Eating from slot: [{}]", (int)slot); - speed++; + } else { + m_pp.thirst_level += increase; - speed = std::abs(speed); + DeleteItemInInventory(slot, 1); - f->speed_z = speed; - } + LogFood("Consuming drink, points added to thirst_level: [{}] current_thirst: [{}]", increase, m_pp.thirst_level); - f->collision = clip_through_walls ? 0 : -1; - f->travel_time = -1; - f->unk3 = 1; - f->disable_fall_damage = 1; - f->new_y = target_y; - f->new_x = target_x; - f->new_z = target_z; - p->priority = 6; - FastQueuePacket(&p); - } + if (!auto_consume) // no message if the client consumed for us + entity_list.MessageCloseString(this, true, 50, 0, DRINKING_MESSAGE, GetName(), item->Name); + + LogFood("Drinking from slot: [{}]", (int)slot); + } } -std::vector Client::GetLearnableDisciplines(uint8 min_level, uint8 max_level) { - std::vector learnable_disciplines; - for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { - bool learnable = true; - if (!IsValidSpell(spell_id)) { - continue; - } +void Client::SendMarqueeMessage(uint32 type, std::string message, uint32 duration) +{ + if (!duration || !message.length()) { + return; + } - if (!IsDiscipline(spell_id)) { - continue; - } + EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); + ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; - if (spells[spell_id].classes[Class::Warrior] == 0) { - continue; - } + cms->type = type; + cms->unk04 = 10; + cms->priority = 510; + cms->fade_in_time = 0; + cms->fade_out_time = 3000; + cms->duration = duration; - if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { - continue; - } + strcpy(cms->msg, message.c_str()); - if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { - continue; - } + QueuePacket(&outapp); +} - if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { - continue; - } +void Client::SendMarqueeMessage(uint32 type, uint32 priority, uint32 fade_in, uint32 fade_out, uint32 duration, std::string message) +{ + if (!duration || !message.length()) { + return; + } - if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { - continue; - } + EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); + ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; - if (HasDisciplineLearned(spell_id)) { - continue; - } + cms->type = type; + cms->unk04 = 10; + cms->priority = priority; + cms->fade_in_time = fade_in; + cms->fade_out_time = fade_out; + cms->duration = duration; - if (RuleB(Spells, EnableSpellGlobals) && !SpellGlobalCheck(spell_id, CharacterID())) { - learnable = false; - } else if (RuleB(Spells, EnableSpellBuckets) && !SpellBucketCheck(spell_id, CharacterID())) { - learnable = false; - } + strcpy(cms->msg, message.c_str()); - if (learnable) { - learnable_disciplines.push_back(spell_id); - } - } - return learnable_disciplines; + QueuePacket(&outapp); } -std::vector Client::GetLearnedDisciplines() { - std::vector learned_disciplines; - for (int index = 0; index < MAX_PP_DISCIPLINES; index++) { - if (IsValidSpell(m_pp.disciplines.values[index])) { - learned_disciplines.push_back(m_pp.disciplines.values[index]); - } - } - return learned_disciplines; +void Client::PlayMP3(const char* fname) +{ + std::string filename = fname; + auto outapp = new EQApplicationPacket(OP_PlayMP3, filename.length() + 1); + PlayMP3_Struct* buf = (PlayMP3_Struct*)outapp->pBuffer; + strncpy(buf->filename, fname, filename.length()); + QueuePacket(outapp); + safe_delete(outapp); } -std::vector Client::GetMemmedSpells() { - std::vector memmed_spells; - for (int index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { - if (IsValidSpell(m_pp.mem_spells[index])) { - memmed_spells.push_back(m_pp.mem_spells[index]); - } - } - return memmed_spells; +void Client::ExpeditionSay(const char *str, int ExpID) { + + std::string query = StringFormat("SELECT `player_name` FROM `cust_inst_players` " + "WHERE `inst_id` = %i", ExpID); + auto results = database.QueryDatabase(query); + if (!results.Success()) + return; + + if(results.RowCount() == 0) { + Message(Chat::Lime, "You say to the expedition, '%s'", str); + return; + } + + for(auto row = results.begin(); row != results.end(); ++row) { + const char* charName = row[0]; + if(strcmp(charName, GetCleanName()) != 0) { + worldserver.SendEmoteMessage( + charName, + 0, + AccountStatus::Player, + Chat::Lime, + fmt::format( + "{} says to the expedition, '{}'", + GetCleanName(), + str + ).c_str() + ); + } + // ChannelList->CreateChannel(ChannelName, ChannelOwner, ChannelPassword, true, Strings::ToInt(row[3])); + } + + } -std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { - std::vector scribeable_spells; - std::unordered_map> spell_group_cache = LoadSpellGroupCache(min_level, max_level); +int Client::GetQuiverHaste(int delay) +{ + const EQ::ItemInstance *pi = nullptr; + for (int r = EQ::invslot::GENERAL_BEGIN; r <= EQ::invslot::GENERAL_END; r++) { + pi = GetInv().GetItem(r); + if (pi && pi->IsClassBag() && pi->GetItem()->BagType == EQ::item::BagTypeQuiver && + pi->GetItem()->BagWR > 0) + break; + if (r == EQ::invslot::GENERAL_END) + // we will get here if we don't find a valid quiver + return 0; + } + return (pi->GetItem()->BagWR * 0.0025f * delay) + 1; +} - for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { - bool scribeable = true; - if (!IsValidSpell(spell_id)) { - continue; - } +void Client::SendColoredText(uint32 color, std::string message) +{ + // arbitrary size limit + if (message.size() > 512) // live does send this with empty strings sometimes ... + return; + auto outapp = new EQApplicationPacket(OP_ColoredText, sizeof(ColoredText_Struct) + message.size()); + ColoredText_Struct *cts = (ColoredText_Struct *)outapp->pBuffer; + cts->color = color; + strcpy(cts->msg, message.c_str()); + QueuePacket(outapp); + safe_delete(outapp); +} - if (IsDiscipline(spell_id)) { - continue; - } - if (spells[spell_id].classes[Class::Warrior] == 0) { - continue; - } +void Client::QuestReward(Mob* target, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, uint32 itemid, uint32 exp, bool faction) +{ - if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { - continue; - } + auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); + memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); + QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; - if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { - continue; - } + qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name + qr->target_id = GetID(); // The Client ID (this) + qr->copper = copper; + qr->silver = silver; + qr->gold = gold; + qr->platinum = platinum; + qr->item_id[0] = itemid; + qr->exp_reward = exp; - if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { - continue; - } + if (copper > 0 || silver > 0 || gold > 0 || platinum > 0) { + AddMoneyToPP(copper, silver, gold, platinum); + } - if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { - continue; - } + if (itemid > 0) { + SummonItemIntoInventory(itemid, -1, 0, 0, 0, 0, 0, 0, false); + } - if (HasSpellScribed(spell_id)) { - continue; - } + if (faction) { + if (target && target->IsNPC() && !target->IsCharmed()) { + int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); + SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); + qr->faction = target->CastToNPC()->GetPrimaryFaction(); + qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. + } + } - if ( - RuleB(Spells, EnableSpellGlobals) && - !SpellGlobalCheck(spell_id, CharacterID()) - ) { - scribeable = false; - } else if ( - RuleB(Spells, EnableSpellBuckets) && - !SpellBucketCheck(spell_id, CharacterID()) - ) { - scribeable = false; - } + if (exp > 0) { + AddEXP(ExpSource::Quest, exp); + } - if (spells[spell_id].spell_group) { - const auto& g = spell_group_cache.find(spells[spell_id].spell_group); - if (g != spell_group_cache.end()) { - for (const auto& s : g->second) { - if ( - EQ::ValueWithin(spells[s].classes[m_pp.class_ - 1], min_level, max_level) && - s == spell_id && - scribeable - ) { - scribeable_spells.push_back(spell_id); - } - continue; - } - } - } else if (scribeable) { - scribeable_spells.push_back(spell_id); - } - } - return scribeable_spells; + QueuePacket(outapp, true, Client::CLIENT_CONNECTED); + safe_delete(outapp); } -std::vector Client::GetScribedSpells() { - std::vector scribed_spells; - for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { - if (IsValidSpell(m_pp.spell_book[index])) { - scribed_spells.push_back(m_pp.spell_book[index]); - } - } - return scribed_spells; -} +void Client::QuestReward(Mob* target, const QuestReward_Struct &reward, bool faction) +{ + auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); + memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); + QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; -void Client::SetAnon(uint8 anon_flag) { - m_pp.anon = anon_flag; - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; - spawn_appearance->spawn_id = GetID(); - spawn_appearance->type = AppearanceType::Anonymous; - spawn_appearance->parameter = anon_flag; - entity_list.QueueClients(this, outapp); - Save(); - UpdateWho(); - safe_delete(outapp); -} + memcpy(qr, &reward, sizeof(QuestReward_Struct)); -void Client::SetAFK(uint8 afk_flag) { - AFK = afk_flag; - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; - spawn_appearance->spawn_id = GetID(); - spawn_appearance->type = AppearanceType::AFK; - spawn_appearance->parameter = afk_flag; - entity_list.QueueClients(this, outapp); - safe_delete(outapp); -} + // not set in caller because reasons + qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name, tasks won't set this -void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { - uint32 zone_id = ZoneID(zone_short_name); - std::string current_instance_type = Strings::ToLower(instance_type); - std::string instance_type_name = "public"; - if (current_instance_type.find("solo") != std::string::npos) { - instance_type_name = GetCleanName(); - } else if (current_instance_type.find("group") != std::string::npos) { - uint32 group_id = (GetGroup() ? GetGroup()->GetID() : 0); - instance_type_name = itoa(group_id); - } else if (current_instance_type.find("raid") != std::string::npos) { - uint32 raid_id = (GetRaid() ? GetRaid()->GetID() : 0); - instance_type_name = itoa(raid_id); - } else if (current_instance_type.find("guild") != std::string::npos) { - uint32 guild_id = (GuildID() > 0 ? GuildID() : 0); - instance_type_name = itoa(guild_id); - } + if (reward.copper > 0 || reward.silver > 0 || reward.gold > 0 || reward.platinum > 0) { + AddMoneyToPP(reward.copper, reward.silver, reward.gold, reward.platinum); + } - std::string full_bucket_name = fmt::format( - "{}_{}_{}_{}", - current_instance_type, - instance_type_name, - instance_identifier, - zone_short_name - ); - std::string current_bucket_value = DataBucket::GetData(full_bucket_name); - uint16 instance_id = 0; - - if (current_bucket_value.length() > 0) { - instance_id = Strings::ToInt(current_bucket_value); - } else { - if(!database.GetUnusedInstanceID(instance_id)) { - Message(Chat::White, "Server was unable to find a free instance id."); - return; - } + for (int i = 0; i < QUESTREWARD_COUNT; ++i) { + if (reward.item_id[i] > 0) { + SummonItemIntoInventory(reward.item_id[i], -1, 0, 0, 0, 0, 0, 0, false); + } + } - if(!database.CreateInstance(instance_id, zone_id, instance_version, duration)) { - Message(Chat::White, "Server was unable to create a new instance."); - return; - } + // only process if both are valid + // if we don't have a target here, we want to just reward, but if there is a target, need to check charm + if (reward.faction && reward.faction_mod && (target == nullptr || !target->IsCharmed())) { + RewardFaction(reward.faction, reward.faction_mod); + } - DataBucket::SetData(full_bucket_name, itoa(instance_id), itoa(duration)); - } + // legacy support + if (faction) { + if (target && target->IsNPC() && !target->IsCharmed()) { + int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); + SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); + qr->faction = target->CastToNPC()->GetPrimaryFaction(); + qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. + } + } - AssignToInstance(instance_id); - MovePC(zone_id, instance_id, x, y, z, heading); + if (reward.exp_reward > 0) { + AddEXP(ExpSource::Quest, reward.exp_reward); + } + + QueuePacket(outapp, true, Client::CLIENT_CONNECTED); + safe_delete(outapp); } -uint32 Client::CountItem(uint32 item_id) +void Client::CashReward(uint32 copper, uint32 silver, uint32 gold, uint32 platinum) { - uint32 quantity = 0; - EQ::ItemInstance *item = nullptr; + auto outapp = std::make_unique(OP_CashReward, sizeof(CashReward_Struct)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->copper = copper; + outbuf->silver = silver; + outbuf->gold = gold; + outbuf->platinum = platinum; - for (const int16& slot_id : GetInventorySlots()) { - item = GetInv().GetItem(slot_id); - if (item && item->GetID() == item_id) { - quantity += (item->IsStackable() ? item->GetCharges() : 1); - } - } + AddMoneyToPP(copper, silver, gold, platinum); - return quantity; + QueuePacket(outapp.get()); } -void Client::ResetItemCooldown(uint32 item_id) +void Client::RewardFaction(int faction_id, int amount) { - EQ::ItemInstance *item = nullptr; - const EQ::ItemData* item_d = database.GetItem(item_id); - if (!item_d) { - return; - } - int recast_type = item_d->RecastType; - bool found_item = false; - - for (const int16& slot_id : GetInventorySlots()) { - item = GetInv().GetItem(slot_id); - if (item) { - item_d = item->GetItem(); - if ( - item_d && - item->GetID() == item_id || - ( - item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && - item_d->RecastType == recast_type - ) - ) { - item->SetRecastTimestamp(0); - DeleteItemRecastTimer(item_d->ID); - SendItemPacket(slot_id, item, ItemPacketCharmUpdate); - found_item = true; - } - } - } + SetFactionLevel2(CharacterID(), faction_id, GetClass(), GetBaseRace(), GetDeity(), amount, false); + + auto f = zone->GetFactionAssociation(faction_id); + if (!f) { + return; + } + + std::vector faction_ids = { + f->id_1, + f->id_2, + f->id_3, + f->id_4, + f->id_5, + f->id_6, + f->id_7, + f->id_8, + f->id_9, + f->id_10 + }; + + std::vector faction_modifiers = { + f->mod_1, + f->mod_2, + f->mod_3, + f->mod_4, + f->mod_5, + f->mod_6, + f->mod_7, + f->mod_8, + f->mod_9, + f->mod_10 + }; + + std::vector temporary_values = { + static_cast(faction_modifiers[0] * amount), + static_cast(faction_modifiers[1] * amount), + static_cast(faction_modifiers[2] * amount), + static_cast(faction_modifiers[3] * amount), + static_cast(faction_modifiers[4] * amount), + static_cast(faction_modifiers[5] * amount), + static_cast(faction_modifiers[6] * amount), + static_cast(faction_modifiers[7] * amount), + static_cast(faction_modifiers[8] * amount), + static_cast(faction_modifiers[9] * amount) + }; + + std::vector signs = { + temporary_values[0] < 0.0f ? -1 : 1, + temporary_values[1] < 0.0f ? -1 : 1, + temporary_values[2] < 0.0f ? -1 : 1, + temporary_values[3] < 0.0f ? -1 : 1, + temporary_values[4] < 0.0f ? -1 : 1, + temporary_values[5] < 0.0f ? -1 : 1, + temporary_values[6] < 0.0f ? -1 : 1, + temporary_values[7] < 0.0f ? -1 : 1, + temporary_values[8] < 0.0f ? -1 : 1, + temporary_values[9] < 0.0f ? -1 : 1 + }; + + std::vector new_values = { + std::max(1, static_cast(std::abs(temporary_values[0]))) * signs[0], + std::max(1, static_cast(std::abs(temporary_values[1]))) * signs[1], + std::max(1, static_cast(std::abs(temporary_values[2]))) * signs[2], + std::max(1, static_cast(std::abs(temporary_values[3]))) * signs[3], + std::max(1, static_cast(std::abs(temporary_values[4]))) * signs[4], + std::max(1, static_cast(std::abs(temporary_values[5]))) * signs[5], + std::max(1, static_cast(std::abs(temporary_values[6]))) * signs[6], + std::max(1, static_cast(std::abs(temporary_values[7]))) * signs[7], + std::max(1, static_cast(std::abs(temporary_values[8]))) * signs[8], + std::max(1, static_cast(std::abs(temporary_values[9]))) * signs[9] + }; + + for (uint16 slot_id = 0; slot_id < faction_ids.size(); slot_id++) { + if (faction_ids[slot_id] > 0) { + SetFactionLevel2( + CharacterID(), + faction_ids[slot_id], + GetClass(), + GetBaseRace(), + GetDeity(), + new_values[slot_id], + false + ); + } + } +} - if (!found_item) { - DeleteItemRecastTimer(item_id); //We didn't find the item but we still want to remove the timer - } +void Client::SendHPUpdateMarquee(){ + if (!IsClient() || !current_hp || !max_hp) { + return; + } + + /* Health Update Marquee Display: Custom*/ + const auto health_percentage = static_cast(current_hp * 100 / max_hp); + if (health_percentage >= 100) { + return; + } + + const auto health_update_notification = fmt::format("Health: {}%%", health_percentage); + SendMarqueeMessage(Chat::Yellow, 510, 0, 3000, 3000, health_update_notification); } -void Client::SetItemCooldown(uint32 item_id, bool use_saved_timer, uint32 in_seconds) +uint32 Client::GetMoney(uint8 type, uint8 subtype) { + uint32 value = 0; + + switch (type) { + case MoneyTypes::Copper: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.copper); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.copper_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.copper_cursor); + break; + default: + break; + } + break; + } + case MoneyTypes::Silver: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.silver); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.silver_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.silver_cursor); + break; + default: + break; + } + break; + } + case MoneyTypes::Gold: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.gold); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.gold_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.gold_cursor); + break; + default: + break; + } + break; + } + case MoneyTypes::Platinum: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.platinum); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.platinum_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.platinum_cursor); + break; + case MoneySubtypes::SharedBank: + value = static_cast(m_pp.platinum_shared); + break; + default: + break; + } + break; + } + default: + break; + } + + return value; +} + +int Client::GetAccountAge() { + return (time(nullptr) - GetAccountCreation()); +} + +void Client::CheckRegionTypeChanges() { - EQ::ItemInstance *item = nullptr; - const EQ::ItemData* item_d = database.GetItem(item_id); - if (!item_d) { - return; - } - int recast_type = item_d->RecastType; - auto timestamps = database.GetItemRecastTimestamps(CharacterID()); - uint32 total_time = 0; - uint32 current_time = static_cast(std::time(nullptr)); - uint32 final_time = 0; - const auto timer_type = item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM ? item_d->RecastType : item_id; - const int timer_id = recast_type != RECAST_TYPE_UNLINKED_ITEM ? (pTimerItemStart + recast_type) : (pTimerNegativeItemReuse * item_id); - - if (use_saved_timer) { - if (item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM) { - total_time = timestamps.count(item_d->RecastType) ? timestamps.at(item_d->RecastType) : 0; - } else { - total_time = timestamps.count(item_id) ? timestamps.at(item_id) : 0; - } - } else { - total_time = current_time + in_seconds; - } + if (!zone->HasWaterMap()) { + return; + } - if (total_time > current_time) { - final_time = total_time - current_time; - } + auto new_region = zone->watermap->ReturnRegionType(glm::vec3(m_Position)); - for (const int16& slot_id : GetInventorySlots()) { - item = GetInv().GetItem(slot_id); - if (item) { - item_d = item->GetItem(); - if ( - item_d && - item->GetID() == item_id || - ( - item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && - item_d->RecastType == recast_type - ) - ) { - item->SetRecastTimestamp(total_time); - SendItemPacket(slot_id, item, ItemPacketCharmUpdate); - } - } - } + // still same region, do nothing + if (last_region_type == new_region) { + return; + } - //Start timers and update in database only when timer is changed - if (!use_saved_timer) { - GetPTimers().Clear(&database, timer_id); - GetPTimers().Start((timer_id), in_seconds); - database.UpdateItemRecast( - CharacterID(), - timer_type, - GetPTimers().Get(timer_id)->GetReadyTimestamp() - ); - } - SendItemRecastTimer(recast_type, final_time, true); + // If we got out of water clear any water aggro for water only npcs + if (last_region_type == RegionTypeWater) { + entity_list.ClearWaterAggro(this); + } + + // region type changed + last_region_type = new_region; + + // PVP is the only state we need to keep track of, so we can just return now for PVP servers + if (RuleI(World, PVPSettings) > 0) { + return; + } + + if (last_region_type == RegionTypePVP && RuleB(World, EnablePVPRegions)) { + temp_pvp = true; + } else if (temp_pvp) { + temp_pvp = false; + } } -uint32 Client::GetItemCooldown(uint32 item_id) +void Client::ProcessAggroMeter() { - const EQ::ItemData* item_d = database.GetItem(item_id); - if (!item_d) { - return 0; - } + if (!AggroMeterAvailable()) { + aggro_meter_timer.Disable(); + return; + } + + // we need to decide if we need to send OP_AggroMeterTargetInfo now + // This packet sends the current lock target ID and the current target ID + // target ID will be either our target or our target of target when we're targeting a PC + bool send_targetinfo = false; + auto cur_tar = GetTarget(); + + // probably should have PVP rules ... + if (cur_tar && cur_tar != this) { + if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerClient() && cur_tar->GetID() != m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(cur_tar->GetID()); + send_targetinfo = true; + } else if ((cur_tar->IsPetOwnerClient() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(cur_tar->GetTarget()->GetID()); + send_targetinfo = true; + } + } else if (m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(0); + send_targetinfo = true; + } + + if (m_aggrometer.update_lock()) + send_targetinfo = true; + + if (send_targetinfo) { + auto app = new EQApplicationPacket(OP_AggroMeterTargetInfo, sizeof(uint32) * 2); + app->WriteUInt32(m_aggrometer.get_lock_id()); + app->WriteUInt32(m_aggrometer.get_target_id()); + FastQueuePacket(&app); + } + + // we could just calculate how big the packet would need to be ... but it's easier this way :P should be 87 bytes + auto app = new EQApplicationPacket(OP_AggroMeterUpdate, m_aggrometer.max_packet_size()); + + cur_tar = entity_list.GetMob(m_aggrometer.get_target_id()); + + // first we must check the secondary + // TODO: lock target should affect secondary as well + bool send = false; + Mob *secondary = nullptr; + bool has_aggro = false; + if (cur_tar) { + if (cur_tar->GetTarget() == this) {// we got aggro + secondary = cur_tar->GetSecondaryHate(this); + has_aggro = true; + } else { + secondary = cur_tar->CheckAggro(cur_tar->GetTarget()) ? cur_tar->GetTarget() : nullptr; // make sure they are targeting for aggro reasons + } + } + + if (secondary && secondary->GetID() != m_aggrometer.get_secondary_id()) { + m_aggrometer.set_secondary_id(secondary->GetID()); + app->WriteUInt8(1); + app->WriteUInt32(m_aggrometer.get_secondary_id()); + send = true; + } else if (!secondary && m_aggrometer.get_secondary_id()) { + m_aggrometer.set_secondary_id(0); + app->WriteUInt8(1); + app->WriteUInt32(0); + send = true; + } else { // might not need to send in this case + app->WriteUInt8(0); + } + + auto count_offset = app->GetWritePosition(); + app->WriteUInt8(0); + + int count = 0; + auto add_entry = [&app, &count, this](AggroMeter::AggroTypes i) { + count++; + app->WriteUInt8(i); + app->WriteUInt16(m_aggrometer.get_pct(i)); + }; + // TODO: Player entry should either be lock or yourself, ignoring lock for now + // player, secondary, and group depend on your target/lock + if (cur_tar) { + if (m_aggrometer.set_pct(AggroMeter::AT_Player, cur_tar->GetHateRatio(cur_tar->GetTarget(), this))) + add_entry(AggroMeter::AT_Player); + + if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, has_aggro ? cur_tar->GetHateRatio(this, secondary) : secondary ? 100 : 0)) + add_entry(AggroMeter::AT_Secondary); + + if (IsRaidGrouped()) { + auto raid = GetRaid(); + if (raid) { + auto gid = raid->GetGroup(this); + if (gid < MAX_RAID_GROUPS) { + int at_id = AggroMeter::AT_Group1; + for (const auto& m : raid->members) { + if (m.member && m.member != this && m.group_number == gid) { + if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), m.member))) + add_entry(static_cast(at_id)); + at_id++; + if (at_id > AggroMeter::AT_Group5) + break; + } + } + } + } + } else if (IsGrouped()) { + auto group = GetGroup(); + if (group) { + int at_id = AggroMeter::AT_Group1; + for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { + if (group->members[i] && group->members[i] != this) { + if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), group->members[i]))) + add_entry(static_cast(at_id)); + at_id++; + } + } + } + } + } else { // we might need to clear out some data now + if (m_aggrometer.set_pct(AggroMeter::AT_Player, 0)) + add_entry(AggroMeter::AT_Player); + if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, 0)) + add_entry(AggroMeter::AT_Secondary); + if (m_aggrometer.set_pct(AggroMeter::AT_Group1, 0)) + add_entry(AggroMeter::AT_Group1); + if (m_aggrometer.set_pct(AggroMeter::AT_Group2, 0)) + add_entry(AggroMeter::AT_Group2); + if (m_aggrometer.set_pct(AggroMeter::AT_Group3, 0)) + add_entry(AggroMeter::AT_Group3); + if (m_aggrometer.set_pct(AggroMeter::AT_Group4, 0)) + add_entry(AggroMeter::AT_Group4); + if (m_aggrometer.set_pct(AggroMeter::AT_Group5, 0)) + add_entry(AggroMeter::AT_Group5); + } + + // now to go over our xtargets + // if the entry is an NPC it's our hate relative to the NPCs current tank + // if it's a PC, it's their hate relative to our current target + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].ID) { + auto mob = entity_list.GetMob(XTargets[i].ID); + if (mob) { + int ratio = 0; + if (mob->IsNPC()) + ratio = mob->GetHateRatio(mob->GetTarget(), this); + else if (cur_tar) + ratio = cur_tar->GetHateRatio(cur_tar->GetTarget(), mob); + if (m_aggrometer.set_pct(static_cast(AggroMeter::AT_XTarget1 + i), ratio)) + add_entry(static_cast(AggroMeter::AT_XTarget1 + i)); + } + } + } + + if (send || count) { + app->size = app->GetWritePosition(); // this should be safe, although not recommended + // but this way we can have a smaller buffer created for the packet dispatched to the client w/o resizing this one + app->SetWritePosition(count_offset); + app->WriteUInt8(count); + FastQueuePacket(&app); + } else { + safe_delete(app); + } +} - int recast_type = item_d->RecastType; - auto timestamps = database.GetItemRecastTimestamps(CharacterID()); - const auto timer_type = recast_type != RECAST_TYPE_UNLINKED_ITEM ? recast_type : item_id; - uint32 total_time = 0; - uint32 current_time = static_cast(std::time(nullptr)); - uint32 final_time = 0; +void Client::SetPetCommandState(int button, int state) +{ + auto app = new EQApplicationPacket(OP_PetCommandState, sizeof(PetCommandState_Struct)); + auto pcs = (PetCommandState_Struct *)app->pBuffer; + pcs->button_id = button; + pcs->state = state; + FastQueuePacket(&app); +} - total_time = timestamps.count(timer_type) ? timestamps.at(timer_type) : 0; +bool Client::CanMedOnHorse() +{ + // no horse is false + if (GetHorseId() == 0) + return false; - if (total_time > current_time) { - final_time = total_time - current_time; - } + // can't med while attacking + if (auto_attack) + return false; - return final_time; + return animation == 0 && m_Delta.x == 0.0f && m_Delta.y == 0.0f; // TODO: animation is SpeedRun } -void Client::RemoveItem(uint32 item_id, uint32 quantity) +void Client::EnableAreaHPRegen(int value) { - uint32 removed_count = 0; - EQ::ItemInstance *item = nullptr; - - for (const int16& slot_id : GetInventorySlots()) { - if (removed_count == quantity) { - break; - } + AreaHPRegen = value * 0.001f; + SendAppearancePacket(AppearanceType::AreaHealthRegen, value, false); +} - item = GetInv().GetItem(slot_id); - if (item && item->GetID() == item_id) { - uint32 charges = item->IsStackable() ? item->GetCharges() : 0; - uint32 stack_size = std::max(charges, static_cast(1)); - if ((removed_count + stack_size) <= quantity) { - removed_count += stack_size; - DeleteItemInInventory(slot_id, charges, true); - } else { - uint32 amount_left = (quantity - removed_count); - if (amount_left > 0 && stack_size >= amount_left) { - removed_count += amount_left; - DeleteItemInInventory(slot_id, amount_left, true); - } - } - } - } +void Client::DisableAreaHPRegen() +{ + AreaHPRegen = 1.0f; + SendAppearancePacket(AppearanceType::AreaHealthRegen, 1000, false); } -void Client::SetGMStatus(int new_status) { - if (Admin() != new_status) { - database.UpdateGMStatus(AccountID(), new_status); - UpdateAdmin(); - } +void Client::EnableAreaManaRegen(int value) +{ + AreaManaRegen = value * 0.001f; + SendAppearancePacket(AppearanceType::AreaManaRegen, value, false); } -void Client::ApplyWeaponsStance() +void Client::DisableAreaManaRegen() { - /* + AreaManaRegen = 1.0f; + SendAppearancePacket(AppearanceType::AreaManaRegen, 1000, false); +} - If you have a weapons stance bonus from at least one bonus type, each time you change weapons this function will ensure the correct - associated buffs are applied, and previous buff is removed. If your weapon stance bonus is completely removed it will, ensure buff is - also removed (ie, removing an item that has worn effect with weapon stance, or clicking off a buff). If client no longer has/never had - any spells/item/aa bonuses with weapon stance effect this function will only do a simple bool check. +void Client::EnableAreaEndRegen(int value) +{ + AreaEndRegen = value * 0.001f; + SendAppearancePacket(AppearanceType::AreaEnduranceRegen, value, false); +} - Note: Live like behavior is once you have the triggered buff you can manually click it off to remove it. Swaping any items in inventory will - reapply it automatically. +void Client::DisableAreaEndRegen() +{ + AreaEndRegen = 1.0f; + SendAppearancePacket(AppearanceType::AreaEnduranceRegen, 1000, false); +} - Only buff spells should be used as triggered spell effect. IsBuffSpell function also checks spell id validity. - WeaponStance bonus arrary: 0=2H Weapon 1=Shield 2=Dualweild +void Client::EnableAreaRegens(int value) +{ + EnableAreaHPRegen(value); + EnableAreaManaRegen(value); + EnableAreaEndRegen(value); +} - Toggling ON or OFF - - From spells, just remove the Primary buff that contains the WeaponStance effect in it. - - For items with worn effect, unequip the item. - - For AA abilities, a hotkey is used to Enable and Disable the effect. See. Client::TogglePassiveAlternativeAdvancement in aa.cpp for extensive details. +void Client::DisableAreaRegens() +{ + DisableAreaHPRegen(); + DisableAreaManaRegen(); + DisableAreaEndRegen(); +} - Rank - - Most important for AA, but if you have more than one of WeaponStance effect for a given type, the spell trigger buff will apply whatever has the highest - 'rank' value from the spells table. AA's on live for this effect naturally do this. Be awere of this if making custom spells/worn effects/AA. +void Client::InitInnates() +{ + // this function on the client also inits the level one innate skills (like swimming, hide, etc) + // we won't do that here, lets just do the InnateSkills for now. Basically translation of what the client is doing + // A lot of these we could probably have ignored because they have no known use or are 100% client side + // but I figured just in case we'll do them all out + // + // The client calls this in a few places. When you remove a vision buff and in SetHeights, which is called in + // illusions, mounts, and a bunch of other cases. All of the calls to InitInnates are wrapped in restoring regen + // besides the call initializing the first time + auto race = GetRace(); + auto class_ = GetClass(); + + for (int i = 0; i < InnateSkillMax; ++i) { + m_pp.InnateSkills[i] = InnateDisabled; + } + + m_pp.InnateSkills[InnateInspect] = InnateEnabled; + m_pp.InnateSkills[InnateOpen] = InnateEnabled; + + if (race >= Race::Froglok2) { + if (race == Race::Skeleton2 || race == Race::Froglok2) { + m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; + } else { + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + } + } + + switch (race) { + case Race::Barbarian: + case Race::HalasCitizen: + m_pp.InnateSkills[InnateSlam] = InnateEnabled; + break; + case Race::Erudite: + case Race::EruditeCitizen: + m_pp.InnateSkills[InnateLore] = InnateEnabled; + break; + case Race::WoodElf: + case Race::Fayguard: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Gnome: + case Race::HighElf: + case Race::Felguard: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + m_pp.InnateSkills[InnateLore] = InnateEnabled; + break; + case Race::Troll: + case Race::GrobbCitizen: + m_pp.InnateSkills[InnateRegen] = InnateEnabled; + m_pp.InnateSkills[InnateSlam] = InnateEnabled; + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Dwarf: + case Race::KaladimCitizen: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Ogre: + case Race::OggokCitizen: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + m_pp.InnateSkills[InnateSlam] = InnateEnabled; + m_pp.InnateSkills[InnateNoBash] = InnateEnabled; + m_pp.InnateSkills[InnateBashDoor] = InnateEnabled; + break; + case Race::Halfling: + case Race::RivervaleCitizen: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Iksar: + m_pp.InnateSkills[InnateRegen] = InnateEnabled; + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::VahShir: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::DarkElf: + case Race::NeriakCitizen: + case Race::ElfVampire: + case Race::FroglokGhoul: + case Race::Ghost: + case Race::Ghoul: + case Race::Skeleton: + case Race::Vampire: + case Race::Wisp: + case Race::Zombie: + case Race::Spectre: + case Race::DwarfGhost: + case Race::EruditeGhost: + case Race::DragonSkeleton: + case Race::Innoruuk: + m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; + break; + case Race::Human: + case Race::FreeportGuard: + case Race::HumanBeggar: + case Race::HighpassCitizen: + case Race::QeynosCitizen: + case Race::Froglok2: // client does froglok weird, but this should work out fine + break; + default: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + } + + switch (class_) { + case Class::Druid: + m_pp.InnateSkills[InnateHarmony] = InnateEnabled; + break; + case Class::Bard: + m_pp.InnateSkills[InnateReveal] = InnateEnabled; + break; + case Class::Rogue: + m_pp.InnateSkills[InnateSurprise] = InnateEnabled; + m_pp.InnateSkills[InnateReveal] = InnateEnabled; + break; + case Class::Ranger: + m_pp.InnateSkills[InnateAwareness] = InnateEnabled; + break; + case Class::Monk: + m_pp.InnateSkills[InnateSurprise] = InnateEnabled; + m_pp.InnateSkills[InnateAwareness] = InnateEnabled; + default: + break; + } +} - When creating weapon stance effects, you do not need to use all three types. For example, can make an effect where you only get a buff from equiping shield. +bool Client::GetDisplayMobInfoWindow() const +{ + return display_mob_info_window; +} - */ +void Client::SetDisplayMobInfoWindow(bool display_mob_info_window) +{ + Client::display_mob_info_window = display_mob_info_window; +} - if (!IsWeaponStanceEnabled()) { - return; - } +bool Client::IsDevToolsEnabled() const +{ + return dev_tools_enabled && GetGM() && RuleB(World, EnableDevTools); +} - bool enabled = false; - bool item_bonus_exists = false; - bool aa_bonus_exists = false; +void Client::SetDevToolsEnabled(bool in_dev_tools_enabled) +{ + const auto dev_tools_key = fmt::format("{}-dev-tools-disabled", AccountID()); - if (weaponstance.spellbonus_enabled) { + if (in_dev_tools_enabled) { + DataBucket::DeleteData(dev_tools_key); + } else { + DataBucket::SetData(dev_tools_key, "true"); + } - if (spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { + Client::dev_tools_enabled = in_dev_tools_enabled; +} - enabled = true; +bool Client::IsEXPEnabled() const { + return m_exp_enabled; +} - // Check if no longer has correct combination of weapon type and buff, if so remove buff. - if (!HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && - FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); - } - else if (!HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && - FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); - } - else if (!HasDualWeaponsEquipped() && - IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && - FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); - } - // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. - if (HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); - } - weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; - } - else if (HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { +void Client::SetEXPEnabled(bool is_exp_enabled) +{ + auto c = CharacterDataRepository::FindOne(database, CharacterID()); - if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); - } - weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; - } - else if (HasDualWeaponsEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + c.exp_enabled = is_exp_enabled; - if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); - } - weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; - } - } - } + auto updated = CharacterDataRepository::UpdateOne(database, c); - // Spellbonus effect removal is checked in BuffFadeBySlot(int slot, bool iRecalcBonuses) in spell_effects.cpp when the buff is clicked off or fades. + if (!updated) { + return; + } - if (weaponstance.itembonus_enabled) { + m_exp_enabled = is_exp_enabled; +} - if (itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { +void Client::SetPrimaryWeaponOrnamentation(uint32 model_id) +{ + auto primary_item = m_inv.GetItem(EQ::invslot::slotPrimary); + if (primary_item) { + auto l = InventoryRepository::GetWhere( + database, + fmt::format( + "`charid` = {} AND `slotid` = {}", + character_id, + EQ::invslot::slotPrimary + ) + ); - enabled = true; - item_bonus_exists = true; + if (l.empty()) { + return; + } + auto e = l.front(); - // Edge case check if have multiple items with WeaponStance worn effect. Make sure correct buffs are applied if items are removed but others left on. - if (weaponstance.itembonus_buff_spell_id) { + e.ornamentidfile = model_id; - bool buff_desync = true; - if (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || - weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - buff_desync = false; - } + const int updated = InventoryRepository::UpdateOne(database, e); - if (buff_desync) { - int fade_spell = weaponstance.itembonus_buff_spell_id; - weaponstance.itembonus_buff_spell_id = 0; //Need to zero this before we fade to prevent any recursive loops. - BuffFadeBySpellID(fade_spell); - } - } + if (updated) { + primary_item->SetOrnamentationIDFile(model_id); + SendItemPacket(EQ::invslot::slotPrimary, primary_item, ItemPacketTrade); + WearChange(EQ::textures::weaponPrimary, model_id, 0); - // Check if no longer has correct combination of weapon type and buff, if so remove buff. - if (!HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && - FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); - } - else if (!HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && - FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); - } - else if (!HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && - FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); - } + Message(Chat::Yellow, "Your primary weapon appearance has been modified."); + } + } +} - // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. - if (HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { +void Client::SetSecondaryWeaponOrnamentation(uint32 model_id) +{ + auto secondary_item = m_inv.GetItem(EQ::invslot::slotSecondary); + if (secondary_item) { + auto l = InventoryRepository::GetWhere( + database, + fmt::format( + "`charid` = {} AND `slotid` = {}", + character_id, + EQ::invslot::slotSecondary + ) + ); - if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); - } - weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; - } - else if (HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + if (l.empty()) { + return; + } - if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); - } - weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; - } - else if (HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); - } - weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; - } - } - } + auto e = l.front(); - // Itembonus effect removal when item is removed - if (!item_bonus_exists && weaponstance.itembonus_enabled) { - weaponstance.itembonus_enabled = false; + e.ornamentidfile = model_id; - if (weaponstance.itembonus_buff_spell_id) { - BuffFadeBySpellID(weaponstance.itembonus_buff_spell_id); - weaponstance.itembonus_buff_spell_id = 0; - } - } + const int updated = InventoryRepository::UpdateOne(database, e); - if (weaponstance.aabonus_enabled) { + if (updated) { + secondary_item->SetOrnamentationIDFile(model_id); + SendItemPacket(EQ::invslot::slotSecondary, secondary_item, ItemPacketTrade); + WearChange(EQ::textures::weaponSecondary, model_id, 0); - if (aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { + Message(Chat::Yellow, "Your secondary weapon appearance has been modified."); + } + } +} - enabled = true; - aa_bonus_exists = true; +/** + * Used in #goto + * + * @param player_name + */ +bool Client::GotoPlayer(const std::string& player_name) +{ + const auto& l = CharacterDataRepository::GetWhere( + database, + fmt::format( + "name = '{}' AND last_login > (UNIX_TIMESTAMP() - 600) LIMIT 1", + Strings::Escape(player_name) + ) + ); - //Check if no longer has correct combination of weapon type and buff, if so remove buff. - if (!HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && - FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); - } + if (l.empty()) { + return false; + } - else if (!HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && - FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); - } + const auto& e = l.front(); - else if (!HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && - FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); - } + if (e.zone_instance > 0 && !database.CheckInstanceExists(e.zone_instance)) { + Message(Chat::Yellow, "Instance no longer exists..."); + return false; + } - //If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. - if (HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); - } - weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; - } + if (e.zone_instance > 0) { + database.AddClientToInstance(e.zone_instance, CharacterID()); + } - else if (HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); - } - weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; - } + MovePC(e.zone_id, e.zone_instance, e.x, e.y, e.z, e.heading); - else if (HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + return true; +} - if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); - } - weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; - } - } - } +bool Client::GotoPlayerGroup(const std::string& player_name) +{ + if (!GetGroup()) { + return GotoPlayer(player_name); + } - // AA bonus removal is checked in TogglePassiveAA in aa.cpp. when the hot key is toggled. + for (auto &m: GetGroup()->members) { + if (m && m->IsClient()) { + auto c = m->CastToClient(); + if (!c->GotoPlayer(player_name)) { + return false; + } + } + } - // If no bonuses remain present, prevent additional future checks until new bonus is applied. - if (!enabled) { - SetWeaponStanceEnabled(false); - weaponstance.aabonus_enabled = false; - weaponstance.itembonus_enabled = false; - weaponstance.spellbonus_enabled = false; - } + return true; } -uint16 Client::GetDoorToolEntityId() const +bool Client::GotoPlayerRaid(const std::string& player_name) { - return m_door_tool_entity_id; + if (!GetRaid()) { + return GotoPlayer(player_name); + } + + for (auto &m: GetRaid()->members) { + if (m.member && m.member->IsClient()) { + auto c = m.member->CastToClient(); + if (!c->GotoPlayer(player_name)) { + return false; + } + } + } + + return true; } -void Client::SetDoorToolEntityId(uint16 door_tool_entity_id) +void Client::SendToGuildHall() { - Client::m_door_tool_entity_id = door_tool_entity_id; + std::string zone_short_name = "guildhall"; + uint32 zone_id = ZoneID(zone_short_name.c_str()); + if (zone_id == 0) { + return; + } + + uint32 expiration_time = (RuleI(Instances, GuildHallExpirationDays) * 86400); + uint16 instance_id = 0; + std::string guild_hall_instance_key = fmt::format("guild-hall-instance-{}", GuildID()); + std::string instance_data = DataBucket::GetData(guild_hall_instance_key); + if (!instance_data.empty() && Strings::ToInt(instance_data) > 0) { + instance_id = Strings::ToInt(instance_data); + } + + if (instance_id <= 0) { + if (!database.GetUnusedInstanceID(instance_id)) { + Message(Chat::Red, "Server was unable to find a free instance id."); + return; + } + + if (!database.CreateInstance(instance_id, zone_id, 1, expiration_time)) { + Message(Chat::Red, "Server was unable to create a new instance."); + return; + } + + DataBucket::SetData( + guild_hall_instance_key, + std::to_string(instance_id), + std::to_string(expiration_time) + ); + } + + AssignToInstance(instance_id); + MovePC(345, instance_id, -1.00, -1.00, 3.34, 0, 1); } -uint16 Client::GetObjectToolEntityId() const +void Client::CheckVirtualZoneLines() { - return m_object_tool_entity_id; + for (auto &virtual_zone_point : zone->virtual_zone_point_list) { + float half_width = ((float) virtual_zone_point.width / 2); + + if ( + GetX() > (virtual_zone_point.x - half_width) && + GetX() < (virtual_zone_point.x + half_width) && + GetY() > (virtual_zone_point.y - half_width) && + GetY() < (virtual_zone_point.y + half_width) && + GetZ() >= (virtual_zone_point.z - 10) && + GetZ() < (virtual_zone_point.z + (float) virtual_zone_point.height) + ) { + + MovePC( + virtual_zone_point.target_zone_id, + virtual_zone_point.target_instance, + virtual_zone_point.target_x, + virtual_zone_point.target_y, + virtual_zone_point.target_z, + virtual_zone_point.target_heading + ); + + LogZonePoints( + "Virtual Zone Box Sending player [{}] to [{}]", + GetCleanName(), + ZoneLongName(virtual_zone_point.target_zone_id) + ); + } + } } -void Client::SetObjectToolEntityId(uint16 object_tool_entity_id) +void Client::ShowDevToolsMenu() { - Client::m_object_tool_entity_id = object_tool_entity_id; + std::string menu_search; + std::string menu_show; + std::string menu_reload_one; + std::string menu_reload_two; + std::string menu_reload_three; + std::string menu_reload_four; + std::string menu_reload_five; + std::string menu_reload_six; + std::string menu_reload_seven; + std::string menu_reload_eight; + std::string menu_reload_nine; + std::string menu_toggle; + std::string window_toggle; + + /** + * Search entity commands + */ + menu_search += Saylink::Silent("#list corpses", "Corpses"); + menu_search += " | " + Saylink::Silent("#list doors", "Doors"); + menu_search += " | " + Saylink::Silent("#finditem", "Items"); + menu_search += " | " + Saylink::Silent("#list npcs", "NPC"); + menu_search += " | " + Saylink::Silent("#list objects", "Objects"); + menu_search += " | " + Saylink::Silent("#list players", "Players"); + menu_search += " | " + Saylink::Silent("#findzone", "Zones"); + + /** + * Show + */ + menu_show += Saylink::Silent("#showzonepoints", "Zone Points"); + menu_show += " | " + Saylink::Silent("#showzonegloballoot", "Zone Global Loot"); + menu_show += " | " + Saylink::Silent("#show content_flags", "Content Flags"); + + /** + * Reload + */ + menu_reload_one += Saylink::Silent("#reload aa", "AAs"); + menu_reload_one += " | " + Saylink::Silent("#reload alternate_currencies", "Alternate Currencies"); + menu_reload_one += " | " + Saylink::Silent("#reload base_data", "Base Data"); + menu_reload_one += " | " + Saylink::Silent("#reload blocked_spells", "Blocked Spells"); + + menu_reload_two += Saylink::Silent("#reload commands", "Commands"); + menu_reload_two += " | " + Saylink::Silent("#reload content_flags", "Content Flags"); + + menu_reload_three += Saylink::Silent("#reload data_buckets_cache", "Databuckets"); + menu_reload_three += " | " + Saylink::Silent("#reload doors", "Doors"); + menu_reload_three += " | " + Saylink::Silent("#reload factions", "Factions"); + menu_reload_three += " | " + Saylink::Silent("#reload ground_spawns", "Ground Spawns"); + + menu_reload_four += Saylink::Silent("#reload logs", "Level Based Experience Modifiers"); + menu_reload_four += " | " + Saylink::Silent("#reload logs", "Log Settings"); + menu_reload_four += " | " + Saylink::Silent("#reload Loot", "Loot"); + + menu_reload_five += Saylink::Silent("#reload merchants", "Merchants"); + menu_reload_five += " | " + Saylink::Silent("#reload npc_emotes", "NPC Emotes"); + menu_reload_five += " | " + Saylink::Silent("#reload npc_spells", "NPC Spells"); + menu_reload_five += " | " + Saylink::Silent("#reload objects", "Objects"); + menu_reload_five += " | " + Saylink::Silent("#reload opcodes", "Opcodes"); + + menu_reload_six += Saylink::Silent("#reload perl_export", "Perl Event Export Settings"); + menu_reload_six += " | " + Saylink::Silent("#reload quest", "Quests"); + + menu_reload_seven += Saylink::Silent("#reload rules", "Rules"); + menu_reload_seven += " | " + Saylink::Silent("#reload skill_caps", "Skill Caps"); + menu_reload_seven += " | " + Saylink::Silent("#reload static", "Static Zone Data"); + menu_reload_seven += " | " + Saylink::Silent("#reload tasks", "Tasks"); + + menu_reload_eight += Saylink::Silent("#reload titles", "Titles"); + menu_reload_eight += " | " + Saylink::Silent("#reload traps 1", "Traps"); + menu_reload_eight += " | " + Saylink::Silent("#reload variables", "Variables"); + menu_reload_eight += " | " + Saylink::Silent("#reload veteran_rewards", "Veteran Rewards"); + + menu_reload_nine += Saylink::Silent("#reload world", "World"); + menu_reload_nine += " | " + Saylink::Silent("#reload zone", "Zone"); + menu_reload_nine += " | " + Saylink::Silent("#reload zone_points", "Zone Points"); + + /** + * Show window status + */ + menu_toggle = Saylink::Silent("#devtools menu enable", "Enable"); + if (IsDevToolsEnabled()) { + menu_toggle = Saylink::Silent("#devtools menu disable", "Disable"); + } + + window_toggle = Saylink::Silent("#devtools window enable", "Enable"); + if (GetDisplayMobInfoWindow()) { + window_toggle = Saylink::Silent("#devtools window disable", "Disable"); + } + + /** + * Print menu + */ + SendChatLineBreak(); + + Message(Chat::White, "Developer Tools Menu"); + + Message( + Chat::White, + fmt::format( + "Show Menu | {}", + Saylink::Silent("#dev") + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Toggle Menu | {}", + menu_toggle + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Toggle Window | {}", + window_toggle + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Search | {}", + menu_search + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Show | {}", + menu_show + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_one + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_two + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_three + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_four + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_five + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_six + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_seven + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_eight + ).c_str() + ); + + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_nine + ).c_str() + ); + + auto help_link = Saylink::Silent("#help"); + + Message( + Chat::White, + fmt::format( + "Note: You can search for commands with {} [Search String]", + help_link + ).c_str() + ); + + SendChatLineBreak(); + + Message( + Chat::White, + fmt::format( + "Current Expansion | {} ({})", + content_service.GetCurrentExpansionName(), + content_service.GetCurrentExpansion() + ).c_str() + ); + + + auto z = GetZoneVersionWithFallback(zone->GetZoneID(), zone->GetInstanceVersion()); + + if (z) { + Message( + Chat::White, + fmt::format( + "Current Zone | [{}] ({}) version [{}] instance_id [{}] min/max expansion ({}/{}) content_flags [{}]", + z->short_name, + z->long_name, + z->version, + zone->GetInstanceID(), + z->min_expansion, + z->max_expansion, + z->content_flags + ).c_str() + ); + } + + SendChatLineBreak(); } -int Client::GetIPExemption() -{ - return database.GetIPExemption(GetIPString()); +void Client::SendChatLineBreak(uint16 color) { + Message(color, "------------------------------------------------"); } -std::string Client::GetIPString() -{ - in_addr client_ip{}; - client_ip.s_addr = GetIP(); - return inet_ntoa(client_ip); +void Client::SendCrossZoneMessage( + Client* client, const std::string& character_name, uint16_t chat_type, const std::string& message) +{ + // if client is null, falls back to sending a cross zone message by name + if (!client && !character_name.empty()) + { + client = entity_list.GetClientByName(character_name.c_str()); + } + + if (client) + { + client->Message(chat_type, message.c_str()); + } + else if (!character_name.empty() && !message.empty()) + { + uint32_t pack_size = sizeof(CZMessage_Struct); + auto pack = std::make_unique(ServerOP_CZMessage, pack_size); + auto buf = reinterpret_cast(pack->pBuffer); + uint8 update_type = CZUpdateType_Character; + int update_identifier = 0; + buf->update_type = update_type; + buf->update_identifier = update_identifier; + buf->type = chat_type; + strn0cpy(buf->message, message.c_str(), sizeof(buf->message)); + strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); + + worldserver.SendPacket(pack.get()); + } } -void Client::SetIPExemption(int exemption_amount) -{ - database.SetIPExemption(GetIPString(), exemption_amount); +void Client::SendCrossZoneMessageString( + Client* client, const std::string& character_name, uint16_t chat_type, + uint32_t string_id, const std::initializer_list& arguments) +{ + // if client is null, falls back to sending a cross zone message by name + if (!client && !character_name.empty()) // double check client isn't in this zone + { + client = entity_list.GetClientByName(character_name.c_str()); + } + + if (!client && character_name.empty()) + { + return; + } + + SerializeBuffer argument_buffer; + for (const auto& argument : arguments) + { + argument_buffer.WriteString(argument); + } + + uint32_t args_size = static_cast(argument_buffer.size()); + uint32_t pack_size = sizeof(CZClientMessageString_Struct) + args_size; + auto pack = std::make_unique(ServerOP_CZClientMessageString, pack_size); + auto buf = reinterpret_cast(pack->pBuffer); + buf->string_id = string_id; + buf->chat_type = chat_type; + strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); + buf->args_size = args_size; + memcpy(buf->args, argument_buffer.buffer(), argument_buffer.size()); + + if (client) + { + client->MessageString(buf); + } + else + { + worldserver.SendPacket(pack.get()); + } } -void Client::ReadBookByName(std::string book_name, uint8 book_type) +void Client::SendDynamicZoneUpdates() { - auto b = content_db.GetBook(book_name); + // bit inefficient since each do lookups but it avoids duplicating code here + SendDzCompassUpdate(); + SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Online); - if (!b.text.empty()) { - LogDebug("Book Name: [{}] Text: [{}]", book_name, b.text); + m_expedition_lockouts = ExpeditionDatabase::LoadCharacterLockouts(CharacterID()); - auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); + // expeditions are the only dz type that keep the window updated + auto expedition = GetExpedition(); + if (expedition) + { + expedition->GetDynamicZone()->SendClientWindowUpdate(this); - auto o = (BookText_Struct *) outapp->pBuffer; + // live synchronizes lockouts obtained during the active expedition to + // members once they zone into the expedition's dynamic zone instance + if (expedition->GetDynamicZone()->IsCurrentZoneDzInstance()) + { + expedition->SyncCharacterLockouts(CharacterID(), m_expedition_lockouts); + } + } - o->window = std::numeric_limits::max(); - o->type = book_type; - o->invslot = 0; + SendExpeditionLockoutTimers(); - memcpy(o->booktext, b.text.c_str(), b.text.size()); + // ask world for any pending invite we saved from a previous zone + RequestPendingExpeditionInvite(); +} - if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { - if (m_pp.languages[b.language] < Language::MaxValue) { - GarbleMessage(o->booktext, (Language::MaxValue - m_pp.languages[b.language])); - } - } +Expedition* Client::CreateExpedition(DynamicZone& dz, bool disable_messages) +{ + return Expedition::TryCreate(this, dz, disable_messages); +} - QueuePacket(outapp); - safe_delete(outapp); - } +Expedition* Client::CreateExpedition( + const std::string& zone_name, uint32 version, uint32 duration, const std::string& expedition_name, + uint32 min_players, uint32 max_players, bool disable_messages) +{ + DynamicZone dz{ ZoneID(zone_name), version, duration, DynamicZoneType::Expedition }; + dz.SetName(expedition_name); + dz.SetMinPlayers(min_players); + dz.SetMaxPlayers(max_players); + + return Expedition::TryCreate(this, dz, disable_messages); } -// this will fetch raid clients if exists -// fallback to group if raid doesn't exist -// fallback to self if group doesn't exist -std::vector Client::GetPartyMembers() +Expedition* Client::CreateExpeditionFromTemplate(uint32_t dz_template_id) { - // get clients to update - std::vector clients_to_update = {}; + Expedition* expedition = nullptr; + auto it = zone->dz_template_cache.find(dz_template_id); + if (it != zone->dz_template_cache.end()) + { + DynamicZone dz(DynamicZoneType::Expedition); + dz.LoadTemplate(it->second); + expedition = Expedition::TryCreate(this, dz, false); + } + return expedition; +} - // raid - if (const auto raid = entity_list.GetRaidByClient(this)) { - for (auto &m : raid->members) { - if (m.is_bot) { - continue; - } +void Client::CreateTaskDynamicZone(int task_id, DynamicZone& dz_request) +{ + if (task_state) + { + task_state->CreateTaskDynamicZone(this, task_id, dz_request); + } +} - if (m.member && m.member->IsClient()) { - clients_to_update.push_back(m.member->CastToClient()); - } - } - } +Expedition* Client::GetExpedition() const +{ + if (zone && m_expedition_id) + { + auto expedition_cache_iter = zone->expedition_cache.find(m_expedition_id); + if (expedition_cache_iter != zone->expedition_cache.end()) + { + return expedition_cache_iter->second.get(); + } + } + return nullptr; +} - // group - if (clients_to_update.empty()) { - Group *group = entity_list.GetGroupByClient(this); - if (group) { - for (auto &m : group->members) { - if (m && m->IsClient()) { - clients_to_update.push_back(m->CastToClient()); - } - } - } - } +void Client::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db) +{ + // todo: support for account based lockouts like live AoC expeditions - // solo - if (clients_to_update.empty()) { - clients_to_update.push_back(this); - } + // if client already has this lockout, we're replacing it with the new one + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& existing_lockout) { + return existing_lockout.IsSameLockout(lockout); + } + ), m_expedition_lockouts.end()); + + m_expedition_lockouts.emplace_back(lockout); - return clients_to_update; + if (update_db) // for quest api + { + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { lockout }); + } + + SendExpeditionLockoutTimers(); } -void Client::SummonBaggedItems(uint32 bag_item_id, const std::vector& bag_items) +void Client::AddNewExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, uint32_t seconds, std::string uuid) { - if (bag_items.empty()) - { - return; - } + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + AddExpeditionLockout(lockout, true); +} - // todo: maybe some common functions for SE_SummonItem and SE_SummonItemIntoBag +void Client::AddExpeditionLockoutDuration( + const std::string& expedition_name, const std::string& event_name, int seconds, + const std::string& uuid, bool update_db) +{ + auto it = std::find_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + }); - const EQ::ItemData* bag_item = database.GetItem(bag_item_id); - if (!bag_item) - { - Message(Chat::Red, fmt::format("Unable to summon item [{}]. Item not found.", bag_item_id).c_str()); - return; - } + if (it != m_expedition_lockouts.end()) + { + it->AddLockoutTime(seconds); - if (CheckLoreConflict(bag_item)) - { - DuplicateLoreMessage(bag_item_id); - return; - } + if (!uuid.empty()) + { + it->SetUUID(uuid); + } - int bag_item_charges = 1; // just summoning a single bag - EQ::ItemInstance* summoned_bag = database.CreateItem(bag_item_id, bag_item_charges); - if (!summoned_bag || !summoned_bag->IsClassBag()) - { - Message(Chat::Red, fmt::format("Failed to summon bag item [{}]", bag_item_id).c_str()); - safe_delete(summoned_bag); - return; - } + if (update_db) + { + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { *it }); + } - for (const auto& item : bag_items) - { - uint8 open_slot = summoned_bag->FirstOpenSlot(); - if (open_slot == 0xff) - { - Message(Chat::Red, "Attempting to summon item in to bag, but there is no room in the summoned bag!"); - break; - } + SendExpeditionLockoutTimers(); + } + else if (seconds > 0) // missing lockouts inserted for reductions would be instantly expired + { + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + AddExpeditionLockout(lockout, update_db); + } +} - const EQ::ItemData* current_item = database.GetItem(item.item_id); +void Client::RemoveExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool update_db) +{ + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + } + ), m_expedition_lockouts.end()); - if (CheckLoreConflict(current_item)) - { - DuplicateLoreMessage(item.item_id); - } - else - { - EQ::ItemInstance* summoned_bag_item = database.CreateItem( - item.item_id, - item.charges, - item.aug_1, - item.aug_2, - item.aug_3, - item.aug_4, - item.aug_5, - item.aug_6, - item.attuned, - item.custom_data, - item.ornamenticon, - item.ornamentidfile, - item.ornament_hero_model - ); - if (summoned_bag_item) - { - summoned_bag->PutItem(open_slot, *summoned_bag_item); - safe_delete(summoned_bag_item); - } - } - } + if (update_db) // for quest api + { + ExpeditionDatabase::DeleteCharacterLockout(CharacterID(), expedition_name, event_name); + } - PushItemOnCursor(*summoned_bag); - SendItemPacket(EQ::invslot::slotCursor, summoned_bag, ItemPacketLimbo); - safe_delete(summoned_bag); + SendExpeditionLockoutTimers(); } -void Client::SaveSpells() +void Client::RemoveAllExpeditionLockouts(const std::string& expedition_name, bool update_db) { - std::vector character_spells = {}; - - for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { - if (IsValidSpell(m_pp.spell_book[index])) { - auto spell = CharacterSpellsRepository::NewEntity(); - spell.id = CharacterID(); - spell.slot_id = index; - spell.spell_id = m_pp.spell_book[index]; - character_spells.emplace_back(spell); - } - } + if (expedition_name.empty()) + { + if (update_db) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID()); + } + m_expedition_lockouts.clear(); + } + else + { + if (update_db) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID(), expedition_name); + } + + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.GetExpeditionName() == expedition_name; + } + ), m_expedition_lockouts.end()); + } + + SendExpeditionLockoutTimers(); +} - CharacterSpellsRepository::DeleteWhere(database, fmt::format("id = {}", CharacterID())); +const ExpeditionLockoutTimer* Client::GetExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) const +{ + for (const auto& expedition_lockout : m_expedition_lockouts) + { + if ((include_expired || !expedition_lockout.IsExpired()) && + expedition_lockout.IsSameLockout(expedition_name, event_name)) + { + return &expedition_lockout; + } + } + return nullptr; +} - if (!character_spells.empty()) { - CharacterSpellsRepository::InsertMany(database, character_spells); - } +std::vector Client::GetExpeditionLockouts( + const std::string& expedition_name, bool include_expired) +{ + std::vector lockouts; + for (const auto& lockout : m_expedition_lockouts) + { + if ((include_expired || !lockout.IsExpired()) && + lockout.GetExpeditionName() == expedition_name) + { + lockouts.emplace_back(lockout); + } + } + return lockouts; } -void Client::SaveDisciplines() +bool Client::HasExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) { - std::vector v; + return (GetExpeditionLockout(expedition_name, event_name, include_expired) != nullptr); +} - std::vector delete_slots; +void Client::SendExpeditionLockoutTimers() +{ + std::vector lockout_entries; + + // client displays lockouts rounded down to nearest minute, send lockouts + // with 60s offset added to compensate (live does this too) + constexpr uint32_t rounding_seconds = 60; + + // erases expired lockouts while building lockout timer list + for (auto it = m_expedition_lockouts.begin(); it != m_expedition_lockouts.end();) + { + uint32_t seconds_remaining = it->GetSecondsRemaining(); + if (seconds_remaining == 0) + { + it = m_expedition_lockouts.erase(it); + } + else + { + ExpeditionLockoutTimerEntry_Struct lockout; + strn0cpy(lockout.expedition_name, it->GetExpeditionName().c_str(), sizeof(lockout.expedition_name)); + lockout.seconds_remaining = seconds_remaining + rounding_seconds; + lockout.event_type = it->IsReplayTimer() ? Expedition::REPLAY_TIMER_ID : Expedition::EVENT_TIMER_ID; + strn0cpy(lockout.event_name, it->GetEventName().c_str(), sizeof(lockout.event_name)); + + lockout_entries.emplace_back(lockout); + ++it; + } + } + + uint32_t lockout_count = static_cast(lockout_entries.size()); + uint32_t lockout_entries_size = sizeof(ExpeditionLockoutTimerEntry_Struct) * lockout_count; + uint32_t outsize = sizeof(ExpeditionLockoutTimers_Struct) + lockout_entries_size; + auto outapp = std::make_unique(OP_DzExpeditionLockoutTimers, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = lockout_count; + if (!lockout_entries.empty()) + { + memcpy(outbuf->timers, lockout_entries.data(), lockout_entries_size); + } + QueuePacket(outapp.get()); +} - for (uint16 slot_id = 0; slot_id < MAX_PP_DISCIPLINES; slot_id++) { - if (IsValidSpell(m_pp.disciplines.values[slot_id])) { - auto e = CharacterDisciplinesRepository::NewEntity(); +void Client::RequestPendingExpeditionInvite() +{ + uint32_t packsize = sizeof(ServerExpeditionCharacterID_Struct); + auto pack = std::make_unique(ServerOP_ExpeditionRequestInvite, packsize); + auto packbuf = reinterpret_cast(pack->pBuffer); + packbuf->character_id = CharacterID(); + worldserver.SendPacket(pack.get()); +} - e.id = CharacterID(); - e.slot_id = slot_id; - e.disc_id = m_pp.disciplines.values[slot_id]; +void Client::DzListTimers() +{ + // only lists player's current replay timer lockouts, not all event lockouts + bool found = false; + for (const auto& lockout : m_expedition_lockouts) + { + if (lockout.IsReplayTimer()) + { + found = true; + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + MessageString( + Chat::Yellow, DZLIST_REPLAY_TIMER, + time_remaining.days.c_str(), + time_remaining.hours.c_str(), + time_remaining.mins.c_str(), + lockout.GetExpeditionName().c_str() + ); + } + } + + if (!found) + { + MessageString(Chat::Yellow, EXPEDITION_NO_TIMERS); + } +} - v.emplace_back(e); - } else { - delete_slots.emplace_back(std::to_string(slot_id)); - } - } +void Client::SetDzRemovalTimer(bool enable_timer) +{ + uint32_t timer_ms = RuleI(DynamicZone, ClientRemovalDelayMS); - if (!delete_slots.empty()) { - CharacterDisciplinesRepository::DeleteWhere( - database, - fmt::format( - "`id` = {} AND `slot_id` IN ({})", - CharacterID(), - Strings::Join(delete_slots, ", ") - ) - ); - } + LogDynamicZones( + "Character [{}] instance [{}] removal timer enabled: [{}] delay (ms): [{}]", + CharacterID(), zone ? zone->GetInstanceID() : 0, enable_timer, timer_ms + ); - if (!v.empty()) { - CharacterDisciplinesRepository::ReplaceMany(database, v); - } + if (enable_timer) + { + dynamiczone_removal_timer.Start(timer_ms); + } + else + { + dynamiczone_removal_timer.Disable(); + } } -uint16 Client::ScribeSpells(uint8 min_level, uint8 max_level) +void Client::SendDzCompassUpdate() { - auto available_book_slot = GetNextAvailableSpellBookSlot(); - std::vector spell_ids = GetScribeableSpells(min_level, max_level); - uint16 scribed_spells = 0; - - if (!spell_ids.empty()) { - for (const auto& spell_id : spell_ids) { - if (available_book_slot == -1) { - Message( - Chat::Red, - fmt::format( - "Unable to scribe {} ({}) to Spell Book because your Spell Book is full.", - spells[spell_id].name, - spell_id - ).c_str() - ); - break; - } + // client may be associated with multiple dynamic zone compasses in this zone + std::vector compass_entries; + + // need to sort by local doorid in case multiple have same dz switch id (live only sends first) + // todo: just store zone's door list ordered and ditch this + std::vector switches; + switches.reserve(entity_list.GetDoorsList().size()); + for (const auto& door_pair : entity_list.GetDoorsList()) + { + switches.push_back(door_pair.second); + } + std::sort(switches.begin(), switches.end(), + [](Doors* lhs, Doors* rhs) { return lhs->GetDoorID() < rhs->GetDoorID(); }); + + for (const auto& client_dz : GetDynamicZones()) + { + auto& compass = client_dz->GetCompassLocation(); + if (zone && zone->IsZone(compass.zone_id, 0)) + { + DynamicZoneCompassEntry_Struct entry{}; + entry.dz_zone_id = client_dz->GetZoneID(); + entry.dz_instance_id = client_dz->GetInstanceID(); + entry.dz_type = static_cast(client_dz->GetType()); + entry.x = compass.x; + entry.y = compass.y; + entry.z = compass.z; + + compass_entries.emplace_back(entry); + } + + // if client has a dz with a switch id add compass to any switch locs that share it + if (client_dz->GetSwitchID() != 0) + { + // live only sends one if multiple in zone have the same switch id + auto it = std::find_if(switches.begin(), switches.end(), + [&](const auto& eqswitch) { + return eqswitch->GetDzSwitchID() == client_dz->GetSwitchID(); + }); + + if (it != switches.end()) + { + DynamicZoneCompassEntry_Struct entry{}; + entry.dz_zone_id = client_dz->GetZoneID(); + entry.dz_instance_id = client_dz->GetInstanceID(); + entry.dz_type = static_cast(client_dz->GetType()); + entry.dz_switch_id = client_dz->GetSwitchID(); + entry.x = (*it)->GetX(); + entry.y = (*it)->GetY(); + entry.z = (*it)->GetZ(); + + compass_entries.emplace_back(entry); + } + } + } + + // compass set via MarkSingleCompassLocation() + if (m_has_quest_compass) + { + DynamicZoneCompassEntry_Struct entry{}; + entry.dz_zone_id = 0; + entry.dz_instance_id = 0; + entry.dz_type = 0; + entry.x = m_quest_compass.x; + entry.y = m_quest_compass.y; + entry.z = m_quest_compass.z; + + compass_entries.emplace_back(entry); + } + + QueuePacket(CreateCompassPacket(compass_entries).get()); +} - if (HasSpellScribed(spell_id)) { - continue; - } +std::unique_ptr Client::CreateCompassPacket( + const std::vector& compass_entries) +{ + uint32 count = static_cast(compass_entries.size()); + uint32 entries_size = sizeof(DynamicZoneCompassEntry_Struct) * count; + uint32 outsize = sizeof(DynamicZoneCompass_Struct) + entries_size; + auto outapp = std::make_unique(OP_DzCompass, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = count; + memcpy(outbuf->entries, compass_entries.data(), entries_size); - // defer saving per spell and bulk save at the end - ScribeSpell(spell_id, available_book_slot, true, true); - available_book_slot = GetNextAvailableSpellBookSlot(available_book_slot); - scribed_spells++; - } - } + return outapp; +} - if (scribed_spells > 0) { - std::string spell_message = ( - scribed_spells == 1 ? - "a new spell" : - fmt::format("{} new spells", scribed_spells) - ); - Message(Chat::White, fmt::format("You have learned {}!", spell_message).c_str()); +void Client::GoToDzSafeReturnOrBind(const DynamicZone* dynamic_zone) +{ + if (dynamic_zone) + { + auto safereturn = dynamic_zone->GetSafeReturnLocation(); + if (safereturn.zone_id != 0) + { + LogDynamicZonesDetail("Sending [{}] to safereturn zone [{}]", CharacterID(), safereturn.zone_id); + MovePC(safereturn.zone_id, 0, safereturn.x, safereturn.y, safereturn.z, safereturn.heading); + return; + } + } - // bulk insert spells - SaveSpells(); - } - return scribed_spells; + GoToBind(); } -uint16 Client::LearnDisciplines(uint8 min_level, uint8 max_level) +void Client::AddDynamicZoneID(uint32_t dz_id) { - auto available_discipline_slot = GetNextAvailableDisciplineSlot(); - auto character_id = CharacterID(); - std::vector spell_ids = GetLearnableDisciplines(min_level, max_level); - uint16 learned_disciplines = 0; - - if (!spell_ids.empty()) { - for (const auto& spell_id : spell_ids) { - if (available_discipline_slot == -1) { - Message( - Chat::Red, - fmt::format( - "Unable to learn {} ({}) because your Discipline slots are full.", - spells[spell_id].name, - spell_id - ).c_str() - ); - break; - } + auto it = std::find_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), + [&](uint32_t current_dz_id) { return current_dz_id == dz_id; }); - if (HasDisciplineLearned(spell_id)) { - continue; - } + if (it == m_dynamic_zone_ids.end()) + { + LogDynamicZonesDetail("Adding dz [{}] to client [{}]", dz_id, GetName()); + m_dynamic_zone_ids.push_back(dz_id); + } +} - GetPP().disciplines.values[available_discipline_slot] = spell_id; - available_discipline_slot = GetNextAvailableDisciplineSlot(available_discipline_slot); - learned_disciplines++; - } - } +void Client::RemoveDynamicZoneID(uint32_t dz_id) +{ + LogDynamicZonesDetail("Removing dz [{}] from client [{}]", dz_id, GetName()); + m_dynamic_zone_ids.erase(std::remove_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), + [&](uint32_t current_dz_id) { return current_dz_id == dz_id; } + ), m_dynamic_zone_ids.end()); +} - if (learned_disciplines > 0) { - std::string discipline_message = ( - learned_disciplines == 1 ? - "a new discipline" : - fmt::format("{} new disciplines", learned_disciplines) - ); - Message(Chat::White, fmt::format("You have learned {}!", discipline_message).c_str()); - SendDisciplineUpdate(); - SaveDisciplines(); - } +std::vector Client::GetDynamicZones(uint32_t zone_id, int zone_version) +{ + std::vector client_dzs; - return learned_disciplines; + for (uint32_t dz_id : m_dynamic_zone_ids) + { + auto dz = DynamicZone::FindDynamicZoneByID(dz_id); + if (dz && + (zone_id == 0 || dz->GetZoneID() == zone_id) && + (zone_version < 0 || dz->GetZoneVersion() == zone_version)) + { + client_dzs.emplace_back(dz); + } + } + + return client_dzs; } -uint16 Client::GetClassTrackingDistanceMultiplier(uint16 class_) { - switch (class_) { - case Class::Warrior: - return RuleI(Character, WarriorTrackingDistanceMultiplier); - case Class::Cleric: - return RuleI(Character, ClericTrackingDistanceMultiplier); - case Class::Paladin: - return RuleI(Character, PaladinTrackingDistanceMultiplier); - case Class::Ranger: - return RuleI(Character, RangerTrackingDistanceMultiplier); - case Class::ShadowKnight: - return RuleI(Character, ShadowKnightTrackingDistanceMultiplier); - case Class::Druid: - return RuleI(Character, DruidTrackingDistanceMultiplier); - case Class::Monk: - return RuleI(Character, MonkTrackingDistanceMultiplier); - case Class::Bard: - return RuleI(Character, BardTrackingDistanceMultiplier); - case Class::Rogue: - return RuleI(Character, RogueTrackingDistanceMultiplier); - case Class::Shaman: - return RuleI(Character, ShamanTrackingDistanceMultiplier); - case Class::Necromancer: - return RuleI(Character, NecromancerTrackingDistanceMultiplier); - case Class::Wizard: - return RuleI(Character, WizardTrackingDistanceMultiplier); - case Class::Magician: - return RuleI(Character, MagicianTrackingDistanceMultiplier); - case Class::Enchanter: - return RuleI(Character, EnchanterTrackingDistanceMultiplier); - case Class::Beastlord: - return RuleI(Character, BeastlordTrackingDistanceMultiplier); - case Class::Berserker: - return RuleI(Character, BerserkerTrackingDistanceMultiplier); - default: - return 0; - } +void Client::SetDynamicZoneMemberStatus(DynamicZoneMemberStatus status) +{ + // sets status on all associated dzs client may have. if client is online + // inside a dz, only that dz has the "In Dynamic Zone" status set + for (auto& dz : GetDynamicZones()) + { + // the rule to disable this status is handled internally by the dz + if (status == DynamicZoneMemberStatus::Online && dz->IsCurrentZoneDzInstance()) + { + status = DynamicZoneMemberStatus::InDynamicZone; + } + dz->SetMemberStatus(CharacterID(), status); + } } -bool Client::CanThisClassTrack() { - return (GetClassTrackingDistanceMultiplier(GetClass()) > 0) ? true : false; +void Client::MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid) +{ + if (zone_id == 0) + { + return; + } + + auto client_dzs = GetDynamicZones(zone_id, zone_version); + + if (client_dzs.empty()) + { + if (msg_if_invalid) + { + MessageString(Chat::Red, DYNAMICZONE_WAY_IS_BLOCKED); // unconfirmed message + } + } + else if (client_dzs.size() == 1) + { + auto dz = client_dzs.front(); + DynamicZoneLocation zonein = dz->GetZoneInLocation(); + ZoneMode zone_mode = dz->HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords; + MovePC(zone_id, dz->GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode); + } + else + { + LogDynamicZonesDetail("Sending DzSwitchListWnd to [{}] for zone [{}] with [{}] dynamic zone(s)", + CharacterID(), zone_id, client_dzs.size()); + + // client has more than one dz for this zone, send out the switchlist window + QueuePacket(CreateDzSwitchListPacket(client_dzs).get()); + } } -void Client::ReconnectUCS() +bool Client::TryMovePCDynamicZoneSwitch(int dz_switch_id) { - EQApplicationPacket *outapp = nullptr; - std::string buffer; - std::string mail_key = m_mail_key; - EQ::versions::UCSVersion connection_type = EQ::versions::ucsUnknown; - - // chat server packet - switch (ClientVersion()) { - case EQ::versions::ClientVersion::Titanium: - connection_type = EQ::versions::ucsTitaniumChat; - break; - case EQ::versions::ClientVersion::SoF: - connection_type = EQ::versions::ucsSoFCombined; - break; - case EQ::versions::ClientVersion::SoD: - connection_type = EQ::versions::ucsSoDCombined; - break; - case EQ::versions::ClientVersion::UF: - connection_type = EQ::versions::ucsUFCombined; - break; - case EQ::versions::ClientVersion::RoF: - connection_type = EQ::versions::ucsRoFCombined; - break; - case EQ::versions::ClientVersion::RoF2: - connection_type = EQ::versions::ucsRoF2Combined; - break; - default: - connection_type = EQ::versions::ucsUnknown; - break; - } + auto client_dzs = GetDynamicZones(); - buffer = StringFormat( - "%s,%i,%s.%s,%c%s", - Config->GetUCSHost().c_str(), - Config->GetUCSPort(), - Config->ShortName.c_str(), - GetName(), - connection_type, - mail_key.c_str() - ); + std::vector switch_dzs; + auto it = std::copy_if(client_dzs.begin(), client_dzs.end(), std::back_inserter(switch_dzs), + [&](const DynamicZone* dz) { return dz->GetSwitchID() == dz_switch_id; }); - outapp = new EQApplicationPacket(OP_SetChatServer, (buffer.length() + 1)); - memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); - outapp->pBuffer[buffer.length()] = '\0'; - - QueuePacket(outapp); - safe_delete(outapp); - - // mail server packet - switch (ClientVersion()) { - case EQ::versions::ClientVersion::Titanium: - connection_type = EQ::versions::ucsTitaniumMail; - break; - default: - // retain value from previous switch - break; - } + if (switch_dzs.size() == 1) + { + LogDynamicZonesDetail("Moving client [{}] to dz with switch id [{}]", GetName(), dz_switch_id); + switch_dzs.front()->MovePCInto(this, true); + } + else if (switch_dzs.size() > 1) + { + QueuePacket(CreateDzSwitchListPacket(switch_dzs).get()); + } - buffer = StringFormat( - "%s,%i,%s.%s,%c%s", - Config->GetUCSHost().c_str(), - Config->GetUCSPort(), - Config->ShortName.c_str(), - GetName(), - connection_type, - mail_key.c_str() - ); + return !switch_dzs.empty(); +} - outapp = new EQApplicationPacket(OP_SetChatServer2, (buffer.length() + 1)); - memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); - outapp->pBuffer[buffer.length()] = '\0'; +std::unique_ptr Client::CreateDzSwitchListPacket( + const std::vector& client_dzs) +{ + uint32 count = static_cast(client_dzs.size()); + uint32 entries_size = sizeof(DynamicZoneChooseZoneEntry_Struct) * count; + uint32 outsize = sizeof(DynamicZoneChooseZone_Struct) + entries_size; + auto outapp = std::make_unique(OP_DzChooseZone, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = count; + for (int i = 0; i < client_dzs.size(); ++i) + { + outbuf->choices[i].dz_zone_id = client_dzs[i]->GetZoneID(); + outbuf->choices[i].dz_instance_id = client_dzs[i]->GetInstanceID(); + outbuf->choices[i].dz_type = static_cast(client_dzs[i]->GetType()); + strn0cpy(outbuf->choices[i].description, client_dzs[i]->GetName().c_str(), sizeof(outbuf->choices[i].description)); + strn0cpy(outbuf->choices[i].leader_name, client_dzs[i]->GetLeaderName().c_str(), sizeof(outbuf->choices[i].leader_name)); + } + return outapp; +} - QueuePacket(outapp); - safe_delete(outapp); +void Client::MovePCDynamicZone(const std::string& zone_name, int zone_version, bool msg_if_invalid) +{ + auto zone_id = ZoneID(zone_name.c_str()); + MovePCDynamicZone(zone_id, zone_version, msg_if_invalid); } -void Client::SendReloadCommandMessages() { - SendChatLineBreak(); +void Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clip_through_walls, bool calculate_speed) { + BuffFadeByEffect(SE_Levitate); + if (CheckLosFN(target_x, target_y, target_z, 6.0f) || ignore_los) { + auto p = new EQApplicationPacket(OP_Fling, sizeof(fling_struct)); + auto* f = (fling_struct*) p->pBuffer; - auto aa_link = Saylink::Silent("#reload aa"); + if (!calculate_speed) { + f->speed_z = value; + } else { + auto speed = 1.0f; + const auto distance = CalculateDistance(target_x, target_y, target_z); - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Alternate Advancement Data globally", - aa_link - ).c_str() - ); + auto z_diff = target_z - GetZ(); + if (z_diff != 0.0f) { + speed += std::abs(z_diff) / 12.0f; + } - auto alternate_currencies_link = Saylink::Silent("#reload alternate_currencies"); + speed += distance / 200.0f; - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Alternate Currencies globally", - alternate_currencies_link - ).c_str() - ); + speed++; - auto base_data_link = Saylink::Silent("#reload base_data"); + speed = std::abs(speed); - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Base Data globally", - base_data_link - ).c_str() - ); + f->speed_z = speed; + } - auto blocked_spells_link = Saylink::Silent("#reload blocked_spells"); + f->collision = clip_through_walls ? 0 : -1; + f->travel_time = -1; + f->unk3 = 1; + f->disable_fall_damage = 1; + f->new_y = target_y; + f->new_x = target_x; + f->new_z = target_z; + p->priority = 6; + FastQueuePacket(&p); + } +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Blocked Spells globally", - blocked_spells_link - ).c_str() - ); +std::vector Client::GetLearnableDisciplines(uint8 min_level, uint8 max_level) { + std::vector learnable_disciplines; + for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { + bool learnable = true; + if (!IsValidSpell(spell_id)) { + continue; + } + + if (!IsDiscipline(spell_id)) { + continue; + } + + if (spells[spell_id].classes[Class::Warrior] == 0) { + continue; + } + + if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { + continue; + } + + if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { + continue; + } + + if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { + continue; + } + + if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { + continue; + } + + if (HasDisciplineLearned(spell_id)) { + continue; + } + + if (RuleB(Spells, EnableSpellGlobals) && !SpellGlobalCheck(spell_id, CharacterID())) { + learnable = false; + } else if (RuleB(Spells, EnableSpellBuckets) && !SpellBucketCheck(spell_id, CharacterID())) { + learnable = false; + } + + if (learnable) { + learnable_disciplines.push_back(spell_id); + } + } + return learnable_disciplines; +} - auto commands_link = Saylink::Silent("#reload commands"); +std::vector Client::GetLearnedDisciplines() { + std::vector learned_disciplines; + for (int index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (IsValidSpell(m_pp.disciplines.values[index])) { + learned_disciplines.push_back(m_pp.disciplines.values[index]); + } + } + return learned_disciplines; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Commands globally", - commands_link - ).c_str() - ); +std::vector Client::GetMemmedSpells() { + std::vector memmed_spells; + for (int index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { + if (IsValidSpell(m_pp.mem_spells[index])) { + memmed_spells.push_back(m_pp.mem_spells[index]); + } + } + return memmed_spells; +} - auto content_flags_link = Saylink::Silent("#reload content_flags"); +std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { + std::vector scribeable_spells; + std::unordered_map> spell_group_cache = LoadSpellGroupCache(min_level, max_level); + + for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { + bool scribeable = true; + if (!IsValidSpell(spell_id)) { + continue; + } + + if (IsDiscipline(spell_id)) { + continue; + } + + if (spells[spell_id].classes[Class::Warrior] == 0) { + continue; + } + + if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { + continue; + } + + if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { + continue; + } + + if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { + continue; + } + + if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { + continue; + } + + if (HasSpellScribed(spell_id)) { + continue; + } + + if ( + RuleB(Spells, EnableSpellGlobals) && + !SpellGlobalCheck(spell_id, CharacterID()) + ) { + scribeable = false; + } else if ( + RuleB(Spells, EnableSpellBuckets) && + !SpellBucketCheck(spell_id, CharacterID()) + ) { + scribeable = false; + } + + if (spells[spell_id].spell_group) { + const auto& g = spell_group_cache.find(spells[spell_id].spell_group); + if (g != spell_group_cache.end()) { + for (const auto& s : g->second) { + if ( + EQ::ValueWithin(spells[s].classes[m_pp.class_ - 1], min_level, max_level) && + s == spell_id && + scribeable + ) { + scribeable_spells.push_back(spell_id); + } + continue; + } + } + } else if (scribeable) { + scribeable_spells.push_back(spell_id); + } + } + return scribeable_spells; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Content Flags globally", - content_flags_link - ).c_str() - ); +std::vector Client::GetScribedSpells() { + std::vector scribed_spells; + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + if (IsValidSpell(m_pp.spell_book[index])) { + scribed_spells.push_back(m_pp.spell_book[index]); + } + } + return scribed_spells; +} - auto doors_link = Saylink::Silent("#reload doors"); +void Client::SetAnon(uint8 anon_flag) { + m_pp.anon = anon_flag; + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; + spawn_appearance->spawn_id = GetID(); + spawn_appearance->type = AppearanceType::Anonymous; + spawn_appearance->parameter = anon_flag; + entity_list.QueueClients(this, outapp); + Save(); + UpdateWho(); + safe_delete(outapp); +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Doors globally", - doors_link - ).c_str() - ); +void Client::SetAFK(uint8 afk_flag) { + AFK = afk_flag; + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; + spawn_appearance->spawn_id = GetID(); + spawn_appearance->type = AppearanceType::AFK; + spawn_appearance->parameter = afk_flag; + entity_list.QueueClients(this, outapp); + safe_delete(outapp); +} - auto data_buckets_link = Saylink::Silent("#reload data_buckets_cache"); +void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { + uint32 zone_id = ZoneID(zone_short_name); + std::string current_instance_type = Strings::ToLower(instance_type); + std::string instance_type_name = "public"; + if (current_instance_type.find("solo") != std::string::npos) { + instance_type_name = GetCleanName(); + } else if (current_instance_type.find("group") != std::string::npos) { + uint32 group_id = (GetGroup() ? GetGroup()->GetID() : 0); + instance_type_name = itoa(group_id); + } else if (current_instance_type.find("raid") != std::string::npos) { + uint32 raid_id = (GetRaid() ? GetRaid()->GetID() : 0); + instance_type_name = itoa(raid_id); + } else if (current_instance_type.find("guild") != std::string::npos) { + uint32 guild_id = (GuildID() > 0 ? GuildID() : 0); + instance_type_name = itoa(guild_id); + } + + std::string full_bucket_name = fmt::format( + "{}_{}_{}_{}", + current_instance_type, + instance_type_name, + instance_identifier, + zone_short_name + ); + std::string current_bucket_value = DataBucket::GetData(full_bucket_name); + uint16 instance_id = 0; + + if (current_bucket_value.length() > 0) { + instance_id = Strings::ToInt(current_bucket_value); + } else { + if(!database.GetUnusedInstanceID(instance_id)) { + Message(Chat::White, "Server was unable to find a free instance id."); + return; + } + + if(!database.CreateInstance(instance_id, zone_id, instance_version, duration)) { + Message(Chat::White, "Server was unable to create a new instance."); + return; + } + + DataBucket::SetData(full_bucket_name, itoa(instance_id), itoa(duration)); + } + + AssignToInstance(instance_id); + MovePC(zone_id, instance_id, x, y, z, heading); +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads data buckets cache globally", - data_buckets_link - ).c_str() - ); +uint32 Client::CountItem(uint32 item_id) +{ + uint32 quantity = 0; + EQ::ItemInstance *item = nullptr; - auto dztemplates_link = Saylink::Silent("#reload dztemplates"); + for (const int16& slot_id : GetInventorySlots()) { + item = GetInv().GetItem(slot_id); + if (item && item->GetID() == item_id) { + quantity += (item->IsStackable() ? item->GetCharges() : 1); + } + } - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Dynamic Zone Templates globally", - dztemplates_link - ).c_str() - ); + return quantity; +} - auto factions_link = Saylink::Silent("#reload factions"); +void Client::ResetItemCooldown(uint32 item_id) +{ + EQ::ItemInstance *item = nullptr; + const EQ::ItemData* item_d = database.GetItem(item_id); + if (!item_d) { + return; + } + int recast_type = item_d->RecastType; + bool found_item = false; + + for (const int16& slot_id : GetInventorySlots()) { + item = GetInv().GetItem(slot_id); + if (item) { + item_d = item->GetItem(); + if ( + item_d && + item->GetID() == item_id || + ( + item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && + item_d->RecastType == recast_type + ) + ) { + item->SetRecastTimestamp(0); + DeleteItemRecastTimer(item_d->ID); + SendItemPacket(slot_id, item, ItemPacketCharmUpdate); + found_item = true; + } + } + } + + if (!found_item) { + DeleteItemRecastTimer(item_id); //We didn't find the item but we still want to remove the timer + } +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Factions globally", - factions_link - ).c_str() - ); +void Client::SetItemCooldown(uint32 item_id, bool use_saved_timer, uint32 in_seconds) +{ + EQ::ItemInstance *item = nullptr; + const EQ::ItemData* item_d = database.GetItem(item_id); + if (!item_d) { + return; + } + int recast_type = item_d->RecastType; + auto timestamps = database.GetItemRecastTimestamps(CharacterID()); + uint32 total_time = 0; + uint32 current_time = static_cast(std::time(nullptr)); + uint32 final_time = 0; + const auto timer_type = item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM ? item_d->RecastType : item_id; + const int timer_id = recast_type != RECAST_TYPE_UNLINKED_ITEM ? (pTimerItemStart + recast_type) : (pTimerNegativeItemReuse * item_id); + + if (use_saved_timer) { + if (item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM) { + total_time = timestamps.count(item_d->RecastType) ? timestamps.at(item_d->RecastType) : 0; + } else { + total_time = timestamps.count(item_id) ? timestamps.at(item_id) : 0; + } + } else { + total_time = current_time + in_seconds; + } + + if (total_time > current_time) { + final_time = total_time - current_time; + } + + for (const int16& slot_id : GetInventorySlots()) { + item = GetInv().GetItem(slot_id); + if (item) { + item_d = item->GetItem(); + if ( + item_d && + item->GetID() == item_id || + ( + item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && + item_d->RecastType == recast_type + ) + ) { + item->SetRecastTimestamp(total_time); + SendItemPacket(slot_id, item, ItemPacketCharmUpdate); + } + } + } + + //Start timers and update in database only when timer is changed + if (!use_saved_timer) { + GetPTimers().Clear(&database, timer_id); + GetPTimers().Start((timer_id), in_seconds); + database.UpdateItemRecast( + CharacterID(), + timer_type, + GetPTimers().Get(timer_id)->GetReadyTimestamp() + ); + } + SendItemRecastTimer(recast_type, final_time, true); +} - auto ground_spawns_link = Saylink::Silent("#reload ground_spawns"); +uint32 Client::GetItemCooldown(uint32 item_id) +{ + const EQ::ItemData* item_d = database.GetItem(item_id); + if (!item_d) { + return 0; + } - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Ground Spawns globally", - ground_spawns_link - ).c_str() - ); + int recast_type = item_d->RecastType; + auto timestamps = database.GetItemRecastTimestamps(CharacterID()); + const auto timer_type = recast_type != RECAST_TYPE_UNLINKED_ITEM ? recast_type : item_id; + uint32 total_time = 0; + uint32 current_time = static_cast(std::time(nullptr)); + uint32 final_time = 0; - auto level_mods_link = Saylink::Silent("#reload level_mods"); + total_time = timestamps.count(timer_type) ? timestamps.at(timer_type) : 0; - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Level Based Experience Modifiers globally", - level_mods_link - ).c_str() - ); + if (total_time > current_time) { + final_time = total_time - current_time; + } - auto logs_link = Saylink::Silent("#reload logs"); + return final_time; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Log Settings globally", - logs_link - ).c_str() - ); +void Client::RemoveItem(uint32 item_id, uint32 quantity) +{ + uint32 removed_count = 0; + EQ::ItemInstance *item = nullptr; + + for (const int16& slot_id : GetInventorySlots()) { + if (removed_count == quantity) { + break; + } + + item = GetInv().GetItem(slot_id); + if (item && item->GetID() == item_id) { + uint32 charges = item->IsStackable() ? item->GetCharges() : 0; + uint32 stack_size = std::max(charges, static_cast(1)); + if ((removed_count + stack_size) <= quantity) { + removed_count += stack_size; + DeleteItemInInventory(slot_id, charges, true); + } else { + uint32 amount_left = (quantity - removed_count); + if (amount_left > 0 && stack_size >= amount_left) { + removed_count += amount_left; + DeleteItemInInventory(slot_id, amount_left, true); + } + } + } + } +} - auto loot_link = Saylink::Silent("#reload loot"); +void Client::SetGMStatus(int new_status) { + if (Admin() != new_status) { + database.UpdateGMStatus(AccountID(), new_status); + UpdateAdmin(); + } +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Loot globally", - loot_link - ).c_str() - ); +void Client::ApplyWeaponsStance() +{ + /* + + If you have a weapons stance bonus from at least one bonus type, each time you change weapons this function will ensure the correct + associated buffs are applied, and previous buff is removed. If your weapon stance bonus is completely removed it will, ensure buff is + also removed (ie, removing an item that has worn effect with weapon stance, or clicking off a buff). If client no longer has/never had + any spells/item/aa bonuses with weapon stance effect this function will only do a simple bool check. + + Note: Live like behavior is once you have the triggered buff you can manually click it off to remove it. Swaping any items in inventory will + reapply it automatically. + + Only buff spells should be used as triggered spell effect. IsBuffSpell function also checks spell id validity. + WeaponStance bonus arrary: 0=2H Weapon 1=Shield 2=Dualweild + + Toggling ON or OFF + - From spells, just remove the Primary buff that contains the WeaponStance effect in it. + - For items with worn effect, unequip the item. + - For AA abilities, a hotkey is used to Enable and Disable the effect. See. Client::TogglePassiveAlternativeAdvancement in aa.cpp for extensive details. + + Rank + - Most important for AA, but if you have more than one of WeaponStance effect for a given type, the spell trigger buff will apply whatever has the highest + 'rank' value from the spells table. AA's on live for this effect naturally do this. Be awere of this if making custom spells/worn effects/AA. + + When creating weapon stance effects, you do not need to use all three types. For example, can make an effect where you only get a buff from equiping shield. + + */ + + if (!IsWeaponStanceEnabled()) { + return; + } + + bool enabled = false; + bool item_bonus_exists = false; + bool aa_bonus_exists = false; + + if (weaponstance.spellbonus_enabled) { + + if (spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { + + enabled = true; + + // Check if no longer has correct combination of weapon type and buff, if so remove buff. + if (!HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && + FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); + } + else if (!HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && + FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); + } + else if (!HasDualWeaponsEquipped() && + IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && + FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); + } + // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. + if (HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); + } + weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; + } + else if (HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + + if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); + } + weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; + } + else if (HasDualWeaponsEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + + if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); + } + weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; + } + } + } + + // Spellbonus effect removal is checked in BuffFadeBySlot(int slot, bool iRecalcBonuses) in spell_effects.cpp when the buff is clicked off or fades. + + if (weaponstance.itembonus_enabled) { + + if (itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { + + enabled = true; + item_bonus_exists = true; + + + // Edge case check if have multiple items with WeaponStance worn effect. Make sure correct buffs are applied if items are removed but others left on. + if (weaponstance.itembonus_buff_spell_id) { + + bool buff_desync = true; + if (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || + weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + buff_desync = false; + } + + if (buff_desync) { + int fade_spell = weaponstance.itembonus_buff_spell_id; + weaponstance.itembonus_buff_spell_id = 0; //Need to zero this before we fade to prevent any recursive loops. + BuffFadeBySpellID(fade_spell); + } + } + + // Check if no longer has correct combination of weapon type and buff, if so remove buff. + if (!HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && + FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); + } + else if (!HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && + FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); + } + else if (!HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && + FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); + } + + // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. + if (HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + + if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); + } + weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; + } + else if (HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + + if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); + } + weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; + } + else if (HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); + } + weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; + } + } + } + + // Itembonus effect removal when item is removed + if (!item_bonus_exists && weaponstance.itembonus_enabled) { + weaponstance.itembonus_enabled = false; + + if (weaponstance.itembonus_buff_spell_id) { + BuffFadeBySpellID(weaponstance.itembonus_buff_spell_id); + weaponstance.itembonus_buff_spell_id = 0; + } + } + + if (weaponstance.aabonus_enabled) { + + if (aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { + + enabled = true; + aa_bonus_exists = true; + + //Check if no longer has correct combination of weapon type and buff, if so remove buff. + if (!HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && + FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); + } + + else if (!HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && + FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); + } + + else if (!HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && + FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); + } + + //If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. + if (HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); + } + weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; + } + + else if (HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); + } + weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; + } + + else if (HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + + if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); + } + weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; + } + } + } + + // AA bonus removal is checked in TogglePassiveAA in aa.cpp. when the hot key is toggled. + + // If no bonuses remain present, prevent additional future checks until new bonus is applied. + if (!enabled) { + SetWeaponStanceEnabled(false); + weaponstance.aabonus_enabled = false; + weaponstance.itembonus_enabled = false; + weaponstance.spellbonus_enabled = false; + } +} - auto merchants_link = Saylink::Silent("#reload merchants"); +uint16 Client::GetDoorToolEntityId() const +{ + return m_door_tool_entity_id; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Merchants globally", - merchants_link - ).c_str() - ); +void Client::SetDoorToolEntityId(uint16 door_tool_entity_id) +{ + Client::m_door_tool_entity_id = door_tool_entity_id; +} - auto npc_emotes_link = Saylink::Silent("#reload npc_emotes"); +uint16 Client::GetObjectToolEntityId() const +{ + return m_object_tool_entity_id; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads NPC Emotes globally", - npc_emotes_link - ).c_str() - ); +void Client::SetObjectToolEntityId(uint16 object_tool_entity_id) +{ + Client::m_object_tool_entity_id = object_tool_entity_id; +} - auto npc_spells_link = Saylink::Silent("#reload npc_spells"); - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads NPC Spells globally", - npc_spells_link - ).c_str() - ); +int Client::GetIPExemption() +{ + return database.GetIPExemption(GetIPString()); +} + +std::string Client::GetIPString() +{ + in_addr client_ip{}; + client_ip.s_addr = GetIP(); + return inet_ntoa(client_ip); +} - auto objects_link = Saylink::Silent("#reload objects"); +void Client::SetIPExemption(int exemption_amount) +{ + database.SetIPExemption(GetIPString(), exemption_amount); +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Objects globally", - objects_link - ).c_str() - ); +void Client::ReadBookByName(std::string book_name, uint8 book_type) +{ + auto b = content_db.GetBook(book_name); - auto opcodes_link = Saylink::Silent("#reload opcodes"); + if (!b.text.empty()) { + LogDebug("Book Name: [{}] Text: [{}]", book_name, b.text); - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Opcodes globally", - opcodes_link - ).c_str() - ); + auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); - auto perl_export_link = Saylink::Silent("#reload perl_export"); + auto o = (BookText_Struct *) outapp->pBuffer; - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Perl Event Export Settings globally", - perl_export_link - ).c_str() - ); + o->window = std::numeric_limits::max(); + o->type = book_type; + o->invslot = 0; - auto quest_link_one = Saylink::Silent("#reload quest"); - auto quest_link_two = Saylink::Silent("#reload quest", "0"); - auto quest_link_three = Saylink::Silent("#reload quest 1", "1"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [{}|{}] - Reloads Quests and Timers in your current zone if specified (0 = Do Not Reload Timers, 1 = Reload Timers)", - quest_link_one, - quest_link_two, - quest_link_three - ).c_str() - ); + memcpy(o->booktext, b.text.c_str(), b.text.size()); - auto rules_link = Saylink::Silent("#reload rules"); + if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { + if (m_pp.languages[b.language] < Language::MaxValue) { + GarbleMessage(o->booktext, (Language::MaxValue - m_pp.languages[b.language])); + } + } - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Rules globally", - rules_link - ).c_str() - ); + QueuePacket(outapp); + safe_delete(outapp); + } +} - auto skill_caps_link = Saylink::Silent("#reload skill_caps"); +// this will fetch raid clients if exists +// fallback to group if raid doesn't exist +// fallback to self if group doesn't exist +std::vector Client::GetPartyMembers() +{ + // get clients to update + std::vector clients_to_update = {}; + + // raid + if (const auto raid = entity_list.GetRaidByClient(this)) { + for (auto &m : raid->members) { + if (m.is_bot) { + continue; + } + + if (m.member && m.member->IsClient()) { + clients_to_update.push_back(m.member->CastToClient()); + } + } + } + + // group + if (clients_to_update.empty()) { + Group *group = entity_list.GetGroupByClient(this); + if (group) { + for (auto &m : group->members) { + if (m && m->IsClient()) { + clients_to_update.push_back(m->CastToClient()); + } + } + } + } + + // solo + if (clients_to_update.empty()) { + clients_to_update.push_back(this); + } + + return clients_to_update; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Skill Caps globally", - skill_caps_link - ).c_str() - ); +void Client::SummonBaggedItems(uint32 bag_item_id, const std::vector& bag_items) +{ + if (bag_items.empty()) + { + return; + } + + // todo: maybe some common functions for SE_SummonItem and SE_SummonItemIntoBag + + const EQ::ItemData* bag_item = database.GetItem(bag_item_id); + if (!bag_item) + { + Message(Chat::Red, fmt::format("Unable to summon item [{}]. Item not found.", bag_item_id).c_str()); + return; + } + + if (CheckLoreConflict(bag_item)) + { + DuplicateLoreMessage(bag_item_id); + return; + } + + int bag_item_charges = 1; // just summoning a single bag + EQ::ItemInstance* summoned_bag = database.CreateItem(bag_item_id, bag_item_charges); + if (!summoned_bag || !summoned_bag->IsClassBag()) + { + Message(Chat::Red, fmt::format("Failed to summon bag item [{}]", bag_item_id).c_str()); + safe_delete(summoned_bag); + return; + } + + for (const auto& item : bag_items) + { + uint8 open_slot = summoned_bag->FirstOpenSlot(); + if (open_slot == 0xff) + { + Message(Chat::Red, "Attempting to summon item in to bag, but there is no room in the summoned bag!"); + break; + } + + const EQ::ItemData* current_item = database.GetItem(item.item_id); + + if (CheckLoreConflict(current_item)) + { + DuplicateLoreMessage(item.item_id); + } + else + { + EQ::ItemInstance* summoned_bag_item = database.CreateItem( + item.item_id, + item.charges, + item.aug_1, + item.aug_2, + item.aug_3, + item.aug_4, + item.aug_5, + item.aug_6, + item.attuned, + item.custom_data, + item.ornamenticon, + item.ornamentidfile, + item.ornament_hero_model + ); + if (summoned_bag_item) + { + summoned_bag->PutItem(open_slot, *summoned_bag_item); + safe_delete(summoned_bag_item); + } + } + } + + PushItemOnCursor(*summoned_bag); + SendItemPacket(EQ::invslot::slotCursor, summoned_bag, ItemPacketLimbo); + safe_delete(summoned_bag); +} - auto static_link = Saylink::Silent("#reload static"); +void Client::SaveSpells() +{ + std::vector character_spells = {}; - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Static Zone Data globally", - static_link - ).c_str() - ); + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + if (IsValidSpell(m_pp.spell_book[index])) { + auto spell = CharacterSpellsRepository::NewEntity(); + spell.id = CharacterID(); + spell.slot_id = index; + spell.spell_id = m_pp.spell_book[index]; + character_spells.emplace_back(spell); + } + } - auto tasks_link = Saylink::Silent("#reload tasks"); + CharacterSpellsRepository::DeleteWhere(database, fmt::format("id = {}", CharacterID())); - Message( - Chat::White, - fmt::format( - "Usage: {} [Task ID] - Reloads Tasks globally or by ID if specified", - tasks_link - ).c_str() - ); + if (!character_spells.empty()) { + CharacterSpellsRepository::InsertMany(database, character_spells); + } +} - auto titles_link = Saylink::Silent("#reload titles"); +void Client::SaveDisciplines() +{ + std::vector v; - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Titles globally", - titles_link - ).c_str() - ); + std::vector delete_slots; - auto traps_link_one = Saylink::Silent("#reload traps"); - auto traps_link_two = Saylink::Silent("#reload traps", "0"); - auto traps_link_three = Saylink::Silent("#reload traps 1", "1"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [{}|{}] - Reloads Traps in your current zone or globally if specified", - traps_link_one, - traps_link_two, - traps_link_three - ).c_str() - ); + for (uint16 slot_id = 0; slot_id < MAX_PP_DISCIPLINES; slot_id++) { + if (IsValidSpell(m_pp.disciplines.values[slot_id])) { + auto e = CharacterDisciplinesRepository::NewEntity(); - auto variables_link = Saylink::Silent("#reload variables"); + e.id = CharacterID(); + e.slot_id = slot_id; + e.disc_id = m_pp.disciplines.values[slot_id]; - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Variables globally", - variables_link - ).c_str() - ); + v.emplace_back(e); + } else { + delete_slots.emplace_back(std::to_string(slot_id)); + } + } - auto veteran_rewards_link = Saylink::Silent("#reload veteran_rewards"); + if (!delete_slots.empty()) { + CharacterDisciplinesRepository::DeleteWhere( + database, + fmt::format( + "`id` = {} AND `slot_id` IN ({})", + CharacterID(), + Strings::Join(delete_slots, ", ") + ) + ); + } - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Veteran Rewards globally", - veteran_rewards_link - ).c_str() - ); + if (!v.empty()) { + CharacterDisciplinesRepository::ReplaceMany(database, v); + } +} - auto world_link_one = Saylink::Silent("#reload world"); - auto world_link_two = Saylink::Silent("#reload world", "0"); - auto world_link_three = Saylink::Silent("#reload world 1", "1"); - auto world_link_four = Saylink::Silent("#reload world 2", "2"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [{}|{}|{}] - Reloads Quests and repops globally if specified (0 = No Repop, 1 = Repop, 2 = Force Repop)", - world_link_one, - world_link_two, - world_link_three, - world_link_four - ).c_str() - ); +uint16 Client::ScribeSpells(uint8 min_level, uint8 max_level) +{ + auto available_book_slot = GetNextAvailableSpellBookSlot(); + std::vector spell_ids = GetScribeableSpells(min_level, max_level); + uint16 scribed_spells = 0; + + if (!spell_ids.empty()) { + for (const auto& spell_id : spell_ids) { + if (available_book_slot == -1) { + Message( + Chat::Red, + fmt::format( + "Unable to scribe {} ({}) to Spell Book because your Spell Book is full.", + spells[spell_id].name, + spell_id + ).c_str() + ); + break; + } + + if (HasSpellScribed(spell_id)) { + continue; + } + + // defer saving per spell and bulk save at the end + ScribeSpell(spell_id, available_book_slot, true, true); + available_book_slot = GetNextAvailableSpellBookSlot(available_book_slot); + scribed_spells++; + } + } + + if (scribed_spells > 0) { + std::string spell_message = ( + scribed_spells == 1 ? + "a new spell" : + fmt::format("{} new spells", scribed_spells) + ); + Message(Chat::White, fmt::format("You have learned {}!", spell_message).c_str()); + + // bulk insert spells + SaveSpells(); + } + return scribed_spells; +} - auto zone_link = Saylink::Silent("#reload zone"); +uint16 Client::LearnDisciplines(uint8 min_level, uint8 max_level) +{ + auto available_discipline_slot = GetNextAvailableDisciplineSlot(); + auto character_id = CharacterID(); + std::vector spell_ids = GetLearnableDisciplines(min_level, max_level); + uint16 learned_disciplines = 0; + + if (!spell_ids.empty()) { + for (const auto& spell_id : spell_ids) { + if (available_discipline_slot == -1) { + Message( + Chat::Red, + fmt::format( + "Unable to learn {} ({}) because your Discipline slots are full.", + spells[spell_id].name, + spell_id + ).c_str() + ); + break; + } + + if (HasDisciplineLearned(spell_id)) { + continue; + } + + GetPP().disciplines.values[available_discipline_slot] = spell_id; + available_discipline_slot = GetNextAvailableDisciplineSlot(available_discipline_slot); + learned_disciplines++; + } + } + + if (learned_disciplines > 0) { + std::string discipline_message = ( + learned_disciplines == 1 ? + "a new discipline" : + fmt::format("{} new disciplines", learned_disciplines) + ); + Message(Chat::White, fmt::format("You have learned {}!", discipline_message).c_str()); + SendDisciplineUpdate(); + SaveDisciplines(); + } + + return learned_disciplines; +} - Message( - Chat::White, - fmt::format( - "Usage: {} [Zone ID] [Version] - Reloads Zone configuration for your current zone, can load another Zone's configuration if specified", - zone_link - ).c_str() - ); +uint16 Client::GetClassTrackingDistanceMultiplier(uint16 class_) { + switch (class_) { + case Class::Warrior: + return RuleI(Character, WarriorTrackingDistanceMultiplier); + case Class::Cleric: + return RuleI(Character, ClericTrackingDistanceMultiplier); + case Class::Paladin: + return RuleI(Character, PaladinTrackingDistanceMultiplier); + case Class::Ranger: + return RuleI(Character, RangerTrackingDistanceMultiplier); + case Class::ShadowKnight: + return RuleI(Character, ShadowKnightTrackingDistanceMultiplier); + case Class::Druid: + return RuleI(Character, DruidTrackingDistanceMultiplier); + case Class::Monk: + return RuleI(Character, MonkTrackingDistanceMultiplier); + case Class::Bard: + return RuleI(Character, BardTrackingDistanceMultiplier); + case Class::Rogue: + return RuleI(Character, RogueTrackingDistanceMultiplier); + case Class::Shaman: + return RuleI(Character, ShamanTrackingDistanceMultiplier); + case Class::Necromancer: + return RuleI(Character, NecromancerTrackingDistanceMultiplier); + case Class::Wizard: + return RuleI(Character, WizardTrackingDistanceMultiplier); + case Class::Magician: + return RuleI(Character, MagicianTrackingDistanceMultiplier); + case Class::Enchanter: + return RuleI(Character, EnchanterTrackingDistanceMultiplier); + case Class::Beastlord: + return RuleI(Character, BeastlordTrackingDistanceMultiplier); + case Class::Berserker: + return RuleI(Character, BerserkerTrackingDistanceMultiplier); + default: + return 0; + } +} - auto zone_points_link = Saylink::Silent("#reload zone_points"); +bool Client::CanThisClassTrack() { + return (GetClassTrackingDistanceMultiplier(GetClass()) > 0) ? true : false; +} - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Zone Points globally", - zone_points_link - ).c_str() - ); +void Client::ReconnectUCS() +{ + EQApplicationPacket *outapp = nullptr; + std::string buffer; + std::string mail_key = m_mail_key; + EQ::versions::UCSVersion connection_type = EQ::versions::ucsUnknown; + + // chat server packet + switch (ClientVersion()) { + case EQ::versions::ClientVersion::Titanium: + connection_type = EQ::versions::ucsTitaniumChat; + break; + case EQ::versions::ClientVersion::SoF: + connection_type = EQ::versions::ucsSoFCombined; + break; + case EQ::versions::ClientVersion::SoD: + connection_type = EQ::versions::ucsSoDCombined; + break; + case EQ::versions::ClientVersion::UF: + connection_type = EQ::versions::ucsUFCombined; + break; + case EQ::versions::ClientVersion::RoF: + connection_type = EQ::versions::ucsRoFCombined; + break; + case EQ::versions::ClientVersion::RoF2: + connection_type = EQ::versions::ucsRoF2Combined; + break; + default: + connection_type = EQ::versions::ucsUnknown; + break; + } + + buffer = StringFormat( + "%s,%i,%s.%s,%c%s", + Config->GetUCSHost().c_str(), + Config->GetUCSPort(), + Config->ShortName.c_str(), + GetName(), + connection_type, + mail_key.c_str() + ); + + outapp = new EQApplicationPacket(OP_SetChatServer, (buffer.length() + 1)); + memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); + outapp->pBuffer[buffer.length()] = '\0'; + + QueuePacket(outapp); + safe_delete(outapp); + + // mail server packet + switch (ClientVersion()) { + case EQ::versions::ClientVersion::Titanium: + connection_type = EQ::versions::ucsTitaniumMail; + break; + default: + // retain value from previous switch + break; + } + + buffer = StringFormat( + "%s,%i,%s.%s,%c%s", + Config->GetUCSHost().c_str(), + Config->GetUCSPort(), + Config->ShortName.c_str(), + GetName(), + connection_type, + mail_key.c_str() + ); + + outapp = new EQApplicationPacket(OP_SetChatServer2, (buffer.length() + 1)); + memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); + outapp->pBuffer[buffer.length()] = '\0'; + + QueuePacket(outapp); + safe_delete(outapp); +} - SendChatLineBreak(); +void Client::SendReloadCommandMessages() { + SendChatLineBreak(); + + auto aa_link = Saylink::Silent("#reload aa"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Alternate Advancement Data globally", + aa_link + ).c_str() + ); + + auto alternate_currencies_link = Saylink::Silent("#reload alternate_currencies"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Alternate Currencies globally", + alternate_currencies_link + ).c_str() + ); + + auto base_data_link = Saylink::Silent("#reload base_data"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Base Data globally", + base_data_link + ).c_str() + ); + + auto blocked_spells_link = Saylink::Silent("#reload blocked_spells"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Blocked Spells globally", + blocked_spells_link + ).c_str() + ); + + auto commands_link = Saylink::Silent("#reload commands"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Commands globally", + commands_link + ).c_str() + ); + + auto content_flags_link = Saylink::Silent("#reload content_flags"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Content Flags globally", + content_flags_link + ).c_str() + ); + + auto doors_link = Saylink::Silent("#reload doors"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Doors globally", + doors_link + ).c_str() + ); + + auto data_buckets_link = Saylink::Silent("#reload data_buckets_cache"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads data buckets cache globally", + data_buckets_link + ).c_str() + ); + + auto dztemplates_link = Saylink::Silent("#reload dztemplates"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Dynamic Zone Templates globally", + dztemplates_link + ).c_str() + ); + + auto factions_link = Saylink::Silent("#reload factions"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Factions globally", + factions_link + ).c_str() + ); + + auto ground_spawns_link = Saylink::Silent("#reload ground_spawns"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Ground Spawns globally", + ground_spawns_link + ).c_str() + ); + + auto level_mods_link = Saylink::Silent("#reload level_mods"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Level Based Experience Modifiers globally", + level_mods_link + ).c_str() + ); + + auto logs_link = Saylink::Silent("#reload logs"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Log Settings globally", + logs_link + ).c_str() + ); + + auto loot_link = Saylink::Silent("#reload loot"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Loot globally", + loot_link + ).c_str() + ); + + auto merchants_link = Saylink::Silent("#reload merchants"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Merchants globally", + merchants_link + ).c_str() + ); + + auto npc_emotes_link = Saylink::Silent("#reload npc_emotes"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads NPC Emotes globally", + npc_emotes_link + ).c_str() + ); + + auto npc_spells_link = Saylink::Silent("#reload npc_spells"); + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads NPC Spells globally", + npc_spells_link + ).c_str() + ); + + auto objects_link = Saylink::Silent("#reload objects"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Objects globally", + objects_link + ).c_str() + ); + + auto opcodes_link = Saylink::Silent("#reload opcodes"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Opcodes globally", + opcodes_link + ).c_str() + ); + + auto perl_export_link = Saylink::Silent("#reload perl_export"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Perl Event Export Settings globally", + perl_export_link + ).c_str() + ); + + auto quest_link_one = Saylink::Silent("#reload quest"); + auto quest_link_two = Saylink::Silent("#reload quest", "0"); + auto quest_link_three = Saylink::Silent("#reload quest 1", "1"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [{}|{}] - Reloads Quests and Timers in your current zone if specified (0 = Do Not Reload Timers, 1 = Reload Timers)", + quest_link_one, + quest_link_two, + quest_link_three + ).c_str() + ); + + auto rules_link = Saylink::Silent("#reload rules"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Rules globally", + rules_link + ).c_str() + ); + + auto skill_caps_link = Saylink::Silent("#reload skill_caps"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Skill Caps globally", + skill_caps_link + ).c_str() + ); + + auto static_link = Saylink::Silent("#reload static"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Static Zone Data globally", + static_link + ).c_str() + ); + + auto tasks_link = Saylink::Silent("#reload tasks"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [Task ID] - Reloads Tasks globally or by ID if specified", + tasks_link + ).c_str() + ); + + auto titles_link = Saylink::Silent("#reload titles"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Titles globally", + titles_link + ).c_str() + ); + + auto traps_link_one = Saylink::Silent("#reload traps"); + auto traps_link_two = Saylink::Silent("#reload traps", "0"); + auto traps_link_three = Saylink::Silent("#reload traps 1", "1"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [{}|{}] - Reloads Traps in your current zone or globally if specified", + traps_link_one, + traps_link_two, + traps_link_three + ).c_str() + ); + + auto variables_link = Saylink::Silent("#reload variables"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Variables globally", + variables_link + ).c_str() + ); + + auto veteran_rewards_link = Saylink::Silent("#reload veteran_rewards"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Veteran Rewards globally", + veteran_rewards_link + ).c_str() + ); + + auto world_link_one = Saylink::Silent("#reload world"); + auto world_link_two = Saylink::Silent("#reload world", "0"); + auto world_link_three = Saylink::Silent("#reload world 1", "1"); + auto world_link_four = Saylink::Silent("#reload world 2", "2"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [{}|{}|{}] - Reloads Quests and repops globally if specified (0 = No Repop, 1 = Repop, 2 = Force Repop)", + world_link_one, + world_link_two, + world_link_three, + world_link_four + ).c_str() + ); + + auto zone_link = Saylink::Silent("#reload zone"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [Zone ID] [Version] - Reloads Zone configuration for your current zone, can load another Zone's configuration if specified", + zone_link + ).c_str() + ); + + auto zone_points_link = Saylink::Silent("#reload zone_points"); + + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Zone Points globally", + zone_points_link + ).c_str() + ); + + SendChatLineBreak(); } void Client::Undye() { - for (uint8 slot = EQ::textures::textureBegin; slot <= EQ::textures::LastTexture; slot++) { - auto inventory_slot = SlotConvert(slot); - auto inst = m_inv.GetItem(inventory_slot); + for (uint8 slot = EQ::textures::textureBegin; slot <= EQ::textures::LastTexture; slot++) { + auto inventory_slot = SlotConvert(slot); + auto inst = m_inv.GetItem(inventory_slot); - if (inst) { - inst->SetColor(inst->GetItem()->Color); - database.SaveInventory(CharacterID(), inst, inventory_slot); - } + if (inst) { + inst->SetColor(inst->GetItem()->Color); + database.SaveInventory(CharacterID(), inst, inventory_slot); + } - m_pp.item_tint.Slot[slot].Color = 0; - SendWearChange(slot); - } + m_pp.item_tint.Slot[slot].Color = 0; + SendWearChange(slot); + } - database.DeleteCharacterMaterialColor(CharacterID()); + database.DeleteCharacterMaterialColor(CharacterID()); } void Client::SetTrackingID(uint32 entity_id) { - if (!entity_id) { - TrackingID = 0; - return; - } + if (!entity_id) { + TrackingID = 0; + return; + } - auto *m = entity_list.GetMob(entity_id); - if (!m) { - TrackingID = 0; - return; - } + auto *m = entity_list.GetMob(entity_id); + if (!m) { + TrackingID = 0; + return; + } - TrackingID = entity_id; + TrackingID = entity_id; } int Client::GetRecipeMadeCount(uint32 recipe_id) { - auto r = CharRecipeListRepository::GetWhere( - database, - fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) - ); + auto r = CharRecipeListRepository::GetWhere( + database, + fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) + ); - if (!r.empty() && r[0].recipe_id) { - return r[0].madecount; - } + if (!r.empty() && r[0].recipe_id) { + return r[0].madecount; + } - return 0; + return 0; } bool Client::HasRecipeLearned(uint32 recipe_id) { - auto r = CharRecipeListRepository::GetWhere( - database, - fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) - ); + auto r = CharRecipeListRepository::GetWhere( + database, + fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) + ); - if (!r.empty() && r[0].recipe_id) { - return true; - } + if (!r.empty() && r[0].recipe_id) { + return true; + } - return false; + return false; } bool Client::IsLockSavePosition() const { - return m_lock_save_position; + return m_lock_save_position; } void Client::SetLockSavePosition(bool lock_save_position) { - Client::m_lock_save_position = lock_save_position; + Client::m_lock_save_position = lock_save_position; } void Client::SetAAPoints(uint32 points) { - const uint32 current_points = m_pp.aapoints; + const uint32 current_points = m_pp.aapoints; - m_pp.aapoints = points; + m_pp.aapoints = points; - QuestEventID event_id = points > current_points ? EVENT_AA_GAIN : EVENT_AA_LOSS; - const uint32 change = event_id == EVENT_AA_GAIN ? points - current_points : current_points - points; + QuestEventID event_id = points > current_points ? EVENT_AA_GAIN : EVENT_AA_LOSS; + const uint32 change = event_id == EVENT_AA_GAIN ? points - current_points : current_points - points; - if (parse->PlayerHasQuestSub(event_id)) { - parse->EventPlayer(event_id, this, std::to_string(change), 0); - } + if (parse->PlayerHasQuestSub(event_id)) { + parse->EventPlayer(event_id, this, std::to_string(change), 0); + } - SendAlternateAdvancementStats(); + SendAlternateAdvancementStats(); } bool Client::RemoveAAPoints(uint32 points) { - if (m_pp.aapoints < points) { - return false; - } + if (m_pp.aapoints < points) { + return false; + } - m_pp.aapoints -= points; + m_pp.aapoints -= points; - if (parse->PlayerHasQuestSub(EVENT_AA_LOSS)) { - parse->EventPlayer(EVENT_AA_LOSS, this, std::to_string(points), 0); - } + if (parse->PlayerHasQuestSub(EVENT_AA_LOSS)) { + parse->EventPlayer(EVENT_AA_LOSS, this, std::to_string(points), 0); + } - SendAlternateAdvancementStats(); + SendAlternateAdvancementStats(); - return true; + return true; } void Client::AddAAPoints(uint32 points) { - m_pp.aapoints += points; + m_pp.aapoints += points; - if (parse->PlayerHasQuestSub(EVENT_AA_GAIN)) { - parse->EventPlayer(EVENT_AA_GAIN, this, std::to_string(points), 0); - } + if (parse->PlayerHasQuestSub(EVENT_AA_GAIN)) { + parse->EventPlayer(EVENT_AA_GAIN, this, std::to_string(points), 0); + } - if (points == 1 && m_pp.aapoints == 1) { - MessageString(Chat::Yellow, GAIN_SINGLE_AA_SINGLE_AA, fmt::format_int(m_pp.aapoints).c_str()); - } else if (points == 1 && m_pp.aapoints > 1) { - MessageString(Chat::Yellow, GAIN_SINGLE_AA_MULTI_AA, fmt::format_int(m_pp.aapoints).c_str()); - } else { - MessageString(Chat::Yellow, GAIN_MULTI_AA_MULTI_AA, fmt::format_int(points).c_str(), fmt::format_int(m_pp.aapoints).c_str()); - } + if (points == 1 && m_pp.aapoints == 1) { + MessageString(Chat::Yellow, GAIN_SINGLE_AA_SINGLE_AA, fmt::format_int(m_pp.aapoints).c_str()); + } else if (points == 1 && m_pp.aapoints > 1) { + MessageString(Chat::Yellow, GAIN_SINGLE_AA_MULTI_AA, fmt::format_int(m_pp.aapoints).c_str()); + } else { + MessageString(Chat::Yellow, GAIN_MULTI_AA_MULTI_AA, fmt::format_int(points).c_str(), fmt::format_int(m_pp.aapoints).c_str()); + } - SendAlternateAdvancementStats(); + SendAlternateAdvancementStats(); } bool Client::SendGMCommand(std::string message, bool ignore_status) { - return command_dispatch(this, message, ignore_status) >= 0 ? true : false; + return command_dispatch(this, message, ignore_status) >= 0 ? true : false; } void Client::RegisterBug(BugReport_Struct* r) { - if (!r) { - return; - } - - auto b = BugReportsRepository::NewEntity(); - - b.zone = zone->GetShortName(); - b.client_version_id = static_cast(ClientVersion()); - b.client_version_name = EQ::versions::ClientVersionName(ClientVersion()); - b.account_id = AccountID(); - b.character_id = CharacterID(); - b.character_name = GetName(); - b.reporter_spoof = (strcmp(GetCleanName(), r->reporter_name) != 0 ? 1 : 0); - b.category_id = r->category_id; - b.category_name = r->category_name; - b.reporter_name = r->reporter_name; - b.ui_path = r->ui_path; - b.pos_x = r->pos_x; - b.pos_y = r->pos_y; - b.pos_z = r->pos_z; - b.heading = r->heading; - b.time_played = r->time_played; - b.target_id = r->target_id; - b.target_name = r->target_name; - b.optional_info_mask = r->optional_info_mask; - b._can_duplicate = ((r->optional_info_mask & Bug::InformationFlag::Repeatable) != 0 ? 1 : 0); - b._crash_bug = ((r->optional_info_mask & Bug::InformationFlag::Crash) != 0 ? 1 : 0); - b._target_info = ((r->optional_info_mask & Bug::InformationFlag::TargetInfo) != 0 ? 1 : 0); - b._character_flags = ((r->optional_info_mask & Bug::InformationFlag::CharacterFlags) != 0 ? 1 : 0); - b._unknown_value = ((r->optional_info_mask & Bug::InformationFlag::Unknown) != 0 ? 1 : 0); - b.bug_report = r->bug_report; - b.system_info = r->system_info; + if (!r) { + return; + } + + auto b = BugReportsRepository::NewEntity(); + + b.zone = zone->GetShortName(); + b.client_version_id = static_cast(ClientVersion()); + b.client_version_name = EQ::versions::ClientVersionName(ClientVersion()); + b.account_id = AccountID(); + b.character_id = CharacterID(); + b.character_name = GetName(); + b.reporter_spoof = (strcmp(GetCleanName(), r->reporter_name) != 0 ? 1 : 0); + b.category_id = r->category_id; + b.category_name = r->category_name; + b.reporter_name = r->reporter_name; + b.ui_path = r->ui_path; + b.pos_x = r->pos_x; + b.pos_y = r->pos_y; + b.pos_z = r->pos_z; + b.heading = r->heading; + b.time_played = r->time_played; + b.target_id = r->target_id; + b.target_name = r->target_name; + b.optional_info_mask = r->optional_info_mask; + b._can_duplicate = ((r->optional_info_mask & Bug::InformationFlag::Repeatable) != 0 ? 1 : 0); + b._crash_bug = ((r->optional_info_mask & Bug::InformationFlag::Crash) != 0 ? 1 : 0); + b._target_info = ((r->optional_info_mask & Bug::InformationFlag::TargetInfo) != 0 ? 1 : 0); + b._character_flags = ((r->optional_info_mask & Bug::InformationFlag::CharacterFlags) != 0 ? 1 : 0); + b._unknown_value = ((r->optional_info_mask & Bug::InformationFlag::Unknown) != 0 ? 1 : 0); + b.bug_report = r->bug_report; + b.system_info = r->system_info; #ifdef LUA_EQEMU - bool ignore_default = false; - LuaParser::Instance()->RegisterBug(this, b, ignore_default); - if (ignore_default) { - return; - } + bool ignore_default = false; + LuaParser::Instance()->RegisterBug(this, b, ignore_default); + if (ignore_default) { + return; + } #endif - auto n = BugReportsRepository::InsertOne(database, b); - if (!n.id) { - Message(Chat::White, "Failed to created your bug report."); // Client sends success message - return; - } - - LogBugs("id [{}] report [{}] account [{}] name [{}] charid [{}] zone [{}]", n.id, r->bug_report, AccountID(), GetCleanName(), CharacterID(), zone->GetShortName()); - - worldserver.SendEmoteMessage( - 0, - 0, - AccountStatus::QuestTroupe, - Chat::Yellow, - fmt::format( - "{} has created a new bug report, would you like to {} it?", - GetCleanName(), - Saylink::Silent( - fmt::format( - "#bugs view {}", - n.id - ), - "view" - ) - ).c_str() - ); + auto n = BugReportsRepository::InsertOne(database, b); + if (!n.id) { + Message(Chat::White, "Failed to created your bug report."); // Client sends success message + return; + } + + LogBugs("id [{}] report [{}] account [{}] name [{}] charid [{}] zone [{}]", n.id, r->bug_report, AccountID(), GetCleanName(), CharacterID(), zone->GetShortName()); + + worldserver.SendEmoteMessage( + 0, + 0, + AccountStatus::QuestTroupe, + Chat::Yellow, + fmt::format( + "{} has created a new bug report, would you like to {} it?", + GetCleanName(), + Saylink::Silent( + fmt::format( + "#bugs view {}", + n.id + ), + "view" + ) + ).c_str() + ); } std::vector Client::GetApplySpellList( - ApplySpellType apply_type, - bool allow_pets, - bool is_raid_group_only, - bool allow_bots + ApplySpellType apply_type, + bool allow_pets, + bool is_raid_group_only, + bool allow_bots ) { - std::vector l; - - if (apply_type == ApplySpellType::Raid && IsRaidGrouped()) { - auto* r = GetRaid(); - if (r) { - auto group_id = r->GetGroup(this); - if (EQ::ValueWithin(group_id, 0, (MAX_RAID_GROUPS - 1))) { - for (const auto& m : r->members) { - if (m.member && m.member->IsClient() && (!is_raid_group_only || r->GetGroup(m.member) == group_id)) { - l.push_back(m.member); - - if (allow_pets && m.member->HasPet()) { - l.push_back(m.member->GetPet()); - } - - if (allow_bots) { - const auto& sbl = entity_list.GetBotListByCharacterID(m.member->CharacterID()); - for (const auto& b : sbl) { - l.push_back(b); - } - } - } - } - } - } - } else if (apply_type == ApplySpellType::Group && IsGrouped()) { - auto* g = GetGroup(); - if (g) { - for (auto i = 0; i < MAX_GROUP_MEMBERS; i++) { - auto* m = g->members[i]; - if (m && m->IsClient()) { - l.push_back(m->CastToClient()); - - if (allow_pets && m->HasPet()) { - l.push_back(m->GetPet()); - } - - if (allow_bots) { - const auto& sbl = entity_list.GetBotListByCharacterID(m->CastToClient()->CharacterID()); - for (const auto& b : sbl) { - l.push_back(b); - } - } - } - } - } - } else { - l.push_back(this); - - if (allow_pets && HasPet()) { - l.push_back(GetPet()); - } - - if (allow_bots) { - const auto& sbl = entity_list.GetBotListByCharacterID(CharacterID()); - for (const auto& b : sbl) { - l.push_back(b); - } - } - } - - return l; + std::vector l; + + if (apply_type == ApplySpellType::Raid && IsRaidGrouped()) { + auto* r = GetRaid(); + if (r) { + auto group_id = r->GetGroup(this); + if (EQ::ValueWithin(group_id, 0, (MAX_RAID_GROUPS - 1))) { + for (const auto& m : r->members) { + if (m.member && m.member->IsClient() && (!is_raid_group_only || r->GetGroup(m.member) == group_id)) { + l.push_back(m.member); + + if (allow_pets && m.member->HasPet()) { + l.push_back(m.member->GetPet()); + } + + if (allow_bots) { + const auto& sbl = entity_list.GetBotListByCharacterID(m.member->CharacterID()); + for (const auto& b : sbl) { + l.push_back(b); + } + } + } + } + } + } + } else if (apply_type == ApplySpellType::Group && IsGrouped()) { + auto* g = GetGroup(); + if (g) { + for (auto i = 0; i < MAX_GROUP_MEMBERS; i++) { + auto* m = g->members[i]; + if (m && m->IsClient()) { + l.push_back(m->CastToClient()); + + if (allow_pets && m->HasPet()) { + l.push_back(m->GetPet()); + } + + if (allow_bots) { + const auto& sbl = entity_list.GetBotListByCharacterID(m->CastToClient()->CharacterID()); + for (const auto& b : sbl) { + l.push_back(b); + } + } + } + } + } + } else { + l.push_back(this); + + if (allow_pets && HasPet()) { + l.push_back(GetPet()); + } + + if (allow_bots) { + const auto& sbl = entity_list.GetBotListByCharacterID(CharacterID()); + for (const auto& b : sbl) { + l.push_back(b); + } + } + } + + return l; } void Client::ApplySpell( - int spell_id, - int duration, - int level, - ApplySpellType apply_type, - bool allow_pets, - bool is_raid_group_only, - bool allow_bots + int spell_id, + int duration, + int level, + ApplySpellType apply_type, + bool allow_pets, + bool is_raid_group_only, + bool allow_bots ) { - const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); + const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); - for (const auto& m : l) { - m->ApplySpellBuff(spell_id, duration, level); - } + for (const auto& m : l) { + m->ApplySpellBuff(spell_id, duration, level); + } } void Client::SetSpellDuration( - int spell_id, - int duration, - int level, - ApplySpellType apply_type, - bool allow_pets, - bool is_raid_group_only, - bool allow_bots + int spell_id, + int duration, + int level, + ApplySpellType apply_type, + bool allow_pets, + bool is_raid_group_only, + bool allow_bots ) { - const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); + const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); - for (const auto& m : l) { - m->SetBuffDuration(spell_id, duration, level); - } + for (const auto& m : l) { + m->SetBuffDuration(spell_id, duration, level); + } } std::string Client::GetGuildPublicNote() { - if (!IsInAGuild()) { - return std::string(); - } + if (!IsInAGuild()) { + return std::string(); + } - CharGuildInfo gci; - if (!guild_mgr.GetCharInfo(character_id, gci)) { - return std::string(); - } + CharGuildInfo gci; + if (!guild_mgr.GetCharInfo(character_id, gci)) { + return std::string(); + } - return gci.public_note; + return gci.public_note; } void Client::MaxSkills() { - for (const auto &s : EQ::skills::GetSkillTypeMap()) { - auto current_skill_value = ( - EQ::skills::IsSpecializedSkill(s.first) ? - MAX_SPECIALIZED_SKILL : - skill_caps.GetSkillCap(GetClass(), s.first, GetLevel()).cap - ); + for (const auto &s : EQ::skills::GetSkillTypeMap()) { + auto current_skill_value = ( + EQ::skills::IsSpecializedSkill(s.first) ? + MAX_SPECIALIZED_SKILL : + skill_caps.GetSkillCap(GetClass(), s.first, GetLevel()).cap + ); - if (GetSkill(s.first) < current_skill_value) { - SetSkill(s.first, current_skill_value); - } - } + if (GetSkill(s.first) < current_skill_value) { + SetSkill(s.first, current_skill_value); + } + } } void Client::SendPath(Mob* target) { - if (!target) { - EQApplicationPacket outapp(OP_FindPersonReply, 0); - QueuePacket(&outapp); - return; - } - - - if ( - !RuleB(Pathing, Find) && - RuleB(Bazaar, EnableWarpToTrader) && - target->IsClient() && - ( - target->CastToClient()->IsTrader() || - target->CastToClient()->IsBuyer() - ) - ) { - Message( - Chat::Yellow, - fmt::format( - "Moving you to Trader {}.", - target->GetName() - ).c_str() - ); - MovePC( - zone->GetZoneID(), - zone->GetInstanceID(), - target->GetX(), - target->GetY(), - target->GetZ(), - 0.0f - ); - return; - } - - std::vector points; - - if (!RuleB(Pathing, Find) || !zone->pathing) { - points.clear(); - FindPerson_Point a; - FindPerson_Point b; - - a.x = GetX(); - a.y = GetY(); - a.z = GetZ(); - b.x = target->GetX(); - b.y = target->GetY(); - b.z = target->GetZ(); - - points.push_back(a); - points.push_back(b); - } - else { - glm::vec3 path_start( - GetX(), - GetY(), - GetZ() + (GetSize() < 6.0 ? 6 : GetSize()) * HEAD_POSITION - ); - - glm::vec3 path_end( - target->GetX(), - target->GetY(), - target->GetZ() + (target->GetSize() < 6.0 ? 6 : target->GetSize()) * HEAD_POSITION - ); - - bool partial = false; - bool stuck = false; - auto path_list = zone->pathing->FindRoute(path_start, path_end, partial, stuck); - - if (path_list.empty() || partial) { - EQApplicationPacket outapp(OP_FindPersonReply, 0); - QueuePacket(&outapp); - return; - } - - // Live appears to send the points in this order: - // Final destination. - // Current Position. - // rest of the points. - FindPerson_Point p; - - int point_number = 0; - - bool leads_to_teleporter = false; - - auto v = path_list.back(); - - p.x = v.pos.x; - p.y = v.pos.y; - p.z = v.pos.z; - points.push_back(p); - - p.x = GetX(); - p.y = GetY(); - p.z = GetZ(); - points.push_back(p); - - for (const auto &n: path_list) { - if (n.teleport) { - leads_to_teleporter = true; - break; - } - - glm::vec3 v = n.pos; - p.x = v.x; - p.y = v.y; - p.z = v.z; - - points.push_back(p); - - ++point_number; - } - - if (!leads_to_teleporter) { - p.x = target->GetX(); - p.y = target->GetY(); - p.z = target->GetZ(); - - points.push_back(p); - } - } - - SendPathPacket(points); + if (!target) { + EQApplicationPacket outapp(OP_FindPersonReply, 0); + QueuePacket(&outapp); + return; + } + + + if ( + !RuleB(Pathing, Find) && + RuleB(Bazaar, EnableWarpToTrader) && + target->IsClient() && + ( + target->CastToClient()->IsTrader() || + target->CastToClient()->IsBuyer() + ) + ) { + Message( + Chat::Yellow, + fmt::format( + "Moving you to Trader {}.", + target->GetName() + ).c_str() + ); + MovePC( + zone->GetZoneID(), + zone->GetInstanceID(), + target->GetX(), + target->GetY(), + target->GetZ(), + 0.0f + ); + return; + } + + std::vector points; + + if (!RuleB(Pathing, Find) || !zone->pathing) { + points.clear(); + FindPerson_Point a; + FindPerson_Point b; + + a.x = GetX(); + a.y = GetY(); + a.z = GetZ(); + b.x = target->GetX(); + b.y = target->GetY(); + b.z = target->GetZ(); + + points.push_back(a); + points.push_back(b); + } + else { + glm::vec3 path_start( + GetX(), + GetY(), + GetZ() + (GetSize() < 6.0 ? 6 : GetSize()) * HEAD_POSITION + ); + + glm::vec3 path_end( + target->GetX(), + target->GetY(), + target->GetZ() + (target->GetSize() < 6.0 ? 6 : target->GetSize()) * HEAD_POSITION + ); + + bool partial = false; + bool stuck = false; + auto path_list = zone->pathing->FindRoute(path_start, path_end, partial, stuck); + + if (path_list.empty() || partial) { + EQApplicationPacket outapp(OP_FindPersonReply, 0); + QueuePacket(&outapp); + return; + } + + // Live appears to send the points in this order: + // Final destination. + // Current Position. + // rest of the points. + FindPerson_Point p; + + int point_number = 0; + + bool leads_to_teleporter = false; + + auto v = path_list.back(); + + p.x = v.pos.x; + p.y = v.pos.y; + p.z = v.pos.z; + points.push_back(p); + + p.x = GetX(); + p.y = GetY(); + p.z = GetZ(); + points.push_back(p); + + for (const auto &n: path_list) { + if (n.teleport) { + leads_to_teleporter = true; + break; + } + + glm::vec3 v = n.pos; + p.x = v.x; + p.y = v.y; + p.z = v.z; + + points.push_back(p); + + ++point_number; + } + + if (!leads_to_teleporter) { + p.x = target->GetX(); + p.y = target->GetY(); + p.z = target->GetZ(); + + points.push_back(p); + } + } + + SendPathPacket(points); } 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; - Object::HandleAugmentation(this, in_augment, nullptr); - safe_delete_array(in_augment); + auto in_augment = new AugmentItem_Struct[sizeof(AugmentItem_Struct)]; + in_augment->container_slot = container_slot; + 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; + 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); + 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); } void Client::NPCHandinEventLog(Trade* t, NPC* n) { - Client* c = t->GetOwner()->CastToClient(); - - std::vector hi = {}; - std::vector ri = {}; - PlayerEvent::HandinMoney hm{}; - PlayerEvent::HandinMoney rm{}; - - if ( - c->EntityVariableExists("HANDIN_ITEMS") && - c->EntityVariableExists("HANDIN_MONEY") && - c->EntityVariableExists("RETURN_ITEMS") && - c->EntityVariableExists("RETURN_MONEY") - ) { - const std::string& handin_items = c->GetEntityVariable("HANDIN_ITEMS"); - const std::string& return_items = c->GetEntityVariable("RETURN_ITEMS"); - const std::string& handin_money = c->GetEntityVariable("HANDIN_MONEY"); - const std::string& return_money = c->GetEntityVariable("RETURN_MONEY"); - - // 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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - if (item_id != 0) { - const auto* item = database.GetItem(item_id); - - if (item) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - - // Handin Money - if (!handin_money.empty()) { - const auto hms = Strings::Split(handin_money, "|"); - - hm.copper = Strings::ToUnsignedInt(hms[0]); - hm.silver = Strings::ToUnsignedInt(hms[1]); - hm.gold = Strings::ToUnsignedInt(hms[2]); - hm.platinum = Strings::ToUnsignedInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - ri.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - ri.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - - // Return Money - if (!return_money.empty()) { - const auto rms = Strings::Split(return_money, "|"); - rm.copper = static_cast(Strings::ToUnsignedInt(rms[0])); - rm.silver = static_cast(Strings::ToUnsignedInt(rms[1])); - rm.gold = static_cast(Strings::ToUnsignedInt(rms[2])); - rm.platinum = static_cast(Strings::ToUnsignedInt(rms[3])); - } - - c->DeleteEntityVariable("HANDIN_ITEMS"); - c->DeleteEntityVariable("HANDIN_MONEY"); - c->DeleteEntityVariable("RETURN_ITEMS"); - c->DeleteEntityVariable("RETURN_MONEY"); - - const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; - - const bool event_has_data_to_record = ( - !hi.empty() || handed_in_money - ); - - if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { - auto e = PlayerEvent::HandinEvent{ - .npc_id = n->GetNPCTypeID(), - .npc_name = n->GetCleanName(), - .handin_items = hi, - .handin_money = hm, - .return_items = ri, - .return_money = rm, - .is_quest_handin = true - }; - - RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); - } - - return; - } - - uint8 item_count = 0; - - hm.platinum = t->pp; - hm.gold = t->gp; - hm.silver = t->sp; - hm.copper = t->cp; - - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { - if (c->GetInv().GetItem(i)) { - item_count++; - } - } - - hi.reserve(item_count); - - if (item_count > 0) { - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { - const EQ::ItemInstance* inst = c->GetInv().GetItem(i); - if (inst) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = inst->GetItem()->ID, - .item_name = inst->GetItem()->Name, - .charges = static_cast(inst->GetCharges()), - .attuned = inst->IsAttuned() - } - ); - - if (inst->IsClassBag()) { - for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { - inst = c->GetInv().GetItem(i, j); - if (inst) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = inst->GetItem()->ID, - .item_name = inst->GetItem()->Name, - .charges = static_cast(inst->GetCharges()), - .attuned = inst->IsAttuned() - } - ); - } - } - } - } - } - } - - const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; - - ri = hi; - rm = hm; - - const bool event_has_data_to_record = !hi.empty() || handed_in_money; - - if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { - auto e = PlayerEvent::HandinEvent{ - .npc_id = n->GetNPCTypeID(), - .npc_name = n->GetCleanName(), - .handin_items = hi, - .handin_money = hm, - .return_items = ri, - .return_money = rm, - .is_quest_handin = false - }; - - RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); - } + Client* c = t->GetOwner()->CastToClient(); + + std::vector hi = {}; + std::vector ri = {}; + PlayerEvent::HandinMoney hm{}; + PlayerEvent::HandinMoney rm{}; + + if ( + c->EntityVariableExists("HANDIN_ITEMS") && + c->EntityVariableExists("HANDIN_MONEY") && + c->EntityVariableExists("RETURN_ITEMS") && + c->EntityVariableExists("RETURN_MONEY") + ) { + const std::string& handin_items = c->GetEntityVariable("HANDIN_ITEMS"); + const std::string& return_items = c->GetEntityVariable("RETURN_ITEMS"); + const std::string& handin_money = c->GetEntityVariable("HANDIN_MONEY"); + const std::string& return_money = c->GetEntityVariable("RETURN_MONEY"); + + // 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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + if (item_id != 0) { + const auto* item = database.GetItem(item_id); + + if (item) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + const auto* item = database.GetItem(item_id); + + if (item) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(item_data[2]) ? true : false + } + ); + } + } + } + } + + // Handin Money + if (!handin_money.empty()) { + const auto hms = Strings::Split(handin_money, "|"); + + hm.copper = Strings::ToUnsignedInt(hms[0]); + hm.silver = Strings::ToUnsignedInt(hms[1]); + hm.gold = Strings::ToUnsignedInt(hms[2]); + hm.platinum = Strings::ToUnsignedInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + const auto* item = database.GetItem(item_id); + + if (item) { + ri.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + const auto* item = database.GetItem(item_id); + + if (item) { + ri.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(item_data[2]) ? true : false + } + ); + } + } + } + } + + // Return Money + if (!return_money.empty()) { + const auto rms = Strings::Split(return_money, "|"); + rm.copper = static_cast(Strings::ToUnsignedInt(rms[0])); + rm.silver = static_cast(Strings::ToUnsignedInt(rms[1])); + rm.gold = static_cast(Strings::ToUnsignedInt(rms[2])); + rm.platinum = static_cast(Strings::ToUnsignedInt(rms[3])); + } + + c->DeleteEntityVariable("HANDIN_ITEMS"); + c->DeleteEntityVariable("HANDIN_MONEY"); + c->DeleteEntityVariable("RETURN_ITEMS"); + c->DeleteEntityVariable("RETURN_MONEY"); + + const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; + + const bool event_has_data_to_record = ( + !hi.empty() || handed_in_money + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { + auto e = PlayerEvent::HandinEvent{ + .npc_id = n->GetNPCTypeID(), + .npc_name = n->GetCleanName(), + .handin_items = hi, + .handin_money = hm, + .return_items = ri, + .return_money = rm, + .is_quest_handin = true + }; + + RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); + } + + return; + } + + uint8 item_count = 0; + + hm.platinum = t->pp; + hm.gold = t->gp; + hm.silver = t->sp; + hm.copper = t->cp; + + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { + if (c->GetInv().GetItem(i)) { + item_count++; + } + } + + hi.reserve(item_count); + + if (item_count > 0) { + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { + const EQ::ItemInstance* inst = c->GetInv().GetItem(i); + if (inst) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .attuned = inst->IsAttuned() + } + ); + + if (inst->IsClassBag()) { + for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { + inst = c->GetInv().GetItem(i, j); + if (inst) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .attuned = inst->IsAttuned() + } + ); + } + } + } + } + } + } + + const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; + + ri = hi; + rm = hm; + + const bool event_has_data_to_record = !hi.empty() || handed_in_money; + + if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { + auto e = PlayerEvent::HandinEvent{ + .npc_id = n->GetNPCTypeID(), + .npc_name = n->GetCleanName(), + .handin_items = hi, + .handin_money = hm, + .return_items = ri, + .return_money = rm, + .is_quest_handin = false + }; + + RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); + } } void Client::ShowSpells(Client* c, ShowSpellType show_spell_type) { - std::string spell_string; - - switch (show_spell_type) { - case ShowSpellType::Disciplines: - spell_string = "Discipline"; - break; - case ShowSpellType::Spells: - spell_string = "Spell"; - break; - default: - return; - } - - std::string spell_table; - - // Headers - spell_table += DialogueWindow::TableRow( - fmt::format( - "{}{}{}", - DialogueWindow::TableCell("Slot"), - DialogueWindow::TableCell(spell_string), - DialogueWindow::TableCell("Spell ID") - ) - ); - - std::map m; - auto spell_count = 0; - - if (show_spell_type == ShowSpellType::Disciplines) { - for (auto index = 0; index < MAX_PP_DISCIPLINES; index++) { - if (IsValidSpell(m_pp.disciplines.values[index])) { - m[index] = static_cast(m_pp.disciplines.values[index]); - } - } - } else if (show_spell_type == ShowSpellType::Spells) { - for (auto index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { - if (IsValidSpell(m_pp.mem_spells[index])) { - m[index] = static_cast(m_pp.mem_spells[index]); - } - } - } - - for (const auto& s : m) { - spell_table += DialogueWindow::TableRow( - fmt::format( - "{}{}{}", - DialogueWindow::TableCell(std::to_string(s.first)), - DialogueWindow::TableCell(GetSpellName(s.second)), - DialogueWindow::TableCell(Strings::Commify(s.second)) - ) - ); - - spell_count++; - } - - if (!spell_count) { - c->Message( - Chat::White, - fmt::format( - "{} {} not have any {}s {}.", - c->GetTargetDescription(this, TargetDescriptionType::UCYou), - c == this ? "do" : "does", - Strings::ToLower(spell_string), - show_spell_type == ShowSpellType::Disciplines ? "learned" : "memorized" - ).c_str() - ); - return; - } - - if (spell_table.size() >= 4096) { - for (const auto& [index, spell_id] : m) { - c->Message( - Chat::White, - fmt::format( - "{}. {} ({})", - index, - GetSpellName(spell_id), - spell_id - ).c_str() - ); - } - return; - } - - spell_table = DialogueWindow::Table(spell_table); - - c->SendPopupToClient( - fmt::format( - "{}s for {}", - spell_string, - c->GetTargetDescription(this, TargetDescriptionType::UCSelf) - ).c_str(), - spell_table.c_str() - ); + std::string spell_string; + + switch (show_spell_type) { + case ShowSpellType::Disciplines: + spell_string = "Discipline"; + break; + case ShowSpellType::Spells: + spell_string = "Spell"; + break; + default: + return; + } + + std::string spell_table; + + // Headers + spell_table += DialogueWindow::TableRow( + fmt::format( + "{}{}{}", + DialogueWindow::TableCell("Slot"), + DialogueWindow::TableCell(spell_string), + DialogueWindow::TableCell("Spell ID") + ) + ); + + std::map m; + auto spell_count = 0; + + if (show_spell_type == ShowSpellType::Disciplines) { + for (auto index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (IsValidSpell(m_pp.disciplines.values[index])) { + m[index] = static_cast(m_pp.disciplines.values[index]); + } + } + } else if (show_spell_type == ShowSpellType::Spells) { + for (auto index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { + if (IsValidSpell(m_pp.mem_spells[index])) { + m[index] = static_cast(m_pp.mem_spells[index]); + } + } + } + + for (const auto& s : m) { + spell_table += DialogueWindow::TableRow( + fmt::format( + "{}{}{}", + DialogueWindow::TableCell(std::to_string(s.first)), + DialogueWindow::TableCell(GetSpellName(s.second)), + DialogueWindow::TableCell(Strings::Commify(s.second)) + ) + ); + + spell_count++; + } + + if (!spell_count) { + c->Message( + Chat::White, + fmt::format( + "{} {} not have any {}s {}.", + c->GetTargetDescription(this, TargetDescriptionType::UCYou), + c == this ? "do" : "does", + Strings::ToLower(spell_string), + show_spell_type == ShowSpellType::Disciplines ? "learned" : "memorized" + ).c_str() + ); + return; + } + + if (spell_table.size() >= 4096) { + for (const auto& [index, spell_id] : m) { + c->Message( + Chat::White, + fmt::format( + "{}. {} ({})", + index, + GetSpellName(spell_id), + spell_id + ).c_str() + ); + } + return; + } + + spell_table = DialogueWindow::Table(spell_table); + + c->SendPopupToClient( + fmt::format( + "{}s for {}", + spell_string, + c->GetTargetDescription(this, TargetDescriptionType::UCSelf) + ).c_str(), + spell_table.c_str() + ); } std::string GetZoneModeString(ZoneMode mode) { - switch (mode) { - case ZoneToSafeCoords: - return "ZoneToSafeCoords"; - case GMSummon: - return "GMSummon"; - case GMHiddenSummon: - return "GMHiddenSummon"; - case ZoneToBindPoint: - return "ZoneToBindPoint"; - case ZoneSolicited: - return "ZoneSolicited"; - case ZoneUnsolicited: - return "ZoneUnsolicited"; - case GateToBindPoint: - return "GateToBindPoint"; - case SummonPC: - return "SummonPC"; - case Rewind: - return "Rewind"; - case EvacToSafeCoords: - return "EvacToSafeCoords"; - default: - return "Unknown"; - } + switch (mode) { + case ZoneToSafeCoords: + return "ZoneToSafeCoords"; + case GMSummon: + return "GMSummon"; + case GMHiddenSummon: + return "GMHiddenSummon"; + case ZoneToBindPoint: + return "ZoneToBindPoint"; + case ZoneSolicited: + return "ZoneSolicited"; + case ZoneUnsolicited: + return "ZoneUnsolicited"; + case GateToBindPoint: + return "GateToBindPoint"; + case SummonPC: + return "SummonPC"; + case Rewind: + return "Rewind"; + case EvacToSafeCoords: + return "EvacToSafeCoords"; + default: + return "Unknown"; + } } void Client::ClearXTargets() { - if (!XTargettingAvailable()) { - return; - } + if (!XTargettingAvailable()) { + return; + } - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].ID) { - Mob* m = entity_list.GetMob(XTargets[i].ID); + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].ID) { + Mob* m = entity_list.GetMob(XTargets[i].ID); - if (m) { - RemoveXTarget(m, false); - } + if (m) { + RemoveXTarget(m, false); + } - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - XTargets[i].dirty = false; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + XTargets[i].dirty = false; - SendXTargetPacket(i, nullptr); - } - } + SendXTargetPacket(i, nullptr); + } + } } float Client::GetAAEXPModifier(uint32 zone_id, int16 instance_version) { - return database.GetAAEXPModifierByCharID( - CharacterID(), - zone_id, - instance_version - ); + return database.GetAAEXPModifierByCharID( + CharacterID(), + zone_id, + instance_version + ); } float Client::GetEXPModifier(uint32 zone_id, int16 instance_version) { - return database.GetEXPModifierByCharID( - CharacterID(), - zone_id, - instance_version - ); + return database.GetEXPModifierByCharID( + CharacterID(), + zone_id, + instance_version + ); } void Client::SetAAEXPModifier(uint32 zone_id, float aa_modifier, int16 instance_version) { - database.SetAAEXPModifierByCharID( - CharacterID(), - zone_id, - aa_modifier, - instance_version - ); + database.SetAAEXPModifierByCharID( + CharacterID(), + zone_id, + aa_modifier, + instance_version + ); - database.LoadCharacterEXPModifier(this); + database.LoadCharacterEXPModifier(this); } void Client::SetEXPModifier(uint32 zone_id, float exp_modifier, int16 instance_version) { - database.SetEXPModifierByCharID( - CharacterID(), - zone_id, - exp_modifier, - instance_version - ); + database.SetEXPModifierByCharID( + CharacterID(), + zone_id, + exp_modifier, + instance_version + ); - database.LoadCharacterEXPModifier(this); + database.LoadCharacterEXPModifier(this); } int Client::GetAAEXPPercentage() { - int scaled = static_cast(330.0f * static_cast(GetAAXP()) / GetRequiredAAExperience()); + int scaled = static_cast(330.0f * static_cast(GetAAXP()) / GetRequiredAAExperience()); - return static_cast(std::round(scaled * 100.0 / 330.0)); + return static_cast(std::round(scaled * 100.0 / 330.0)); } int Client::GetEXPPercentage() { - float norm = 0.0f; - uint32_t min = GetEXPForLevel(GetLevel()); - uint32_t max = GetEXPForLevel(GetLevel() + 1); + float norm = 0.0f; + uint32_t min = GetEXPForLevel(GetLevel()); + uint32_t max = GetEXPForLevel(GetLevel() + 1); - if (min != max) { - norm = static_cast(GetEXP() - min) / (max - min); - } + if (min != max) { + norm = static_cast(GetEXP() - min) / (max - min); + } - int scaled = static_cast(330.0f * norm); // scale and truncate + int scaled = static_cast(330.0f * norm); // scale and truncate - return static_cast(std::round(scaled * 100.0 / 330.0)); // unscaled pct + return static_cast(std::round(scaled * 100.0 / 330.0)); // unscaled pct } std::vector Client::GetRaidOrGroupOrSelf(bool clients_only) { - std::vector v; + std::vector v; - if (IsRaidGrouped()) { - Raid* r = GetRaid(); + if (IsRaidGrouped()) { + Raid* r = GetRaid(); - if (r) { - for (const auto& m : r->members) { - if (m.member && (!m.is_bot || !clients_only)) { - v.emplace_back(m.member); - } - } - } - } else if (IsGrouped()) { - Group* g = GetGroup(); + if (r) { + for (const auto& m : r->members) { + if (m.member && (!m.is_bot || !clients_only)) { + v.emplace_back(m.member); + } + } + } + } else if (IsGrouped()) { + Group* g = GetGroup(); - if (g) { - for (const auto& m : g->members) { - if (m && (m->IsClient() || !clients_only)) { - v.emplace_back(m); - } - } - } - } else { - v.emplace_back(this); - } + if (g) { + for (const auto& m : g->members) { + if (m && (m->IsClient() || !clients_only)) { + v.emplace_back(m); + } + } + } + } else { + v.emplace_back(this); + } - return v; + return v; } uint16 Client::GetSkill(EQ::skills::SkillType skill_id) const { - if (skill_id <= EQ::skills::HIGHEST_SKILL) { - return (itembonuses.skillmod[skill_id] > 0 ? (itembonuses.skillmodmax[skill_id] > 0 ? std::min( - m_pp.skills[skill_id] + itembonuses.skillmodmax[skill_id], - m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100 - ) : m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100) : m_pp.skills[skill_id]); - } - return 0; + if (skill_id <= EQ::skills::HIGHEST_SKILL) { + return (itembonuses.skillmod[skill_id] > 0 ? (itembonuses.skillmodmax[skill_id] > 0 ? std::min( + m_pp.skills[skill_id] + itembonuses.skillmodmax[skill_id], + m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100 + ) : m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100) : m_pp.skills[skill_id]); + } + return 0; } void Client::RemoveItemBySerialNumber(uint32 serial_number, uint32 quantity) { - EQ::ItemInstance *item = nullptr; + EQ::ItemInstance *item = nullptr; - uint32 removed_count = 0; + uint32 removed_count = 0; - const auto& slot_ids = GetInventorySlots(); + const auto& slot_ids = GetInventorySlots(); - for (const int16& slot_id : slot_ids) { - if (removed_count == quantity) { - break; - } + for (const int16& slot_id : slot_ids) { + if (removed_count == quantity) { + break; + } - item = GetInv().GetItem(slot_id); - if (item && item->GetSerialNumber() == serial_number) { - uint32 charges = item->IsStackable() ? item->GetCharges() : 0; - uint32 stack_size = std::max(charges, static_cast(1)); - if ((removed_count + stack_size) <= quantity) { - removed_count += stack_size; - DeleteItemInInventory(slot_id, charges, true); - } else { - uint32 amount_left = (quantity - removed_count); - if (amount_left > 0 && stack_size >= amount_left) { - removed_count += amount_left; - DeleteItemInInventory(slot_id, amount_left, true); - } - } - } - } + item = GetInv().GetItem(slot_id); + if (item && item->GetSerialNumber() == serial_number) { + uint32 charges = item->IsStackable() ? item->GetCharges() : 0; + uint32 stack_size = std::max(charges, static_cast(1)); + if ((removed_count + stack_size) <= quantity) { + removed_count += stack_size; + DeleteItemInInventory(slot_id, charges, true); + } else { + uint32 amount_left = (quantity - removed_count); + if (amount_left > 0 && stack_size >= amount_left) { + removed_count += amount_left; + DeleteItemInInventory(slot_id, amount_left, true); + } + } + } + } } void Client::AddMoneyToPPWithOverflow(uint64 copper, bool update_client) { - //I noticed in the ROF2 client that the client auto updates the currency values using overflow - //Therefore, I created this method to ensure that the db matches and clients don't see 10 pp 5 gp - //becoming 9pp 15 gold with the current AddMoneyToPP method. + //I noticed in the ROF2 client that the client auto updates the currency values using overflow + //Therefore, I created this method to ensure that the db matches and clients don't see 10 pp 5 gp + //becoming 9pp 15 gold with the current AddMoneyToPP method. - auto add_pp = copper / 1000; - auto add_gp = (copper - add_pp * 1000) / 100; - auto add_sp = (copper - add_pp * 1000 - add_gp * 100) / 10; - auto add_cp = copper - add_pp * 1000 - add_gp * 100 - add_sp * 10; + auto add_pp = copper / 1000; + auto add_gp = (copper - add_pp * 1000) / 100; + auto add_sp = (copper - add_pp * 1000 - add_gp * 100) / 10; + auto add_cp = copper - add_pp * 1000 - add_gp * 100 - add_sp * 10; - m_pp.copper += add_cp; - if (m_pp.copper >= 10) { - m_pp.silver += m_pp.copper / 10; - m_pp.copper = m_pp.copper % 10; - } + m_pp.copper += add_cp; + if (m_pp.copper >= 10) { + m_pp.silver += m_pp.copper / 10; + m_pp.copper = m_pp.copper % 10; + } - m_pp.silver += add_sp; - if (m_pp.silver >= 10) { - m_pp.gold += m_pp.silver / 10; - m_pp.silver = m_pp.silver % 10; - } + m_pp.silver += add_sp; + if (m_pp.silver >= 10) { + m_pp.gold += m_pp.silver / 10; + m_pp.silver = m_pp.silver % 10; + } - m_pp.gold += add_gp; - if (m_pp.gold >= 10) { - m_pp.platinum += m_pp.gold / 10; - m_pp.gold = m_pp.gold % 10; - } + m_pp.gold += add_gp; + if (m_pp.gold >= 10) { + m_pp.platinum += m_pp.gold / 10; + m_pp.gold = m_pp.gold % 10; + } - m_pp.platinum += add_pp; + m_pp.platinum += add_pp; - if (update_client) { - SendMoneyUpdate(); - } + if (update_client) { + SendMoneyUpdate(); + } - RecalcWeight(); - SaveCurrency(); + RecalcWeight(); + SaveCurrency(); - LogDebug("Client::AddMoneyToPPWithOverflow() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", - GetName(), - m_pp.platinum, - m_pp.gold, - m_pp.silver, - m_pp.copper - ); + LogDebug("Client::AddMoneyToPPWithOverflow() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", + GetName(), + m_pp.platinum, + m_pp.gold, + m_pp.silver, + m_pp.copper + ); } bool Client::TakeMoneyFromPPWithOverFlow(uint64 copper, bool update_client) { - int32 remove_pp = copper / 1000; - int32 remove_gp = (copper - remove_pp * 1000) / 100; - int32 remove_sp = (copper - remove_pp * 1000 - remove_gp * 100) / 10; - int32 remove_cp = copper - remove_pp * 1000 - remove_gp * 100 - remove_sp * 10; - - uint64 current_money = GetCarriedMoney(); - - if (copper > current_money) { - return false; //client does not have enough money on them - } - - m_pp.copper -= remove_cp; - if (m_pp.copper < 0) { - m_pp.silver -= 1; - m_pp.copper = m_pp.copper + 10; - if (m_pp.copper >= 10) { - m_pp.silver += m_pp.copper / 10; - m_pp.copper = m_pp.copper % 10; - } - } - - m_pp.silver -= remove_sp; - if (m_pp.silver < 0) { - m_pp.gold -= 1; - m_pp.silver = m_pp.silver + 10; - if (m_pp.silver >= 10) { - m_pp.gold += m_pp.silver / 10; - m_pp.silver = m_pp.silver % 10; - } - } - - m_pp.gold -= remove_gp; - if (m_pp.gold < 0) { - m_pp.platinum -= 1; - m_pp.gold = m_pp.gold + 10; - if (m_pp.gold >= 10) { - m_pp.platinum += m_pp.gold / 10; - m_pp.gold = m_pp.gold % 10; - } - } - - m_pp.platinum -= remove_pp; - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - RecalcWeight(); - return true; + int32 remove_pp = copper / 1000; + int32 remove_gp = (copper - remove_pp * 1000) / 100; + int32 remove_sp = (copper - remove_pp * 1000 - remove_gp * 100) / 10; + int32 remove_cp = copper - remove_pp * 1000 - remove_gp * 100 - remove_sp * 10; + + uint64 current_money = GetCarriedMoney(); + + if (copper > current_money) { + return false; //client does not have enough money on them + } + + m_pp.copper -= remove_cp; + if (m_pp.copper < 0) { + m_pp.silver -= 1; + m_pp.copper = m_pp.copper + 10; + if (m_pp.copper >= 10) { + m_pp.silver += m_pp.copper / 10; + m_pp.copper = m_pp.copper % 10; + } + } + + m_pp.silver -= remove_sp; + if (m_pp.silver < 0) { + m_pp.gold -= 1; + m_pp.silver = m_pp.silver + 10; + if (m_pp.silver >= 10) { + m_pp.gold += m_pp.silver / 10; + m_pp.silver = m_pp.silver % 10; + } + } + + m_pp.gold -= remove_gp; + if (m_pp.gold < 0) { + m_pp.platinum -= 1; + m_pp.gold = m_pp.gold + 10; + if (m_pp.gold >= 10) { + m_pp.platinum += m_pp.gold / 10; + m_pp.gold = m_pp.gold % 10; + } + } + + m_pp.platinum -= remove_pp; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + RecalcWeight(); + return true; } void Client::SendTopLevelInventory() { - EQ::ItemInstance* inst = nullptr; + EQ::ItemInstance* inst = nullptr; - static const int16 slots[][2] = { - { EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END }, - { EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END }, - { EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END } - }; + static const int16 slots[][2] = { + { EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END }, + { EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END }, + { EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END } + }; - const auto& inv = GetInv(); + const auto& inv = GetInv(); - const size_t slot_index_count = sizeof(slots) / sizeof(slots[0]); - for (int slot_index = 0; slot_index < slot_index_count; ++slot_index) { - for (int slot_id = slots[slot_index][0]; slot_id <= slots[slot_index][1]; ++slot_id) { - inst = inv.GetItem(slot_id); - if (inst) { - SendItemPacket(slot_id, inst, ItemPacketType::ItemPacketTrade); - } - } - } + const size_t slot_index_count = sizeof(slots) / sizeof(slots[0]); + for (int slot_index = 0; slot_index < slot_index_count; ++slot_index) { + for (int slot_id = slots[slot_index][0]; slot_id <= slots[slot_index][1]; ++slot_id) { + inst = inv.GetItem(slot_id); + if (inst) { + SendItemPacket(slot_id, inst, ItemPacketType::ItemPacketTrade); + } + } + } } void Client::CheckSendBulkNpcPositions() @@ -13006,8 +13083,8 @@ void Client::CheckSendBulkNpcPositions() update_range ); - m_last_position_before_bulk_update = GetPosition(); - } + m_last_position_before_bulk_update = GetPosition(); + } } @@ -13016,123 +13093,123 @@ const uint16 scan_npc_aggro_timer_moving = RuleI(Aggro, ClientAggroCheckMovingIn void Client::CheckClientToNpcAggroTimer() { - LogAggroDetail( - "ClientUpdate [{}] {}moving, scan timer [{}]", - GetCleanName(), - IsMoving() ? "" : "NOT ", - m_client_npc_aggro_scan_timer.GetRemainingTime() - ); - - if (IsMoving()) { - if (m_client_npc_aggro_scan_timer.GetRemainingTime() > scan_npc_aggro_timer_moving) { - LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName()); - m_client_npc_aggro_scan_timer.Disable(); - m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_moving); - m_client_npc_aggro_scan_timer.Trigger(); - } - } - else if (m_client_npc_aggro_scan_timer.GetDuration() == scan_npc_aggro_timer_moving) { - LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName()); - m_client_npc_aggro_scan_timer.Disable(); - m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_idle); - } + LogAggroDetail( + "ClientUpdate [{}] {}moving, scan timer [{}]", + GetCleanName(), + IsMoving() ? "" : "NOT ", + m_client_npc_aggro_scan_timer.GetRemainingTime() + ); + + if (IsMoving()) { + if (m_client_npc_aggro_scan_timer.GetRemainingTime() > scan_npc_aggro_timer_moving) { + LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName()); + m_client_npc_aggro_scan_timer.Disable(); + m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_moving); + m_client_npc_aggro_scan_timer.Trigger(); + } + } + else if (m_client_npc_aggro_scan_timer.GetDuration() == scan_npc_aggro_timer_moving) { + LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName()); + m_client_npc_aggro_scan_timer.Disable(); + m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_idle); + } } void Client::ClientToNpcAggroProcess() { - if (zone->CanDoCombat() && !GetFeigned() && m_client_npc_aggro_scan_timer.Check()) { - int npc_scan_count = 0; - for (auto &close_mob: GetCloseMobList()) { - Mob *mob = close_mob.second; - if (!mob) { - continue; - } + if (zone->CanDoCombat() && !GetFeigned() && m_client_npc_aggro_scan_timer.Check()) { + int npc_scan_count = 0; + for (auto &close_mob: GetCloseMobList()) { + Mob *mob = close_mob.second; + if (!mob) { + continue; + } - if (mob->IsClient()) { - continue; - } + if (mob->IsClient()) { + continue; + } - if (mob->CheckWillAggro(this) && !mob->CheckAggro(this)) { - mob->AddToHateList(this, 25); - } + if (mob->CheckWillAggro(this) && !mob->CheckAggro(this)) { + mob->AddToHateList(this, 25); + } - npc_scan_count++; - } - LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count); - } + npc_scan_count++; + } + LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count); + } } const std::vector& Client::GetInventorySlots() { - static const std::vector> slots = { - {EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END}, - {EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END}, - {EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END}, - {EQ::invslot::BANK_BEGIN, EQ::invslot::BANK_END}, - {EQ::invbag::BANK_BAGS_BEGIN, EQ::invbag::BANK_BAGS_END}, - {EQ::invslot::SHARED_BANK_BEGIN, EQ::invslot::SHARED_BANK_END}, - {EQ::invbag::SHARED_BANK_BAGS_BEGIN, EQ::invbag::SHARED_BANK_BAGS_END}, - }; - - static std::vector slot_ids; - - if (slot_ids.empty()) { - for (const auto &[begin, end]: slots) { - for (int16 slot_id = begin; slot_id <= end; ++slot_id) { - slot_ids.emplace_back(slot_id); - } - } - } + static const std::vector> slots = { + {EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END}, + {EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END}, + {EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END}, + {EQ::invslot::BANK_BEGIN, EQ::invslot::BANK_END}, + {EQ::invbag::BANK_BAGS_BEGIN, EQ::invbag::BANK_BAGS_END}, + {EQ::invslot::SHARED_BANK_BEGIN, EQ::invslot::SHARED_BANK_END}, + {EQ::invbag::SHARED_BANK_BAGS_BEGIN, EQ::invbag::SHARED_BANK_BAGS_END}, + }; + + static std::vector slot_ids; - return slot_ids; + if (slot_ids.empty()) { + for (const auto &[begin, end]: slots) { + for (int16 slot_id = begin; slot_id <= end; ++slot_id) { + slot_ids.emplace_back(slot_id); + } + } + } + + return slot_ids; } void Client::ShowZoneShardMenu() { - auto z = GetZone(GetZoneID()); - if (z && !z->shard_at_player_count) { - return; - } - - auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, GetZoneID()); - LogZoning("Zone sharding results count [{}]", results.size()); - - if (results.empty()) { - Message(Chat::White, "No zone shards found."); - return; - } - - if (!results.empty()) { - Message(Chat::White, "Available Zone Shards:"); - } - - int number = 1; - for (auto &e: results) { - std::string teleport_link = Saylink::Silent( - fmt::format("#zoneshard {} {}", e.zone_id, (e.instance_id == 0 ? -1 : e.instance_id)), - "Teleport" - ); - - std::string yours; - if (e.zone_id == GetZoneID() && e.instance_id == GetInstanceID()) { - teleport_link = "Teleport"; - yours = " (Yours)"; - } - - Message( - Chat::White, fmt::format( - " --> [{}] #{} {} ({}) [{}/{}] players {}", - teleport_link, - number, - z->long_name, - e.instance_id, - e.player_count, - z->shard_at_player_count, - yours - ).c_str() - ); - number++; - } + auto z = GetZone(GetZoneID()); + if (z && !z->shard_at_player_count) { + return; + } + + auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, GetZoneID()); + LogZoning("Zone sharding results count [{}]", results.size()); + + if (results.empty()) { + Message(Chat::White, "No zone shards found."); + return; + } + + if (!results.empty()) { + Message(Chat::White, "Available Zone Shards:"); + } + + int number = 1; + for (auto &e: results) { + std::string teleport_link = Saylink::Silent( + fmt::format("#zoneshard {} {}", e.zone_id, (e.instance_id == 0 ? -1 : e.instance_id)), + "Teleport" + ); + + std::string yours; + if (e.zone_id == GetZoneID() && e.instance_id == GetInstanceID()) { + teleport_link = "Teleport"; + yours = " (Yours)"; + } + + Message( + Chat::White, fmt::format( + " --> [{}] #{} {} ({}) [{}/{}] players {}", + teleport_link, + number, + z->long_name, + e.instance_id, + e.player_count, + z->shard_at_player_count, + yours + ).c_str() + ); + number++; + } } void Client::SetAAEXPPercentage(uint8 percentage) diff --git a/zone/client.h b/zone/client.h index 146cabd34d..d7a61f6622 100644 --- a/zone/client.h +++ b/zone/client.h @@ -307,6 +307,11 @@ class Client : public Mob void KeyRingAdd(uint32 item_id); bool KeyRingCheck(uint32 item_id); void KeyRingList(); + bool IsPetNameChangeAllowed(); + void GrantPetNameChange(); + void ClearPetNameChange(); + void InvokeChangePetName(bool immediate = true); + bool ChangePetName(std::string new_name); bool IsClient() const override { return true; } bool IsOfClientBot() const override { return true; } bool IsOfClientBotMerc() const override { return true; } @@ -2267,6 +2272,7 @@ class Client : public Mob const std::string &GetMailKeyFull() const; const std::string &GetMailKey() const; void ShowZoneShardMenu(); + void Handle_OP_ChangePetName(const EQApplicationPacket *app); }; #endif diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 066cb49cf5..77fcf8e602 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -66,6 +66,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/repositories/character_corpses_repository.h" #include "../common/repositories/guild_tributes_repository.h" #include "../common/repositories/buyer_buy_lines_repository.h" +#include "../common/repositories/character_pet_name_repository.h" #include "../common/events/player_event_logs.h" #include "../common/repositories/character_stats_record_repository.h" @@ -160,6 +161,7 @@ void MapOpcodes() ConnectedOpcodes[OP_CancelTrade] = &Client::Handle_OP_CancelTrade; ConnectedOpcodes[OP_CastSpell] = &Client::Handle_OP_CastSpell; ConnectedOpcodes[OP_ChannelMessage] = &Client::Handle_OP_ChannelMessage; + ConnectedOpcodes[OP_ChangePetName] = &Client::Handle_OP_ChangePetName; ConnectedOpcodes[OP_ClearBlockedBuffs] = &Client::Handle_OP_ClearBlockedBuffs; ConnectedOpcodes[OP_ClearNPCMarks] = &Client::Handle_OP_ClearNPCMarks; ConnectedOpcodes[OP_ClearSurname] = &Client::Handle_OP_ClearSurname; @@ -820,6 +822,10 @@ void Client::CompleteConnect() CharacterID() ) ); + + if (IsPetNameChangeAllowed()) { + InvokeChangePetName(false); + } } if(ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { @@ -4568,6 +4574,29 @@ void Client::Handle_OP_ChannelMessage(const EQApplicationPacket *app) return; } +void Client::Handle_OP_ChangePetName(const EQApplicationPacket *app) { + if (app->size != sizeof(ChangePetName_Struct)) { + LogError("Got OP_ChangePetName of incorrect size. Expected [{}], got [{}].", sizeof(ChangePetName_Struct), app->size); + return; + } + + auto payload = (ChangePetName_Struct*)app->pBuffer; + + if (!IsPetNameChangeAllowed()) { + payload->response_code = ChangePetNameResponse::NotEligible; + QueuePacket(app); + return; + } + + if (ChangePetName(payload->new_pet_name)) { + payload->response_code = ChangePetNameResponse::Accepted; + } else { + payload->response_code = ChangePetNameResponse::Denied; // not actually needed but included here for completeness + } + + QueuePacket(app); +} + void Client::Handle_OP_ClearBlockedBuffs(const EQApplicationPacket *app) { if (!RuleB(Spells, EnableBlockedBuffs)) diff --git a/zone/lua_client.cpp b/zone/lua_client.cpp index 4be953bb40..83e2a3fb3e 100644 --- a/zone/lua_client.cpp +++ b/zone/lua_client.cpp @@ -3458,6 +3458,12 @@ void Lua_Client::ShowZoneShardMenu() self->ShowZoneShardMenu(); } +void Lua_Client::GrantPetNameChange() +{ + Lua_Safe_Call_Void(); + self->GrantPetNameChange(); +} + void Lua_Client::SetAAEXPPercentage(uint8 percentage) { Lua_Safe_Call_Void(); @@ -3568,6 +3574,7 @@ luabind::scope lua_register_client() { .def("CanHaveSkill", (bool(Lua_Client::*)(int))&Lua_Client::CanHaveSkill) .def("CashReward", &Lua_Client::CashReward) .def("ChangeLastName", (void(Lua_Client::*)(std::string))&Lua_Client::ChangeLastName) + .def("GrantPetNameChange", &Lua_Client::GrantPetNameChange) .def("CharacterID", (uint32(Lua_Client::*)(void))&Lua_Client::CharacterID) .def("CheckIncreaseSkill", (void(Lua_Client::*)(int,Lua_Mob))&Lua_Client::CheckIncreaseSkill) .def("CheckIncreaseSkill", (void(Lua_Client::*)(int,Lua_Mob,int))&Lua_Client::CheckIncreaseSkill) diff --git a/zone/lua_client.h b/zone/lua_client.h index 082504fc06..ed02e65927 100644 --- a/zone/lua_client.h +++ b/zone/lua_client.h @@ -597,6 +597,7 @@ class Lua_Client : public Lua_Mob bool ReloadDataBuckets(); void ShowZoneShardMenu(); + void GrantPetNameChange(); Lua_Expedition CreateExpedition(luabind::object expedition_info); Lua_Expedition CreateExpedition(std::string zone_name, uint32 version, uint32 duration, std::string expedition_name, uint32 min_players, uint32 max_players); diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index c4a66ec2d3..fcd14c502b 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -3229,6 +3229,11 @@ perl::array Perl_Client_GetInventorySlots(Client* self) return result; } +void Perl_Client_GrantPetNameChange(Client* self) +{ + self->GrantPetNameChange(); +} + void Perl_Client_SetAAEXPPercentage(Client* self, uint8 percentage) { self->SetAAEXPPercentage(percentage); @@ -3335,6 +3340,7 @@ void perl_register_client() package.add("CanHaveSkill", &Perl_Client_CanHaveSkill); package.add("CashReward", &Perl_Client_CashReward); package.add("ChangeLastName", &Perl_Client_ChangeLastName); + package.add("ChangePetName", &Perl_Client_GrantPetNameChange); package.add("CharacterID", &Perl_Client_CharacterID); package.add("CheckIncreaseSkill", (bool(*)(Client*, int))&Perl_Client_CheckIncreaseSkill); package.add("CheckIncreaseSkill", (bool(*)(Client*, int, int))&Perl_Client_CheckIncreaseSkill); diff --git a/zone/pets.cpp b/zone/pets.cpp index 08fc523c15..84f3baee8e 100644 --- a/zone/pets.cpp +++ b/zone/pets.cpp @@ -22,6 +22,7 @@ #include "../common/repositories/pets_repository.h" #include "../common/repositories/pets_beastlord_data_repository.h" +#include "../common/repositories/character_pet_name_repository.h" #include "entity.h" #include "client.h" @@ -164,6 +165,13 @@ void Mob::MakePoweredPet(uint16 spell_id, const char* pettype, int16 petpower, // 4 - Keep DB name // 5 - `s ward + if (IsClient() && !petname) { + const auto vanity_name = CharacterPetNameRepository::FindOne(database, CastToClient()->CharacterID()); + + if (!vanity_name.name.empty()) { + petname = vanity_name.name.c_str(); + } + } if (petname != nullptr) { // Name was provided, use it. From 44343a71b3111d44e31d2b993a79b44057cc4f48 Mon Sep 17 00:00:00 2001 From: Catapultam Habeo Date: Tue, 21 Jan 2025 22:27:31 -0600 Subject: [PATCH 2/7] I blame git for this one --- common/database_schema.h | 818 +++++++++++++++++++-------------------- 1 file changed, 409 insertions(+), 409 deletions(-) diff --git a/common/database_schema.h b/common/database_schema.h index 1b0fbb7410..55aace9540 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -26,427 +26,427 @@ namespace DatabaseSchema { - /** - * Character-specific tables - * - * Does not included related meta-data tables such as 'guilds', 'accounts' - * @return - */ - static std::map GetCharacterTables() - { - return { - {"adventure_stats", "player_id"}, - {"char_recipe_list", "char_id"}, - {"character_activities", "charid"}, - {"character_alt_currency", "char_id"}, - {"character_alternate_abilities", "id"}, - {"character_auras", "id"}, - {"character_bandolier", "id"}, - {"character_bind", "id"}, - {"character_buffs", "character_id"}, - {"character_corpses", "id"}, - {"character_currency", "id"}, - {"character_data", "id"}, - {"character_disciplines", "id"}, - {"character_enabledtasks", "charid"}, - {"character_expedition_lockouts", "character_id"}, - {"character_exp_modifiers", "character_id"}, + /** + * Character-specific tables + * + * Does not included related meta-data tables such as 'guilds', 'accounts' + * @return + */ + static std::map GetCharacterTables() + { + return { + {"adventure_stats", "player_id"}, + {"char_recipe_list", "char_id"}, + {"character_activities", "charid"}, + {"character_alt_currency", "char_id"}, + {"character_alternate_abilities", "id"}, + {"character_auras", "id"}, + {"character_bandolier", "id"}, + {"character_bind", "id"}, + {"character_buffs", "character_id"}, + {"character_corpses", "id"}, + {"character_currency", "id"}, + {"character_data", "id"}, + {"character_disciplines", "id"}, + {"character_enabledtasks", "charid"}, + {"character_expedition_lockouts", "character_id"}, + {"character_exp_modifiers", "character_id"}, {"character_evolving_items", "character_id"}, - {"character_inspect_messages", "id"}, - {"character_instance_safereturns", "character_id"}, - {"character_item_recast", "id"}, - {"character_languages", "id"}, - {"character_leadership_abilities", "id"}, - {"character_material", "id"}, - {"character_memmed_spells", "id"}, - {"character_parcels", "char_id"}, - {"character_parcels_containers", "id"}, - {"character_pet_buffs", "char_id"}, - {"character_pet_info", "char_id"}, - {"character_pet_inventory", "char_id"}, - {"character_pet_name", "character_id"}, - {"character_peqzone_flags", "id"}, - {"character_potionbelt", "id"}, - {"character_skills", "id"}, - {"character_spells", "id"}, - {"character_stats_record", "character_id"}, - {"character_task_timers", "character_id"}, - {"character_tasks", "charid"}, - {"character_tribute", "character_id"}, - {"completed_tasks", "charid"}, - {"data_buckets", "character_id"}, - {"faction_values", "char_id"}, - {"friends", "charid"}, - {"guild_members", "char_id"}, - {"guilds", "id"}, - {"instance_list_player", "id"}, - {"inventory", "charid"}, - {"inventory_snapshots", "charid"}, - {"keyring", "char_id"}, - {"mail", "charid"}, - {"player_titlesets", "char_id"}, - {"quest_globals", "charid"}, - {"timers", "char_id"}, - {"trader", "char_id"}, - {"zone_flags", "charID"} - }; - } + {"character_inspect_messages", "id"}, + {"character_instance_safereturns", "character_id"}, + {"character_item_recast", "id"}, + {"character_languages", "id"}, + {"character_leadership_abilities", "id"}, + {"character_material", "id"}, + {"character_memmed_spells", "id"}, + {"character_parcels", "char_id"}, + {"character_parcels_containers", "id"}, + {"character_pet_buffs", "char_id"}, + {"character_pet_info", "char_id"}, + {"character_pet_inventory", "char_id"}, + {"character_pet_name", "character_id"}, + {"character_peqzone_flags", "id"}, + {"character_potionbelt", "id"}, + {"character_skills", "id"}, + {"character_spells", "id"}, + {"character_stats_record", "character_id"}, + {"character_task_timers", "character_id"}, + {"character_tasks", "charid"}, + {"character_tribute", "character_id"}, + {"completed_tasks", "charid"}, + {"data_buckets", "character_id"}, + {"faction_values", "char_id"}, + {"friends", "charid"}, + {"guild_members", "char_id"}, + {"guilds", "id"}, + {"instance_list_player", "id"}, + {"inventory", "charid"}, + {"inventory_snapshots", "charid"}, + {"keyring", "char_id"}, + {"mail", "charid"}, + {"player_titlesets", "char_id"}, + {"quest_globals", "charid"}, + {"timers", "char_id"}, + {"trader", "char_id"}, + {"zone_flags", "charID"} + }; + } - /** - * @description Gets all player and meta-data tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetPlayerTables() - { - return { - "account", - "account_ip", - "account_flags", - "account_rewards", - "adventure_details", - "adventure_stats", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "char_recipe_list", - "character_activities", - "character_alt_currency", - "character_alternate_abilities", - "character_auras", - "character_bandolier", - "character_bind", - "character_buffs", - "character_corpse_items", - "character_corpses", - "character_currency", - "character_data", - "character_disciplines", - "character_enabledtasks", - "character_expedition_lockouts", - "character_exp_modifiers", + /** + * @description Gets all player and meta-data tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetPlayerTables() + { + return { + "account", + "account_ip", + "account_flags", + "account_rewards", + "adventure_details", + "adventure_stats", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "char_recipe_list", + "character_activities", + "character_alt_currency", + "character_alternate_abilities", + "character_auras", + "character_bandolier", + "character_bind", + "character_buffs", + "character_corpse_items", + "character_corpses", + "character_currency", + "character_data", + "character_disciplines", + "character_enabledtasks", + "character_expedition_lockouts", + "character_exp_modifiers", "character_evolving_items", - "character_inspect_messages", - "character_instance_safereturns", - "character_item_recast", - "character_languages", - "character_leadership_abilities", - "character_material", - "character_memmed_spells", - "character_parcels", - "character_parcels_containers", - "character_pet_buffs", - "character_pet_info", - "character_pet_inventory", - "character_peqzone_flags", - "character_potionbelt", - "character_skills", - "character_spells", - "character_stats_record", - "character_task_timers", - "character_tasks", - "character_tribute", - "completed_tasks", - "data_buckets", - "discovered_items", - "faction_values", - "friends", - "guild_bank", - "guild_members", - "guild_permissions", - "guild_ranks", - "guild_relations", - "guild_tributes", - "guilds", - "instance_list_player", - "inventory", - "inventory_snapshots", - "keyring", - "mail", - "petitions", - "player_titlesets", - "quest_globals", - "sharedbank", - "spell_buckets", - "spell_globals", - "timers", - "trader", - "trader_audit", - "zone_flags" - }; - } + "character_inspect_messages", + "character_instance_safereturns", + "character_item_recast", + "character_languages", + "character_leadership_abilities", + "character_material", + "character_memmed_spells", + "character_parcels", + "character_parcels_containers", + "character_pet_buffs", + "character_pet_info", + "character_pet_inventory", + "character_peqzone_flags", + "character_potionbelt", + "character_skills", + "character_spells", + "character_stats_record", + "character_task_timers", + "character_tasks", + "character_tribute", + "completed_tasks", + "data_buckets", + "discovered_items", + "faction_values", + "friends", + "guild_bank", + "guild_members", + "guild_permissions", + "guild_ranks", + "guild_relations", + "guild_tributes", + "guilds", + "instance_list_player", + "inventory", + "inventory_snapshots", + "keyring", + "mail", + "petitions", + "player_titlesets", + "quest_globals", + "sharedbank", + "spell_buckets", + "spell_globals", + "timers", + "trader", + "trader_audit", + "zone_flags" + }; + } - /** - * Gets content tables - * - * @return - */ - static std::vector GetContentTables() - { - return { - "aa_ability", - "aa_rank_effects", - "aa_rank_prereqs", - "aa_ranks", - "adventure_template", - "adventure_template_entry", - "adventure_template_entry_flavor", - "alternate_currency", - "auras", - "base_data", - "blocked_spells", - "books", - "char_create_combinations", - "char_create_point_allocations", - "damageshieldtypes", - "doors", - "dynamic_zone_templates", - "faction_association", - "faction_base_data", - "faction_list", - "faction_list_mod", - "fishing", - "forage", - "global_loot", - "graveyard", - "grid", - "grid_entries", - "ground_spawns", - "horses", - "items", + /** + * Gets content tables + * + * @return + */ + static std::vector GetContentTables() + { + return { + "aa_ability", + "aa_rank_effects", + "aa_rank_prereqs", + "aa_ranks", + "adventure_template", + "adventure_template_entry", + "adventure_template_entry_flavor", + "alternate_currency", + "auras", + "base_data", + "blocked_spells", + "books", + "char_create_combinations", + "char_create_point_allocations", + "damageshieldtypes", + "doors", + "dynamic_zone_templates", + "faction_association", + "faction_base_data", + "faction_list", + "faction_list_mod", + "fishing", + "forage", + "global_loot", + "graveyard", + "grid", + "grid_entries", + "ground_spawns", + "horses", + "items", "items_evolving_details", - "ldon_trap_entries", - "ldon_trap_templates", - "lootdrop", - "lootdrop_entries", - "loottable", - "loottable_entries", - "merchantlist", - "npc_emotes", - "npc_faction", - "npc_faction_entries", - "npc_scale_global_base", - "npc_spells", - "npc_spells_effects", - "npc_spells_effects_entries", - "npc_spells_entries", - "npc_types", - "npc_types_tint", - "object", - "pets", - "pets_beastlord_data", - "pets_equipmentset", - "pets_equipmentset_entries", - "skill_caps", - "spawn2", - "spawn_conditions", - "spawnentry", - "spawngroup", - "spells_new", - "start_zones", - "starting_items", - "task_activities", - "tasks", - "tasksets", - "tradeskill_recipe", - "tradeskill_recipe_entries", - "traps", - "tribute_levels", - "tributes", - "veteran_reward_templates", - "zone", - "zone_points", - }; - } + "ldon_trap_entries", + "ldon_trap_templates", + "lootdrop", + "lootdrop_entries", + "loottable", + "loottable_entries", + "merchantlist", + "npc_emotes", + "npc_faction", + "npc_faction_entries", + "npc_scale_global_base", + "npc_spells", + "npc_spells_effects", + "npc_spells_effects_entries", + "npc_spells_entries", + "npc_types", + "npc_types_tint", + "object", + "pets", + "pets_beastlord_data", + "pets_equipmentset", + "pets_equipmentset_entries", + "skill_caps", + "spawn2", + "spawn_conditions", + "spawnentry", + "spawngroup", + "spells_new", + "start_zones", + "starting_items", + "task_activities", + "tasks", + "tasksets", + "tradeskill_recipe", + "tradeskill_recipe_entries", + "traps", + "tribute_levels", + "tributes", + "veteran_reward_templates", + "zone", + "zone_points", + }; + } - /** - * Gets server tables - * - * @return - */ - static std::vector GetServerTables() - { - return { - "chatchannels", - "chatchannel_reserved_names", - "command_settings", - "command_subsettings", - "content_flags", - "db_str", - "eqtime", - "launcher", - "launcher_zones", - "spawn_condition_values", - "spawn_events", - "level_exp_mods", - "logsys_categories", - "name_filter", - "perl_event_export_settings", - "profanity_list", - "rule_sets", - "titles", - "rule_values", - "variables", - }; - } + /** + * Gets server tables + * + * @return + */ + static std::vector GetServerTables() + { + return { + "chatchannels", + "chatchannel_reserved_names", + "command_settings", + "command_subsettings", + "content_flags", + "db_str", + "eqtime", + "launcher", + "launcher_zones", + "spawn_condition_values", + "spawn_events", + "level_exp_mods", + "logsys_categories", + "name_filter", + "perl_event_export_settings", + "profanity_list", + "rule_sets", + "titles", + "rule_values", + "variables", + }; + } - /** - * Gets QueryServer tables - * - * @return - */ - static std::vector GetQueryServerTables() - { - return { - "qs_merchant_transaction_record", - "qs_merchant_transaction_record_entries", - "qs_player_aa_rate_hourly", - "qs_player_delete_record", - "qs_player_delete_record_entries", - "qs_player_events", - "qs_player_handin_record", - "qs_player_handin_record_entries", - "qs_player_move_record", - "qs_player_move_record_entries", - "qs_player_npc_kill_record", - "qs_player_npc_kill_record_entries", - "qs_player_speech", - "qs_player_trade_record", - "qs_player_trade_record_entries", - }; - } + /** + * Gets QueryServer tables + * + * @return + */ + static std::vector GetQueryServerTables() + { + return { + "qs_merchant_transaction_record", + "qs_merchant_transaction_record_entries", + "qs_player_aa_rate_hourly", + "qs_player_delete_record", + "qs_player_delete_record_entries", + "qs_player_events", + "qs_player_handin_record", + "qs_player_handin_record_entries", + "qs_player_move_record", + "qs_player_move_record_entries", + "qs_player_npc_kill_record", + "qs_player_npc_kill_record_entries", + "qs_player_speech", + "qs_player_trade_record", + "qs_player_trade_record_entries", + }; + } - /** - * Gets state tables - * Tables that keep track of server state - * - * @return - */ - static std::vector GetStateTables() - { - return { - "adventure_members", - "banned_ips", - "bug_reports", - "bugs", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "completed_shared_task_activity_state", - "completed_shared_task_members", - "completed_shared_tasks", - "discord_webhooks", - "dynamic_zone_members", - "dynamic_zones", - "expedition_lockouts", - "expeditions", - "gm_ips", - "group_id", - "group_leaders", - "instance_list", - "ip_exemptions", - "lfguild", - "merc_buffs", - "merchantlist_temp", - "mercs", - "object_contents", - "raid_details", - "raid_leaders", - "raid_members", - "reports", - "respawn_times", - "saylink", - "server_scheduled_events", - "spawn2_disabled", - "player_event_log_settings", - "player_event_logs", - "shared_task_activity_state", - "shared_task_dynamic_zones", - "shared_task_members", - "shared_tasks", - }; - } + /** + * Gets state tables + * Tables that keep track of server state + * + * @return + */ + static std::vector GetStateTables() + { + return { + "adventure_members", + "banned_ips", + "bug_reports", + "bugs", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "completed_shared_task_activity_state", + "completed_shared_task_members", + "completed_shared_tasks", + "discord_webhooks", + "dynamic_zone_members", + "dynamic_zones", + "expedition_lockouts", + "expeditions", + "gm_ips", + "group_id", + "group_leaders", + "instance_list", + "ip_exemptions", + "lfguild", + "merc_buffs", + "merchantlist_temp", + "mercs", + "object_contents", + "raid_details", + "raid_leaders", + "raid_members", + "reports", + "respawn_times", + "saylink", + "server_scheduled_events", + "spawn2_disabled", + "player_event_log_settings", + "player_event_logs", + "shared_task_activity_state", + "shared_task_dynamic_zones", + "shared_task_members", + "shared_tasks", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetLoginTables() - { - return { - "login_accounts", - "login_api_tokens", - "login_server_admins", - "login_server_list_types", - "login_world_servers", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetLoginTables() + { + return { + "login_accounts", + "login_api_tokens", + "login_server_admins", + "login_server_list_types", + "login_world_servers", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetVersionTables() - { - return { - "db_version", - "inventory_versions", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetVersionTables() + { + return { + "db_version", + "inventory_versions", + }; + } - /** - * @description Gets all player bot tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetBotTables() - { - return { - "bot_buffs", - "bot_command_settings", - "bot_create_combinations", - "bot_data", - "bot_heal_rotation_members", - "bot_heal_rotation_targets", - "bot_heal_rotations", - "bot_inspect_messages", - "bot_inventories", - "bot_owner_options", - "bot_pet_buffs", - "bot_pet_inventories", - "bot_pets", - "bot_spell_casting_chances", - "bot_spell_settings", - "bot_spells_entries", - "bot_stances", - "bot_timers" - }; - } + /** + * @description Gets all player bot tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetBotTables() + { + return { + "bot_buffs", + "bot_command_settings", + "bot_create_combinations", + "bot_data", + "bot_heal_rotation_members", + "bot_heal_rotation_targets", + "bot_heal_rotations", + "bot_inspect_messages", + "bot_inventories", + "bot_owner_options", + "bot_pet_buffs", + "bot_pet_inventories", + "bot_pets", + "bot_spell_casting_chances", + "bot_spell_settings", + "bot_spells_entries", + "bot_stances", + "bot_timers" + }; + } - static std::vector GetMercTables() - { - return { - "merc_armorinfo", - "merc_inventory", - "merc_merchant_entries", - "merc_merchant_template_entries", - "merc_merchant_templates", - "merc_name_types", - "merc_npc_types", - "merc_spell_list_entries", - "merc_spell_lists", - "merc_stance_entries", - "merc_stats", - "merc_subtypes", - "merc_templates", - "merc_types", - "merc_weaponinfo" - }; - } + static std::vector GetMercTables() + { + return { + "merc_armorinfo", + "merc_inventory", + "merc_merchant_entries", + "merc_merchant_template_entries", + "merc_merchant_templates", + "merc_name_types", + "merc_npc_types", + "merc_spell_list_entries", + "merc_spell_lists", + "merc_stance_entries", + "merc_stats", + "merc_subtypes", + "merc_templates", + "merc_types", + "merc_weaponinfo" + }; + } } From c277e7d729d79fa74d3bbdee3a868967e8f0b7c8 Mon Sep 17 00:00:00 2001 From: Catapultam Habeo Date: Tue, 21 Jan 2025 22:30:30 -0600 Subject: [PATCH 3/7] last typo --- zone/perl_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index fcd14c502b..3cf1ad0d75 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -3340,7 +3340,7 @@ void perl_register_client() package.add("CanHaveSkill", &Perl_Client_CanHaveSkill); package.add("CashReward", &Perl_Client_CashReward); package.add("ChangeLastName", &Perl_Client_ChangeLastName); - package.add("ChangePetName", &Perl_Client_GrantPetNameChange); + package.add("GrantPetNameChange", &Perl_Client_GrantPetNameChange); package.add("CharacterID", &Perl_Client_CharacterID); package.add("CheckIncreaseSkill", (bool(*)(Client*, int))&Perl_Client_CheckIncreaseSkill); package.add("CheckIncreaseSkill", (bool(*)(Client*, int, int))&Perl_Client_CheckIncreaseSkill); From be33421502368554db7b56953b0d8764c9d49771 Mon Sep 17 00:00:00 2001 From: Catapultam Habeo Date: Tue, 21 Jan 2025 22:31:57 -0600 Subject: [PATCH 4/7] spaces --- common/database_schema.h | 824 +++++++++++++++++++-------------------- 1 file changed, 412 insertions(+), 412 deletions(-) diff --git a/common/database_schema.h b/common/database_schema.h index 55aace9540..8f7b0fbb0c 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -26,427 +26,427 @@ namespace DatabaseSchema { - /** - * Character-specific tables - * - * Does not included related meta-data tables such as 'guilds', 'accounts' - * @return - */ - static std::map GetCharacterTables() - { - return { - {"adventure_stats", "player_id"}, - {"char_recipe_list", "char_id"}, - {"character_activities", "charid"}, - {"character_alt_currency", "char_id"}, - {"character_alternate_abilities", "id"}, - {"character_auras", "id"}, - {"character_bandolier", "id"}, - {"character_bind", "id"}, - {"character_buffs", "character_id"}, - {"character_corpses", "id"}, - {"character_currency", "id"}, - {"character_data", "id"}, - {"character_disciplines", "id"}, - {"character_enabledtasks", "charid"}, - {"character_expedition_lockouts", "character_id"}, - {"character_exp_modifiers", "character_id"}, - {"character_evolving_items", "character_id"}, - {"character_inspect_messages", "id"}, - {"character_instance_safereturns", "character_id"}, - {"character_item_recast", "id"}, - {"character_languages", "id"}, - {"character_leadership_abilities", "id"}, - {"character_material", "id"}, - {"character_memmed_spells", "id"}, - {"character_parcels", "char_id"}, - {"character_parcels_containers", "id"}, - {"character_pet_buffs", "char_id"}, - {"character_pet_info", "char_id"}, - {"character_pet_inventory", "char_id"}, - {"character_pet_name", "character_id"}, - {"character_peqzone_flags", "id"}, - {"character_potionbelt", "id"}, - {"character_skills", "id"}, - {"character_spells", "id"}, - {"character_stats_record", "character_id"}, - {"character_task_timers", "character_id"}, - {"character_tasks", "charid"}, - {"character_tribute", "character_id"}, - {"completed_tasks", "charid"}, - {"data_buckets", "character_id"}, - {"faction_values", "char_id"}, - {"friends", "charid"}, - {"guild_members", "char_id"}, - {"guilds", "id"}, - {"instance_list_player", "id"}, - {"inventory", "charid"}, - {"inventory_snapshots", "charid"}, - {"keyring", "char_id"}, - {"mail", "charid"}, - {"player_titlesets", "char_id"}, - {"quest_globals", "charid"}, - {"timers", "char_id"}, - {"trader", "char_id"}, - {"zone_flags", "charID"} - }; - } + /** + * Character-specific tables + * + * Does not included related meta-data tables such as 'guilds', 'accounts' + * @return + */ + static std::map GetCharacterTables() + { + return { + {"adventure_stats", "player_id"}, + {"char_recipe_list", "char_id"}, + {"character_activities", "charid"}, + {"character_alt_currency", "char_id"}, + {"character_alternate_abilities", "id"}, + {"character_auras", "id"}, + {"character_bandolier", "id"}, + {"character_bind", "id"}, + {"character_buffs", "character_id"}, + {"character_corpses", "id"}, + {"character_currency", "id"}, + {"character_data", "id"}, + {"character_disciplines", "id"}, + {"character_enabledtasks", "charid"}, + {"character_expedition_lockouts", "character_id"}, + {"character_exp_modifiers", "character_id"}, + {"character_evolving_items", "character_id"}, + {"character_inspect_messages", "id"}, + {"character_instance_safereturns", "character_id"}, + {"character_item_recast", "id"}, + {"character_languages", "id"}, + {"character_leadership_abilities", "id"}, + {"character_material", "id"}, + {"character_memmed_spells", "id"}, + {"character_parcels", "char_id"}, + {"character_parcels_containers", "id"}, + {"character_pet_buffs", "char_id"}, + {"character_pet_info", "char_id"}, + {"character_pet_inventory", "char_id"}, + {"character_pet_name", "character_id"}, + {"character_peqzone_flags", "id"}, + {"character_potionbelt", "id"}, + {"character_skills", "id"}, + {"character_spells", "id"}, + {"character_stats_record", "character_id"}, + {"character_task_timers", "character_id"}, + {"character_tasks", "charid"}, + {"character_tribute", "character_id"}, + {"completed_tasks", "charid"}, + {"data_buckets", "character_id"}, + {"faction_values", "char_id"}, + {"friends", "charid"}, + {"guild_members", "char_id"}, + {"guilds", "id"}, + {"instance_list_player", "id"}, + {"inventory", "charid"}, + {"inventory_snapshots", "charid"}, + {"keyring", "char_id"}, + {"mail", "charid"}, + {"player_titlesets", "char_id"}, + {"quest_globals", "charid"}, + {"timers", "char_id"}, + {"trader", "char_id"}, + {"zone_flags", "charID"} + }; + } - /** - * @description Gets all player and meta-data tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetPlayerTables() - { - return { - "account", - "account_ip", - "account_flags", - "account_rewards", - "adventure_details", - "adventure_stats", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "char_recipe_list", - "character_activities", - "character_alt_currency", - "character_alternate_abilities", - "character_auras", - "character_bandolier", - "character_bind", - "character_buffs", - "character_corpse_items", - "character_corpses", - "character_currency", - "character_data", - "character_disciplines", - "character_enabledtasks", - "character_expedition_lockouts", - "character_exp_modifiers", - "character_evolving_items", - "character_inspect_messages", - "character_instance_safereturns", - "character_item_recast", - "character_languages", - "character_leadership_abilities", - "character_material", - "character_memmed_spells", - "character_parcels", - "character_parcels_containers", - "character_pet_buffs", - "character_pet_info", - "character_pet_inventory", - "character_peqzone_flags", - "character_potionbelt", - "character_skills", - "character_spells", - "character_stats_record", - "character_task_timers", - "character_tasks", - "character_tribute", - "completed_tasks", - "data_buckets", - "discovered_items", - "faction_values", - "friends", - "guild_bank", - "guild_members", - "guild_permissions", - "guild_ranks", - "guild_relations", - "guild_tributes", - "guilds", - "instance_list_player", - "inventory", - "inventory_snapshots", - "keyring", - "mail", - "petitions", - "player_titlesets", - "quest_globals", - "sharedbank", - "spell_buckets", - "spell_globals", - "timers", - "trader", - "trader_audit", - "zone_flags" - }; - } + /** + * @description Gets all player and meta-data tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetPlayerTables() + { + return { + "account", + "account_ip", + "account_flags", + "account_rewards", + "adventure_details", + "adventure_stats", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "char_recipe_list", + "character_activities", + "character_alt_currency", + "character_alternate_abilities", + "character_auras", + "character_bandolier", + "character_bind", + "character_buffs", + "character_corpse_items", + "character_corpses", + "character_currency", + "character_data", + "character_disciplines", + "character_enabledtasks", + "character_expedition_lockouts", + "character_exp_modifiers", + "character_evolving_items", + "character_inspect_messages", + "character_instance_safereturns", + "character_item_recast", + "character_languages", + "character_leadership_abilities", + "character_material", + "character_memmed_spells", + "character_parcels", + "character_parcels_containers", + "character_pet_buffs", + "character_pet_info", + "character_pet_inventory", + "character_peqzone_flags", + "character_potionbelt", + "character_skills", + "character_spells", + "character_stats_record", + "character_task_timers", + "character_tasks", + "character_tribute", + "completed_tasks", + "data_buckets", + "discovered_items", + "faction_values", + "friends", + "guild_bank", + "guild_members", + "guild_permissions", + "guild_ranks", + "guild_relations", + "guild_tributes", + "guilds", + "instance_list_player", + "inventory", + "inventory_snapshots", + "keyring", + "mail", + "petitions", + "player_titlesets", + "quest_globals", + "sharedbank", + "spell_buckets", + "spell_globals", + "timers", + "trader", + "trader_audit", + "zone_flags" + }; + } - /** - * Gets content tables - * - * @return - */ - static std::vector GetContentTables() - { - return { - "aa_ability", - "aa_rank_effects", - "aa_rank_prereqs", - "aa_ranks", - "adventure_template", - "adventure_template_entry", - "adventure_template_entry_flavor", - "alternate_currency", - "auras", - "base_data", - "blocked_spells", - "books", - "char_create_combinations", - "char_create_point_allocations", - "damageshieldtypes", - "doors", - "dynamic_zone_templates", - "faction_association", - "faction_base_data", - "faction_list", - "faction_list_mod", - "fishing", - "forage", - "global_loot", - "graveyard", - "grid", - "grid_entries", - "ground_spawns", - "horses", - "items", - "items_evolving_details", - "ldon_trap_entries", - "ldon_trap_templates", - "lootdrop", - "lootdrop_entries", - "loottable", - "loottable_entries", - "merchantlist", - "npc_emotes", - "npc_faction", - "npc_faction_entries", - "npc_scale_global_base", - "npc_spells", - "npc_spells_effects", - "npc_spells_effects_entries", - "npc_spells_entries", - "npc_types", - "npc_types_tint", - "object", - "pets", - "pets_beastlord_data", - "pets_equipmentset", - "pets_equipmentset_entries", - "skill_caps", - "spawn2", - "spawn_conditions", - "spawnentry", - "spawngroup", - "spells_new", - "start_zones", - "starting_items", - "task_activities", - "tasks", - "tasksets", - "tradeskill_recipe", - "tradeskill_recipe_entries", - "traps", - "tribute_levels", - "tributes", - "veteran_reward_templates", - "zone", - "zone_points", - }; - } + /** + * Gets content tables + * + * @return + */ + static std::vector GetContentTables() + { + return { + "aa_ability", + "aa_rank_effects", + "aa_rank_prereqs", + "aa_ranks", + "adventure_template", + "adventure_template_entry", + "adventure_template_entry_flavor", + "alternate_currency", + "auras", + "base_data", + "blocked_spells", + "books", + "char_create_combinations", + "char_create_point_allocations", + "damageshieldtypes", + "doors", + "dynamic_zone_templates", + "faction_association", + "faction_base_data", + "faction_list", + "faction_list_mod", + "fishing", + "forage", + "global_loot", + "graveyard", + "grid", + "grid_entries", + "ground_spawns", + "horses", + "items", + "items_evolving_details", + "ldon_trap_entries", + "ldon_trap_templates", + "lootdrop", + "lootdrop_entries", + "loottable", + "loottable_entries", + "merchantlist", + "npc_emotes", + "npc_faction", + "npc_faction_entries", + "npc_scale_global_base", + "npc_spells", + "npc_spells_effects", + "npc_spells_effects_entries", + "npc_spells_entries", + "npc_types", + "npc_types_tint", + "object", + "pets", + "pets_beastlord_data", + "pets_equipmentset", + "pets_equipmentset_entries", + "skill_caps", + "spawn2", + "spawn_conditions", + "spawnentry", + "spawngroup", + "spells_new", + "start_zones", + "starting_items", + "task_activities", + "tasks", + "tasksets", + "tradeskill_recipe", + "tradeskill_recipe_entries", + "traps", + "tribute_levels", + "tributes", + "veteran_reward_templates", + "zone", + "zone_points", + }; + } - /** - * Gets server tables - * - * @return - */ - static std::vector GetServerTables() - { - return { - "chatchannels", - "chatchannel_reserved_names", - "command_settings", - "command_subsettings", - "content_flags", - "db_str", - "eqtime", - "launcher", - "launcher_zones", - "spawn_condition_values", - "spawn_events", - "level_exp_mods", - "logsys_categories", - "name_filter", - "perl_event_export_settings", - "profanity_list", - "rule_sets", - "titles", - "rule_values", - "variables", - }; - } + /** + * Gets server tables + * + * @return + */ + static std::vector GetServerTables() + { + return { + "chatchannels", + "chatchannel_reserved_names", + "command_settings", + "command_subsettings", + "content_flags", + "db_str", + "eqtime", + "launcher", + "launcher_zones", + "spawn_condition_values", + "spawn_events", + "level_exp_mods", + "logsys_categories", + "name_filter", + "perl_event_export_settings", + "profanity_list", + "rule_sets", + "titles", + "rule_values", + "variables", + }; + } - /** - * Gets QueryServer tables - * - * @return - */ - static std::vector GetQueryServerTables() - { - return { - "qs_merchant_transaction_record", - "qs_merchant_transaction_record_entries", - "qs_player_aa_rate_hourly", - "qs_player_delete_record", - "qs_player_delete_record_entries", - "qs_player_events", - "qs_player_handin_record", - "qs_player_handin_record_entries", - "qs_player_move_record", - "qs_player_move_record_entries", - "qs_player_npc_kill_record", - "qs_player_npc_kill_record_entries", - "qs_player_speech", - "qs_player_trade_record", - "qs_player_trade_record_entries", - }; - } + /** + * Gets QueryServer tables + * + * @return + */ + static std::vector GetQueryServerTables() + { + return { + "qs_merchant_transaction_record", + "qs_merchant_transaction_record_entries", + "qs_player_aa_rate_hourly", + "qs_player_delete_record", + "qs_player_delete_record_entries", + "qs_player_events", + "qs_player_handin_record", + "qs_player_handin_record_entries", + "qs_player_move_record", + "qs_player_move_record_entries", + "qs_player_npc_kill_record", + "qs_player_npc_kill_record_entries", + "qs_player_speech", + "qs_player_trade_record", + "qs_player_trade_record_entries", + }; + } - /** - * Gets state tables - * Tables that keep track of server state - * - * @return - */ - static std::vector GetStateTables() - { - return { - "adventure_members", - "banned_ips", - "bug_reports", - "bugs", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "completed_shared_task_activity_state", - "completed_shared_task_members", - "completed_shared_tasks", - "discord_webhooks", - "dynamic_zone_members", - "dynamic_zones", - "expedition_lockouts", - "expeditions", - "gm_ips", - "group_id", - "group_leaders", - "instance_list", - "ip_exemptions", - "lfguild", - "merc_buffs", - "merchantlist_temp", - "mercs", - "object_contents", - "raid_details", - "raid_leaders", - "raid_members", - "reports", - "respawn_times", - "saylink", - "server_scheduled_events", - "spawn2_disabled", - "player_event_log_settings", - "player_event_logs", - "shared_task_activity_state", - "shared_task_dynamic_zones", - "shared_task_members", - "shared_tasks", - }; - } + /** + * Gets state tables + * Tables that keep track of server state + * + * @return + */ + static std::vector GetStateTables() + { + return { + "adventure_members", + "banned_ips", + "bug_reports", + "bugs", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "completed_shared_task_activity_state", + "completed_shared_task_members", + "completed_shared_tasks", + "discord_webhooks", + "dynamic_zone_members", + "dynamic_zones", + "expedition_lockouts", + "expeditions", + "gm_ips", + "group_id", + "group_leaders", + "instance_list", + "ip_exemptions", + "lfguild", + "merc_buffs", + "merchantlist_temp", + "mercs", + "object_contents", + "raid_details", + "raid_leaders", + "raid_members", + "reports", + "respawn_times", + "saylink", + "server_scheduled_events", + "spawn2_disabled", + "player_event_log_settings", + "player_event_logs", + "shared_task_activity_state", + "shared_task_dynamic_zones", + "shared_task_members", + "shared_tasks", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetLoginTables() - { - return { - "login_accounts", - "login_api_tokens", - "login_server_admins", - "login_server_list_types", - "login_world_servers", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetLoginTables() + { + return { + "login_accounts", + "login_api_tokens", + "login_server_admins", + "login_server_list_types", + "login_world_servers", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetVersionTables() - { - return { - "db_version", - "inventory_versions", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetVersionTables() + { + return { + "db_version", + "inventory_versions", + }; + } - /** - * @description Gets all player bot tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetBotTables() - { - return { - "bot_buffs", - "bot_command_settings", - "bot_create_combinations", - "bot_data", - "bot_heal_rotation_members", - "bot_heal_rotation_targets", - "bot_heal_rotations", - "bot_inspect_messages", - "bot_inventories", - "bot_owner_options", - "bot_pet_buffs", - "bot_pet_inventories", - "bot_pets", - "bot_spell_casting_chances", - "bot_spell_settings", - "bot_spells_entries", - "bot_stances", - "bot_timers" - }; - } + /** + * @description Gets all player bot tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetBotTables() + { + return { + "bot_buffs", + "bot_command_settings", + "bot_create_combinations", + "bot_data", + "bot_heal_rotation_members", + "bot_heal_rotation_targets", + "bot_heal_rotations", + "bot_inspect_messages", + "bot_inventories", + "bot_owner_options", + "bot_pet_buffs", + "bot_pet_inventories", + "bot_pets", + "bot_spell_casting_chances", + "bot_spell_settings", + "bot_spells_entries", + "bot_stances", + "bot_timers" + }; + } - static std::vector GetMercTables() - { - return { - "merc_armorinfo", - "merc_inventory", - "merc_merchant_entries", - "merc_merchant_template_entries", - "merc_merchant_templates", - "merc_name_types", - "merc_npc_types", - "merc_spell_list_entries", - "merc_spell_lists", - "merc_stance_entries", - "merc_stats", - "merc_subtypes", - "merc_templates", - "merc_types", - "merc_weaponinfo" - }; - } + static std::vector GetMercTables() + { + return { + "merc_armorinfo", + "merc_inventory", + "merc_merchant_entries", + "merc_merchant_template_entries", + "merc_merchant_templates", + "merc_name_types", + "merc_npc_types", + "merc_spell_list_entries", + "merc_spell_lists", + "merc_stance_entries", + "merc_stats", + "merc_subtypes", + "merc_templates", + "merc_types", + "merc_weaponinfo" + }; + } } From e8586f467ecff916feab60866926efeb3f256c81 Mon Sep 17 00:00:00 2001 From: Catapultam Habeo Date: Tue, 21 Jan 2025 23:17:17 -0600 Subject: [PATCH 5/7] formating fixes I think? --- common/database_schema.h | 824 +- zone/client.cpp | 23257 ++++++++++++++++++------------------- 2 files changed, 12040 insertions(+), 12041 deletions(-) diff --git a/common/database_schema.h b/common/database_schema.h index 8f7b0fbb0c..9f23001b18 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -26,427 +26,427 @@ namespace DatabaseSchema { - /** - * Character-specific tables - * - * Does not included related meta-data tables such as 'guilds', 'accounts' - * @return - */ - static std::map GetCharacterTables() - { - return { - {"adventure_stats", "player_id"}, - {"char_recipe_list", "char_id"}, - {"character_activities", "charid"}, - {"character_alt_currency", "char_id"}, - {"character_alternate_abilities", "id"}, - {"character_auras", "id"}, - {"character_bandolier", "id"}, - {"character_bind", "id"}, - {"character_buffs", "character_id"}, - {"character_corpses", "id"}, - {"character_currency", "id"}, - {"character_data", "id"}, - {"character_disciplines", "id"}, - {"character_enabledtasks", "charid"}, - {"character_expedition_lockouts", "character_id"}, - {"character_exp_modifiers", "character_id"}, - {"character_evolving_items", "character_id"}, - {"character_inspect_messages", "id"}, - {"character_instance_safereturns", "character_id"}, - {"character_item_recast", "id"}, - {"character_languages", "id"}, - {"character_leadership_abilities", "id"}, - {"character_material", "id"}, - {"character_memmed_spells", "id"}, - {"character_parcels", "char_id"}, - {"character_parcels_containers", "id"}, - {"character_pet_buffs", "char_id"}, - {"character_pet_info", "char_id"}, - {"character_pet_inventory", "char_id"}, - {"character_pet_name", "character_id"}, - {"character_peqzone_flags", "id"}, - {"character_potionbelt", "id"}, - {"character_skills", "id"}, - {"character_spells", "id"}, - {"character_stats_record", "character_id"}, - {"character_task_timers", "character_id"}, - {"character_tasks", "charid"}, - {"character_tribute", "character_id"}, - {"completed_tasks", "charid"}, - {"data_buckets", "character_id"}, - {"faction_values", "char_id"}, - {"friends", "charid"}, - {"guild_members", "char_id"}, - {"guilds", "id"}, - {"instance_list_player", "id"}, - {"inventory", "charid"}, - {"inventory_snapshots", "charid"}, - {"keyring", "char_id"}, - {"mail", "charid"}, - {"player_titlesets", "char_id"}, - {"quest_globals", "charid"}, - {"timers", "char_id"}, - {"trader", "char_id"}, - {"zone_flags", "charID"} - }; - } + /** + * Character-specific tables + * + * Does not included related meta-data tables such as 'guilds', 'accounts' + * @return + */ + static std::map GetCharacterTables() + { + return { + {"adventure_stats", "player_id"}, + {"char_recipe_list", "char_id"}, + {"character_activities", "charid"}, + {"character_alt_currency", "char_id"}, + {"character_alternate_abilities", "id"}, + {"character_auras", "id"}, + {"character_bandolier", "id"}, + {"character_bind", "id"}, + {"character_buffs", "character_id"}, + {"character_corpses", "id"}, + {"character_currency", "id"}, + {"character_data", "id"}, + {"character_disciplines", "id"}, + {"character_enabledtasks", "charid"}, + {"character_expedition_lockouts", "character_id"}, + {"character_exp_modifiers", "character_id"}, + {"character_evolving_items", "character_id"}, + {"character_inspect_messages", "id"}, + {"character_instance_safereturns", "character_id"}, + {"character_item_recast", "id"}, + {"character_languages", "id"}, + {"character_leadership_abilities", "id"}, + {"character_material", "id"}, + {"character_memmed_spells", "id"}, + {"character_parcels", "char_id"}, + {"character_parcels_containers", "id"}, + {"character_pet_buffs", "char_id"}, + {"character_pet_info", "char_id"}, + {"character_pet_inventory", "char_id"}, + {"character_pet_name", "character_id"}, + {"character_peqzone_flags", "id"}, + {"character_potionbelt", "id"}, + {"character_skills", "id"}, + {"character_spells", "id"}, + {"character_stats_record", "character_id"}, + {"character_task_timers", "character_id"}, + {"character_tasks", "charid"}, + {"character_tribute", "character_id"}, + {"completed_tasks", "charid"}, + {"data_buckets", "character_id"}, + {"faction_values", "char_id"}, + {"friends", "charid"}, + {"guild_members", "char_id"}, + {"guilds", "id"}, + {"instance_list_player", "id"}, + {"inventory", "charid"}, + {"inventory_snapshots", "charid"}, + {"keyring", "char_id"}, + {"mail", "charid"}, + {"player_titlesets", "char_id"}, + {"quest_globals", "charid"}, + {"timers", "char_id"}, + {"trader", "char_id"}, + {"zone_flags", "charID"} + }; + } - /** - * @description Gets all player and meta-data tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetPlayerTables() - { - return { - "account", - "account_ip", - "account_flags", - "account_rewards", - "adventure_details", - "adventure_stats", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "char_recipe_list", - "character_activities", - "character_alt_currency", - "character_alternate_abilities", - "character_auras", - "character_bandolier", - "character_bind", - "character_buffs", - "character_corpse_items", - "character_corpses", - "character_currency", - "character_data", - "character_disciplines", - "character_enabledtasks", - "character_expedition_lockouts", - "character_exp_modifiers", - "character_evolving_items", - "character_inspect_messages", - "character_instance_safereturns", - "character_item_recast", - "character_languages", - "character_leadership_abilities", - "character_material", - "character_memmed_spells", - "character_parcels", - "character_parcels_containers", - "character_pet_buffs", - "character_pet_info", - "character_pet_inventory", - "character_peqzone_flags", - "character_potionbelt", - "character_skills", - "character_spells", - "character_stats_record", - "character_task_timers", - "character_tasks", - "character_tribute", - "completed_tasks", - "data_buckets", - "discovered_items", - "faction_values", - "friends", - "guild_bank", - "guild_members", - "guild_permissions", - "guild_ranks", - "guild_relations", - "guild_tributes", - "guilds", - "instance_list_player", - "inventory", - "inventory_snapshots", - "keyring", - "mail", - "petitions", - "player_titlesets", - "quest_globals", - "sharedbank", - "spell_buckets", - "spell_globals", - "timers", - "trader", - "trader_audit", - "zone_flags" - }; - } + /** + * @description Gets all player and meta-data tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetPlayerTables() + { + return { + "account", + "account_ip", + "account_flags", + "account_rewards", + "adventure_details", + "adventure_stats", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "char_recipe_list", + "character_activities", + "character_alt_currency", + "character_alternate_abilities", + "character_auras", + "character_bandolier", + "character_bind", + "character_buffs", + "character_corpse_items", + "character_corpses", + "character_currency", + "character_data", + "character_disciplines", + "character_enabledtasks", + "character_expedition_lockouts", + "character_exp_modifiers", + "character_evolving_items", + "character_inspect_messages", + "character_instance_safereturns", + "character_item_recast", + "character_languages", + "character_leadership_abilities", + "character_material", + "character_memmed_spells", + "character_parcels", + "character_parcels_containers", + "character_pet_buffs", + "character_pet_info", + "character_pet_inventory", + "character_peqzone_flags", + "character_potionbelt", + "character_skills", + "character_spells", + "character_stats_record", + "character_task_timers", + "character_tasks", + "character_tribute", + "completed_tasks", + "data_buckets", + "discovered_items", + "faction_values", + "friends", + "guild_bank", + "guild_members", + "guild_permissions", + "guild_ranks", + "guild_relations", + "guild_tributes", + "guilds", + "instance_list_player", + "inventory", + "inventory_snapshots", + "keyring", + "mail", + "petitions", + "player_titlesets", + "quest_globals", + "sharedbank", + "spell_buckets", + "spell_globals", + "timers", + "trader", + "trader_audit", + "zone_flags" + }; + } - /** - * Gets content tables - * - * @return - */ - static std::vector GetContentTables() - { - return { - "aa_ability", - "aa_rank_effects", - "aa_rank_prereqs", - "aa_ranks", - "adventure_template", - "adventure_template_entry", - "adventure_template_entry_flavor", - "alternate_currency", - "auras", - "base_data", - "blocked_spells", - "books", - "char_create_combinations", - "char_create_point_allocations", - "damageshieldtypes", - "doors", - "dynamic_zone_templates", - "faction_association", - "faction_base_data", - "faction_list", - "faction_list_mod", - "fishing", - "forage", - "global_loot", - "graveyard", - "grid", - "grid_entries", - "ground_spawns", - "horses", - "items", - "items_evolving_details", - "ldon_trap_entries", - "ldon_trap_templates", - "lootdrop", - "lootdrop_entries", - "loottable", - "loottable_entries", - "merchantlist", - "npc_emotes", - "npc_faction", - "npc_faction_entries", - "npc_scale_global_base", - "npc_spells", - "npc_spells_effects", - "npc_spells_effects_entries", - "npc_spells_entries", - "npc_types", - "npc_types_tint", - "object", - "pets", - "pets_beastlord_data", - "pets_equipmentset", - "pets_equipmentset_entries", - "skill_caps", - "spawn2", - "spawn_conditions", - "spawnentry", - "spawngroup", - "spells_new", - "start_zones", - "starting_items", - "task_activities", - "tasks", - "tasksets", - "tradeskill_recipe", - "tradeskill_recipe_entries", - "traps", - "tribute_levels", - "tributes", - "veteran_reward_templates", - "zone", - "zone_points", - }; - } + /** + * Gets content tables + * + * @return + */ + static std::vector GetContentTables() + { + return { + "aa_ability", + "aa_rank_effects", + "aa_rank_prereqs", + "aa_ranks", + "adventure_template", + "adventure_template_entry", + "adventure_template_entry_flavor", + "alternate_currency", + "auras", + "base_data", + "blocked_spells", + "books", + "char_create_combinations", + "char_create_point_allocations", + "damageshieldtypes", + "doors", + "dynamic_zone_templates", + "faction_association", + "faction_base_data", + "faction_list", + "faction_list_mod", + "fishing", + "forage", + "global_loot", + "graveyard", + "grid", + "grid_entries", + "ground_spawns", + "horses", + "items", + "items_evolving_details", + "ldon_trap_entries", + "ldon_trap_templates", + "lootdrop", + "lootdrop_entries", + "loottable", + "loottable_entries", + "merchantlist", + "npc_emotes", + "npc_faction", + "npc_faction_entries", + "npc_scale_global_base", + "npc_spells", + "npc_spells_effects", + "npc_spells_effects_entries", + "npc_spells_entries", + "npc_types", + "npc_types_tint", + "object", + "pets", + "pets_beastlord_data", + "pets_equipmentset", + "pets_equipmentset_entries", + "skill_caps", + "spawn2", + "spawn_conditions", + "spawnentry", + "spawngroup", + "spells_new", + "start_zones", + "starting_items", + "task_activities", + "tasks", + "tasksets", + "tradeskill_recipe", + "tradeskill_recipe_entries", + "traps", + "tribute_levels", + "tributes", + "veteran_reward_templates", + "zone", + "zone_points", + }; + } - /** - * Gets server tables - * - * @return - */ - static std::vector GetServerTables() - { - return { - "chatchannels", - "chatchannel_reserved_names", - "command_settings", - "command_subsettings", - "content_flags", - "db_str", - "eqtime", - "launcher", - "launcher_zones", - "spawn_condition_values", - "spawn_events", - "level_exp_mods", - "logsys_categories", - "name_filter", - "perl_event_export_settings", - "profanity_list", - "rule_sets", - "titles", - "rule_values", - "variables", - }; - } + /** + * Gets server tables + * + * @return + */ + static std::vector GetServerTables() + { + return { + "chatchannels", + "chatchannel_reserved_names", + "command_settings", + "command_subsettings", + "content_flags", + "db_str", + "eqtime", + "launcher", + "launcher_zones", + "spawn_condition_values", + "spawn_events", + "level_exp_mods", + "logsys_categories", + "name_filter", + "perl_event_export_settings", + "profanity_list", + "rule_sets", + "titles", + "rule_values", + "variables", + }; + } - /** - * Gets QueryServer tables - * - * @return - */ - static std::vector GetQueryServerTables() - { - return { - "qs_merchant_transaction_record", - "qs_merchant_transaction_record_entries", - "qs_player_aa_rate_hourly", - "qs_player_delete_record", - "qs_player_delete_record_entries", - "qs_player_events", - "qs_player_handin_record", - "qs_player_handin_record_entries", - "qs_player_move_record", - "qs_player_move_record_entries", - "qs_player_npc_kill_record", - "qs_player_npc_kill_record_entries", - "qs_player_speech", - "qs_player_trade_record", - "qs_player_trade_record_entries", - }; - } + /** + * Gets QueryServer tables + * + * @return + */ + static std::vector GetQueryServerTables() + { + return { + "qs_merchant_transaction_record", + "qs_merchant_transaction_record_entries", + "qs_player_aa_rate_hourly", + "qs_player_delete_record", + "qs_player_delete_record_entries", + "qs_player_events", + "qs_player_handin_record", + "qs_player_handin_record_entries", + "qs_player_move_record", + "qs_player_move_record_entries", + "qs_player_npc_kill_record", + "qs_player_npc_kill_record_entries", + "qs_player_speech", + "qs_player_trade_record", + "qs_player_trade_record_entries", + }; + } - /** - * Gets state tables - * Tables that keep track of server state - * - * @return - */ - static std::vector GetStateTables() - { - return { - "adventure_members", - "banned_ips", - "bug_reports", - "bugs", - "buyer", - "buyer_buy_lines", - "buyer_trade_items", - "completed_shared_task_activity_state", - "completed_shared_task_members", - "completed_shared_tasks", - "discord_webhooks", - "dynamic_zone_members", - "dynamic_zones", - "expedition_lockouts", - "expeditions", - "gm_ips", - "group_id", - "group_leaders", - "instance_list", - "ip_exemptions", - "lfguild", - "merc_buffs", - "merchantlist_temp", - "mercs", - "object_contents", - "raid_details", - "raid_leaders", - "raid_members", - "reports", - "respawn_times", - "saylink", - "server_scheduled_events", - "spawn2_disabled", - "player_event_log_settings", - "player_event_logs", - "shared_task_activity_state", - "shared_task_dynamic_zones", - "shared_task_members", - "shared_tasks", - }; - } + /** + * Gets state tables + * Tables that keep track of server state + * + * @return + */ + static std::vector GetStateTables() + { + return { + "adventure_members", + "banned_ips", + "bug_reports", + "bugs", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", + "completed_shared_task_activity_state", + "completed_shared_task_members", + "completed_shared_tasks", + "discord_webhooks", + "dynamic_zone_members", + "dynamic_zones", + "expedition_lockouts", + "expeditions", + "gm_ips", + "group_id", + "group_leaders", + "instance_list", + "ip_exemptions", + "lfguild", + "merc_buffs", + "merchantlist_temp", + "mercs", + "object_contents", + "raid_details", + "raid_leaders", + "raid_members", + "reports", + "respawn_times", + "saylink", + "server_scheduled_events", + "spawn2_disabled", + "player_event_log_settings", + "player_event_logs", + "shared_task_activity_state", + "shared_task_dynamic_zones", + "shared_task_members", + "shared_tasks", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetLoginTables() - { - return { - "login_accounts", - "login_api_tokens", - "login_server_admins", - "login_server_list_types", - "login_world_servers", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetLoginTables() + { + return { + "login_accounts", + "login_api_tokens", + "login_server_admins", + "login_server_list_types", + "login_world_servers", + }; + } - /** - * Gets login tables - * - * @return - */ - static std::vector GetVersionTables() - { - return { - "db_version", - "inventory_versions", - }; - } + /** + * Gets login tables + * + * @return + */ + static std::vector GetVersionTables() + { + return { + "db_version", + "inventory_versions", + }; + } - /** - * @description Gets all player bot tables - * @note These tables have no content in the PEQ daily dump - * - * @return - */ - static std::vector GetBotTables() - { - return { - "bot_buffs", - "bot_command_settings", - "bot_create_combinations", - "bot_data", - "bot_heal_rotation_members", - "bot_heal_rotation_targets", - "bot_heal_rotations", - "bot_inspect_messages", - "bot_inventories", - "bot_owner_options", - "bot_pet_buffs", - "bot_pet_inventories", - "bot_pets", - "bot_spell_casting_chances", - "bot_spell_settings", - "bot_spells_entries", - "bot_stances", - "bot_timers" - }; - } + /** + * @description Gets all player bot tables + * @note These tables have no content in the PEQ daily dump + * + * @return + */ + static std::vector GetBotTables() + { + return { + "bot_buffs", + "bot_command_settings", + "bot_create_combinations", + "bot_data", + "bot_heal_rotation_members", + "bot_heal_rotation_targets", + "bot_heal_rotations", + "bot_inspect_messages", + "bot_inventories", + "bot_owner_options", + "bot_pet_buffs", + "bot_pet_inventories", + "bot_pets", + "bot_spell_casting_chances", + "bot_spell_settings", + "bot_spells_entries", + "bot_stances", + "bot_timers" + }; + } - static std::vector GetMercTables() - { - return { - "merc_armorinfo", - "merc_inventory", - "merc_merchant_entries", - "merc_merchant_template_entries", - "merc_merchant_templates", - "merc_name_types", - "merc_npc_types", - "merc_spell_list_entries", - "merc_spell_lists", - "merc_stance_entries", - "merc_stats", - "merc_subtypes", - "merc_templates", - "merc_types", - "merc_weaponinfo" - }; - } + static std::vector GetMercTables() + { + return { + "merc_armorinfo", + "merc_inventory", + "merc_merchant_entries", + "merc_merchant_template_entries", + "merc_merchant_templates", + "merc_name_types", + "merc_npc_types", + "merc_spell_list_entries", + "merc_spell_lists", + "merc_stance_entries", + "merc_stats", + "merc_subtypes", + "merc_templates", + "merc_types", + "merc_weaponinfo" + }; + } } diff --git a/zone/client.cpp b/zone/client.cpp index 797a8938ab..4ce51a77ca 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -1,19 +1,19 @@ /* EQEMu: Everquest Server Emulator - Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) + Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; version 2 of the License. + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY except by those people which sell it, which - are required to give you total support for your newly bought product; - without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY except by those people which sell it, which + are required to give you total support for your newly bought product; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "../common/global_define.h" #include @@ -23,10 +23,10 @@ // for windows compile #ifndef _WINDOWS - #include - #include - #include - #include "../common/unix.h" + #include + #include + #include + #include "../common/unix.h" #endif extern volatile bool RunLoops; @@ -66,17 +66,16 @@ 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/character_pet_name_repository.h" #include "../common/repositories/discovered_items_repository.h" #include "../common/repositories/inventory_repository.h" #include "../common/repositories/keyring_repository.h" #include "../common/repositories/tradeskill_recipe_repository.h" -#include "../common/repositories/character_pet_name_repository.h" #include "../common/events/player_events.h" #include "../common/events/player_event_logs.h" #include "dialogue_window.h" #include "../common/zone_store.h" #include "../common/skill_caps.h" -#include "client.h" extern QueryServ* QServ; @@ -90,62 +89,62 @@ extern PetitionList petition_list; void UpdateWindowTitle(char* iNewTitle); Client::Client(EQStreamInterface *ieqs) : Mob( - "No name", // in_name - "", // in_lastname - 0, // in_cur_hp - 0, // in_max_hp - Gender::Male, // in_gender - Race::Doug, // in_race - Class::None, // in_class - BodyType::Humanoid, // in_bodytype - Deity::Unknown, // in_deity - 0, // in_level - 0, // in_npctype_id - 0.0f, // in_size - 0.7f, // in_runspeed - glm::vec4(), // position - 0, // in_light - 0xFF, // in_texture - 0xFF, // in_helmtexture - 0, // in_ac - 0, // in_atk - 0, // in_str - 0, // in_sta - 0, // in_dex - 0, // in_agi - 0, // in_int - 0, // in_wis - 0, // in_cha - 0, // in_haircolor - 0, // in_beardcolor - 0, // in_eyecolor1 - 0, // in_eyecolor2 - 0, // in_hairstyle - 0, // in_luclinface - 0, // in_beard - 0, // in_drakkin_heritage - 0, // in_drakkin_tattoo - 0, // in_drakkin_details - EQ::TintProfile(), // in_armor_tint - 0xff, // in_aa_title - 0, // in_see_invis - 0, // in_see_invis_undead - 0, // in_see_hide - 0, // in_see_improved_hide - 0, // in_hp_regen - 0, // in_mana_regen - 0, // in_qglobal - 0, // in_maxlevel - 0, // in_scalerate - 0, // in_armtexture - 0, // in_bracertexture - 0, // in_handtexture - 0, // in_legtexture - 0, // in_feettexture - 0, // in_usemodel - false, // in_always_aggros_foes - 0, // in_heroic_strikethrough - false // in_keeps_sold_items + "No name", // in_name + "", // in_lastname + 0, // in_cur_hp + 0, // in_max_hp + Gender::Male, // in_gender + Race::Doug, // in_race + Class::None, // in_class + BodyType::Humanoid, // in_bodytype + Deity::Unknown, // in_deity + 0, // in_level + 0, // in_npctype_id + 0.0f, // in_size + 0.7f, // in_runspeed + glm::vec4(), // position + 0, // in_light + 0xFF, // in_texture + 0xFF, // in_helmtexture + 0, // in_ac + 0, // in_atk + 0, // in_str + 0, // in_sta + 0, // in_dex + 0, // in_agi + 0, // in_int + 0, // in_wis + 0, // in_cha + 0, // in_haircolor + 0, // in_beardcolor + 0, // in_eyecolor1 + 0, // in_eyecolor2 + 0, // in_hairstyle + 0, // in_luclinface + 0, // in_beard + 0, // in_drakkin_heritage + 0, // in_drakkin_tattoo + 0, // in_drakkin_details + EQ::TintProfile(), // in_armor_tint + 0xff, // in_aa_title + 0, // in_see_invis + 0, // in_see_invis_undead + 0, // in_see_hide + 0, // in_see_improved_hide + 0, // in_hp_regen + 0, // in_mana_regen + 0, // in_qglobal + 0, // in_maxlevel + 0, // in_scalerate + 0, // in_armtexture + 0, // in_bracertexture + 0, // in_handtexture + 0, // in_legtexture + 0, // in_feettexture + 0, // in_usemodel + false, // in_always_aggros_foes + 0, // in_heroic_strikethrough + false // in_keeps_sold_items ), hpupdate_timer(2000), camp_timer(29000), @@ -190,12831 +189,12831 @@ Client::Client(EQStreamInterface *ieqs) : Mob( lazy_load_bank_check_timer(1000), bandolier_throttle_timer(0) { - for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) { - SetFilter(client_filter, FilterShow); - } - - cheat_manager.SetClient(this); - mMovementManager->AddClient(this); - character_id = 0; - conn_state = NoPacketsReceived; - client_data_loaded = false; - berserk = false; - dead = false; - eqs = ieqs; - ip = eqs->GetRemoteIP(); - port = ntohs(eqs->GetRemotePort()); - client_state = CLIENT_CONNECTING; - SetTrader(false); - Haste = 0; - SetCustomerID(0); - SetTraderID(0); - TrackingID = 0; - WID = 0; - account_id = 0; - admin = AccountStatus::Player; - lsaccountid = 0; - guild_id = GUILD_NONE; - guildrank = 0; - guild_tribute_opt_in = 0; - SetGuildListDirty(false); - GuildBanker = false; - memset(lskey, 0, sizeof(lskey)); - strcpy(account_name, ""); - tellsoff = false; - last_reported_mana = 0; - last_reported_endurance = 0; - last_reported_endurance_percent = 0; - last_reported_mana_percent = 0; - gm_hide_me = false; - AFK = false; - LFG = false; - LFGFromLevel = 0; - LFGToLevel = 0; - LFGMatchFilter = false; - LFGComments[0] = '\0'; - LFP = false; - gmspeed = 0; - gminvul = false; - playeraction = 0; - SetTarget(0); - auto_attack = false; - auto_fire = false; - runmode = false; - linkdead_timer.Disable(); - zonesummon_id = 0; - zonesummon_ignorerestrictions = 0; - bZoning = false; - m_lock_save_position = false; - zone_mode = ZoneUnsolicited; - casting_spell_id = 0; - npcflag = false; - npclevel = 0; - fishing_timer.Disable(); - dead_timer.Disable(); - camp_timer.Disable(); - autosave_timer.Disable(); - GetMercTimer()->Disable(); - instalog = false; - m_pp.autosplit = false; - // initialise haste variable - m_tradeskill_object = nullptr; - delaytimer = false; - PendingRezzXP = -1; - PendingRezzDBID = 0; - PendingRezzSpellID = 0; - numclients++; - // emuerror; - UpdateWindowTitle(nullptr); - horseId = 0; - tgb = false; - tribute_master_id = 0xFFFFFFFF; - tribute_timer.Disable(); - task_state = nullptr; - TotalSecondsPlayed = 0; - keyring.clear(); - bind_sight_target = nullptr; - p_raid_instance = nullptr; - mercid = 0; - mercSlot = 0; - InitializeMercInfo(); - SetMerc(0); - if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) SetPVP(true, false); - dynamiczone_removal_timer.Disable(); - - //for good measure: - memset(&m_pp, 0, sizeof(m_pp)); - memset(&m_epp, 0, sizeof(m_epp)); - PendingTranslocate = false; - PendingSacrifice = false; - sacrifice_caster_id = 0; - controlling_boat_id = 0; - controlled_mob_id = 0; - qGlobals = nullptr; - - if (!RuleB(Character, PerCharacterQglobalMaxLevel) && !RuleB(Character, PerCharacterBucketMaxLevel)) { - SetClientMaxLevel(0); - } else if (RuleB(Character, PerCharacterQglobalMaxLevel)) { - SetClientMaxLevel(GetCharMaxLevelFromQGlobal()); - } else if (RuleB(Character, PerCharacterBucketMaxLevel)) { - SetClientMaxLevel(GetCharMaxLevelFromBucket()); - } - - KarmaUpdateTimer = new Timer(RuleI(Chat, KarmaUpdateIntervalMS)); - GlobalChatLimiterTimer = new Timer(RuleI(Chat, IntervalDurationMS)); - AttemptedMessages = 0; - TotalKarma = 0; - m_ClientVersion = EQ::versions::ClientVersion::Unknown; - m_ClientVersionBit = 0; - AggroCount = 0; - ooc_regen = false; - AreaHPRegen = 1.0f; - AreaManaRegen = 1.0f; - AreaEndRegen = 1.0f; - XPRate = 100; - current_endurance = 0; - - CanUseReport = true; - aa_los_them_mob = nullptr; - los_status = false; - los_status_facing = false; - HideCorpseMode = HideCorpseNone; - PendingGuildInvitation = false; - - InitializeBuffSlots(); - - adventure_request_timer = nullptr; - adventure_create_timer = nullptr; - adventure_leave_timer = nullptr; - adventure_door_timer = nullptr; - adv_requested_data = nullptr; - adventure_stats_timer = nullptr; - adventure_leaderboard_timer = nullptr; - adv_data = nullptr; - adv_requested_theme = LDoNTheme::Unused; - adv_requested_id = 0; - adv_requested_member_count = 0; - - for(int i = 0; i < XTARGET_HARDCAP; ++i) - { - XTargets[i].Type = Auto; - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - XTargets[i].dirty = false; - } - MaxXTargets = 5; - XTargetAutoAddHaters = true; - m_autohatermgr.SetOwner(this, nullptr, nullptr); - m_activeautohatermgr = &m_autohatermgr; - - initial_respawn_selection = 0; - alternate_currency_loaded = false; - - interrogateinv_flag = false; - - trapid = 0; - - for (int i = 0; i < InnateSkillMax; ++i) - m_pp.InnateSkills[i] = InnateDisabled; - - temp_pvp = false; - - moving = false; - - environment_damage_modifier = 0; - invulnerable_environment_damage = false; - - // rate limiter - m_list_task_timers_rate_limit.Start(1000); - - // gm - SetDisplayMobInfoWindow(true); - SetDevToolsEnabled(true); - - bot_owner_options[booDeathMarquee] = false; - bot_owner_options[booStatsUpdate] = false; - bot_owner_options[booSpawnMessageSay] = false; - bot_owner_options[booSpawnMessageTell] = true; - bot_owner_options[booSpawnMessageClassSpecific] = true; - bot_owner_options[booAutoDefend] = RuleB(Bots, AllowOwnerOptionAutoDefend); - bot_owner_options[booBuffCounter] = false; - bot_owner_options[booMonkWuMessage] = false; - - m_parcel_platinum = 0; - m_parcel_gold = 0; - m_parcel_silver = 0; - m_parcel_copper = 0; - m_parcel_count = 0; - m_parcel_enabled = true; - m_parcel_merchant_engaged = false; - m_parcels.clear(); - - m_buyer_id = 0; - - SetBotPulling(false); - SetBotPrecombat(false); - - AI_Init(); -} + for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) { + SetFilter(client_filter, FilterShow); + } -Client::~Client() { - if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) { - DoParcelCancel(); - } + cheat_manager.SetClient(this); + mMovementManager->AddClient(this); + character_id = 0; + conn_state = NoPacketsReceived; + client_data_loaded = false; + berserk = false; + dead = false; + eqs = ieqs; + ip = eqs->GetRemoteIP(); + port = ntohs(eqs->GetRemotePort()); + client_state = CLIENT_CONNECTING; + SetTrader(false); + Haste = 0; + SetCustomerID(0); + SetTraderID(0); + TrackingID = 0; + WID = 0; + account_id = 0; + admin = AccountStatus::Player; + lsaccountid = 0; + guild_id = GUILD_NONE; + guildrank = 0; + guild_tribute_opt_in = 0; + SetGuildListDirty(false); + GuildBanker = false; + memset(lskey, 0, sizeof(lskey)); + strcpy(account_name, ""); + tellsoff = false; + last_reported_mana = 0; + last_reported_endurance = 0; + last_reported_endurance_percent = 0; + last_reported_mana_percent = 0; + gm_hide_me = false; + AFK = false; + LFG = false; + LFGFromLevel = 0; + LFGToLevel = 0; + LFGMatchFilter = false; + LFGComments[0] = '\0'; + LFP = false; + gmspeed = 0; + gminvul = false; + playeraction = 0; + SetTarget(0); + auto_attack = false; + auto_fire = false; + runmode = false; + linkdead_timer.Disable(); + zonesummon_id = 0; + zonesummon_ignorerestrictions = 0; + bZoning = false; + m_lock_save_position = false; + zone_mode = ZoneUnsolicited; + casting_spell_id = 0; + npcflag = false; + npclevel = 0; + fishing_timer.Disable(); + dead_timer.Disable(); + camp_timer.Disable(); + autosave_timer.Disable(); + GetMercTimer()->Disable(); + instalog = false; + m_pp.autosplit = false; + // initialise haste variable + m_tradeskill_object = nullptr; + delaytimer = false; + PendingRezzXP = -1; + PendingRezzDBID = 0; + PendingRezzSpellID = 0; + numclients++; + // emuerror; + UpdateWindowTitle(nullptr); + horseId = 0; + tgb = false; + tribute_master_id = 0xFFFFFFFF; + tribute_timer.Disable(); + task_state = nullptr; + TotalSecondsPlayed = 0; + keyring.clear(); + bind_sight_target = nullptr; + p_raid_instance = nullptr; + mercid = 0; + mercSlot = 0; + InitializeMercInfo(); + SetMerc(0); + if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) SetPVP(true, false); + dynamiczone_removal_timer.Disable(); + + //for good measure: + memset(&m_pp, 0, sizeof(m_pp)); + memset(&m_epp, 0, sizeof(m_epp)); + PendingTranslocate = false; + PendingSacrifice = false; + sacrifice_caster_id = 0; + controlling_boat_id = 0; + controlled_mob_id = 0; + qGlobals = nullptr; + + if (!RuleB(Character, PerCharacterQglobalMaxLevel) && !RuleB(Character, PerCharacterBucketMaxLevel)) { + SetClientMaxLevel(0); + } else if (RuleB(Character, PerCharacterQglobalMaxLevel)) { + SetClientMaxLevel(GetCharMaxLevelFromQGlobal()); + } else if (RuleB(Character, PerCharacterBucketMaxLevel)) { + SetClientMaxLevel(GetCharMaxLevelFromBucket()); + } - mMovementManager->RemoveClient(this); + KarmaUpdateTimer = new Timer(RuleI(Chat, KarmaUpdateIntervalMS)); + GlobalChatLimiterTimer = new Timer(RuleI(Chat, IntervalDurationMS)); + AttemptedMessages = 0; + TotalKarma = 0; + m_ClientVersion = EQ::versions::ClientVersion::Unknown; + m_ClientVersionBit = 0; + AggroCount = 0; + ooc_regen = false; + AreaHPRegen = 1.0f; + AreaManaRegen = 1.0f; + AreaEndRegen = 1.0f; + XPRate = 100; + current_endurance = 0; + + CanUseReport = true; + aa_los_them_mob = nullptr; + los_status = false; + los_status_facing = false; + HideCorpseMode = HideCorpseNone; + PendingGuildInvitation = false; + + InitializeBuffSlots(); + + adventure_request_timer = nullptr; + adventure_create_timer = nullptr; + adventure_leave_timer = nullptr; + adventure_door_timer = nullptr; + adv_requested_data = nullptr; + adventure_stats_timer = nullptr; + adventure_leaderboard_timer = nullptr; + adv_data = nullptr; + adv_requested_theme = LDoNTheme::Unused; + adv_requested_id = 0; + adv_requested_member_count = 0; + + for(int i = 0; i < XTARGET_HARDCAP; ++i) + { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + XTargets[i].dirty = false; + } + MaxXTargets = 5; + XTargetAutoAddHaters = true; + m_autohatermgr.SetOwner(this, nullptr, nullptr); + m_activeautohatermgr = &m_autohatermgr; - DataBucket::DeleteCachedBuckets(DataBucketLoadType::Account, AccountID()); - DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID()); + initial_respawn_selection = 0; + alternate_currency_loaded = false; - if (RuleB(Bots, Enabled)) { - Bot::ProcessBotOwnerRefDelete(this); - } - - if (zone) { - zone->ClearEXPModifier(this); - } - - if (!IsZoning()) { - if(IsInAGuild()) { - guild_mgr.UpdateDbMemberOnline(CharacterID(), false); - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); - } - } - - Mob* horse = entity_list.GetMob(CastToClient()->GetHorseId()); - if (horse) - horse->Depop(); - - Mob* merc = entity_list.GetMob(GetMercenaryID()); - if (merc) - merc->Depop(); - - if(IsTrader()) { - TraderEndTrader(); - } - - if(IsBuyer()) { - ToggleBuyerMode(false); - } - - if(conn_state != ClientConnectFinished) { - LogDebug("Client [{}] was destroyed before reaching the connected state:", GetName()); - ReportConnectingState(); - } - - if(m_tradeskill_object != nullptr) { - m_tradeskill_object->Close(); - m_tradeskill_object = nullptr; - } - - m_close_mobs.clear(); - - if(IsDueling() && GetDuelTarget() != 0) { - Entity* entity = entity_list.GetID(GetDuelTarget()); - if(entity != nullptr && entity->IsClient()) { - entity->CastToClient()->SetDueling(false); - entity->CastToClient()->SetDuelTarget(0); - entity_list.DuelMessage(entity->CastToClient(),this,true); - } - } - - if(GetTarget()) - GetTarget()->IsTargeted(-1); - - //if we are in a group and we are not zoning, force leave the group - if(isgrouped && !bZoning && is_zone_loaded) - LeaveGroup(); - - UpdateWho(2); - - if(IsHoveringForRespawn()) - { - m_pp.zone_id = m_pp.binds[0].zone_id; - m_pp.zoneInstance = m_pp.binds[0].instance_id; - m_Position.x = m_pp.binds[0].x; - m_Position.y = m_pp.binds[0].y; - m_Position.z = m_pp.binds[0].z; - } - - // we save right now, because the client might be zoning and the world - // will need this data right away - Save(2); // This fails when database destructor is called first on shutdown - - safe_delete(task_state); - safe_delete(KarmaUpdateTimer); - safe_delete(GlobalChatLimiterTimer); - safe_delete(qGlobals); - safe_delete(adventure_request_timer); - safe_delete(adventure_create_timer); - safe_delete(adventure_leave_timer); - safe_delete(adventure_door_timer); - safe_delete(adventure_stats_timer); - safe_delete(adventure_leaderboard_timer); - safe_delete_array(adv_requested_data); - safe_delete_array(adv_data); - - ClearRespawnOptions(); - - numclients--; - UpdateWindowTitle(nullptr); - if(zone) - zone->RemoveAuth(GetName(), lskey); - - //let the stream factory know were done with this stream - eqs->Close(); - eqs->ReleaseFromUse(); - safe_delete(eqs); - - UninitializeBuffSlots(); -} + interrogateinv_flag = false; -void Client::SendZoneInPackets() -{ + trapid = 0; - ////////////////////////////////////////////////////// - // Spawn Appearance Packet - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* sa = (SpawnAppearance_Struct*)outapp->pBuffer; - sa->type = AppearanceType::SpawnID; // Is 0x10 used to set the player id? - sa->parameter = GetID(); // Four bytes for this parameter... - outapp->priority = 6; - QueuePacket(outapp); - safe_delete(outapp); - - // Inform the world about the client - outapp = new EQApplicationPacket(); - - CreateSpawnPacket(outapp); - outapp->priority = 6; - if (!GetHideMe()) entity_list.QueueClients(this, outapp, true); - safe_delete(outapp); - SetSpawned(); - if (GetPVP(false)) //force a PVP update until we fix the spawn struct - SendAppearancePacket(AppearanceType::PVP, GetPVP(false), true, false); - - //Send AA Exp packet: - if (GetLevel() >= 51) - SendAlternateAdvancementStats(); - - // Send exp packets - outapp = new EQApplicationPacket(OP_ExpUpdate, sizeof(ExpUpdate_Struct)); - ExpUpdate_Struct* eu = (ExpUpdate_Struct*)outapp->pBuffer; - uint32 tmpxp1 = GetEXPForLevel(GetLevel() + 1); - uint32 tmpxp2 = GetEXPForLevel(GetLevel()); - - // Crash bug fix... Divide by zero when tmpxp1 and 2 equalled each other, most likely the error case from GetEXPForLevel() (invalid class, etc) - if (tmpxp1 != tmpxp2 && tmpxp1 != 0xFFFFFFFF && tmpxp2 != 0xFFFFFFFF) { - float tmpxp = (float)((float)m_pp.exp - tmpxp2) / ((float)tmpxp1 - tmpxp2); - eu->exp = (uint32)(330.0f * tmpxp); - outapp->priority = 6; - QueuePacket(outapp); - } - safe_delete(outapp); - - SendAlternateAdvancementTimers(); - - outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(ZoneInSendName_Struct)); - ZoneInSendName_Struct* zonesendname = (ZoneInSendName_Struct*)outapp->pBuffer; - strcpy(zonesendname->name, m_pp.name); - strcpy(zonesendname->name2, m_pp.name); - zonesendname->unknown0 = 0x0A; - QueuePacket(outapp); - safe_delete(outapp); - - if (IsInAGuild()) { - guild_mgr.UpdateDbMemberOnline(CharacterID(), true); - //SendGuildMembers(); - SendGuildURL(); - SendGuildChannel(); - SendGuildLFGuildStatus(); - } - SendLFGuildStatus(); - - //No idea why live sends this if even were not in a guild - SendGuildMOTD(); -} + for (int i = 0; i < InnateSkillMax; ++i) + m_pp.InnateSkills[i] = InnateDisabled; -void Client::SendLogoutPackets() { + temp_pvp = false; - auto outapp = new EQApplicationPacket(OP_CancelTrade, sizeof(CancelTrade_Struct)); - CancelTrade_Struct* ct = (CancelTrade_Struct*) outapp->pBuffer; - ct->fromid = GetID(); - ct->action = groupActUpdate; - FastQueuePacket(&outapp); + moving = false; - outapp = new EQApplicationPacket(OP_PreLogoutReply); - FastQueuePacket(&outapp); + environment_damage_modifier = 0; + invulnerable_environment_damage = false; -} + // rate limiter + m_list_task_timers_rate_limit.Start(1000); -void Client::ReportConnectingState() { - switch(conn_state) { - case NoPacketsReceived: //havent gotten anything - LogDebug("Client has not sent us an initial zone entry packet"); - break; - case ReceivedZoneEntry: //got the first packet, loading up PP - LogDebug("Client sent initial zone packet, but we never got their player info from the database"); - break; - case PlayerProfileLoaded: //our DB work is done, sending it - LogDebug("We were sending the player profile, tributes, tasks, spawns, time and weather, but never finished"); - break; - case ZoneInfoSent: //includes PP, tributes, tasks, spawns, time and weather - LogDebug("We successfully sent player info and spawns, waiting for client to request new zone"); - break; - case NewZoneRequested: //received and sent new zone request - LogDebug("We received client's new zone request, waiting for client spawn request"); - break; - case ClientSpawnRequested: //client sent ReqClientSpawn - LogDebug("We received the client spawn request, and were sending objects, doors, zone points and some other stuff, but never finished"); - break; - case ZoneContentsSent: //objects, doors, zone points - LogDebug("The rest of the zone contents were successfully sent, waiting for client ready notification"); - break; - case ClientReadyReceived: //client told us its ready, send them a bunch of crap like guild MOTD, etc - LogDebug("We received client ready notification, but never finished Client::CompleteConnect"); - break; - case ClientConnectFinished: //client finally moved to finished state, were done here - LogDebug("Client is successfully connected"); - break; - }; -} + // gm + SetDisplayMobInfoWindow(true); + SetDevToolsEnabled(true); -bool Client::SaveAA() -{ - std::vector v; + bot_owner_options[booDeathMarquee] = false; + bot_owner_options[booStatsUpdate] = false; + bot_owner_options[booSpawnMessageSay] = false; + bot_owner_options[booSpawnMessageTell] = true; + bot_owner_options[booSpawnMessageClassSpecific] = true; + bot_owner_options[booAutoDefend] = RuleB(Bots, AllowOwnerOptionAutoDefend); + bot_owner_options[booBuffCounter] = false; + bot_owner_options[booMonkWuMessage] = false; - uint32 aa_points_spent = 0; + m_parcel_platinum = 0; + m_parcel_gold = 0; + m_parcel_silver = 0; + m_parcel_copper = 0; + m_parcel_count = 0; + m_parcel_enabled = true; + m_parcel_merchant_engaged = false; + m_parcels.clear(); - auto e = CharacterAlternateAbilitiesRepository::NewEntity(); + m_buyer_id = 0; - for (auto &rank : aa_ranks) { - auto a = zone->GetAlternateAdvancementAbility(rank.first); - if (!a) { - continue; - } + SetBotPulling(false); + SetBotPrecombat(false); - if (rank.second.first > 0) { - auto r = a->GetRankByPointsSpent(rank.second.first); - if (!r) { - continue; - } + AI_Init(); +} - aa_points_spent += r->total_cost; +Client::~Client() { + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) { + DoParcelCancel(); + } - e.id = character_id; - e.aa_id = a->first_rank_id; - e.aa_value = rank.second.first; - e.charges = rank.second.second; + mMovementManager->RemoveClient(this); - v.emplace_back(e); - } - } + DataBucket::DeleteCachedBuckets(DataBucketLoadType::Account, AccountID()); + DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID()); - m_pp.aapoints_spent = aa_points_spent + m_epp.expended_aa; + if (RuleB(Bots, Enabled)) { + Bot::ProcessBotOwnerRefDelete(this); + } - return CharacterAlternateAbilitiesRepository::ReplaceMany(database, v); -} + if (zone) { + zone->ClearEXPModifier(this); + } -void Client::RemoveExpendedAA(int aa_id) -{ - CharacterAlternateAbilitiesRepository::DeleteWhere( - database, - fmt::format( - "`id` = {} AND `aa_id` = {}", - CharacterID(), - aa_id - ) - ); -} + if (!IsZoning()) { + if(IsInAGuild()) { + guild_mgr.UpdateDbMemberOnline(CharacterID(), false); + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); + } + } -bool Client::Save(uint8 iCommitNow) { - if(!ClientDataLoaded()) - return false; - - /* Wrote current basics to PP for saves */ - if (!m_lock_save_position) { - m_pp.x = m_Position.x; - m_pp.y = m_Position.y; - m_pp.z = m_Position.z; - m_pp.heading = m_Position.w; - } - - m_pp.guildrank = guildrank; - - if (dead && GetHP() <= 0) { - m_pp.cur_hp = GetMaxHP(); - m_pp.mana = current_mana; - if (RuleB(Character, FullManaOnDeath)) { - m_pp.mana = GetMaxMana(); - } - - m_pp.endurance = current_endurance; - if (RuleB(Character, FullEndurOnDeath)) { - m_pp.endurance = GetMaxEndurance(); - } - } else { // Otherwise, no changes. - m_pp.cur_hp = GetHP(); - m_pp.mana = current_mana; - m_pp.endurance = current_endurance; - } - - /* Save Character Currency */ - database.SaveCharacterCurrency(CharacterID(), &m_pp); - - // save character binds - // this may not need to be called in Save() but it's here for now - // to maintain the current behavior - database.SaveCharacterBinds(this); - - /* Save Character Buffs */ - database.SaveBuffs(this); - - /* Total Time Played */ - TotalSecondsPlayed += (time(nullptr) - m_pp.lastlogin); - m_pp.timePlayedMin = (TotalSecondsPlayed / 60); - m_pp.RestTimer = GetRestTimer(); - - /* Save Mercs */ - if (GetMercInfo().MercTimerRemaining > RuleI(Mercs, UpkeepIntervalMS)) { - GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS); - } - - if (GetMercTimer()->Enabled()) { - GetMercInfo().MercTimerRemaining = GetMercTimer()->GetRemainingTime(); - } - - if (dead || (!GetMerc() && !GetMercInfo().IsSuspended)) { - memset(&m_mercinfo, 0, sizeof(struct MercInfo)); - } - - m_pp.lastlogin = time(nullptr); - - if (GetPet() && GetPet()->CastToNPC()->GetPetSpellID() && !dead) { - NPC *pet = GetPet()->CastToNPC(); - m_petinfo.SpellID = pet->CastToNPC()->GetPetSpellID(); - m_petinfo.HP = pet->GetHP(); - m_petinfo.Mana = pet->GetMana(); - pet->GetPetState(m_petinfo.Buffs, m_petinfo.Items, m_petinfo.Name); - m_petinfo.petpower = pet->GetPetPower(); - m_petinfo.size = pet->GetSize(); - m_petinfo.taunting = pet->CastToNPC()->IsTaunting(); - } else { - memset(&m_petinfo, 0, sizeof(struct PetInfo)); - } - database.SavePetInfo(this); - - if(tribute_timer.Enabled()) { - m_pp.tribute_time_remaining = tribute_timer.GetRemainingTime(); - } - else { - m_pp.tribute_time_remaining = 0xFFFFFFFF; m_pp.tribute_active = 0; - } - - if (m_pp.hunger_level < 0) - m_pp.hunger_level = 0; - - if (m_pp.thirst_level < 0) - m_pp.thirst_level = 0; - - p_timers.Store(&database); - - database.SaveCharacterTribute(this); - SaveTaskState(); /* Save Character Task */ - - LogFood("Client::Save - hunger_level: [{}] thirst_level: [{}]", m_pp.hunger_level, m_pp.thirst_level); - - // perform snapshot before SaveCharacterData() so that m_epp will contain the updated time - if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { - if (database.SaveCharacterInvSnapshot(CharacterID())) { - SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); - } - else { - SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); - } - } - - database.SaveCharacterData(this, &m_pp, &m_epp); /* Save Character Data */ - - database.SaveCharacterEXPModifier(this); - - return true; -} + Mob* horse = entity_list.GetMob(CastToClient()->GetHorseId()); + if (horse) + horse->Depop(); -CLIENTPACKET::CLIENTPACKET() -{ - app = nullptr; - ack_req = false; -} + Mob* merc = entity_list.GetMob(GetMercenaryID()); + if (merc) + merc->Depop(); -CLIENTPACKET::~CLIENTPACKET() -{ - safe_delete(app); -} + if(IsTrader()) { + TraderEndTrader(); + } -//this assumes we do not own pApp, and clones it. -bool Client::AddPacket(const EQApplicationPacket *pApp, bool bAckreq) { - if (!pApp) - return false; - if(!zoneinpacket_timer.Enabled()) { - //drop the packet because it will never get sent. - return(false); - } + if(IsBuyer()) { + ToggleBuyerMode(false); + } - auto c = std::make_unique(); + if(conn_state != ClientConnectFinished) { + LogDebug("Client [{}] was destroyed before reaching the connected state:", GetName()); + ReportConnectingState(); + } - c->ack_req = bAckreq; - c->app = pApp->Copy(); + if(m_tradeskill_object != nullptr) { + m_tradeskill_object->Close(); + m_tradeskill_object = nullptr; + } - clientpackets.push_back(std::move(c)); - return true; -} + m_close_mobs.clear(); -//this assumes that it owns the object pointed to by *pApp -bool Client::AddPacket(EQApplicationPacket** pApp, bool bAckreq) { - if (!pApp || !(*pApp)) - return false; - if(!zoneinpacket_timer.Enabled()) { - //drop the packet because it will never get sent. - return(false); - } - auto c = std::make_unique(); + if(IsDueling() && GetDuelTarget() != 0) { + Entity* entity = entity_list.GetID(GetDuelTarget()); + if(entity != nullptr && entity->IsClient()) { + entity->CastToClient()->SetDueling(false); + entity->CastToClient()->SetDuelTarget(0); + entity_list.DuelMessage(entity->CastToClient(),this,true); + } + } - c->ack_req = bAckreq; - c->app = *pApp; - *pApp = nullptr; + if(GetTarget()) + GetTarget()->IsTargeted(-1); - clientpackets.push_back(std::move(c)); - return true; -} + //if we are in a group and we are not zoning, force leave the group + if(isgrouped && !bZoning && is_zone_loaded) + LeaveGroup(); -bool Client::SendAllPackets() { - CLIENTPACKET* cp = nullptr; - while (!clientpackets.empty()) { - cp = clientpackets.front().get(); - if(eqs) - eqs->FastQueuePacket((EQApplicationPacket **)&cp->app, cp->ack_req); - clientpackets.pop_front(); - } - return true; -} + UpdateWho(2); -void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CONN_STATUS required_state, eqFilterType filter) { - if (filter != FilterNone && GetFilter(filter) == FilterHide) { - return; - } + if(IsHoveringForRespawn()) + { + m_pp.zone_id = m_pp.binds[0].zone_id; + m_pp.zoneInstance = m_pp.binds[0].instance_id; + m_Position.x = m_pp.binds[0].x; + m_Position.y = m_pp.binds[0].y; + m_Position.z = m_pp.binds[0].z; + } - if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { - AddPacket(app, ack_req); - return; - } + // we save right now, because the client might be zoning and the world + // will need this data right away + Save(2); // This fails when database destructor is called first on shutdown - // if the program doesnt care about the status or if the status isnt what we requested - if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { - // todo: save packets for later use - AddPacket(app, ack_req); - } - else if (eqs) { - eqs->QueuePacket(app, ack_req); - } -} + safe_delete(task_state); + safe_delete(KarmaUpdateTimer); + safe_delete(GlobalChatLimiterTimer); + safe_delete(qGlobals); + safe_delete(adventure_request_timer); + safe_delete(adventure_create_timer); + safe_delete(adventure_leave_timer); + safe_delete(adventure_door_timer); + safe_delete(adventure_stats_timer); + safe_delete(adventure_leaderboard_timer); + safe_delete_array(adv_requested_data); + safe_delete_array(adv_data); -void Client::FastQueuePacket(EQApplicationPacket** app, bool ack_req, CLIENT_CONN_STATUS required_state) { - // if the program doesnt care about the status or if the status isnt what we requested - if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { - // todo: save packets for later use - AddPacket(app, ack_req); - return; - } - else { - if(eqs) - eqs->FastQueuePacket((EQApplicationPacket **)app, ack_req); - else if (app && (*app)) - delete *app; - *app = nullptr; - } - return; -} + ClearRespawnOptions(); -void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_skill, const char* orig_message, const char* targetname, bool is_silent) { - char message[4096]; - strn0cpy(message, orig_message, sizeof(message)); - - LogDebug("Client::ChannelMessageReceived() Channel:[{}] message:[{}]", chan_num, message); - - if (targetname == nullptr) { - targetname = (!GetTarget()) ? "" : GetTarget()->GetName(); - } - - if(RuleB(Chat, EnableAntiSpam)) - { - if(strcmp(targetname, "discard") != 0) - { - if(chan_num == ChatChannel_Shout || chan_num == ChatChannel_Auction || chan_num == ChatChannel_OOC || chan_num == ChatChannel_Tell) - { - if(GlobalChatLimiterTimer) - { - if(GlobalChatLimiterTimer->Check(false)) - { - GlobalChatLimiterTimer->Start(RuleI(Chat, IntervalDurationMS)); - AttemptedMessages = 0; - } - } - - uint32 AllowedMessages = RuleI(Chat, MinimumMessagesPerInterval) + TotalKarma; - AllowedMessages = AllowedMessages > RuleI(Chat, MaximumMessagesPerInterval) ? RuleI(Chat, MaximumMessagesPerInterval) : AllowedMessages; - - if(RuleI(Chat, MinStatusToBypassAntiSpam) <= Admin()) - AllowedMessages = 10000; - - AttemptedMessages++; - if(AttemptedMessages > AllowedMessages) - { - if(AttemptedMessages > RuleI(Chat, MaxMessagesBeforeKick)) - { - Kick("Sent too many chat messages at once."); - return; - } - if(GlobalChatLimiterTimer) - { - Message(0, "You have been rate limited, you can send more messages in %i seconds.", - GlobalChatLimiterTimer->GetRemainingTime() / 1000); - return; - } - else - { - Message(0, "You have been rate limited, you can send more messages in 60 seconds."); - return; - } - } - } - } - } - - /* Logs Player Chat */ - if (RuleB(QueryServ, PlayerLogChat)) { - auto pack = new ServerPacket(ServerOP_Speech, sizeof(Server_Speech_Struct) + strlen(message) + 1); - Server_Speech_Struct* sem = (Server_Speech_Struct*) pack->pBuffer; - - if(chan_num == ChatChannel_Guild) - sem->guilddbid = GuildID(); - else - sem->guilddbid = 0; - - strcpy(sem->message, message); - sem->minstatus = Admin(); - sem->type = chan_num; - if(targetname != 0) - strcpy(sem->to, targetname); - - if(GetName() != 0) - strcpy(sem->from, GetName()); - - if(worldserver.Connected()) - worldserver.SendPacket(pack); - safe_delete(pack); - } - - // Garble the message based on drunkness - if (GetIntoxication() > 0 && !(RuleB(Chat, ServerWideOOC) && chan_num == ChatChannel_OOC) && !GetGM()) { - GarbleMessage(message, (int)(GetIntoxication() / 3)); - language = Language::CommonTongue; // No need for language when drunk - lang_skill = Language::MaxValue; - } - - // some channels don't use languages - if ( - chan_num == ChatChannel_OOC || - chan_num == ChatChannel_GMSAY || - chan_num == ChatChannel_Broadcast || - chan_num == ChatChannel_Petition - ) { - language = Language::CommonTongue; - lang_skill = Language::MaxValue; - } - - // Censor the message - if (EQ::ProfanityManager::IsCensorshipActive() && (chan_num != ChatChannel_Say)) - EQ::ProfanityManager::RedactMessage(message); - - switch(chan_num) - { - case ChatChannel_Guild: { /* Guild Chat */ - if (!IsInAGuild()) { - MessageString(Chat::DefaultText, GUILD_NOT_MEMBER2); //You are not a member of any guild. - } else if (!guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_GUILD_CHAT_SPEAK_IN)) { - MessageString(Chat::EchoGuild, NO_PROPER_ACCESS); - } else if (!worldserver.SendChannelMessage(this, targetname, chan_num, GuildID(), language, lang_skill, message)) { - Message(Chat::White, "Error: World server disconnected"); - } - break; - } - case ChatChannel_Group: { /* Group Chat */ - Raid* raid = entity_list.GetRaidByClient(this); - if(raid) { - raid->RaidGroupSay((const char*) message, this, language, lang_skill); - break; - } - - Group* group = GetGroup(); - if(group != nullptr) { - group->GroupMessage(this,language,lang_skill,(const char*) message); - } - break; - } - case ChatChannel_Raid: { /* Raid Say */ - Raid* raid = entity_list.GetRaidByClient(this); - if(raid){ - raid->RaidSay((const char*) message, this, language, lang_skill); - } - break; - } - case ChatChannel_Shout: { /* Shout */ - Mob *sender = this; - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) - sender = GetPet(); - - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - break; - } - case ChatChannel_Auction: { /* Auction */ - if(RuleB(Chat, ServerWideAuction)) - { - if(!global_channel_timer.Check()) - { - if(strlen(targetname) == 0) - ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? - else - return; - } - - if(GetRevoked()) - { - Message(0, "You have been revoked. You may not talk on Auction."); - return; - } - - if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) - { - if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) - { - Message(0, "You do not have permission to talk in Auction at this time."); - return; - } - } - - if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - } - else if(!RuleB(Chat, ServerWideAuction)) { - Mob *sender = this; - - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) - sender = GetPet(); - - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - } - break; - } - case ChatChannel_OOC: { /* OOC */ - if(RuleB(Chat, ServerWideOOC)) - { - if(!global_channel_timer.Check()) - { - if(strlen(targetname) == 0) - ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? - else - return; - } - if(worldserver.IsOOCMuted() && admin < AccountStatus::GMAdmin) - { - Message(0,"OOC has been muted. Try again later."); - return; - } - - if(GetRevoked()) - { - Message(0, "You have been revoked. You may not talk on OOC."); - return; - } - - if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) - { - if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) - { - Message(0, "You do not have permission to talk in OOC at this time."); - return; - } - } - - if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) - { - Message(0, "Error: World server disconnected"); - } - } - else if(!RuleB(Chat, ServerWideOOC)) - { - Mob *sender = this; - - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) - sender = GetPet(); - - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - } - break; - } - case ChatChannel_Broadcast: /* Broadcast */ - case ChatChannel_GMSAY: { /* GM Say */ - if (!(admin >= AccountStatus::QuestTroupe)) - Message(0, "Error: Only GMs can use this channel"); - else if (!worldserver.SendChannelMessage(this, targetname, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - break; - } - case ChatChannel_Tell: { /* Tell */ - if(!global_channel_timer.Check()) - { - if(strlen(targetname) == 0) - ChannelMessageReceived(ChatChannel_Tell, language, lang_skill, message, "discard"); //Fast typer or spammer?? - else - return; - } - - if(GetRevoked()) - { - Message(0, "You have been revoked. You may not send tells."); - return; - } - - if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) - { - if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) - { - Message(0, "You do not have permission to send tells at this time."); - return; - } - } - - char target_name[64]; - - if(targetname) - { - size_t i = strlen(targetname); - int x; - for(x = 0; x < i; ++x) - { - if(targetname[x] == '%') - { - target_name[x] = '/'; - } - else - { - target_name[x] = targetname[x]; - } - } - target_name[x] = '\0'; - } - - if(!worldserver.SendChannelMessage(this, target_name, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - 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)) { - int i = parse->EventPlayer(EVENT_COMMAND, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Command '%s' not recognized.", message); - } - } - else if (parse->PlayerHasQuestSub(EVENT_SAY)) { - int i = parse->EventPlayer(EVENT_SAY, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Command '%s' not recognized.", message); - } - } - else { - if (!RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Command '%s' not recognized.", message); - } - } - } - break; - } - - if (message[0] == BOT_COMMAND_CHAR) { - if (RuleB(Bots, Enabled)) { - if (bot_command_dispatch(this, message) == -2) { - if (parse->PlayerHasQuestSub(EVENT_BOT_COMMAND)) { - int i = parse->EventPlayer(EVENT_BOT_COMMAND, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Bot command '%s' not recognized.", message); - } - } - else if (parse->PlayerHasQuestSub(EVENT_SAY)) { - int i = parse->EventPlayer(EVENT_SAY, this, message, 0); - if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Bot command '%s' not recognized.", message); - } - } - else { - if (!RuleB(Chat, SuppressCommandErrors)) { - Message(Chat::Red, "Bot command '%s' not recognized.", message); - } - } - } - } else { - Message(Chat::Red, "Bots are disabled on this server."); - } - break; - } - - if (EQ::ProfanityManager::IsCensorshipActive()) { - EQ::ProfanityManager::RedactMessage(message); - } - - Mob* sender = this; - if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) { - sender = GetPet(); - } - - if (!is_silent) { - entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); - } - - if (parse->PlayerHasQuestSub(EVENT_SAY)) { - parse->EventPlayer(EVENT_SAY, this, message, language); - } - - if (sender != this) { - break; - } - - if (quest_manager.ProximitySayInUse()) { - entity_list.ProcessProximitySay(message, this, language); - } - - Mob* t = GetTarget(); - - if ( - t && - !IsInvisible(t) && - DistanceNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say) - ) { - const bool is_engaged = t->IsEngaged(); - - if (is_engaged) { - parse->EventBotMercNPC(EVENT_AGGRO_SAY, t, this, [&]() { return message; }, language); - } else { - parse->EventBotMercNPC(EVENT_SAY, t, this, [&]() { return message; }, language); - } - - if (t->IsNPC() && !is_engaged) { - CheckLDoNHail(t->CastToNPC()); - CheckEmoteHail(t->CastToNPC(), message); - - if (RuleB(TaskSystem, EnableTaskSystem)) { - if (UpdateTasksOnSpeakWith(t->CastToNPC())) { - t->CastToNPC()->DoQuestPause(this); - } - } - } - } - break; - } - case ChatChannel_UCSRelay: - { - // UCS Relay for Underfoot and later. - if(!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) - Message(0, "Error: World server disconnected"); - break; - } - case ChatChannel_Emotes: - { - // Emotes for Underfoot and later. - // crash protection -- cheater - message[1023] = '\0'; - size_t msg_len = strlen(message); - if (msg_len > 512) - message[512] = '\0'; - - auto outapp = new EQApplicationPacket(OP_Emote, 4 + msg_len + strlen(GetName()) + 2); - Emote_Struct* es = (Emote_Struct*)outapp->pBuffer; - char *Buffer = (char *)es; - Buffer += 4; - snprintf(Buffer, sizeof(Emote_Struct) - 4, "%s %s", GetName(), message); - entity_list.QueueCloseClients(this, outapp, true, RuleI(Range, Emote), 0, true, FilterSocials); - safe_delete(outapp); - break; - } - default: { - Message(0, "Channel (%i) not implemented", (uint16)chan_num); - } - } -} + numclients--; + UpdateWindowTitle(nullptr); + if(zone) + zone->RemoveAuth(GetName(), lskey); -void Client::ChannelMessageSend( - const char *from, - const char *to, - uint8 channel_id, - uint8 language_id, - uint8 language_skill, - const char *message, - ... -) -{ - if ( - (channel_id == ChatChannel_Petition && Admin() < AccountStatus::QuestTroupe) || - (channel_id == ChatChannel_GMSAY && !GetGM()) - ) { - return; - } - - va_list argptr; - char buffer[4096]; - char message_sender[64]; - - va_start(argptr, message); - vsnprintf(buffer, 4096, message, argptr); - va_end(argptr); - - EQApplicationPacket app(OP_ChannelMessage, sizeof(ChannelMessage_Struct) + strlen(buffer) + 1); - - auto* cm = (ChannelMessage_Struct *) app.pBuffer; - - if (from == 0) { - strcpy(cm->sender, "ZServer"); - } else if (from[0] == 0) { - strcpy(cm->sender, "ZServer"); - } else { - CleanMobName(from, message_sender); - strcpy(cm->sender, message_sender); - } - - if (to != 0) { - strcpy((char *) cm->targetname, to); - } else if (channel_id == ChatChannel_Tell) { - strcpy(cm->targetname, m_pp.name); - } else { - cm->targetname[0] = 0; - } - - uint8 listener_skill; - - const bool is_valid_language = EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27); - - if (is_valid_language) { - listener_skill = m_pp.languages[language_id]; - cm->language = listener_skill < 24 ? Language::Unknown27 : language_id; - } else { - listener_skill = m_pp.languages[Language::CommonTongue]; - cm->language = Language::CommonTongue; - } - - // set effective language skill = lower of sender and receiver skills - uint8 effective_skill = (language_skill < listener_skill ? language_skill : listener_skill); - if (effective_skill > Language::MaxValue) { - effective_skill = Language::MaxValue; - } - - cm->skill_in_language = effective_skill; - - cm->chan_num = channel_id; - strcpy(&cm->message[0], buffer); - - QueuePacket(&app); - - const bool can_train_self = RuleB(Client, SelfLanguageLearning); - const bool is_not_sender = strcmp(GetCleanName(), cm->sender); - - if (can_train_self || is_not_sender) { - if ( - channel_id == ChatChannel_Group && - listener_skill < Language::MaxValue - ) { // group message in non-mastered language, check for skill up - if (is_valid_language && m_pp.languages[language_id] <= language_skill) { - CheckLanguageSkillIncrease(language_id, language_skill); - } - } - } -} + //let the stream factory know were done with this stream + eqs->Close(); + eqs->ReleaseFromUse(); + safe_delete(eqs); -void Client::Message(uint32 type, const char* message, ...) { - if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) - return; - if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... - return; - if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) - return; - - va_list argptr; - auto buffer = new char[4096]; - va_start(argptr, message); - vsnprintf(buffer, 4096, message, argptr); - va_end(argptr); - - SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); - buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); - buf.WriteInt8(static_cast(Journal::Mode::None)); - buf.WriteInt8(0); // language - buf.WriteUInt32(type); - buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here - buf.WriteString(""); // send name, not applicable here - buf.WriteInt32(0); // location, client seems to ignore - buf.WriteInt32(0); - buf.WriteInt32(0); - buf.WriteString(buffer); - - auto app = new EQApplicationPacket(OP_SpecialMesg, buf); - - FastQueuePacket(&app); - - safe_delete_array(buffer); + UninitializeBuffSlots(); } -void Client::FilteredMessage(Mob *sender, uint32 type, eqFilterType filter, const char* message, ...) { - if (!FilteredMessageCheck(sender, filter)) - return; +void Client::SendZoneInPackets() +{ - va_list argptr; - auto buffer = new char[4096]; - va_start(argptr, message); - vsnprintf(buffer, 4096, message, argptr); - va_end(argptr); + ////////////////////////////////////////////////////// + // Spawn Appearance Packet + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* sa = (SpawnAppearance_Struct*)outapp->pBuffer; + sa->type = AppearanceType::SpawnID; // Is 0x10 used to set the player id? + sa->parameter = GetID(); // Four bytes for this parameter... + outapp->priority = 6; + QueuePacket(outapp); + safe_delete(outapp); + + // Inform the world about the client + outapp = new EQApplicationPacket(); + + CreateSpawnPacket(outapp); + outapp->priority = 6; + if (!GetHideMe()) entity_list.QueueClients(this, outapp, true); + safe_delete(outapp); + SetSpawned(); + if (GetPVP(false)) //force a PVP update until we fix the spawn struct + SendAppearancePacket(AppearanceType::PVP, GetPVP(false), true, false); + + //Send AA Exp packet: + if (GetLevel() >= 51) + SendAlternateAdvancementStats(); + + // Send exp packets + outapp = new EQApplicationPacket(OP_ExpUpdate, sizeof(ExpUpdate_Struct)); + ExpUpdate_Struct* eu = (ExpUpdate_Struct*)outapp->pBuffer; + uint32 tmpxp1 = GetEXPForLevel(GetLevel() + 1); + uint32 tmpxp2 = GetEXPForLevel(GetLevel()); + + // Crash bug fix... Divide by zero when tmpxp1 and 2 equalled each other, most likely the error case from GetEXPForLevel() (invalid class, etc) + if (tmpxp1 != tmpxp2 && tmpxp1 != 0xFFFFFFFF && tmpxp2 != 0xFFFFFFFF) { + float tmpxp = (float)((float)m_pp.exp - tmpxp2) / ((float)tmpxp1 - tmpxp2); + eu->exp = (uint32)(330.0f * tmpxp); + outapp->priority = 6; + QueuePacket(outapp); + } + safe_delete(outapp); + + SendAlternateAdvancementTimers(); + + outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(ZoneInSendName_Struct)); + ZoneInSendName_Struct* zonesendname = (ZoneInSendName_Struct*)outapp->pBuffer; + strcpy(zonesendname->name, m_pp.name); + strcpy(zonesendname->name2, m_pp.name); + zonesendname->unknown0 = 0x0A; + QueuePacket(outapp); + safe_delete(outapp); + + if (IsInAGuild()) { + guild_mgr.UpdateDbMemberOnline(CharacterID(), true); + //SendGuildMembers(); + SendGuildURL(); + SendGuildChannel(); + SendGuildLFGuildStatus(); + } + SendLFGuildStatus(); - SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); - buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); - buf.WriteInt8(static_cast(Journal::Mode::None)); - buf.WriteInt8(0); // language - buf.WriteUInt32(type); - buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here - buf.WriteString(""); // send name, not applicable here - buf.WriteInt32(0); // location, client seems to ignore - buf.WriteInt32(0); - buf.WriteInt32(0); - buf.WriteString(buffer); + //No idea why live sends this if even were not in a guild + SendGuildMOTD(); +} - auto app = new EQApplicationPacket(OP_SpecialMesg, buf); +void Client::SendLogoutPackets() { - FastQueuePacket(&app); + auto outapp = new EQApplicationPacket(OP_CancelTrade, sizeof(CancelTrade_Struct)); + CancelTrade_Struct* ct = (CancelTrade_Struct*) outapp->pBuffer; + ct->fromid = GetID(); + ct->action = groupActUpdate; + FastQueuePacket(&outapp); - safe_delete_array(buffer); -} + outapp = new EQApplicationPacket(OP_PreLogoutReply); + FastQueuePacket(&outapp); -void Client::SetMaxHP() { - if(dead) - return; - SetHP(CalcMaxHP()); - SendHPUpdate(); - Save(); } -bool Client::UpdateLDoNPoints(uint32 theme_id, int points) -{ - if (points < 0) { - if (m_pp.ldon_points_available < (0 - points)) { - return false; - } - } - - bool is_loss = false; - - switch (theme_id) { - case LDoNTheme::Unused: { // No theme, so distribute evenly across all - int split_points = (points / 5); - - int guk_points = (split_points + (points % 5)); - int mir_points = split_points; - int mmc_points = split_points; - int ruj_points = split_points; - int tak_points = split_points; - - split_points = 0; - - if (points < 0) { - if (m_pp.ldon_points_available < (0 - points)) { - return false; - } - - is_loss = true; - - if (m_pp.ldon_points_guk < (0 - guk_points)) { - mir_points += (guk_points + m_pp.ldon_points_guk); - guk_points = (0 - m_pp.ldon_points_guk); - } - - if (m_pp.ldon_points_mir < (0 - mir_points)) { - mmc_points += (mir_points + m_pp.ldon_points_mir); - mir_points = (0 - m_pp.ldon_points_mir); - } - - if (m_pp.ldon_points_mmc < (0 - mmc_points)) { - ruj_points += (mmc_points + m_pp.ldon_points_mmc); - mmc_points = (0 - m_pp.ldon_points_mmc); - } - - if (m_pp.ldon_points_ruj < (0 - ruj_points)) { - tak_points += (ruj_points + m_pp.ldon_points_ruj); - ruj_points = (0 - m_pp.ldon_points_ruj); - } - - if (m_pp.ldon_points_tak < (0 - tak_points)) { - split_points = (tak_points + m_pp.ldon_points_tak); - tak_points = (0 - m_pp.ldon_points_tak); - } - } - - m_pp.ldon_points_guk += guk_points; - m_pp.ldon_points_mir += mir_points; - m_pp.ldon_points_mmc += mmc_points; - m_pp.ldon_points_ruj += ruj_points; - m_pp.ldon_points_tak += tak_points; - - points -= split_points; - - if (split_points != 0) { // if anything left, recursively loop thru again - UpdateLDoNPoints(LDoNTheme::Unused, split_points); - } - - break; - } - case LDoNTheme::GUK: { - if (points < 0) { - if (m_pp.ldon_points_guk < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_guk += points; - break; - } - case LDoNTheme::MIR: { - if (points < 0) { - if (m_pp.ldon_points_mir < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_mir += points; - break; - } - case LDoNTheme::MMC: { - if (points < 0) { - if (m_pp.ldon_points_mmc < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_mmc += points; - break; - } - case LDoNTheme::RUJ: { - if (points < 0) { - if (m_pp.ldon_points_ruj < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_ruj += points; - break; - } - case LDoNTheme::TAK: { - if (points < 0) { - if (m_pp.ldon_points_tak < (0 - points)) { - return false; - } - - is_loss = true; - } - - m_pp.ldon_points_tak += points; - break; - } - } - - m_pp.ldon_points_available += points; - - QuestEventID event_id = is_loss ? EVENT_LDON_POINTS_LOSS : EVENT_LDON_POINTS_GAIN; - - if (parse->PlayerHasQuestSub(event_id)) { - const std::string &export_string = fmt::format( - "{} {}", - theme_id, - std::abs(points) - ); - - parse->EventPlayer(event_id, this, export_string, 0); - } - - auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); - auto *apus = (AdventurePoints_Update_Struct *) outapp->pBuffer; - - apus->ldon_available_points = m_pp.ldon_points_available; - apus->ldon_guk_points = m_pp.ldon_points_guk; - apus->ldon_mirugal_points = m_pp.ldon_points_mir; - apus->ldon_mistmoore_points = m_pp.ldon_points_mmc; - apus->ldon_rujarkian_points = m_pp.ldon_points_ruj; - apus->ldon_takish_points = m_pp.ldon_points_tak; - - outapp->priority = 6; - - QueuePacket(outapp); - safe_delete(outapp); - return true; +void Client::ReportConnectingState() { + switch(conn_state) { + case NoPacketsReceived: //havent gotten anything + LogDebug("Client has not sent us an initial zone entry packet"); + break; + case ReceivedZoneEntry: //got the first packet, loading up PP + LogDebug("Client sent initial zone packet, but we never got their player info from the database"); + break; + case PlayerProfileLoaded: //our DB work is done, sending it + LogDebug("We were sending the player profile, tributes, tasks, spawns, time and weather, but never finished"); + break; + case ZoneInfoSent: //includes PP, tributes, tasks, spawns, time and weather + LogDebug("We successfully sent player info and spawns, waiting for client to request new zone"); + break; + case NewZoneRequested: //received and sent new zone request + LogDebug("We received client's new zone request, waiting for client spawn request"); + break; + case ClientSpawnRequested: //client sent ReqClientSpawn + LogDebug("We received the client spawn request, and were sending objects, doors, zone points and some other stuff, but never finished"); + break; + case ZoneContentsSent: //objects, doors, zone points + LogDebug("The rest of the zone contents were successfully sent, waiting for client ready notification"); + break; + case ClientReadyReceived: //client told us its ready, send them a bunch of crap like guild MOTD, etc + LogDebug("We received client ready notification, but never finished Client::CompleteConnect"); + break; + case ClientConnectFinished: //client finally moved to finished state, were done here + LogDebug("Client is successfully connected"); + break; + }; } -void Client::SetLDoNPoints(uint32 theme_id, uint32 points) +bool Client::SaveAA() { - switch (theme_id) { - case LDoNTheme::GUK: { - m_pp.ldon_points_guk = points; - break; - } - case LDoNTheme::MIR: { - m_pp.ldon_points_mir = points; - break; - } - case LDoNTheme::MMC: { - m_pp.ldon_points_mmc = points; - break; - } - case LDoNTheme::RUJ: { - m_pp.ldon_points_ruj = points; - break; - } - case LDoNTheme::TAK: { - m_pp.ldon_points_tak = points; - break; - } - } - - m_pp.ldon_points_available = ( - m_pp.ldon_points_guk + - m_pp.ldon_points_mir + - m_pp.ldon_points_mmc + - m_pp.ldon_points_ruj + - m_pp.ldon_points_tak - ); - - auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); - - auto a = (AdventurePoints_Update_Struct*) outapp->pBuffer; - - a->ldon_available_points = m_pp.ldon_points_available; - a->ldon_guk_points = m_pp.ldon_points_guk; - a->ldon_mirugal_points = m_pp.ldon_points_mir; - a->ldon_mistmoore_points = m_pp.ldon_points_mmc; - a->ldon_rujarkian_points = m_pp.ldon_points_ruj; - a->ldon_takish_points = m_pp.ldon_points_tak; - - outapp->priority = 6; - - QueuePacket(outapp); - safe_delete(outapp); -} - -void Client::SetSkill(EQ::skills::SkillType skillid, uint16 value) { - if (skillid > EQ::skills::HIGHEST_SKILL) - return; - m_pp.skills[skillid] = value; // We need to be able to #setskill 254 and 255 to reset skills + std::vector v; - database.SaveCharacterSkill(CharacterID(), skillid, value); - auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); - SkillUpdate_Struct* skill = (SkillUpdate_Struct*)outapp->pBuffer; - skill->skillId=skillid; - skill->value=value; - QueuePacket(outapp); - safe_delete(outapp); -} + uint32 aa_points_spent = 0; -void Client::IncreaseLanguageSkill(uint8 language_id, uint8 increase) -{ - if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { - return; - } + auto e = CharacterAlternateAbilitiesRepository::NewEntity(); - m_pp.languages[language_id] += increase; + for (auto &rank : aa_ranks) { + auto a = zone->GetAlternateAdvancementAbility(rank.first); + if (!a) { + continue; + } - if (m_pp.languages[language_id] > Language::MaxValue) { - m_pp.languages[language_id] = Language::MaxValue; - } + if (rank.second.first > 0) { + auto r = a->GetRankByPointsSpent(rank.second.first); + if (!r) { + continue; + } - database.SaveCharacterLanguage(CharacterID(), language_id, m_pp.languages[language_id]); + aa_points_spent += r->total_cost; - auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); - auto* s = (SkillUpdate_Struct*) outapp->pBuffer; + e.id = character_id; + e.aa_id = a->first_rank_id; + e.aa_value = rank.second.first; + e.charges = rank.second.second; - s->skillId = 100 + language_id; - s->value = m_pp.languages[language_id]; + v.emplace_back(e); + } + } - QueuePacket(outapp); - safe_delete(outapp); + m_pp.aapoints_spent = aa_points_spent + m_epp.expended_aa; - MessageString(Chat::Skills, LANG_SKILL_IMPROVED); + return CharacterAlternateAbilitiesRepository::ReplaceMany(database, v); } -void Client::AddSkill(EQ::skills::SkillType skillid, uint16 value) { - if (skillid > EQ::skills::HIGHEST_SKILL) - return; - value = GetRawSkill(skillid) + value; - uint16 max = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); - if (value > max) - value = max; - SetSkill(skillid, value); +void Client::RemoveExpendedAA(int aa_id) +{ + CharacterAlternateAbilitiesRepository::DeleteWhere( + database, + fmt::format( + "`id` = {} AND `aa_id` = {}", + CharacterID(), + aa_id + ) + ); } -void Client::SendSound(){//Makes a sound. - auto outapp = new EQApplicationPacket(OP_Sound, 68); - unsigned char x[68]; - memset(x, 0, 68); - x[0]=0x22; - memset(&x[4],0x8002,sizeof(uint16)); - memset(&x[8],0x8624,sizeof(uint16)); - memset(&x[12],0x4A01,sizeof(uint16)); - x[16]=0x05; - x[28]=0x00;//change this value to give gold to the client - memset(&x[40],0xFFFFFFFF,sizeof(uint32)); - memset(&x[44],0xFFFFFFFF,sizeof(uint32)); - memset(&x[48],0xFFFFFFFF,sizeof(uint32)); - memset(&x[52],0xFFFFFFFF,sizeof(uint32)); - memset(&x[56],0xFFFFFFFF,sizeof(uint32)); - memset(&x[60],0xFFFFFFFF,sizeof(uint32)); - memset(&x[64],0xffffffff,sizeof(uint32)); - memcpy(outapp->pBuffer,x,outapp->size); - QueuePacket(outapp); - safe_delete(outapp); +bool Client::Save(uint8 iCommitNow) { + if(!ClientDataLoaded()) + return false; + + /* Wrote current basics to PP for saves */ + if (!m_lock_save_position) { + m_pp.x = m_Position.x; + m_pp.y = m_Position.y; + m_pp.z = m_Position.z; + m_pp.heading = m_Position.w; + } -} -void Client::UpdateWho(uint8 remove) -{ - if (account_id == 0) { - return; - } - if (!worldserver.Connected()) { - return; - } - - auto pack = new ServerPacket(ServerOP_ClientList, sizeof(ServerClientList_Struct)); - auto *s = (ServerClientList_Struct *) pack->pBuffer; - s->remove = remove; - s->wid = GetWID(); - s->IP = GetIP(); - s->charid = CharacterID(); - strcpy(s->name, GetName()); - - s->gm = GetGM(); - s->Admin = Admin(); - s->AccountID = AccountID(); - strcpy(s->AccountName, AccountName()); - - s->LSAccountID = LSAccountID(); - strn0cpy(s->lskey, lskey, sizeof(s->lskey)); - - s->zone = zone->GetZoneID(); - s->instance_id = zone->GetInstanceID(); - s->race = GetRace(); - s->class_ = GetClass(); - s->level = GetLevel(); - - if (m_pp.anon == 0) { - s->anon = 0; - } - else if (m_pp.anon == 1) { - s->anon = 1; - } - else if (m_pp.anon >= 2) { - s->anon = 2; - } - - s->ClientVersion = static_cast(ClientVersion()); - s->tellsoff = tellsoff; - s->guild_id = guild_id; - s->guild_rank = guildrank; - s->guild_tribute_opt_in = guild_tribute_opt_in; - s->LFG = LFG; - if (LFG) { - s->LFGFromLevel = LFGFromLevel; - s->LFGToLevel = LFGToLevel; - s->LFGMatchFilter = LFGMatchFilter; - memcpy(s->LFGComments, LFGComments, sizeof(s->LFGComments)); - } - - worldserver.SendPacket(pack); - safe_delete(pack); -} + m_pp.guildrank = guildrank; -void Client::WhoAll(Who_All_Struct* whom) { + if (dead && GetHP() <= 0) { + m_pp.cur_hp = GetMaxHP(); + m_pp.mana = current_mana; + if (RuleB(Character, FullManaOnDeath)) { + m_pp.mana = GetMaxMana(); + } - if (!worldserver.Connected()) - Message(0, "Error: World server disconnected"); - else { - auto pack = new ServerPacket(ServerOP_Who, sizeof(ServerWhoAll_Struct)); - ServerWhoAll_Struct* whoall = (ServerWhoAll_Struct*) pack->pBuffer; - whoall->admin = Admin(); - whoall->fromid=GetID(); - strcpy(whoall->from, GetName()); - strn0cpy(whoall->whom, whom->whom, 64); - whoall->lvllow = whom->lvllow; - whoall->lvlhigh = whom->lvlhigh; - whoall->gmlookup = whom->gmlookup; - whoall->wclass = whom->wclass; - whoall->wrace = whom->wrace; - worldserver.SendPacket(pack); - safe_delete(pack); - } -} + m_pp.endurance = current_endurance; + if (RuleB(Character, FullEndurOnDeath)) { + m_pp.endurance = GetMaxEndurance(); + } + } else { // Otherwise, no changes. + m_pp.cur_hp = GetHP(); + m_pp.mana = current_mana; + m_pp.endurance = current_endurance; + } -void Client::FriendsWho(char *FriendsString) { + /* Save Character Currency */ + database.SaveCharacterCurrency(CharacterID(), &m_pp); - if (!worldserver.Connected()) - Message(0, "Error: World server disconnected"); - else { - auto pack = - new ServerPacket(ServerOP_FriendsWho, sizeof(ServerFriendsWho_Struct) + strlen(FriendsString)); - ServerFriendsWho_Struct* FriendsWho = (ServerFriendsWho_Struct*) pack->pBuffer; - FriendsWho->FromID = GetID(); - strcpy(FriendsWho->FromName, GetName()); - strcpy(FriendsWho->FriendsString, FriendsString); - worldserver.SendPacket(pack); - safe_delete(pack); - } -} + // save character binds + // this may not need to be called in Save() but it's here for now + // to maintain the current behavior + database.SaveCharacterBinds(this); -void Client::UpdateAdmin(bool from_database) { - int16 tmp = admin; - if (from_database) { - admin = database.GetAccountStatus(account_id); - } + /* Save Character Buffs */ + database.SaveBuffs(this); - if (tmp == admin && from_database) { - return; - } + /* Total Time Played */ + TotalSecondsPlayed += (time(nullptr) - m_pp.lastlogin); + m_pp.timePlayedMin = (TotalSecondsPlayed / 60); + m_pp.RestTimer = GetRestTimer(); - if (m_pp.gm) { - LogInfo("[{}] - [{}] is a GM", __FUNCTION__ , GetName()); - petition_list.UpdateGMQueue(); - } + /* Save Mercs */ + if (GetMercInfo().MercTimerRemaining > RuleI(Mercs, UpkeepIntervalMS)) { + GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS); + } - UpdateWho(); -} + if (GetMercTimer()->Enabled()) { + GetMercInfo().MercTimerRemaining = GetMercTimer()->GetRemainingTime(); + } -void Client::SetStats(uint8 type,int16 set_val){ - if(type>STAT_DISEASE){ - printf("Error in Client::IncStats, received invalid type of: %i\n",type); - return; - } - auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); - IncreaseStat_Struct* iss=(IncreaseStat_Struct*)outapp->pBuffer; - switch(type){ - case STAT_STR: - if(set_val>0) - iss->str=set_val; - if(set_val<0) - m_pp.STR=0; - else if(set_val>255) - m_pp.STR=255; - else - m_pp.STR=set_val; - break; - case STAT_STA: - if(set_val>0) - iss->sta=set_val; - if(set_val<0) - m_pp.STA=0; - else if(set_val>255) - m_pp.STA=255; - else - m_pp.STA=set_val; - break; - case STAT_AGI: - if(set_val>0) - iss->agi=set_val; - if(set_val<0) - m_pp.AGI=0; - else if(set_val>255) - m_pp.AGI=255; - else - m_pp.AGI=set_val; - break; - case STAT_DEX: - if(set_val>0) - iss->dex=set_val; - if(set_val<0) - m_pp.DEX=0; - else if(set_val>255) - m_pp.DEX=255; - else - m_pp.DEX=set_val; - break; - case STAT_INT: - if(set_val>0) - iss->int_=set_val; - if(set_val<0) - m_pp.INT=0; - else if(set_val>255) - m_pp.INT=255; - else - m_pp.INT=set_val; - break; - case STAT_WIS: - if(set_val>0) - iss->wis=set_val; - if(set_val<0) - m_pp.WIS=0; - else if(set_val>255) - m_pp.WIS=255; - else - m_pp.WIS=set_val; - break; - case STAT_CHA: - if(set_val>0) - iss->cha=set_val; - if(set_val<0) - m_pp.CHA=0; - else if(set_val>255) - m_pp.CHA=255; - else - m_pp.CHA=set_val; - break; - } - QueuePacket(outapp); - safe_delete(outapp); -} + if (dead || (!GetMerc() && !GetMercInfo().IsSuspended)) { + memset(&m_mercinfo, 0, sizeof(struct MercInfo)); + } -void Client::IncStats(uint8 type, int16 increase_val) -{ - if (type > STAT_DISEASE) { - printf("Error in Client::IncStats, received invalid type of: %i\n", type); - return; - } - auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); - IncreaseStat_Struct *iss = (IncreaseStat_Struct *) outapp->pBuffer; - switch (type) { - case STAT_STR: - if (increase_val > 0) { - iss->str = increase_val; - } - - if ((m_pp.STR + increase_val * 2) > 255) { - m_pp.STR = 255; - } else { - m_pp.STR += increase_val * 2; - } - break; - case STAT_STA: - if (increase_val > 0) { - iss->sta = increase_val; - } - - if ((m_pp.STA + increase_val * 2) > 255) { - m_pp.STA = 255; - } else { - m_pp.STA += increase_val * 2; - } - break; - case STAT_AGI: - if (increase_val > 0) { - iss->agi = increase_val; - } - if ((m_pp.AGI + increase_val * 2) > 255) { - m_pp.AGI = 255; - } else { - m_pp.AGI += increase_val * 2; - } - break; - case STAT_DEX: - if (increase_val > 0) { - iss->dex = increase_val; - } - - if ((m_pp.DEX + increase_val * 2) > 255) { - m_pp.DEX = 255; - } else { - m_pp.DEX += increase_val * 2; - } - break; - case STAT_INT: - if (increase_val > 0) { - iss->int_ = increase_val; - } - - if ((m_pp.INT + increase_val * 2) > 255) { - m_pp.INT = 255; - } else { - m_pp.INT += increase_val * 2; - } - break; - case STAT_WIS: - if (increase_val > 0) { - iss->wis = increase_val; - } - - if ((m_pp.WIS + increase_val * 2) > 255) { - m_pp.WIS = 255; - } else { - m_pp.WIS += increase_val * 2; - } - break; - case STAT_CHA: - if (increase_val > 0) { - iss->cha = increase_val; - } - - if ((m_pp.CHA + increase_val * 2) > 255) { - m_pp.CHA = 255; - } else { - m_pp.CHA += increase_val * 2; - } - break; - } - QueuePacket(outapp); - safe_delete(outapp); -} + m_pp.lastlogin = time(nullptr); + + if (GetPet() && GetPet()->CastToNPC()->GetPetSpellID() && !dead) { + NPC *pet = GetPet()->CastToNPC(); + m_petinfo.SpellID = pet->CastToNPC()->GetPetSpellID(); + m_petinfo.HP = pet->GetHP(); + m_petinfo.Mana = pet->GetMana(); + pet->GetPetState(m_petinfo.Buffs, m_petinfo.Items, m_petinfo.Name); + m_petinfo.petpower = pet->GetPetPower(); + m_petinfo.size = pet->GetSize(); + m_petinfo.taunting = pet->CastToNPC()->IsTaunting(); + } else { + memset(&m_petinfo, 0, sizeof(struct PetInfo)); + } + database.SavePetInfo(this); -const int64& Client::SetMana(int64 amount) { - bool update = false; - if (amount < 0) - amount = 0; - if (amount > GetMaxMana()) - amount = GetMaxMana(); - if (amount != current_mana) - update = true; - current_mana = amount; - if (update) - Mob::SetMana(amount); - CheckManaEndUpdate(); - return current_mana; -} + if(tribute_timer.Enabled()) { + m_pp.tribute_time_remaining = tribute_timer.GetRemainingTime(); + } + else { + m_pp.tribute_time_remaining = 0xFFFFFFFF; m_pp.tribute_active = 0; + } -void Client::CheckManaEndUpdate() { - if (!Connected()) - return; - - if (last_reported_mana != current_mana || last_reported_endurance != current_endurance) { - - if (ClientVersion() >= EQ::versions::ClientVersion::SoD) { - SendManaUpdate(); - SendEnduranceUpdate(); - } - - auto outapp = new EQApplicationPacket(OP_ManaChange, sizeof(ManaChange_Struct)); - ManaChange_Struct* mana_change = (ManaChange_Struct*)outapp->pBuffer; - mana_change->new_mana = current_mana; - mana_change->stamina = current_endurance; - mana_change->spell_id = casting_spell_id; - mana_change->keepcasting = 1; - mana_change->slot = -1; - outapp->priority = 6; - QueuePacket(outapp); - safe_delete(outapp); - - /* Let others know when our mana percent has changed */ - if (GetManaPercent() != last_reported_mana_percent) { - Group *group = GetGroup(); - Raid *raid = GetRaid(); - - if (raid) { - raid->SendManaPacketFrom(this); - } - else if (group) { - group->SendManaPacketFrom(this); - } - - auto mana_packet = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); - ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_packet->pBuffer; - mana_update->cur_mana = GetMana(); - mana_update->max_mana = GetMaxMana(); - mana_update->spawn_id = GetID(); - if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) - QueuePacket(mana_packet); // do we need this with the OP_ManaChange packet above? - entity_list.QueueClientsByXTarget(this, mana_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); - safe_delete(mana_packet); - - last_reported_mana_percent = GetManaPercent(); - } - - /* Let others know when our endurance percent has changed */ - if (GetEndurancePercent() != last_reported_endurance_percent) { - Group *group = GetGroup(); - Raid *raid = GetRaid(); - - if (raid) { - raid->SendEndurancePacketFrom(this); - } - else if (group) { - group->SendEndurancePacketFrom(this); - } - - auto endurance_packet = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); - EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)endurance_packet->pBuffer; - endurance_update->cur_end = GetEndurance(); - endurance_update->max_end = GetMaxEndurance(); - endurance_update->spawn_id = GetID(); - if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) - QueuePacket(endurance_packet); // do we need this with the OP_ManaChange packet above? - entity_list.QueueClientsByXTarget(this, endurance_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); - safe_delete(endurance_packet); - - last_reported_endurance_percent = GetEndurancePercent(); - } - - last_reported_mana = current_mana; - last_reported_endurance = current_endurance; - } -} + if (m_pp.hunger_level < 0) + m_pp.hunger_level = 0; -// sends mana update to self -void Client::SendManaUpdate() -{ - auto mana_app = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); - ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_app->pBuffer; - mana_update->cur_mana = GetMana(); - mana_update->max_mana = GetMaxMana(); - mana_update->spawn_id = GetID(); - QueuePacket(mana_app); - safe_delete(mana_app); -} + if (m_pp.thirst_level < 0) + m_pp.thirst_level = 0; -// sends endurance update to self -void Client::SendEnduranceUpdate() -{ - auto end_app = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); - EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)end_app->pBuffer; - endurance_update->cur_end = GetEndurance(); - endurance_update->max_end = GetMaxEndurance(); - endurance_update->spawn_id = GetID(); - QueuePacket(end_app); - safe_delete(end_app); -} + p_timers.Store(&database); -void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) -{ - Mob::FillSpawnStruct(ns, ForWho); + database.SaveCharacterTribute(this); + SaveTaskState(); /* Save Character Task */ - // Populate client-specific spawn information - ns->spawn.afk = AFK; - ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live - ns->spawn.anon = m_pp.anon; - ns->spawn.gm = GetGM() ? 1 : 0; - ns->spawn.guildID = GuildID(); - ns->spawn.trader = IsTrader(); - ns->spawn.buyer = IsBuyer(); -// ns->spawn.linkdead = IsLD() ? 1 : 0; -// ns->spawn.pvp = GetPVP(false) ? 1 : 0; - ns->spawn.show_name = true; - - strcpy(ns->spawn.title, m_pp.title); - strcpy(ns->spawn.suffix, m_pp.suffix); - - if (IsBecomeNPC() == true) - ns->spawn.NPC = 1; - else if (ForWho == this) - ns->spawn.NPC = 10; - else - ns->spawn.NPC = 0; - ns->spawn.is_pet = 0; - - if (!IsInAGuild()) { - ns->spawn.guildrank = 0xFF; - } else { - ns->spawn.guildrank = guild_mgr.GetDisplayedRank(GuildID(), GuildRank(), CharacterID()); - ns->spawn.guild_show = guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_DISPLAY_GUILD_NAME); - } - ns->spawn.size = 0; // Changing size works, but then movement stops! (wth?) - ns->spawn.runspeed = (gmspeed == 0) ? runspeed : 3.125f; - ns->spawn.showhelm = m_pp.showhelm ? 1 : 0; - - UpdateEquipmentLight(); - UpdateActiveLight(); - ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive]; -} + LogFood("Client::Save - hunger_level: [{}] thirst_level: [{}]", m_pp.hunger_level, m_pp.thirst_level); -bool Client::GMHideMe(Client* client) { - if (gm_hide_me) { - if (client == 0) - return true; - else if (admin > client->Admin()) - return true; - else - return false; - } - else - return false; -} + // perform snapshot before SaveCharacterData() so that m_epp will contain the updated time + if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { + if (database.SaveCharacterInvSnapshot(CharacterID())) { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); + } + else { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); + } + } -void Client::Duck() { - SetAppearance(eaCrouching, false); + database.SaveCharacterData(this, &m_pp, &m_epp); /* Save Character Data */ + + database.SaveCharacterEXPModifier(this); + + return true; } -void Client::Stand() { - SetAppearance(eaStanding, false); +CLIENTPACKET::CLIENTPACKET() +{ + app = nullptr; + ack_req = false; } -void Client::Sit() { - SetAppearance(eaSitting, false); +CLIENTPACKET::~CLIENTPACKET() +{ + safe_delete(app); } -void Client::ChangeLastName(std::string last_name) { - memset(m_pp.last_name, 0, sizeof(m_pp.last_name)); - strn0cpy(m_pp.last_name, last_name.c_str(), sizeof(m_pp.last_name)); - auto outapp = new EQApplicationPacket(OP_GMLastName, sizeof(GMLastName_Struct)); - auto gmn = (GMLastName_Struct*) outapp->pBuffer; - strn0cpy(gmn->name, name, sizeof(gmn->name)); - strn0cpy(gmn->gmname, name, sizeof(gmn->gmname)); - strn0cpy(gmn->lastname, last_name.c_str(), sizeof(gmn->lastname)); +//this assumes we do not own pApp, and clones it. +bool Client::AddPacket(const EQApplicationPacket *pApp, bool bAckreq) { + if (!pApp) + return false; + if(!zoneinpacket_timer.Enabled()) { + //drop the packet because it will never get sent. + return(false); + } - gmn->unknown[0] = 1; - gmn->unknown[1] = 1; - gmn->unknown[2] = 1; - gmn->unknown[3] = 1; + auto c = std::make_unique(); - entity_list.QueueClients(this, outapp, false); + c->ack_req = bAckreq; + c->app = pApp->Copy(); - safe_delete(outapp); + clientpackets.push_back(std::move(c)); + return true; } -bool Client::ChangeFirstName(const char* in_firstname, const char* gmname) -{ - // check duplicate name - bool used_name = database.IsNameUsed((const char*) in_firstname); - if (used_name) { - return false; - } - - // update character_ - if(!database.UpdateName(GetName(), in_firstname)) - return false; - - // update pp - memset(m_pp.name, 0, sizeof(m_pp.name)); - snprintf(m_pp.name, sizeof(m_pp.name), "%s", in_firstname); - strcpy(name, m_pp.name); - Save(); - - // send name update packet - auto outapp = new EQApplicationPacket(OP_GMNameChange, sizeof(GMName_Struct)); - GMName_Struct* gmn=(GMName_Struct*)outapp->pBuffer; - strn0cpy(gmn->gmname,gmname,64); - strn0cpy(gmn->oldname,GetName(),64); - strn0cpy(gmn->newname,in_firstname,64); - gmn->unknown[0] = 1; - gmn->unknown[1] = 1; - gmn->unknown[2] = 1; - entity_list.QueueClients(this, outapp, false); - safe_delete(outapp); - - // finally, update the /who list - UpdateWho(); - - // success - return true; -} +//this assumes that it owns the object pointed to by *pApp +bool Client::AddPacket(EQApplicationPacket** pApp, bool bAckreq) { + if (!pApp || !(*pApp)) + return false; + if(!zoneinpacket_timer.Enabled()) { + //drop the packet because it will never get sent. + return(false); + } + auto c = std::make_unique(); -void Client::SetGM(bool toggle) { - m_pp.gm = toggle ? 1 : 0; - m_inv.SetGMInventory((bool)m_pp.gm); - Message( - Chat::White, - fmt::format( - "You are {} flagged as a GM.", - m_pp.gm ? "now" : "no longer" - ).c_str() - ); - SendAppearancePacket(AppearanceType::GM, m_pp.gm); - Save(); - UpdateWho(); -} + c->ack_req = bAckreq; + c->app = *pApp; + *pApp = nullptr; -void Client::ReadBook(BookRequest_Struct* book) -{ - const std::string& text_file = book->txtfile; - - if (text_file.empty()) { - return; - } - - auto b = content_db.GetBook(text_file); - - if (!b.text.empty()) { - auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); - auto inst = const_cast(m_inv[book->invslot]); - - auto t = (BookText_Struct*) outapp->pBuffer; - - t->window = book->window; - t->type = book->type; - t->invslot = book->invslot; - t->target_id = book->target_id; - t->can_cast = 0; // todo: implement - t->can_scribe = false; - - if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END) { - if (inst && inst->GetItem()) { - auto recipe = TradeskillRecipeRepository::GetWhere( - content_db, - fmt::format( - "learned_by_item_id = {} LIMIT 1", - inst->GetItem()->ID - ) - ); - - t->type = inst->GetItem()->Book; - t->can_scribe = !recipe.empty(); - } - } - - memcpy(t->booktext, b.text.c_str(), b.text.size()); - - if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { - if (m_pp.languages[b.language] < Language::MaxValue) { - GarbleMessage(t->booktext, (Language::MaxValue - m_pp.languages[b.language])); - } - } - - // Send only books and scrolls to this event - if (parse->PlayerHasQuestSub(EVENT_READ_ITEM) && t->type != BookType::ItemInfo) { - std::vector args = { - b.text, - t->can_cast, - t->can_scribe, - t->invslot, - t->target_id, - t->type, - inst - }; - - parse->EventPlayer(EVENT_READ_ITEM, this, book->txtfile, inst ? inst->GetID() : 0, &args); - } - - QueuePacket(outapp); - safe_delete(outapp); - } + clientpackets.push_back(std::move(c)); + return true; } -void Client::QuestReadBook(const char* text, uint8 type) { - std::string booktxt2 = text; - int length = booktxt2.length(); - if (booktxt2[0] != '\0') { - auto outapp = new EQApplicationPacket(OP_ReadBook, length + sizeof(BookText_Struct)); - BookText_Struct *out = (BookText_Struct *) outapp->pBuffer; - out->window = 0xFF; - out->type = type; - out->invslot = 0; - memcpy(out->booktext, booktxt2.c_str(), length); - QueuePacket(outapp); - safe_delete(outapp); - } +bool Client::SendAllPackets() { + CLIENTPACKET* cp = nullptr; + while (!clientpackets.empty()) { + cp = clientpackets.front().get(); + if(eqs) + eqs->FastQueuePacket((EQApplicationPacket **)&cp->app, cp->ack_req); + clientpackets.pop_front(); + } + return true; } -uint32 Client::GetCarriedPlatinum() { - return ( - GetMoney(MoneyTypes::Platinum, MoneySubtypes::Personal) + - (GetMoney(MoneyTypes::Gold, MoneySubtypes::Personal) / 10) + - (GetMoney(MoneyTypes::Silver, MoneySubtypes::Personal) / 100) + - (GetMoney(MoneyTypes::Copper, MoneySubtypes::Personal) / 1000) - ); -} +void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CONN_STATUS required_state, eqFilterType filter) { + if (filter != FilterNone && GetFilter(filter) == FilterHide) { + return; + } -bool Client::TakePlatinum(uint32 platinum, bool update_client) { - if (GetCarriedPlatinum() >= platinum) { - const auto copper = static_cast(platinum) * 1000; - return TakeMoneyFromPP(copper, update_client); - } + if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { + AddPacket(app, ack_req); + return; + } - return false; + // if the program doesnt care about the status or if the status isnt what we requested + if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { + // todo: save packets for later use + AddPacket(app, ack_req); + } + else if (eqs) { + eqs->QueuePacket(app, ack_req); + } } -bool Client::TakeMoneyFromPP(uint64 copper, bool update_client) { - int64 player_copper, silver, gold, platinum; - player_copper = m_pp.copper; - silver = static_cast(m_pp.silver) * 10; - gold = static_cast(m_pp.gold) * 100; - platinum = static_cast(m_pp.platinum) * 1000; - - int64 client_total = player_copper + silver + gold + platinum; - - client_total -= copper; - if (client_total < 0) { - return false; // Not enough money! - } else { - player_copper -= copper; - if(player_copper <= 0) { - copper = std::abs(player_copper); - m_pp.copper = 0; - } else { - m_pp.copper = player_copper; - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - return true; - } - - silver -= copper; - if (silver <= 0) { - copper = std::abs(silver); - m_pp.silver = 0; - } else { - m_pp.silver = silver / 10; - m_pp.copper += (silver - (m_pp.silver * 10)); - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - return true; - } - - gold -=copper; - - if (gold <= 0) { - copper = std::abs(gold); - m_pp.gold = 0; - } else { - m_pp.gold = gold / 100; - uint64 silver_test = (gold - (static_cast(m_pp.gold) * 100)) / 10; - m_pp.silver += silver_test; - uint64 copper_test = (gold - (static_cast(m_pp.gold) * 100 + silver_test * 10)); - m_pp.copper += copper_test; - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - return true; - } - - platinum -= copper; - - //Impossible for plat to be negative, already checked above - - m_pp.platinum = platinum / 1000; - uint64 gold_test = (platinum - (static_cast(m_pp.platinum) * 1000)) / 100; - m_pp.gold += gold_test; - uint64 silver_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100)) / 10; - m_pp.silver += silver_test; - uint64 copper_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100 + silver_test * 10)); - m_pp.copper = copper_test; - - if (update_client) { - SendMoneyUpdate(); - } - - RecalcWeight(); - SaveCurrency(); - return true; - } +void Client::FastQueuePacket(EQApplicationPacket** app, bool ack_req, CLIENT_CONN_STATUS required_state) { + // if the program doesnt care about the status or if the status isnt what we requested + if (required_state != CLIENT_CONNECTINGALL && client_state != required_state) { + // todo: save packets for later use + AddPacket(app, ack_req); + return; + } + else { + if(eqs) + eqs->FastQueuePacket((EQApplicationPacket **)app, ack_req); + else if (app && (*app)) + delete *app; + *app = nullptr; + } + return; } -void Client::AddPlatinum(uint32 platinum, bool update_client) { - const auto copper = static_cast(platinum) * 1000; - AddMoneyToPP(copper, update_client); -} +void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_skill, const char* orig_message, const char* targetname, bool is_silent) { + char message[4096]; + strn0cpy(message, orig_message, sizeof(message)); -void Client::AddMoneyToPP(uint64 copper, bool update_client){ - uint64 temporary_copper; - uint64 temporary_copper_two; - temporary_copper = copper; + LogDebug("Client::ChannelMessageReceived() Channel:[{}] message:[{}]", chan_num, message); - /* Add Amount of Platinum */ - temporary_copper_two = temporary_copper / 1000; - int32 new_value = m_pp.platinum + temporary_copper_two; + if (targetname == nullptr) { + targetname = (!GetTarget()) ? "" : GetTarget()->GetName(); + } - if (new_value < 0) { - m_pp.platinum = 0; - } else { - m_pp.platinum = m_pp.platinum + temporary_copper_two; - } + if(RuleB(Chat, EnableAntiSpam)) + { + if(strcmp(targetname, "discard") != 0) + { + if(chan_num == ChatChannel_Shout || chan_num == ChatChannel_Auction || chan_num == ChatChannel_OOC || chan_num == ChatChannel_Tell) + { + if(GlobalChatLimiterTimer) + { + if(GlobalChatLimiterTimer->Check(false)) + { + GlobalChatLimiterTimer->Start(RuleI(Chat, IntervalDurationMS)); + AttemptedMessages = 0; + } + } - temporary_copper -= temporary_copper_two * 1000; + uint32 AllowedMessages = RuleI(Chat, MinimumMessagesPerInterval) + TotalKarma; + AllowedMessages = AllowedMessages > RuleI(Chat, MaximumMessagesPerInterval) ? RuleI(Chat, MaximumMessagesPerInterval) : AllowedMessages; + + if(RuleI(Chat, MinStatusToBypassAntiSpam) <= Admin()) + AllowedMessages = 10000; + + AttemptedMessages++; + if(AttemptedMessages > AllowedMessages) + { + if(AttemptedMessages > RuleI(Chat, MaxMessagesBeforeKick)) + { + Kick("Sent too many chat messages at once."); + return; + } + if(GlobalChatLimiterTimer) + { + Message(0, "You have been rate limited, you can send more messages in %i seconds.", + GlobalChatLimiterTimer->GetRemainingTime() / 1000); + return; + } + else + { + Message(0, "You have been rate limited, you can send more messages in 60 seconds."); + return; + } + } + } + } + } - /* Add Amount of Gold */ - temporary_copper_two = temporary_copper / 100; - new_value = m_pp.gold + temporary_copper_two; + /* Logs Player Chat */ + if (RuleB(QueryServ, PlayerLogChat)) { + auto pack = new ServerPacket(ServerOP_Speech, sizeof(Server_Speech_Struct) + strlen(message) + 1); + Server_Speech_Struct* sem = (Server_Speech_Struct*) pack->pBuffer; - if (new_value < 0) { - m_pp.gold = 0; - } else { - m_pp.gold = m_pp.gold + temporary_copper_two; - } + if(chan_num == ChatChannel_Guild) + sem->guilddbid = GuildID(); + else + sem->guilddbid = 0; - temporary_copper -= temporary_copper_two * 100; + strcpy(sem->message, message); + sem->minstatus = Admin(); + sem->type = chan_num; + if(targetname != 0) + strcpy(sem->to, targetname); - /* Add Amount of Silver */ - temporary_copper_two = temporary_copper / 10; - new_value = m_pp.silver + temporary_copper_two; + if(GetName() != 0) + strcpy(sem->from, GetName()); - if (new_value < 0) { - m_pp.silver = 0; - } else { - m_pp.silver = m_pp.silver + temporary_copper_two; - } + if(worldserver.Connected()) + worldserver.SendPacket(pack); + safe_delete(pack); + } - temporary_copper -= temporary_copper_two * 10; + // Garble the message based on drunkness + if (GetIntoxication() > 0 && !(RuleB(Chat, ServerWideOOC) && chan_num == ChatChannel_OOC) && !GetGM()) { + GarbleMessage(message, (int)(GetIntoxication() / 3)); + language = Language::CommonTongue; // No need for language when drunk + lang_skill = Language::MaxValue; + } - /* Add Amount of Copper */ - temporary_copper_two = temporary_copper; - new_value = m_pp.copper + temporary_copper_two; + // some channels don't use languages + if ( + chan_num == ChatChannel_OOC || + chan_num == ChatChannel_GMSAY || + chan_num == ChatChannel_Broadcast || + chan_num == ChatChannel_Petition + ) { + language = Language::CommonTongue; + lang_skill = Language::MaxValue; + } - if (new_value < 0) { - m_pp.copper = 0; - } else { - m_pp.copper = m_pp.copper + temporary_copper_two; - } + // Censor the message + if (EQ::ProfanityManager::IsCensorshipActive() && (chan_num != ChatChannel_Say)) + EQ::ProfanityManager::RedactMessage(message); + + switch(chan_num) + { + case ChatChannel_Guild: { /* Guild Chat */ + if (!IsInAGuild()) { + MessageString(Chat::DefaultText, GUILD_NOT_MEMBER2); //You are not a member of any guild. + } else if (!guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_GUILD_CHAT_SPEAK_IN)) { + MessageString(Chat::EchoGuild, NO_PROPER_ACCESS); + } else if (!worldserver.SendChannelMessage(this, targetname, chan_num, GuildID(), language, lang_skill, message)) { + Message(Chat::White, "Error: World server disconnected"); + } + break; + } + case ChatChannel_Group: { /* Group Chat */ + Raid* raid = entity_list.GetRaidByClient(this); + if(raid) { + raid->RaidGroupSay((const char*) message, this, language, lang_skill); + break; + } - //send them all at once, since the above code stopped working. - if (update_client) { - SendMoneyUpdate(); - } + Group* group = GetGroup(); + if(group != nullptr) { + group->GroupMessage(this,language,lang_skill,(const char*) message); + } + break; + } + case ChatChannel_Raid: { /* Raid Say */ + Raid* raid = entity_list.GetRaidByClient(this); + if(raid){ + raid->RaidSay((const char*) message, this, language, lang_skill); + } + break; + } + case ChatChannel_Shout: { /* Shout */ + Mob *sender = this; + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) + sender = GetPet(); - RecalcWeight(); + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + break; + } + case ChatChannel_Auction: { /* Auction */ + if(RuleB(Chat, ServerWideAuction)) + { + if(!global_channel_timer.Check()) + { + if(strlen(targetname) == 0) + ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? + else + return; + } - SaveCurrency(); + if(GetRevoked()) + { + Message(0, "You have been revoked. You may not talk on Auction."); + return; + } - LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); -} + if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) + { + if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) + { + Message(0, "You do not have permission to talk in Auction at this time."); + return; + } + } -void Client::EVENT_ITEM_ScriptStopReturn(){ - /* Set a timestamp in an entity variable for plugin check_handin.pl in return_items - This will stopgap players from items being returned if global_npc.pl has a catch all return_items - */ - struct timeval read_time; - char buffer[50]; - gettimeofday(&read_time, 0); - sprintf(buffer, "%li.%li \n", read_time.tv_sec, read_time.tv_usec); - SetEntityVariable("Stop_Return", buffer); -} + if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + } + else if(!RuleB(Chat, ServerWideAuction)) { + Mob *sender = this; -void Client::AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 platinum, bool update_client){ - EVENT_ITEM_ScriptStopReturn(); + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) + sender = GetPet(); - int32 new_value = m_pp.platinum + platinum; - if (new_value >= 0 && new_value > m_pp.platinum) { - m_pp.platinum += platinum; - } + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + } + break; + } + case ChatChannel_OOC: { /* OOC */ + if(RuleB(Chat, ServerWideOOC)) + { + if(!global_channel_timer.Check()) + { + if(strlen(targetname) == 0) + ChannelMessageReceived(chan_num, language, lang_skill, message, "discard"); //Fast typer or spammer?? + else + return; + } + if(worldserver.IsOOCMuted() && admin < AccountStatus::GMAdmin) + { + Message(0,"OOC has been muted. Try again later."); + return; + } - new_value = m_pp.gold + gold; - if (new_value >= 0 && new_value > m_pp.gold) { - m_pp.gold += gold; - } + if(GetRevoked()) + { + Message(0, "You have been revoked. You may not talk on OOC."); + return; + } - new_value = m_pp.silver + silver; - if (new_value >= 0 && new_value > m_pp.silver) { - m_pp.silver += silver; - } + if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) + { + if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) + { + Message(0, "You do not have permission to talk in OOC at this time."); + return; + } + } - new_value = m_pp.copper + copper; - if (new_value >= 0 && new_value > m_pp.copper) { - m_pp.copper += copper; - } + if (!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) + { + Message(0, "Error: World server disconnected"); + } + } + else if(!RuleB(Chat, ServerWideOOC)) + { + Mob *sender = this; - if (update_client) { - SendMoneyUpdate(); - } + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) + sender = GetPet(); - RecalcWeight(); - SaveCurrency(); + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + } + break; + } + case ChatChannel_Broadcast: /* Broadcast */ + case ChatChannel_GMSAY: { /* GM Say */ + if (!(admin >= AccountStatus::QuestTroupe)) + Message(0, "Error: Only GMs can use this channel"); + else if (!worldserver.SendChannelMessage(this, targetname, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + break; + } + case ChatChannel_Tell: { /* Tell */ + if(!global_channel_timer.Check()) + { + if(strlen(targetname) == 0) + ChannelMessageReceived(ChatChannel_Tell, language, lang_skill, message, "discard"); //Fast typer or spammer?? + else + return; + } -#if (EQDEBUG>=5) - LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", - GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); -#endif -} + if(GetRevoked()) + { + Message(0, "You have been revoked. You may not send tells."); + return; + } -void Client::SendMoneyUpdate() { - auto outapp = new EQApplicationPacket(OP_MoneyUpdate, sizeof(MoneyUpdate_Struct)); - MoneyUpdate_Struct* mus= (MoneyUpdate_Struct*)outapp->pBuffer; + if(TotalKarma < RuleI(Chat, KarmaGlobalChatLimit)) + { + if(GetLevel() < RuleI(Chat, GlobalChatLevelLimit)) + { + Message(0, "You do not have permission to send tells at this time."); + return; + } + } - mus->platinum = m_pp.platinum; - mus->gold = m_pp.gold; - mus->silver = m_pp.silver; - mus->copper = m_pp.copper; + char target_name[64]; + + if(targetname) + { + size_t i = strlen(targetname); + int x; + for(x = 0; x < i; ++x) + { + if(targetname[x] == '%') + { + target_name[x] = '/'; + } + else + { + target_name[x] = targetname[x]; + } + } + target_name[x] = '\0'; + } - FastQueuePacket(&outapp); -} + if(!worldserver.SendChannelMessage(this, target_name, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + 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); + } + } -bool Client::HasMoney(uint64 copper) { + if (message[0] == COMMAND_CHAR) { + if (command_dispatch(this, message, false) == -2) { + if (parse->PlayerHasQuestSub(EVENT_COMMAND)) { + int i = parse->EventPlayer(EVENT_COMMAND, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Command '%s' not recognized.", message); + } + } + else if (parse->PlayerHasQuestSub(EVENT_SAY)) { + int i = parse->EventPlayer(EVENT_SAY, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Command '%s' not recognized.", message); + } + } + else { + if (!RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Command '%s' not recognized.", message); + } + } + } + break; + } - if ( - (static_cast(m_pp.copper) + - (static_cast(m_pp.silver) * 10) + - (static_cast(m_pp.gold) * 100) + - (static_cast(m_pp.platinum) * 1000)) >= copper - ) { - return true; - } + if (message[0] == BOT_COMMAND_CHAR) { + if (RuleB(Bots, Enabled)) { + if (bot_command_dispatch(this, message) == -2) { + if (parse->PlayerHasQuestSub(EVENT_BOT_COMMAND)) { + int i = parse->EventPlayer(EVENT_BOT_COMMAND, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Bot command '%s' not recognized.", message); + } + } + else if (parse->PlayerHasQuestSub(EVENT_SAY)) { + int i = parse->EventPlayer(EVENT_SAY, this, message, 0); + if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Bot command '%s' not recognized.", message); + } + } + else { + if (!RuleB(Chat, SuppressCommandErrors)) { + Message(Chat::Red, "Bot command '%s' not recognized.", message); + } + } + } + } else { + Message(Chat::Red, "Bots are disabled on this server."); + } + break; + } - return false; -} + if (EQ::ProfanityManager::IsCensorshipActive()) { + EQ::ProfanityManager::RedactMessage(message); + } -uint64 Client::GetCarriedMoney() { + Mob* sender = this; + if (GetPet() && GetTarget() == GetPet() && GetPet()->FindType(SE_VoiceGraft)) { + sender = GetPet(); + } - return ( - ( - static_cast(m_pp.copper) + - (static_cast(m_pp.silver) * 10) + - (static_cast(m_pp.gold) * 100) + - (static_cast(m_pp.platinum) * 1000) - ) - ); -} + if (!is_silent) { + entity_list.ChannelMessage(sender, chan_num, language, lang_skill, message); + } -uint64 Client::GetAllMoney() { + if (parse->PlayerHasQuestSub(EVENT_SAY)) { + parse->EventPlayer(EVENT_SAY, this, message, language); + } - return ( - ( - static_cast(m_pp.copper) + - (static_cast(m_pp.silver) * 10) + - (static_cast(m_pp.gold) * 100) + - (static_cast(m_pp.platinum) * 1000) + - ( - static_cast(m_pp.copper_bank) + - (static_cast(m_pp.silver_bank) * 10) + - (static_cast(m_pp.gold_bank) * 100) + - (static_cast(m_pp.platinum_bank) * 1000) + - ( - static_cast(m_pp.copper_cursor) + - (static_cast(m_pp.silver_cursor) * 10) + - (static_cast(m_pp.gold_cursor) * 100) + - (static_cast(m_pp.platinum_cursor) * 1000) + - (static_cast(m_pp.platinum_shared) * 1000) - ) - ) - ) - ); + if (sender != this) { + break; + } + + if (quest_manager.ProximitySayInUse()) { + entity_list.ProcessProximitySay(message, this, language); + } + + Mob* t = GetTarget(); + + if ( + t && + !IsInvisible(t) && + DistanceNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say) + ) { + const bool is_engaged = t->IsEngaged(); + + if (is_engaged) { + parse->EventBotMercNPC(EVENT_AGGRO_SAY, t, this, [&]() { return message; }, language); + } else { + parse->EventBotMercNPC(EVENT_SAY, t, this, [&]() { return message; }, language); + } + + if (t->IsNPC() && !is_engaged) { + CheckLDoNHail(t->CastToNPC()); + CheckEmoteHail(t->CastToNPC(), message); + + if (RuleB(TaskSystem, EnableTaskSystem)) { + if (UpdateTasksOnSpeakWith(t->CastToNPC())) { + t->CastToNPC()->DoQuestPause(this); + } + } + } + } + break; + } + case ChatChannel_UCSRelay: + { + // UCS Relay for Underfoot and later. + if(!worldserver.SendChannelMessage(this, 0, chan_num, 0, language, lang_skill, message)) + Message(0, "Error: World server disconnected"); + break; + } + case ChatChannel_Emotes: + { + // Emotes for Underfoot and later. + // crash protection -- cheater + message[1023] = '\0'; + size_t msg_len = strlen(message); + if (msg_len > 512) + message[512] = '\0'; + + auto outapp = new EQApplicationPacket(OP_Emote, 4 + msg_len + strlen(GetName()) + 2); + Emote_Struct* es = (Emote_Struct*)outapp->pBuffer; + char *Buffer = (char *)es; + Buffer += 4; + snprintf(Buffer, sizeof(Emote_Struct) - 4, "%s %s", GetName(), message); + entity_list.QueueCloseClients(this, outapp, true, RuleI(Range, Emote), 0, true, FilterSocials); + safe_delete(outapp); + break; + } + default: { + Message(0, "Channel (%i) not implemented", (uint16)chan_num); + } + } +} + +void Client::ChannelMessageSend( + const char *from, + const char *to, + uint8 channel_id, + uint8 language_id, + uint8 language_skill, + const char *message, + ... +) +{ + if ( + (channel_id == ChatChannel_Petition && Admin() < AccountStatus::QuestTroupe) || + (channel_id == ChatChannel_GMSAY && !GetGM()) + ) { + return; + } + + va_list argptr; + char buffer[4096]; + char message_sender[64]; + + va_start(argptr, message); + vsnprintf(buffer, 4096, message, argptr); + va_end(argptr); + + EQApplicationPacket app(OP_ChannelMessage, sizeof(ChannelMessage_Struct) + strlen(buffer) + 1); + + auto* cm = (ChannelMessage_Struct *) app.pBuffer; + + if (from == 0) { + strcpy(cm->sender, "ZServer"); + } else if (from[0] == 0) { + strcpy(cm->sender, "ZServer"); + } else { + CleanMobName(from, message_sender); + strcpy(cm->sender, message_sender); + } + + if (to != 0) { + strcpy((char *) cm->targetname, to); + } else if (channel_id == ChatChannel_Tell) { + strcpy(cm->targetname, m_pp.name); + } else { + cm->targetname[0] = 0; + } + + uint8 listener_skill; + + const bool is_valid_language = EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27); + + if (is_valid_language) { + listener_skill = m_pp.languages[language_id]; + cm->language = listener_skill < 24 ? Language::Unknown27 : language_id; + } else { + listener_skill = m_pp.languages[Language::CommonTongue]; + cm->language = Language::CommonTongue; + } + + // set effective language skill = lower of sender and receiver skills + uint8 effective_skill = (language_skill < listener_skill ? language_skill : listener_skill); + if (effective_skill > Language::MaxValue) { + effective_skill = Language::MaxValue; + } + + cm->skill_in_language = effective_skill; + + cm->chan_num = channel_id; + strcpy(&cm->message[0], buffer); + + QueuePacket(&app); + + const bool can_train_self = RuleB(Client, SelfLanguageLearning); + const bool is_not_sender = strcmp(GetCleanName(), cm->sender); + + if (can_train_self || is_not_sender) { + if ( + channel_id == ChatChannel_Group && + listener_skill < Language::MaxValue + ) { // group message in non-mastered language, check for skill up + if (is_valid_language && m_pp.languages[language_id] <= language_skill) { + CheckLanguageSkillIncrease(language_id, language_skill); + } + } + } +} + +void Client::Message(uint32 type, const char* message, ...) { + if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) + return; + if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... + return; + if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) + return; + + va_list argptr; + auto buffer = new char[4096]; + va_start(argptr, message); + vsnprintf(buffer, 4096, message, argptr); + va_end(argptr); + + SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); + buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); + buf.WriteInt8(static_cast(Journal::Mode::None)); + buf.WriteInt8(0); // language + buf.WriteUInt32(type); + buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here + buf.WriteString(""); // send name, not applicable here + buf.WriteInt32(0); // location, client seems to ignore + buf.WriteInt32(0); + buf.WriteInt32(0); + buf.WriteString(buffer); + + auto app = new EQApplicationPacket(OP_SpecialMesg, buf); + + FastQueuePacket(&app); + + safe_delete_array(buffer); +} + +void Client::FilteredMessage(Mob *sender, uint32 type, eqFilterType filter, const char* message, ...) { + if (!FilteredMessageCheck(sender, filter)) + return; + + va_list argptr; + auto buffer = new char[4096]; + va_start(argptr, message); + vsnprintf(buffer, 4096, message, argptr); + va_end(argptr); + + SerializeBuffer buf(sizeof(SpecialMesgHeader_Struct) + 12 + 64 + 64); + buf.WriteInt8(static_cast(Journal::SpeakMode::Raw)); + buf.WriteInt8(static_cast(Journal::Mode::None)); + buf.WriteInt8(0); // language + buf.WriteUInt32(type); + buf.WriteUInt32(0); // target spawn ID used for journal filtering, ignored here + buf.WriteString(""); // send name, not applicable here + buf.WriteInt32(0); // location, client seems to ignore + buf.WriteInt32(0); + buf.WriteInt32(0); + buf.WriteString(buffer); + + auto app = new EQApplicationPacket(OP_SpecialMesg, buf); + + FastQueuePacket(&app); + + safe_delete_array(buffer); +} + +void Client::SetMaxHP() { + if(dead) + return; + SetHP(CalcMaxHP()); + SendHPUpdate(); + Save(); +} + +bool Client::UpdateLDoNPoints(uint32 theme_id, int points) +{ + if (points < 0) { + if (m_pp.ldon_points_available < (0 - points)) { + return false; + } + } + + bool is_loss = false; + + switch (theme_id) { + case LDoNTheme::Unused: { // No theme, so distribute evenly across all + int split_points = (points / 5); + + int guk_points = (split_points + (points % 5)); + int mir_points = split_points; + int mmc_points = split_points; + int ruj_points = split_points; + int tak_points = split_points; + + split_points = 0; + + if (points < 0) { + if (m_pp.ldon_points_available < (0 - points)) { + return false; + } + + is_loss = true; + + if (m_pp.ldon_points_guk < (0 - guk_points)) { + mir_points += (guk_points + m_pp.ldon_points_guk); + guk_points = (0 - m_pp.ldon_points_guk); + } + + if (m_pp.ldon_points_mir < (0 - mir_points)) { + mmc_points += (mir_points + m_pp.ldon_points_mir); + mir_points = (0 - m_pp.ldon_points_mir); + } + + if (m_pp.ldon_points_mmc < (0 - mmc_points)) { + ruj_points += (mmc_points + m_pp.ldon_points_mmc); + mmc_points = (0 - m_pp.ldon_points_mmc); + } + + if (m_pp.ldon_points_ruj < (0 - ruj_points)) { + tak_points += (ruj_points + m_pp.ldon_points_ruj); + ruj_points = (0 - m_pp.ldon_points_ruj); + } + + if (m_pp.ldon_points_tak < (0 - tak_points)) { + split_points = (tak_points + m_pp.ldon_points_tak); + tak_points = (0 - m_pp.ldon_points_tak); + } + } + + m_pp.ldon_points_guk += guk_points; + m_pp.ldon_points_mir += mir_points; + m_pp.ldon_points_mmc += mmc_points; + m_pp.ldon_points_ruj += ruj_points; + m_pp.ldon_points_tak += tak_points; + + points -= split_points; + + if (split_points != 0) { // if anything left, recursively loop thru again + UpdateLDoNPoints(LDoNTheme::Unused, split_points); + } + + break; + } + case LDoNTheme::GUK: { + if (points < 0) { + if (m_pp.ldon_points_guk < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_guk += points; + break; + } + case LDoNTheme::MIR: { + if (points < 0) { + if (m_pp.ldon_points_mir < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_mir += points; + break; + } + case LDoNTheme::MMC: { + if (points < 0) { + if (m_pp.ldon_points_mmc < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_mmc += points; + break; + } + case LDoNTheme::RUJ: { + if (points < 0) { + if (m_pp.ldon_points_ruj < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_ruj += points; + break; + } + case LDoNTheme::TAK: { + if (points < 0) { + if (m_pp.ldon_points_tak < (0 - points)) { + return false; + } + + is_loss = true; + } + + m_pp.ldon_points_tak += points; + break; + } + } + + m_pp.ldon_points_available += points; + + QuestEventID event_id = is_loss ? EVENT_LDON_POINTS_LOSS : EVENT_LDON_POINTS_GAIN; + + if (parse->PlayerHasQuestSub(event_id)) { + const std::string &export_string = fmt::format( + "{} {}", + theme_id, + std::abs(points) + ); + + parse->EventPlayer(event_id, this, export_string, 0); + } + + auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); + auto *apus = (AdventurePoints_Update_Struct *) outapp->pBuffer; + + apus->ldon_available_points = m_pp.ldon_points_available; + apus->ldon_guk_points = m_pp.ldon_points_guk; + apus->ldon_mirugal_points = m_pp.ldon_points_mir; + apus->ldon_mistmoore_points = m_pp.ldon_points_mmc; + apus->ldon_rujarkian_points = m_pp.ldon_points_ruj; + apus->ldon_takish_points = m_pp.ldon_points_tak; + + outapp->priority = 6; + + QueuePacket(outapp); + safe_delete(outapp); + return true; +} + +void Client::SetLDoNPoints(uint32 theme_id, uint32 points) +{ + switch (theme_id) { + case LDoNTheme::GUK: { + m_pp.ldon_points_guk = points; + break; + } + case LDoNTheme::MIR: { + m_pp.ldon_points_mir = points; + break; + } + case LDoNTheme::MMC: { + m_pp.ldon_points_mmc = points; + break; + } + case LDoNTheme::RUJ: { + m_pp.ldon_points_ruj = points; + break; + } + case LDoNTheme::TAK: { + m_pp.ldon_points_tak = points; + break; + } + } + + m_pp.ldon_points_available = ( + m_pp.ldon_points_guk + + m_pp.ldon_points_mir + + m_pp.ldon_points_mmc + + m_pp.ldon_points_ruj + + m_pp.ldon_points_tak + ); + + auto outapp = new EQApplicationPacket(OP_AdventurePointsUpdate, sizeof(AdventurePoints_Update_Struct)); + + auto a = (AdventurePoints_Update_Struct*) outapp->pBuffer; + + a->ldon_available_points = m_pp.ldon_points_available; + a->ldon_guk_points = m_pp.ldon_points_guk; + a->ldon_mirugal_points = m_pp.ldon_points_mir; + a->ldon_mistmoore_points = m_pp.ldon_points_mmc; + a->ldon_rujarkian_points = m_pp.ldon_points_ruj; + a->ldon_takish_points = m_pp.ldon_points_tak; + + outapp->priority = 6; + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SetSkill(EQ::skills::SkillType skillid, uint16 value) { + if (skillid > EQ::skills::HIGHEST_SKILL) + return; + m_pp.skills[skillid] = value; // We need to be able to #setskill 254 and 255 to reset skills + + database.SaveCharacterSkill(CharacterID(), skillid, value); + auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); + SkillUpdate_Struct* skill = (SkillUpdate_Struct*)outapp->pBuffer; + skill->skillId=skillid; + skill->value=value; + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::IncreaseLanguageSkill(uint8 language_id, uint8 increase) +{ + if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { + return; + } + + m_pp.languages[language_id] += increase; + + if (m_pp.languages[language_id] > Language::MaxValue) { + m_pp.languages[language_id] = Language::MaxValue; + } + + database.SaveCharacterLanguage(CharacterID(), language_id, m_pp.languages[language_id]); + + auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); + auto* s = (SkillUpdate_Struct*) outapp->pBuffer; + + s->skillId = 100 + language_id; + s->value = m_pp.languages[language_id]; + + QueuePacket(outapp); + safe_delete(outapp); + + MessageString(Chat::Skills, LANG_SKILL_IMPROVED); +} + +void Client::AddSkill(EQ::skills::SkillType skillid, uint16 value) { + if (skillid > EQ::skills::HIGHEST_SKILL) + return; + value = GetRawSkill(skillid) + value; + uint16 max = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); + if (value > max) + value = max; + SetSkill(skillid, value); +} + +void Client::SendSound(){//Makes a sound. + auto outapp = new EQApplicationPacket(OP_Sound, 68); + unsigned char x[68]; + memset(x, 0, 68); + x[0]=0x22; + memset(&x[4],0x8002,sizeof(uint16)); + memset(&x[8],0x8624,sizeof(uint16)); + memset(&x[12],0x4A01,sizeof(uint16)); + x[16]=0x05; + x[28]=0x00;//change this value to give gold to the client + memset(&x[40],0xFFFFFFFF,sizeof(uint32)); + memset(&x[44],0xFFFFFFFF,sizeof(uint32)); + memset(&x[48],0xFFFFFFFF,sizeof(uint32)); + memset(&x[52],0xFFFFFFFF,sizeof(uint32)); + memset(&x[56],0xFFFFFFFF,sizeof(uint32)); + memset(&x[60],0xFFFFFFFF,sizeof(uint32)); + memset(&x[64],0xffffffff,sizeof(uint32)); + memcpy(outapp->pBuffer,x,outapp->size); + QueuePacket(outapp); + safe_delete(outapp); + +} +void Client::UpdateWho(uint8 remove) +{ + if (account_id == 0) { + return; + } + if (!worldserver.Connected()) { + return; + } + + auto pack = new ServerPacket(ServerOP_ClientList, sizeof(ServerClientList_Struct)); + auto *s = (ServerClientList_Struct *) pack->pBuffer; + s->remove = remove; + s->wid = GetWID(); + s->IP = GetIP(); + s->charid = CharacterID(); + strcpy(s->name, GetName()); + + s->gm = GetGM(); + s->Admin = Admin(); + s->AccountID = AccountID(); + strcpy(s->AccountName, AccountName()); + + s->LSAccountID = LSAccountID(); + strn0cpy(s->lskey, lskey, sizeof(s->lskey)); + + s->zone = zone->GetZoneID(); + s->instance_id = zone->GetInstanceID(); + s->race = GetRace(); + s->class_ = GetClass(); + s->level = GetLevel(); + + if (m_pp.anon == 0) { + s->anon = 0; + } + else if (m_pp.anon == 1) { + s->anon = 1; + } + else if (m_pp.anon >= 2) { + s->anon = 2; + } + + s->ClientVersion = static_cast(ClientVersion()); + s->tellsoff = tellsoff; + s->guild_id = guild_id; + s->guild_rank = guildrank; + s->guild_tribute_opt_in = guild_tribute_opt_in; + s->LFG = LFG; + if (LFG) { + s->LFGFromLevel = LFGFromLevel; + s->LFGToLevel = LFGToLevel; + s->LFGMatchFilter = LFGMatchFilter; + memcpy(s->LFGComments, LFGComments, sizeof(s->LFGComments)); + } + + worldserver.SendPacket(pack); + safe_delete(pack); +} + +void Client::WhoAll(Who_All_Struct* whom) { + + if (!worldserver.Connected()) + Message(0, "Error: World server disconnected"); + else { + auto pack = new ServerPacket(ServerOP_Who, sizeof(ServerWhoAll_Struct)); + ServerWhoAll_Struct* whoall = (ServerWhoAll_Struct*) pack->pBuffer; + whoall->admin = Admin(); + whoall->fromid=GetID(); + strcpy(whoall->from, GetName()); + strn0cpy(whoall->whom, whom->whom, 64); + whoall->lvllow = whom->lvllow; + whoall->lvlhigh = whom->lvlhigh; + whoall->gmlookup = whom->gmlookup; + whoall->wclass = whom->wclass; + whoall->wrace = whom->wrace; + worldserver.SendPacket(pack); + safe_delete(pack); + } +} + +void Client::FriendsWho(char *FriendsString) { + + if (!worldserver.Connected()) + Message(0, "Error: World server disconnected"); + else { + auto pack = + new ServerPacket(ServerOP_FriendsWho, sizeof(ServerFriendsWho_Struct) + strlen(FriendsString)); + ServerFriendsWho_Struct* FriendsWho = (ServerFriendsWho_Struct*) pack->pBuffer; + FriendsWho->FromID = GetID(); + strcpy(FriendsWho->FromName, GetName()); + strcpy(FriendsWho->FriendsString, FriendsString); + worldserver.SendPacket(pack); + safe_delete(pack); + } +} + +void Client::UpdateAdmin(bool from_database) { + int16 tmp = admin; + if (from_database) { + admin = database.GetAccountStatus(account_id); + } + + if (tmp == admin && from_database) { + return; + } + + if (m_pp.gm) { + LogInfo("[{}] - [{}] is a GM", __FUNCTION__ , GetName()); + petition_list.UpdateGMQueue(); + } + + UpdateWho(); +} + +void Client::SetStats(uint8 type,int16 set_val){ + if(type>STAT_DISEASE){ + printf("Error in Client::IncStats, received invalid type of: %i\n",type); + return; + } + auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); + IncreaseStat_Struct* iss=(IncreaseStat_Struct*)outapp->pBuffer; + switch(type){ + case STAT_STR: + if(set_val>0) + iss->str=set_val; + if(set_val<0) + m_pp.STR=0; + else if(set_val>255) + m_pp.STR=255; + else + m_pp.STR=set_val; + break; + case STAT_STA: + if(set_val>0) + iss->sta=set_val; + if(set_val<0) + m_pp.STA=0; + else if(set_val>255) + m_pp.STA=255; + else + m_pp.STA=set_val; + break; + case STAT_AGI: + if(set_val>0) + iss->agi=set_val; + if(set_val<0) + m_pp.AGI=0; + else if(set_val>255) + m_pp.AGI=255; + else + m_pp.AGI=set_val; + break; + case STAT_DEX: + if(set_val>0) + iss->dex=set_val; + if(set_val<0) + m_pp.DEX=0; + else if(set_val>255) + m_pp.DEX=255; + else + m_pp.DEX=set_val; + break; + case STAT_INT: + if(set_val>0) + iss->int_=set_val; + if(set_val<0) + m_pp.INT=0; + else if(set_val>255) + m_pp.INT=255; + else + m_pp.INT=set_val; + break; + case STAT_WIS: + if(set_val>0) + iss->wis=set_val; + if(set_val<0) + m_pp.WIS=0; + else if(set_val>255) + m_pp.WIS=255; + else + m_pp.WIS=set_val; + break; + case STAT_CHA: + if(set_val>0) + iss->cha=set_val; + if(set_val<0) + m_pp.CHA=0; + else if(set_val>255) + m_pp.CHA=255; + else + m_pp.CHA=set_val; + break; + } + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::IncStats(uint8 type, int16 increase_val) +{ + if (type > STAT_DISEASE) { + printf("Error in Client::IncStats, received invalid type of: %i\n", type); + return; + } + auto outapp = new EQApplicationPacket(OP_IncreaseStats, sizeof(IncreaseStat_Struct)); + IncreaseStat_Struct *iss = (IncreaseStat_Struct *) outapp->pBuffer; + switch (type) { + case STAT_STR: + if (increase_val > 0) { + iss->str = increase_val; + } + + if ((m_pp.STR + increase_val * 2) > 255) { + m_pp.STR = 255; + } else { + m_pp.STR += increase_val * 2; + } + break; + case STAT_STA: + if (increase_val > 0) { + iss->sta = increase_val; + } + + if ((m_pp.STA + increase_val * 2) > 255) { + m_pp.STA = 255; + } else { + m_pp.STA += increase_val * 2; + } + break; + case STAT_AGI: + if (increase_val > 0) { + iss->agi = increase_val; + } + if ((m_pp.AGI + increase_val * 2) > 255) { + m_pp.AGI = 255; + } else { + m_pp.AGI += increase_val * 2; + } + break; + case STAT_DEX: + if (increase_val > 0) { + iss->dex = increase_val; + } + + if ((m_pp.DEX + increase_val * 2) > 255) { + m_pp.DEX = 255; + } else { + m_pp.DEX += increase_val * 2; + } + break; + case STAT_INT: + if (increase_val > 0) { + iss->int_ = increase_val; + } + + if ((m_pp.INT + increase_val * 2) > 255) { + m_pp.INT = 255; + } else { + m_pp.INT += increase_val * 2; + } + break; + case STAT_WIS: + if (increase_val > 0) { + iss->wis = increase_val; + } + + if ((m_pp.WIS + increase_val * 2) > 255) { + m_pp.WIS = 255; + } else { + m_pp.WIS += increase_val * 2; + } + break; + case STAT_CHA: + if (increase_val > 0) { + iss->cha = increase_val; + } + + if ((m_pp.CHA + increase_val * 2) > 255) { + m_pp.CHA = 255; + } else { + m_pp.CHA += increase_val * 2; + } + break; + } + QueuePacket(outapp); + safe_delete(outapp); +} + +const int64& Client::SetMana(int64 amount) { + bool update = false; + if (amount < 0) + amount = 0; + if (amount > GetMaxMana()) + amount = GetMaxMana(); + if (amount != current_mana) + update = true; + current_mana = amount; + if (update) + Mob::SetMana(amount); + CheckManaEndUpdate(); + return current_mana; +} + +void Client::CheckManaEndUpdate() { + if (!Connected()) + return; + + if (last_reported_mana != current_mana || last_reported_endurance != current_endurance) { + + if (ClientVersion() >= EQ::versions::ClientVersion::SoD) { + SendManaUpdate(); + SendEnduranceUpdate(); + } + + auto outapp = new EQApplicationPacket(OP_ManaChange, sizeof(ManaChange_Struct)); + ManaChange_Struct* mana_change = (ManaChange_Struct*)outapp->pBuffer; + mana_change->new_mana = current_mana; + mana_change->stamina = current_endurance; + mana_change->spell_id = casting_spell_id; + mana_change->keepcasting = 1; + mana_change->slot = -1; + outapp->priority = 6; + QueuePacket(outapp); + safe_delete(outapp); + + /* Let others know when our mana percent has changed */ + if (GetManaPercent() != last_reported_mana_percent) { + Group *group = GetGroup(); + Raid *raid = GetRaid(); + + if (raid) { + raid->SendManaPacketFrom(this); + } + else if (group) { + group->SendManaPacketFrom(this); + } + + auto mana_packet = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); + ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_packet->pBuffer; + mana_update->cur_mana = GetMana(); + mana_update->max_mana = GetMaxMana(); + mana_update->spawn_id = GetID(); + if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) + QueuePacket(mana_packet); // do we need this with the OP_ManaChange packet above? + entity_list.QueueClientsByXTarget(this, mana_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); + safe_delete(mana_packet); + + last_reported_mana_percent = GetManaPercent(); + } + + /* Let others know when our endurance percent has changed */ + if (GetEndurancePercent() != last_reported_endurance_percent) { + Group *group = GetGroup(); + Raid *raid = GetRaid(); + + if (raid) { + raid->SendEndurancePacketFrom(this); + } + else if (group) { + group->SendEndurancePacketFrom(this); + } + + auto endurance_packet = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); + EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)endurance_packet->pBuffer; + endurance_update->cur_end = GetEndurance(); + endurance_update->max_end = GetMaxEndurance(); + endurance_update->spawn_id = GetID(); + if ((ClientVersionBit() & EQ::versions::ClientVersionBitmask::maskSoDAndLater) != 0) + QueuePacket(endurance_packet); // do we need this with the OP_ManaChange packet above? + entity_list.QueueClientsByXTarget(this, endurance_packet, false, EQ::versions::ClientVersionBitmask::maskSoDAndLater); + safe_delete(endurance_packet); + + last_reported_endurance_percent = GetEndurancePercent(); + } + + last_reported_mana = current_mana; + last_reported_endurance = current_endurance; + } +} + +// sends mana update to self +void Client::SendManaUpdate() +{ + auto mana_app = new EQApplicationPacket(OP_ManaUpdate, sizeof(ManaUpdate_Struct)); + ManaUpdate_Struct* mana_update = (ManaUpdate_Struct*)mana_app->pBuffer; + mana_update->cur_mana = GetMana(); + mana_update->max_mana = GetMaxMana(); + mana_update->spawn_id = GetID(); + QueuePacket(mana_app); + safe_delete(mana_app); +} + +// sends endurance update to self +void Client::SendEnduranceUpdate() +{ + auto end_app = new EQApplicationPacket(OP_EnduranceUpdate, sizeof(EnduranceUpdate_Struct)); + EnduranceUpdate_Struct* endurance_update = (EnduranceUpdate_Struct*)end_app->pBuffer; + endurance_update->cur_end = GetEndurance(); + endurance_update->max_end = GetMaxEndurance(); + endurance_update->spawn_id = GetID(); + QueuePacket(end_app); + safe_delete(end_app); +} + +void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) +{ + Mob::FillSpawnStruct(ns, ForWho); + + // Populate client-specific spawn information + ns->spawn.afk = AFK; + ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live + ns->spawn.anon = m_pp.anon; + ns->spawn.gm = GetGM() ? 1 : 0; + ns->spawn.guildID = GuildID(); + ns->spawn.trader = IsTrader(); + ns->spawn.buyer = IsBuyer(); +// ns->spawn.linkdead = IsLD() ? 1 : 0; +// ns->spawn.pvp = GetPVP(false) ? 1 : 0; + ns->spawn.show_name = true; + + strcpy(ns->spawn.title, m_pp.title); + strcpy(ns->spawn.suffix, m_pp.suffix); + + if (IsBecomeNPC() == true) + ns->spawn.NPC = 1; + else if (ForWho == this) + ns->spawn.NPC = 10; + else + ns->spawn.NPC = 0; + ns->spawn.is_pet = 0; + + if (!IsInAGuild()) { + ns->spawn.guildrank = 0xFF; + } else { + ns->spawn.guildrank = guild_mgr.GetDisplayedRank(GuildID(), GuildRank(), CharacterID()); + ns->spawn.guild_show = guild_mgr.CheckPermission(GuildID(), GuildRank(), GUILD_ACTION_DISPLAY_GUILD_NAME); + } + ns->spawn.size = 0; // Changing size works, but then movement stops! (wth?) + ns->spawn.runspeed = (gmspeed == 0) ? runspeed : 3.125f; + ns->spawn.showhelm = m_pp.showhelm ? 1 : 0; + + UpdateEquipmentLight(); + UpdateActiveLight(); + ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive]; +} + +bool Client::GMHideMe(Client* client) { + if (gm_hide_me) { + if (client == 0) + return true; + else if (admin > client->Admin()) + return true; + else + return false; + } + else + return false; +} + +void Client::Duck() { + SetAppearance(eaCrouching, false); +} + +void Client::Stand() { + SetAppearance(eaStanding, false); +} + +void Client::Sit() { + SetAppearance(eaSitting, false); +} + +void Client::ChangeLastName(std::string last_name) { + memset(m_pp.last_name, 0, sizeof(m_pp.last_name)); + strn0cpy(m_pp.last_name, last_name.c_str(), sizeof(m_pp.last_name)); + auto outapp = new EQApplicationPacket(OP_GMLastName, sizeof(GMLastName_Struct)); + auto gmn = (GMLastName_Struct*) outapp->pBuffer; + strn0cpy(gmn->name, name, sizeof(gmn->name)); + strn0cpy(gmn->gmname, name, sizeof(gmn->gmname)); + strn0cpy(gmn->lastname, last_name.c_str(), sizeof(gmn->lastname)); + + gmn->unknown[0] = 1; + gmn->unknown[1] = 1; + gmn->unknown[2] = 1; + gmn->unknown[3] = 1; + + entity_list.QueueClients(this, outapp, false); + + safe_delete(outapp); +} + +bool Client::ChangeFirstName(const char* in_firstname, const char* gmname) +{ + // check duplicate name + bool used_name = database.IsNameUsed((const char*) in_firstname); + if (used_name) { + return false; + } + + // update character_ + if(!database.UpdateName(GetName(), in_firstname)) + return false; + + // update pp + memset(m_pp.name, 0, sizeof(m_pp.name)); + snprintf(m_pp.name, sizeof(m_pp.name), "%s", in_firstname); + strcpy(name, m_pp.name); + Save(); + + // send name update packet + auto outapp = new EQApplicationPacket(OP_GMNameChange, sizeof(GMName_Struct)); + GMName_Struct* gmn=(GMName_Struct*)outapp->pBuffer; + strn0cpy(gmn->gmname,gmname,64); + strn0cpy(gmn->oldname,GetName(),64); + strn0cpy(gmn->newname,in_firstname,64); + gmn->unknown[0] = 1; + gmn->unknown[1] = 1; + gmn->unknown[2] = 1; + entity_list.QueueClients(this, outapp, false); + safe_delete(outapp); + + // finally, update the /who list + UpdateWho(); + + // success + return true; +} + +void Client::SetGM(bool toggle) { + m_pp.gm = toggle ? 1 : 0; + m_inv.SetGMInventory((bool)m_pp.gm); + Message( + Chat::White, + fmt::format( + "You are {} flagged as a GM.", + m_pp.gm ? "now" : "no longer" + ).c_str() + ); + SendAppearancePacket(AppearanceType::GM, m_pp.gm); + Save(); + UpdateWho(); +} + +void Client::ReadBook(BookRequest_Struct* book) +{ + const std::string& text_file = book->txtfile; + + if (text_file.empty()) { + return; + } + + auto b = content_db.GetBook(text_file); + + if (!b.text.empty()) { + auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); + auto inst = const_cast(m_inv[book->invslot]); + + auto t = (BookText_Struct*) outapp->pBuffer; + + t->window = book->window; + t->type = book->type; + t->invslot = book->invslot; + t->target_id = book->target_id; + t->can_cast = 0; // todo: implement + t->can_scribe = false; + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END) { + if (inst && inst->GetItem()) { + auto recipe = TradeskillRecipeRepository::GetWhere( + content_db, + fmt::format( + "learned_by_item_id = {} LIMIT 1", + inst->GetItem()->ID + ) + ); + + t->type = inst->GetItem()->Book; + t->can_scribe = !recipe.empty(); + } + } + + memcpy(t->booktext, b.text.c_str(), b.text.size()); + + if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { + if (m_pp.languages[b.language] < Language::MaxValue) { + GarbleMessage(t->booktext, (Language::MaxValue - m_pp.languages[b.language])); + } + } + + // Send only books and scrolls to this event + if (parse->PlayerHasQuestSub(EVENT_READ_ITEM) && t->type != BookType::ItemInfo) { + std::vector args = { + b.text, + t->can_cast, + t->can_scribe, + t->invslot, + t->target_id, + t->type, + inst + }; + + parse->EventPlayer(EVENT_READ_ITEM, this, book->txtfile, inst ? inst->GetID() : 0, &args); + } + + QueuePacket(outapp); + safe_delete(outapp); + } +} + +void Client::QuestReadBook(const char* text, uint8 type) { + std::string booktxt2 = text; + int length = booktxt2.length(); + if (booktxt2[0] != '\0') { + auto outapp = new EQApplicationPacket(OP_ReadBook, length + sizeof(BookText_Struct)); + BookText_Struct *out = (BookText_Struct *) outapp->pBuffer; + out->window = 0xFF; + out->type = type; + out->invslot = 0; + memcpy(out->booktext, booktxt2.c_str(), length); + QueuePacket(outapp); + safe_delete(outapp); + } +} + +uint32 Client::GetCarriedPlatinum() { + return ( + GetMoney(MoneyTypes::Platinum, MoneySubtypes::Personal) + + (GetMoney(MoneyTypes::Gold, MoneySubtypes::Personal) / 10) + + (GetMoney(MoneyTypes::Silver, MoneySubtypes::Personal) / 100) + + (GetMoney(MoneyTypes::Copper, MoneySubtypes::Personal) / 1000) + ); +} + +bool Client::TakePlatinum(uint32 platinum, bool update_client) { + if (GetCarriedPlatinum() >= platinum) { + const auto copper = static_cast(platinum) * 1000; + return TakeMoneyFromPP(copper, update_client); + } + + return false; +} + +bool Client::TakeMoneyFromPP(uint64 copper, bool update_client) { + int64 player_copper, silver, gold, platinum; + player_copper = m_pp.copper; + silver = static_cast(m_pp.silver) * 10; + gold = static_cast(m_pp.gold) * 100; + platinum = static_cast(m_pp.platinum) * 1000; + + int64 client_total = player_copper + silver + gold + platinum; + + client_total -= copper; + if (client_total < 0) { + return false; // Not enough money! + } else { + player_copper -= copper; + if(player_copper <= 0) { + copper = std::abs(player_copper); + m_pp.copper = 0; + } else { + m_pp.copper = player_copper; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + return true; + } + + silver -= copper; + if (silver <= 0) { + copper = std::abs(silver); + m_pp.silver = 0; + } else { + m_pp.silver = silver / 10; + m_pp.copper += (silver - (m_pp.silver * 10)); + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + return true; + } + + gold -=copper; + + if (gold <= 0) { + copper = std::abs(gold); + m_pp.gold = 0; + } else { + m_pp.gold = gold / 100; + uint64 silver_test = (gold - (static_cast(m_pp.gold) * 100)) / 10; + m_pp.silver += silver_test; + uint64 copper_test = (gold - (static_cast(m_pp.gold) * 100 + silver_test * 10)); + m_pp.copper += copper_test; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + return true; + } + + platinum -= copper; + + //Impossible for plat to be negative, already checked above + + m_pp.platinum = platinum / 1000; + uint64 gold_test = (platinum - (static_cast(m_pp.platinum) * 1000)) / 100; + m_pp.gold += gold_test; + uint64 silver_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100)) / 10; + m_pp.silver += silver_test; + uint64 copper_test = (platinum - (static_cast(m_pp.platinum) * 1000 + gold_test * 100 + silver_test * 10)); + m_pp.copper = copper_test; + + if (update_client) { + SendMoneyUpdate(); + } + + RecalcWeight(); + SaveCurrency(); + return true; + } +} + +void Client::AddPlatinum(uint32 platinum, bool update_client) { + const auto copper = static_cast(platinum) * 1000; + AddMoneyToPP(copper, update_client); +} + +void Client::AddMoneyToPP(uint64 copper, bool update_client){ + uint64 temporary_copper; + uint64 temporary_copper_two; + temporary_copper = copper; + + /* Add Amount of Platinum */ + temporary_copper_two = temporary_copper / 1000; + int32 new_value = m_pp.platinum + temporary_copper_two; + + if (new_value < 0) { + m_pp.platinum = 0; + } else { + m_pp.platinum = m_pp.platinum + temporary_copper_two; + } + + temporary_copper -= temporary_copper_two * 1000; + + /* Add Amount of Gold */ + temporary_copper_two = temporary_copper / 100; + new_value = m_pp.gold + temporary_copper_two; + + if (new_value < 0) { + m_pp.gold = 0; + } else { + m_pp.gold = m_pp.gold + temporary_copper_two; + } + + temporary_copper -= temporary_copper_two * 100; + + /* Add Amount of Silver */ + temporary_copper_two = temporary_copper / 10; + new_value = m_pp.silver + temporary_copper_two; + + if (new_value < 0) { + m_pp.silver = 0; + } else { + m_pp.silver = m_pp.silver + temporary_copper_two; + } + + temporary_copper -= temporary_copper_two * 10; + + /* Add Amount of Copper */ + temporary_copper_two = temporary_copper; + new_value = m_pp.copper + temporary_copper_two; + + if (new_value < 0) { + m_pp.copper = 0; + } else { + m_pp.copper = m_pp.copper + temporary_copper_two; + } + + //send them all at once, since the above code stopped working. + if (update_client) { + SendMoneyUpdate(); + } + + RecalcWeight(); + + SaveCurrency(); + + LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); +} + +void Client::EVENT_ITEM_ScriptStopReturn(){ + /* Set a timestamp in an entity variable for plugin check_handin.pl in return_items + This will stopgap players from items being returned if global_npc.pl has a catch all return_items + */ + struct timeval read_time; + char buffer[50]; + gettimeofday(&read_time, 0); + sprintf(buffer, "%li.%li \n", read_time.tv_sec, read_time.tv_usec); + SetEntityVariable("Stop_Return", buffer); +} + +void Client::AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 platinum, bool update_client){ + EVENT_ITEM_ScriptStopReturn(); + + int32 new_value = m_pp.platinum + platinum; + if (new_value >= 0 && new_value > m_pp.platinum) { + m_pp.platinum += platinum; + } + + new_value = m_pp.gold + gold; + if (new_value >= 0 && new_value > m_pp.gold) { + m_pp.gold += gold; + } + + new_value = m_pp.silver + silver; + if (new_value >= 0 && new_value > m_pp.silver) { + m_pp.silver += silver; + } + + new_value = m_pp.copper + copper; + if (new_value >= 0 && new_value > m_pp.copper) { + m_pp.copper += copper; + } + + if (update_client) { + SendMoneyUpdate(); + } + + RecalcWeight(); + SaveCurrency(); + +#if (EQDEBUG>=5) + LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", + GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); +#endif +} + +void Client::SendMoneyUpdate() { + auto outapp = new EQApplicationPacket(OP_MoneyUpdate, sizeof(MoneyUpdate_Struct)); + MoneyUpdate_Struct* mus= (MoneyUpdate_Struct*)outapp->pBuffer; + + mus->platinum = m_pp.platinum; + mus->gold = m_pp.gold; + mus->silver = m_pp.silver; + mus->copper = m_pp.copper; + + FastQueuePacket(&outapp); +} + +bool Client::HasMoney(uint64 copper) { + + if ( + (static_cast(m_pp.copper) + + (static_cast(m_pp.silver) * 10) + + (static_cast(m_pp.gold) * 100) + + (static_cast(m_pp.platinum) * 1000)) >= copper + ) { + return true; + } + + return false; +} + +uint64 Client::GetCarriedMoney() { + + return ( + ( + static_cast(m_pp.copper) + + (static_cast(m_pp.silver) * 10) + + (static_cast(m_pp.gold) * 100) + + (static_cast(m_pp.platinum) * 1000) + ) + ); +} + +uint64 Client::GetAllMoney() { + + return ( + ( + static_cast(m_pp.copper) + + (static_cast(m_pp.silver) * 10) + + (static_cast(m_pp.gold) * 100) + + (static_cast(m_pp.platinum) * 1000) + + ( + static_cast(m_pp.copper_bank) + + (static_cast(m_pp.silver_bank) * 10) + + (static_cast(m_pp.gold_bank) * 100) + + (static_cast(m_pp.platinum_bank) * 1000) + + ( + static_cast(m_pp.copper_cursor) + + (static_cast(m_pp.silver_cursor) * 10) + + (static_cast(m_pp.gold_cursor) * 100) + + (static_cast(m_pp.platinum_cursor) * 1000) + + (static_cast(m_pp.platinum_shared) * 1000) + ) + ) + ) + ); +} + +bool Client::CheckIncreaseSkill(EQ::skills::SkillType skillid, Mob *against_who, int chancemodi) { + if (IsDead() || IsUnconscious()) { + return false; + } + + if (IsAIControlled()) { // no skillups while chamred =p + return false; + } + + if (against_who && against_who->IsCorpse()) { // no skillups on corpses + return false; + } + + if (skillid > EQ::skills::HIGHEST_SKILL) { + return false; + } + + auto skillval = GetRawSkill(skillid); + auto maxskill = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); + + if (parse->PlayerHasQuestSub(EVENT_USE_SKILL)) { + const auto& export_string = fmt::format( + "{} {}", + skillid, + skillval + ); + + parse->EventPlayer(EVENT_USE_SKILL, this, export_string, 0); + } + + if (against_who) { + if ( + against_who->GetSpecialAbility(SpecialAbility::AggroImmunity) || + against_who->GetSpecialAbility(SpecialAbility::ClientAggroImmunity) || + against_who->IsClient() || + GetLevelCon(against_who->GetLevel()) == ConsiderColor::Gray + ) { + return false; + } + } + + // Make sure we're not already at skill cap + if (skillval < maxskill) + { + double Chance = 0; + if (RuleI(Character, SkillUpMaximumChancePercentage) + chancemodi - RuleI(Character, SkillUpMinimumChancePercentage) <= RuleI(Character, SkillUpMinimumChancePercentage)) { + Chance = RuleI(Character, SkillUpMinimumChancePercentage); + } + else { + // f(x) = (max - min + modification) * .99^skillval + min + // This results in a exponential decay where as you skill up, you lose a slight chance to skill up, ranging from your modified maximum to approaching your minimum + // This result is increased by the existing SkillUpModifier rule + double working_chance = (((RuleI(Character, SkillUpMaximumChancePercentage) - RuleI(Character, SkillUpMinimumChancePercentage) + chancemodi) * (pow(0.99, skillval))) + RuleI(Character, SkillUpMinimumChancePercentage)); + Chance = (working_chance * RuleI(Character, SkillUpModifier) / 100); + } + + if(zone->random.Real(0, 99) < Chance) + { + SetSkill(skillid, GetRawSkill(skillid) + 1); + + if (player_event_logs.IsEventEnabled(PlayerEvent::SKILL_UP)) { + auto e = PlayerEvent::SkillUpEvent{ + .skill_id = static_cast(skillid), + .value = static_cast((skillval + 1)), + .max_skill = static_cast(maxskill), + .against_who = (against_who) ? against_who->GetCleanName() : GetCleanName(), + }; + RecordPlayerEventLog(PlayerEvent::SKILL_UP, e); + } + + if (parse->PlayerHasQuestSub(EVENT_SKILL_UP)) { + const auto& export_string = fmt::format( + "{} {} {} {}", + skillid, + skillval + 1, + maxskill, + 0 + ); + + parse->EventPlayer(EVENT_SKILL_UP, this, export_string, 0); + } + + LogSkills("Skill [{}] at value [{}] successfully gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); + return true; + } else { + LogSkills("Skill [{}] at value [{}] failed to gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); + } + } else { + LogSkills("Skill [{}] at value [{}] cannot increase due to maxmum [{}]", skillid, skillval, maxskill); + } + return false; +} + +void Client::CheckLanguageSkillIncrease(uint8 language_id, uint8 teacher_skill) { + if (IsDead() || IsUnconscious()) { + return; + } + + if (IsAIControlled()) { + return; + } + + if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { + return; + } + + const uint8 language_skill = m_pp.languages[language_id]; // get current language skill + + if (language_skill < Language::MaxValue) { // if the language isn't already maxed + int chance = 5 + ((teacher_skill - language_skill) / 10); // greater chance to learn if teacher's skill is much higher than yours + chance = (chance * RuleI(Character, SkillUpModifier) / 100); + + if (zone->random.Real(0, 100) < chance) { // if they make the roll + IncreaseLanguageSkill(language_id); + + if (parse->PlayerHasQuestSub(EVENT_LANGUAGE_SKILL_UP)) { + const auto &export_string = fmt::format( + "{} {} {}", + language_id, + language_skill + 1, + Language::MaxValue + ); + + parse->EventPlayer(EVENT_LANGUAGE_SKILL_UP, this, export_string, 0); + } + + LogSkills("Language [{}] at value [{}] successfully gain with [{}] % chance", language_id, language_skill, chance); + } else { + LogSkills("Language [{}] at value [{}] failed to gain with [{}] % chance", language_id, language_skill, chance); + } + } +} + +bool Client::HasSkill(EQ::skills::SkillType skill_id) const +{ + return GetSkill(skill_id) > 0 && CanHaveSkill(skill_id); +} + +bool Client::CanHaveSkill(EQ::skills::SkillType skill_id) const +{ + if ( + ClientVersion() < EQ::versions::ClientVersion::RoF2 && + class_ == Class::Berserker && + skill_id == EQ::skills::Skill1HPiercing + ) { + skill_id = EQ::skills::Skill2HPiercing; + } + + return skill_caps.GetSkillCap(GetClass(), skill_id, RuleI(Character, MaxLevel)).cap > 0; +} + +uint16 Client::MaxSkill(EQ::skills::SkillType skill_id, uint8 class_id, uint8 level) const +{ + if ( + ClientVersion() < EQ::versions::ClientVersion::RoF2 && + class_id == Class::Berserker && + skill_id == EQ::skills::Skill1HPiercing + ) { + skill_id = EQ::skills::Skill2HPiercing; + } + + return skill_caps.GetSkillCap(class_id, skill_id, level).cap; +} + +uint8 Client::GetSkillTrainLevel(EQ::skills::SkillType skill_id, uint8 class_id) +{ + if ( + ClientVersion() < EQ::versions::ClientVersion::RoF2 && + class_id == Class::Berserker && + skill_id == EQ::skills::Skill1HPiercing + ) { + skill_id = EQ::skills::Skill2HPiercing; + } + + return skill_caps.GetSkillTrainLevel(class_id, skill_id, RuleI(Character, MaxLevel)); +} + +uint16 Client::GetMaxSkillAfterSpecializationRules(EQ::skills::SkillType skillid, uint16 maxSkill) +{ + uint16 Result = maxSkill; + + uint16 PrimarySpecialization = 0, SecondaryForte = 0; + + uint16 PrimarySkillValue = 0, SecondarySkillValue = 0; + + uint16 MaxSpecializations = aabonuses.SecondaryForte ? 2 : 1; + + if (skillid >= EQ::skills::SkillSpecializeAbjure && skillid <= EQ::skills::SkillSpecializeEvocation) + { + bool HasPrimarySpecSkill = false; + + int NumberOfPrimarySpecSkills = 0; + + for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) + { + if(m_pp.skills[i] > 50) + { + HasPrimarySpecSkill = true; + NumberOfPrimarySpecSkills++; + } + if(m_pp.skills[i] > PrimarySkillValue) + { + if(PrimarySkillValue > SecondarySkillValue) + { + SecondarySkillValue = PrimarySkillValue; + SecondaryForte = PrimarySpecialization; + } + + PrimarySpecialization = i; + PrimarySkillValue = m_pp.skills[i]; + } + else if(m_pp.skills[i] > SecondarySkillValue) + { + SecondaryForte = i; + SecondarySkillValue = m_pp.skills[i]; + } + } + + if(SecondarySkillValue <=50) + SecondaryForte = 0; + + if(HasPrimarySpecSkill) + { + if(NumberOfPrimarySpecSkills <= MaxSpecializations) + { + if(MaxSpecializations == 1) + { + if(skillid != PrimarySpecialization) + { + Result = 50; + } + } + else + { + if((skillid != PrimarySpecialization) && ((skillid == SecondaryForte) || (SecondaryForte == 0))) + { + if((PrimarySkillValue > 100) || (!SecondaryForte)) + Result = 100; + } + else if(skillid != PrimarySpecialization) + { + Result = 50; + } + } + } + else + { + Message(Chat::Red, "Your spell casting specializations skills have been reset. " + "Only %i primary specialization skill is allowed.", MaxSpecializations); + + for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) + SetSkill((EQ::skills::SkillType)i, 1); + + Save(); + + LogInfo("Reset [{}]'s caster specialization skills to 1" + "Too many specializations skills were above 50.", GetCleanName()); + } + + } + } + + Result += spellbonuses.RaiseSkillCap[skillid] + itembonuses.RaiseSkillCap[skillid] + aabonuses.RaiseSkillCap[skillid]; + + if (skillid == EQ::skills::SkillType::SkillForage) + Result += aabonuses.GrantForage; + + return Result; +} + +void Client::SetPVP(bool toggle, bool message) { + m_pp.pvp = toggle ? 1 : 0; + + if (message) { + if(GetPVP()) { + MessageString(Chat::Shout, PVP_ON); + } else { + Message(Chat::Shout, "You now follow the ways of Order."); + } + } + + SendAppearancePacket(AppearanceType::PVP, GetPVP()); + Save(); +} + +void Client::Kick(const std::string &reason) { + client_state = CLIENT_KICKED; + + LogClientLogin("Client [{}] kicked, reason [{}]", GetCleanName(), reason.c_str()); +} + +void Client::WorldKick() { + auto outapp = new EQApplicationPacket(OP_GMKick, sizeof(GMKick_Struct)); + GMKick_Struct* gmk = (GMKick_Struct *)outapp->pBuffer; + strcpy(gmk->name,GetName()); + QueuePacket(outapp); + safe_delete(outapp); + Kick("World kick issued"); +} + +void Client::GMKill() { + auto outapp = new EQApplicationPacket(OP_GMKill, sizeof(GMKill_Struct)); + GMKill_Struct* gmk = (GMKill_Struct *)outapp->pBuffer; + strcpy(gmk->name,GetName()); + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::MemorizeSpell(uint32 slot, uint32 spell_id, uint32 scribing, uint32 reduction){ + if ( + !EQ::ValueWithin( + slot, + 0, + (EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize - 1) + ) + ) { + return; + } + + if ( + !EQ::ValueWithin( + spell_id, + 3, + EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellIdMax + ) && + spell_id != UINT32_MAX + ) { + return; + } + + auto outapp = new EQApplicationPacket(OP_MemorizeSpell, sizeof(MemorizeSpell_Struct)); + + auto* mss = (MemorizeSpell_Struct*) outapp->pBuffer; + + mss->scribing = scribing; + mss->slot = slot; + mss->spell_id = spell_id; + mss->reduction = reduction; + + outapp->priority = 5; + + if ( + parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) || + parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) || + parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) + ) { + const auto export_string = fmt::format("{} {}", slot, spell_id); + + if ( + scribing == ScribeSpellActions::Memorize && + parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) + ) { + parse->EventPlayer(EVENT_MEMORIZE_SPELL, this, export_string, 0); + } else if ( + scribing == ScribeSpellActions::Unmemorize && + parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) + ) { + parse->EventPlayer(EVENT_UNMEMORIZE_SPELL, this, export_string, 0); + } else if ( + scribing == ScribeSpellActions::Scribe && + parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) + ) { + parse->EventPlayer(EVENT_SCRIBE_SPELL, this, export_string, 0); + } + } + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::Disarm(Client* disarmer, int chance) { + int16 slot = EQ::invslot::SLOT_INVALID; + const EQ::ItemInstance *inst = GetInv().GetItem(EQ::invslot::slotPrimary); + if (inst && inst->IsWeapon()) { + slot = EQ::invslot::slotPrimary; + } + else { + inst = GetInv().GetItem(EQ::invslot::slotSecondary); + if (inst && inst->IsWeapon()) + slot = EQ::invslot::slotSecondary; + } + if (slot != EQ::invslot::SLOT_INVALID && inst->IsClassCommon()) { + // We have an item that can be disarmed. + if (zone->random.Int(0, 1000) <= chance) { + // Find a free inventory slot + int16 slot_id = EQ::invslot::SLOT_INVALID; + slot_id = m_inv.FindFreeSlot(false, true, inst->GetItem()->Size, (inst->GetItem()->ItemType == EQ::item::ItemTypeArrow)); + if (slot_id != EQ::invslot::SLOT_INVALID) + { + EQ::ItemInstance *InvItem = m_inv.PopItem(slot); + if (InvItem) { // there should be no way it is not there, but check anyway + EQApplicationPacket* outapp = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); + MoveItem_Struct* mi = (MoveItem_Struct*)outapp->pBuffer; + mi->from_slot = slot; + mi->to_slot = 0xFFFFFFFF; + if (inst->IsStackable()) // it should not be stackable + mi->number_in_stack = inst->GetCharges(); + else + mi->number_in_stack = 0; + FastQueuePacket(&outapp); // this deletes item from the weapon slot on the client + if (PutItemInInventory(slot_id, *InvItem, true)) + database.SaveInventory(CharacterID(), NULL, slot); + auto matslot = (slot == EQ::invslot::slotPrimary ? EQ::textures::weaponPrimary : EQ::textures::weaponSecondary); + if (matslot != EQ::textures::materialInvalid) + SendWearChange(matslot); + } + MessageString(Chat::Skills, DISARMED); + if (disarmer != this) + disarmer->MessageString(Chat::Skills, DISARM_SUCCESS, GetCleanName()); + if (chance != 1000) + disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 4); + CalcBonuses(); + // CalcEnduranceWeightFactor(); + return; + } + disarmer->MessageString(Chat::Skills, DISARM_FAILED); + if (chance != 1000) + disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 2); + return; + } + } + disarmer->MessageString(Chat::Skills, DISARM_FAILED); +} + +bool Client::BindWound(Mob *bindmob, bool start, bool fail) +{ + EQApplicationPacket *outapp = nullptr; + if (!fail) { + outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); + BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; + // Start bind + if (!bindwound_timer.Enabled()) { + // make sure we actually have a bandage... and consume it. + int16 bslot = m_inv.HasItemByUse(EQ::item::ItemTypeBandage, 1, invWhereWorn | invWherePersonal); + if (bslot == INVALID_INDEX) { + bind_out->type = 3; + QueuePacket(outapp); + bind_out->type = 7; // this is the wrong message, dont know the right one. + QueuePacket(outapp); + safe_delete(outapp); + return (true); + } + DeleteItemInInventory(bslot, 1, true); // do we need client update? + + // start complete timer + bindwound_timer.Start(10000); + bindwound_target = bindmob; + + // Send client unlock + bind_out->type = 3; + QueuePacket(outapp); + bind_out->type = 0; + // Client Unlocked + if (!bindmob) { + // send "bindmob dead" to client + bind_out->type = 4; + QueuePacket(outapp); + bind_out->type = 0; + bindwound_timer.Disable(); + bindwound_target = 0; + } else { + // send bindmob "stand still" + if (!bindmob->IsAIControlled() && bindmob != this) { + bindmob->CastToClient()->MessageString(Chat::Yellow, + YOU_ARE_BEING_BANDAGED); + } else if (bindmob->IsAIControlled() && bindmob != this) { + ; // Tell IPC to stand still? + } else { + ; // Binding self + } + } + } else if (bindwound_timer.Check()) // Did the timer finish? + { + // finish bind + // disable complete timer + bindwound_timer.Disable(); + bindwound_target = 0; + if (!bindmob) { + // send "bindmob gone" to client + bind_out->type = 5; // not in zone + QueuePacket(outapp); + bind_out->type = 0; + } + + else { + if (!GetFeigned() && (DistanceSquared(bindmob->GetPosition(), m_Position) <= 400)) { + // send bindmob bind done + if (!bindmob->IsAIControlled() && bindmob != this) { + + } else if (bindmob->IsAIControlled() && bindmob != this) { + // Tell IPC to resume?? + } else { + // Binding self + } + // Send client bind done + + bind_out->type = 1; // Done + QueuePacket(outapp); + bind_out->type = 0; + CheckIncreaseSkill(EQ::skills::SkillBindWound, nullptr, 5); + + if (RuleB(Character, UseOldBindWound)) { + int maxHPBonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + + aabonuses.MaxBindWound; + + int max_percent = 50 + maxHPBonus; + + if (GetClass() == Class::Monk && GetSkill(EQ::skills::SkillBindWound) > 200) { + max_percent = 70 + maxHPBonus; + } + + int64 max_hp = bindmob->GetMaxHP() * max_percent / 100; + + // send bindmob new hp's + if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() <= (max_hp)-1) { + // 0.120 per skill point, 0.60 per skill level, minimum 3 max 30 + int bindhps = 3; + + if (GetSkill(EQ::skills::SkillBindWound) > 200) { + bindhps += GetSkill(EQ::skills::SkillBindWound) * 4 / 10; + } + else if (GetSkill(EQ::skills::SkillBindWound) >= 10) { + bindhps += GetSkill(EQ::skills::SkillBindWound) / 4; + } + + // Implementation of aaMithanielsBinding is a guess (the multiplier) + int bindBonus = spellbonuses.BindWound + itembonuses.BindWound + + aabonuses.BindWound; + + bindhps += bindhps * bindBonus / 100; + + // if the bind takes them above the max bindable + // cap it at that value. Dont know if live does it this way + // but it makes sense to me. + int chp = bindmob->GetHP() + bindhps; + if (chp > max_hp) + chp = max_hp; + + bindmob->SetHP(chp); + bindmob->SendHPUpdate(); + } + else { + // I dont have the real, live + Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", + max_percent); + if (bindmob != this && bindmob->IsClient()) + bindmob->CastToClient()->Message( + 15, + "You cannot have your wounds bound above %d%% hitpoints.", + max_percent); + // Too many hp message goes here. + } + } + else { + int percent_base = 50; + if (GetRawSkill(EQ::skills::SkillBindWound) > 200) { + if ((GetClass() == Class::Monk) || (GetClass() == Class::Beastlord)) + percent_base = 70; + else if ((GetLevel() > 50) && ((GetClass() == Class::Warrior) || (GetClass() == Class::Rogue) || (GetClass() == Class::Cleric))) + percent_base = 70; + } + + int percent_bonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + aabonuses.MaxBindWound; + + int max_percent = percent_base + percent_bonus; + if (max_percent < 0) + max_percent = 0; + if (max_percent > 100) + max_percent = 100; + + int max_hp = (bindmob->GetMaxHP() * max_percent) / 100; + if (max_hp > bindmob->GetMaxHP()) + max_hp = bindmob->GetMaxHP(); + + if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() < max_hp) { + int bindhps = 3; // base bind hp + if (percent_base >= 70) + bindhps = (GetSkill(EQ::skills::SkillBindWound) * 4) / 10; // 8:5 skill-to-hp ratio + else if (GetSkill(EQ::skills::SkillBindWound) >= 12) + bindhps = GetSkill(EQ::skills::SkillBindWound) / 4; // 4:1 skill-to-hp ratio + + int bonus_hp_percent = spellbonuses.BindWound + itembonuses.BindWound + aabonuses.BindWound; + + bindhps += (bindhps * bonus_hp_percent) / 100; + + if (bindhps < 3) + bindhps = 3; + + bindhps += bindmob->GetHP(); + if (bindhps > max_hp) + bindhps = max_hp; + + bindmob->SetHP(bindhps); + bindmob->SendHPUpdate(); + } + else { + Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", max_percent); + if (bindmob != this && bindmob->IsClient()) + bindmob->CastToClient()->Message(Chat::Yellow, "You cannot have your wounds bound above %d%% hitpoints.", max_percent); + } + } + } + else { + // Send client bind failed + if (bindmob != this) + bind_out->type = 6; // They moved + else + bind_out->type = 7; // Bandager moved + + QueuePacket(outapp); + bind_out->type = 0; + } + } + } + } else if (bindwound_timer.Enabled()) { + // You moved + outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); + BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; + bindwound_timer.Disable(); + bindwound_target = 0; + bind_out->type = 7; + QueuePacket(outapp); + bind_out->type = 3; + QueuePacket(outapp); + } + safe_delete(outapp); + return true; +} + +void Client::SetMaterial(int16 in_slot, uint32 item_id) +{ + const EQ::ItemData *item = database.GetItem(item_id); + if (item && item->IsClassCommon()) { + uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); + if (matslot != EQ::textures::materialInvalid) { + m_pp.item_material.Slot[matslot].Material = GetEquipmentMaterial(matslot); + } + } +} + +void Client::ServerFilter(SetServerFilter_Struct* filter){ + +/* this code helps figure out the filter IDs in the packet if needed + static SetServerFilter_Struct ssss; + int r; + uint32 *o = (uint32 *) &ssss; + uint32 *n = (uint32 *) filter; + for(r = 0; r < (sizeof(SetServerFilter_Struct)/4); r++) { + if(*o != *n) + LogFile->write(EQEMuLog::Debug, "Filter %d changed from %d to %d", r, *o, *n); + o++; n++; + } + memcpy(&ssss, filter, sizeof(SetServerFilter_Struct)); +*/ +#define Filter0(type) \ + if(filter->filters[type] == 1) \ + SetFilter(type, FilterShow); \ + else \ + SetFilter(type, FilterHide); +#define Filter1(type) \ + if(filter->filters[type] == 0) \ + SetFilter(type, FilterShow); \ + else \ + SetFilter(type, FilterHide); + + Filter0(FilterGuildChat); + Filter0(FilterSocials); + Filter0(FilterGroupChat); + Filter0(FilterShouts); + Filter0(FilterAuctions); + Filter0(FilterOOC); + Filter0(FilterBadWords); + + if (filter->filters[FilterPCSpells] == 0) { + SetFilter(FilterPCSpells, FilterShow); + } else if (filter->filters[FilterPCSpells] == 1) { + SetFilter(FilterPCSpells, FilterHide); + } else { + SetFilter(FilterPCSpells, FilterShowGroupOnly); + } + + Filter1(FilterNPCSpells); + + if (filter->filters[FilterBardSongs] == 0) { + SetFilter(FilterBardSongs, FilterShow); + } else if (filter->filters[FilterBardSongs] == 1) { + SetFilter(FilterBardSongs, FilterShowSelfOnly); + } else if (filter->filters[FilterBardSongs] == 2) { + SetFilter(FilterBardSongs, FilterShowGroupOnly); + } else { + SetFilter(FilterBardSongs, FilterHide); + } + + if (filter->filters[FilterSpellCrits] == 0) { + SetFilter(FilterSpellCrits, FilterShow); + } else if (filter->filters[FilterSpellCrits] == 1) { + SetFilter(FilterSpellCrits, FilterShowSelfOnly); + } else { + SetFilter(FilterSpellCrits, FilterHide); + } + + if (filter->filters[FilterMeleeCrits] == 0) { + SetFilter(FilterMeleeCrits, FilterShow); + } else if (filter->filters[FilterMeleeCrits] == 1) { + SetFilter(FilterMeleeCrits, FilterShowSelfOnly); + } else { + SetFilter(FilterMeleeCrits, FilterHide); + } + + if (filter->filters[FilterSpellDamage] == 0) { + SetFilter(FilterSpellDamage, FilterShow); + } else if (filter->filters[FilterSpellDamage] == 1) { + SetFilter(FilterSpellDamage, FilterShowSelfOnly); + } else { + SetFilter(FilterSpellDamage, FilterHide); + } + + Filter0(FilterMyMisses); + Filter0(FilterOthersMiss); + Filter0(FilterOthersHit); + Filter0(FilterMissedMe); + Filter1(FilterDamageShields); + + if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { + if (filter->filters[FilterDOT] == 0) { + SetFilter(FilterDOT, FilterShow); + } else if (filter->filters[FilterDOT] == 1) { + SetFilter(FilterDOT, FilterShowSelfOnly); + } else if (filter->filters[FilterDOT] == 2) { + SetFilter(FilterDOT, FilterShowGroupOnly); + } else { + SetFilter(FilterDOT, FilterHide); + } + } else { + if (filter->filters[FilterDOT] == 0) { // show functions as self only + SetFilter(FilterDOT, FilterShowSelfOnly); + } else { + SetFilter(FilterDOT, FilterHide); + } + } + + Filter1(FilterPetHits); + Filter1(FilterPetMisses); + Filter1(FilterFocusEffects); + Filter1(FilterPetSpells); + + if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { + if (filter->filters[FilterHealOverTime] == 0) { + SetFilter(FilterHealOverTime, FilterShow); + } else if (filter->filters[FilterHealOverTime] == 1) { + SetFilter(FilterHealOverTime, FilterShowSelfOnly); + } else { + SetFilter(FilterHealOverTime, FilterHide); + } + } else { // these clients don't have a 'self only' filter + Filter1(FilterHealOverTime); + } + + Filter1(FilterItemSpeech); + Filter1(FilterStrikethrough); + Filter1(FilterStuns); + Filter1(FilterBardSongsOnPets); +} + +// this version is for messages with no parameters +void Client::MessageString(uint32 type, uint32 string_id, uint32 distance) +{ + if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) + return; + if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... + return; + if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) + return; + auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); + SimpleMessage_Struct* sms = (SimpleMessage_Struct*)outapp->pBuffer; + sms->color=type; + sms->string_id=string_id; + + sms->unknown8=0; + + if(distance>0) + entity_list.QueueCloseClients(this,outapp,false,distance); + else + QueuePacket(outapp); + safe_delete(outapp); +} + +// +// this list of 9 args isn't how I want to do it, but to use va_arg +// you have to know how many args you're expecting, and to do that we have +// to load the eqstr file and count them in the string. +// This hack sucks but it's gonna work for now. +// +void Client::MessageString(uint32 type, uint32 string_id, const char* message1, + const char* message2,const char* message3,const char* message4, + const char* message5,const char* message6,const char* message7, + const char* message8,const char* message9, uint32 distance) +{ + if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) + return; + if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... + return; + if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) + return; + if (GetFilter(FilterDamageShields) == FilterHide && type == Chat::DamageShield) + return; + if (GetFilter(FilterFocusEffects) == FilterHide && type == Chat::FocusEffect) + return; + + if (type == Chat::Emote) + type = 4; + + if (!message1) { + MessageString(type, string_id); // use the simple message instead + return; + } + + const char *message_arg[] = { + message1, message2, message3, message4, message5, + message6, message7, message8, message9 + }; + + SerializeBuffer buf(20); + buf.WriteInt32(0); // unknown + buf.WriteInt32(string_id); + buf.WriteInt32(type); + for (auto &m : message_arg) { + if (m == nullptr) + break; + buf.WriteString(m); + } + + buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime + + auto outapp = std::make_unique(OP_FormattedMessage, buf); + + if (distance > 0) + entity_list.QueueCloseClients(this, outapp.get(), false, distance); + else + QueuePacket(outapp.get()); +} + +void Client::MessageString(const CZClientMessageString_Struct* msg) +{ + if (msg) + { + if (msg->args_size == 0) + { + MessageString(msg->chat_type, msg->string_id); + } + else + { + uint32_t outsize = sizeof(FormattedMessage_Struct) + msg->args_size; + auto outapp = std::make_unique(OP_FormattedMessage, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->string_id = msg->string_id; + outbuf->type = msg->chat_type; + memcpy(outbuf->message, msg->args, msg->args_size); + QueuePacket(outapp.get()); + } + } +} + +// helper function, returns true if we should see the message +bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) +{ + eqFilterMode mode = GetFilter(filter); + // easy ones first + if (mode == FilterShow) { + return true; + } else if (mode == FilterHide) { + return false; + } + + if (sender != this && mode == FilterShowSelfOnly) { + return false; + } else if (sender) { + if (mode == FilterShowGroupOnly) { + auto g = GetGroup(); + auto r = GetRaid(); + if (g) { + if (g->IsGroupMember(sender)) { + return true; + } + } else if (r && sender->IsClient()) { + auto rgid1 = r->GetGroup(this); + auto rgid2 = r->GetGroup(sender->CastToClient()); + if (rgid1 != RAID_GROUPLESS && rgid1 == rgid2) { + return true; + } + } else { + return false; + } + } + } + + // we passed our checks + return true; +} + +void Client::FilteredMessageString(Mob *sender, uint32 type, + eqFilterType filter, uint32 string_id) +{ + if (!FilteredMessageCheck(sender, filter)) + return; + + auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); + SimpleMessage_Struct *sms = (SimpleMessage_Struct *)outapp->pBuffer; + sms->color = type; + sms->string_id = string_id; + + sms->unknown8 = 0; + + QueuePacket(outapp); + safe_delete(outapp); + + return; +} + +void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id, + const char *message1, const char *message2, const char *message3, + const char *message4, const char *message5, const char *message6, + const char *message7, const char *message8, const char *message9) +{ + if (!FilteredMessageCheck(sender, filter)) + return; + + if (type == Chat::Emote) + type = 4; + + if (!message1) { + FilteredMessageString(sender, type, filter, string_id); // use the simple message instead + return; + } + + const char *message_arg[] = { + message1, message2, message3, message4, message5, + message6, message7, message8, message9 + }; + + SerializeBuffer buf(20); + buf.WriteInt32(0); // unknown + buf.WriteInt32(string_id); + buf.WriteInt32(type); + for (auto &m : message_arg) { + if (m == nullptr) + break; + buf.WriteString(m); + } + + buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime + + auto outapp = std::make_unique(OP_FormattedMessage, buf); + + QueuePacket(outapp.get()); +} + +void Client::Tell_StringID(uint32 string_id, const char *who, const char *message) +{ + char string_id_str[10]; + snprintf(string_id_str, 10, "%d", string_id); + + MessageString(Chat::EchoTell, TELL_QUEUED_MESSAGE, who, string_id_str, message); +} + +void Client::SetTint(int16 in_slot, uint32 color) { + EQ::textures::Tint_Struct new_color; + new_color.Color = color; + SetTint(in_slot, new_color); + database.SaveCharacterMaterialColor(CharacterID(), in_slot, color); +} + +// Still need to reconcile bracer01 versus bracer02 +void Client::SetTint(int16 in_slot, EQ::textures::Tint_Struct& color) { + + uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); + if (matslot != EQ::textures::materialInvalid) + { + m_pp.item_tint.Slot[matslot].Color = color.Color; + database.SaveCharacterMaterialColor(CharacterID(), in_slot, color.Color); + } + +} + +void Client::SetHideMe(bool flag) +{ + EQApplicationPacket app; + + gm_hide_me = flag; + + if (gm_hide_me) { + database.SetHideMe(AccountID(), true); + CreateDespawnPacket(&app, false); + entity_list.RemoveFromTargets(this); + trackable = false; + if (RuleB(Command, HideMeCommandDisablesTells)) { + tellsoff = true; + } + } else { + database.SetHideMe(AccountID(), false); + CreateSpawnPacket(&app); + trackable = true; + tellsoff = false; + } + + entity_list.QueueClientsStatus(this, &app, true, 0, Admin() - 1); + UpdateWho(); +} + +void Client::SetLanguageSkill(uint8 language_id, uint8 language_skill) +{ + if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { + return; + } + + if (language_skill > Language::MaxValue) { + language_skill = Language::MaxValue; + } + + m_pp.languages[language_id] = language_skill; + + database.SaveCharacterLanguage(CharacterID(), language_id, language_skill); + + auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); + auto* s = (SkillUpdate_Struct*) outapp->pBuffer; + + s->skillId = 100 + language_id; + s->value = m_pp.languages[language_id]; + + QueuePacket(outapp); + safe_delete(outapp); + + MessageString(Chat::Skills, LANG_SKILL_IMPROVED); +} + +void Client::LinkDead() +{ + if (GetGroup()) + { + entity_list.MessageGroup(this,true,15,"%s has gone linkdead.",GetName()); + GetGroup()->DelMember(this); + if (GetMerc()) + { + GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); + } + } + Raid *raid = entity_list.GetRaidByClient(this); + if(raid){ + raid->MemberZoned(this); + } + + SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::LinkDead); + +// save_timer.Start(2500); + linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); + SendAppearancePacket(AppearanceType::Linkdead, 1); + client_state = CLIENT_LINKDEAD; + AI_Start(CLIENT_LD_TIMEOUT); +} + +uint8 Client::SlotConvert(uint8 slot,bool bracer){ + uint8 slot2 = 0; // why are we returning MainCharm instead of INVALID_INDEX? (must be a pre-charm segment...) + if(bracer) + return EQ::invslot::slotWrist2; + switch(slot) { + case EQ::textures::armorHead: + slot2 = EQ::invslot::slotHead; + break; + case EQ::textures::armorChest: + slot2 = EQ::invslot::slotChest; + break; + case EQ::textures::armorArms: + slot2 = EQ::invslot::slotArms; + break; + case EQ::textures::armorWrist: + slot2 = EQ::invslot::slotWrist1; + break; + case EQ::textures::armorHands: + slot2 = EQ::invslot::slotHands; + break; + case EQ::textures::armorLegs: + slot2 = EQ::invslot::slotLegs; + break; + case EQ::textures::armorFeet: + slot2 = EQ::invslot::slotFeet; + break; + } + return slot2; +} + +uint8 Client::SlotConvert2(uint8 slot){ + uint8 slot2 = 0; // same as above... + switch(slot){ + case EQ::invslot::slotHead: + slot2 = EQ::textures::armorHead; + break; + case EQ::invslot::slotChest: + slot2 = EQ::textures::armorChest; + break; + case EQ::invslot::slotArms: + slot2 = EQ::textures::armorArms; + break; + case EQ::invslot::slotWrist1: + slot2 = EQ::textures::armorWrist; + break; + case EQ::invslot::slotHands: + slot2 = EQ::textures::armorHands; + break; + case EQ::invslot::slotLegs: + slot2 = EQ::textures::armorLegs; + break; + case EQ::invslot::slotFeet: + slot2 = EQ::textures::armorFeet; + break; + } + return slot2; +} + +void Client::Escape() +{ + entity_list.RemoveFromTargets(this, true); + SetInvisible(Invisibility::Invisible); + MessageString(Chat::Skills, ESCAPE); +} + +float Client::CalcClassicPriceMod(Mob* other, bool reverse) { + float price_multiplier = 0.8f; + + if (other && other->IsNPC()) { + FACTION_VALUE faction_level = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); + int32 cha = GetCHA(); + + if (faction_level <= FACTION_AMIABLY) { + cha += 11; // amiable faction grants a defacto 11 charisma bonus + } + + uint8 greed = other->CastToNPC()->GetGreedPercent(); + + // Sony's precise algorithm is unknown, but this produces output that is virtually identical + if (faction_level <= FACTION_INDIFFERENTLY) { + if (cha > 75) { + if (greed) { + // this is derived from curve fitting to a lot of price data + price_multiplier = -0.2487768 + (1.599635 - -0.2487768) / (1 + pow((cha / 135.1495), 1.001983)); + price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' + price_multiplier = 1.0f / price_multiplier; + } + else { + // non-greedy merchants use a linear scale + price_multiplier = 1.0f - ((115.0f - cha) * 0.004f); + } + } + else if (cha > 60) { + price_multiplier = 1.0f / (1.25f + (greed / 100.0f)); + } + else { + price_multiplier = 1.0f / ((1.0f - (cha - 120.0f) / 220.0f) + (greed / 100.0f)); + } + } + else { // apprehensive + if (cha > 75) { + if (greed) { + // this is derived from curve fitting to a lot of price data + price_multiplier = -0.25f + (1.823662 - -0.25f) / (1 + (cha / 135.0f)); + price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' + price_multiplier = 1.0f / price_multiplier; + } + else { + price_multiplier = (100.0f - (145.0f - cha) / 2.8f) / 100.0f; + } + } + else if (cha > 60) { + price_multiplier = 1.0f / (1.4f + greed / 100.0f); + } + else { + price_multiplier = 1.0f / ((1.0f + (143.574 - cha) / 196.434) + (greed / 100.0f)); + } + } + + float maxResult = 1.0f / 1.05; // price reduction caps at this amount + if (price_multiplier > maxResult) { + price_multiplier = maxResult; + } + + if (!reverse) { + price_multiplier = 1.0f / price_multiplier; + } + } + + LogMerchants( + "[{}] [{}] items at [{}] price multiplier [{}] [{}]", + other->GetName(), + reverse ? "buys" : "sells", + price_multiplier, + reverse ? "from" : "to", + GetName() + ); + + return price_multiplier; +} + +float Client::CalcNewPriceMod(Mob* other, bool reverse) +{ + float chaformula = 0; + if (other) + { + int factionlvl = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetFactionRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); + if (factionlvl >= FACTION_APPREHENSIVELY) // Apprehensive or worse. + { + if (GetCHA() > 103) + { + chaformula = (GetCHA() - 103)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. + if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) + chaformula = -1*(RuleI(Merchant, PriceBonusPct)); + } + else if (GetCHA() < 103) + { + chaformula = (103 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // This will bottom out price penalty. + if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) + chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); + } + } + if (factionlvl <= FACTION_INDIFFERENTLY) // Indifferent or better. + { + if (GetCHA() > 75) + { + chaformula = (GetCHA() - 75)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. + if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) + chaformula = -1*(RuleI(Merchant, PriceBonusPct)); + } + else if (GetCHA() < 75) + { + chaformula = (75 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // Faction modifier keeps up from reaching bottom price penalty. + if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) + chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); + } + } + } + + if (reverse) + chaformula *= -1; //For selling + //Now we have, for example, 10 + chaformula /= 100; //Convert to 0.10 + chaformula += 1; //Convert to 1.10; + return chaformula; //Returns 1.10, expensive stuff! +} + +float Client::CalcPriceMod(Mob* other, bool reverse) +{ + float price_mod = CalcNewPriceMod(other, reverse); + + if (RuleB(Merchant, UseClassicPriceMod)) { + price_mod = CalcClassicPriceMod(other, reverse); + } + + return price_mod; +} + +void Client::GetGroupAAs(GroupLeadershipAA_Struct *into) const { + memcpy(into, &m_pp.leader_abilities.group, sizeof(GroupLeadershipAA_Struct)); +} + +void Client::GetRaidAAs(RaidLeadershipAA_Struct *into) const { + memcpy(into, &m_pp.leader_abilities.raid, sizeof(RaidLeadershipAA_Struct)); +} + +void Client::EnteringMessages(Client* client) +{ + std::string rules = RuleS(World, Rules); + + if (!rules.empty() || database.GetVariable("Rules", rules)) { + const uint8 flag = database.GetAgreementFlag(client->AccountID()); + if (!flag) { + const std::string& rules_link = Saylink::Silent("#serverrules", "rules"); + + client->Message( + Chat::White, + fmt::format( + "You must agree to the {} before you can move.", + rules_link + ).c_str() + ); + + client->SendAppearancePacket(AppearanceType::Animation, Animation::Freeze); + } + } +} + +void Client::SendRules() +{ + std::string rules = RuleS(World, Rules); + + if (rules.empty() && !database.GetVariable("Rules", rules)) { + return; + } + + auto lines = Strings::Split(rules, "|"); + auto line_number = 1; + for (auto&& line : lines) { + Message( + Chat::White, + fmt::format( + "{}. {}", + line_number, + line + ).c_str() + ); + line_number++; + } +} + +void Client::SetEndurance(int32 newEnd) +{ + /*Endurance can't be less than 0 or greater than max*/ + if(newEnd < 0) + newEnd = 0; + else if(newEnd > GetMaxEndurance()){ + newEnd = GetMaxEndurance(); + } + + current_endurance = newEnd; + CheckManaEndUpdate(); +} + +void Client::SacrificeConfirm(Mob *caster) +{ + auto outapp = new EQApplicationPacket(OP_Sacrifice, sizeof(Sacrifice_Struct)); + Sacrifice_Struct *ss = (Sacrifice_Struct *)outapp->pBuffer; + + if (!caster || PendingSacrifice) { + safe_delete(outapp); + return; + } + + if (GetLevel() < RuleI(Spells, SacrificeMinLevel)) { + caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. + safe_delete(outapp); + return; + } + + if (GetLevel() > RuleI(Spells, SacrificeMaxLevel)) { + caster->MessageString(Chat::Red, SAC_TOO_HIGH); + safe_delete(outapp); + return; + } + + ss->CasterID = caster->GetID(); + ss->TargetID = GetID(); + ss->Confirm = 0; + QueuePacket(outapp); + safe_delete(outapp); + // We store the Caster's id, because when the packet comes back, it only has the victim's entityID in it, + // not the caster. + sacrifice_caster_id = caster->GetID(); + PendingSacrifice = true; +} + +//Essentially a special case death function +void Client::Sacrifice(Mob *caster) +{ + if (GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)) { + int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000); + if (exploss < GetEXP()) { + SetEXP(ExpSource::Sacrifice, GetEXP() - exploss, GetAAXP(), false); + SendLogoutPackets(); + + // make our become corpse packet, and queue to ourself before OP_Death. + EQApplicationPacket app2(OP_BecomeCorpse, sizeof(BecomeCorpse_Struct)); + BecomeCorpse_Struct *bc = (BecomeCorpse_Struct *)app2.pBuffer; + bc->spawn_id = GetID(); + bc->x = GetX(); + bc->y = GetY(); + bc->z = GetZ(); + QueuePacket(&app2); + + // make death packet + EQApplicationPacket app(OP_Death, sizeof(Death_Struct)); + Death_Struct *d = (Death_Struct *)app.pBuffer; + d->spawn_id = GetID(); + d->killer_id = caster ? caster->GetID() : 0; + d->bindzoneid = GetPP().binds[0].zone_id; + d->spell_id = SPELL_UNKNOWN; + d->attack_skill = 0xe7; + d->damage = 0; + app.priority = 6; + entity_list.QueueClients(this, &app); + + BuffFadeAll(); + UnmemSpellAll(); + Group *g = GetGroup(); + if (g) { + g->MemberZoned(this); + } + Raid *r = entity_list.GetRaidByClient(this); + if (r) { + r->MemberZoned(this); + } + ClearAllProximities(); + if (RuleB(Character, LeaveCorpses)) { + auto new_corpse = new Corpse(this, 0); + entity_list.AddCorpse(new_corpse, GetID()); + SetID(0); + entity_list.QueueClients(this, &app2, true); + } + Save(); + GoToDeath(); + if (caster && caster->IsClient()) { + caster->CastToClient()->SummonItem(RuleI(Spells, SacrificeItemID)); + } else if (caster && caster->IsNPC()) { + caster->CastToNPC()->AddItem(RuleI(Spells, SacrificeItemID), 1, false); + } + } + } else { + caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. + } +} + +void Client::SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID) { + + if(!Caster || PendingTranslocate) + return; + + const SPDat_Spell_Struct &Spell = spells[SpellID]; + + auto outapp = new EQApplicationPacket(OP_Translocate, sizeof(Translocate_Struct)); + Translocate_Struct *ts = (Translocate_Struct*)outapp->pBuffer; + + strcpy(ts->Caster, Caster->GetName()); + PendingTranslocateData.spell_id = ts->SpellID = SpellID; + + if((SpellID == 1422) || (SpellID == 1334) || (SpellID == 3243)) { + PendingTranslocateData.zone_id = ts->ZoneID = m_pp.binds[0].zone_id; + PendingTranslocateData.instance_id = m_pp.binds[0].instance_id; + PendingTranslocateData.x = ts->x = m_pp.binds[0].x; + PendingTranslocateData.y = ts->y = m_pp.binds[0].y; + PendingTranslocateData.z = ts->z = m_pp.binds[0].z; + PendingTranslocateData.heading = m_pp.binds[0].heading; + } + else { + PendingTranslocateData.zone_id = ts->ZoneID = ZoneID(Spell.teleport_zone); + PendingTranslocateData.instance_id = 0; + PendingTranslocateData.y = ts->y = Spell.base_value[0]; + PendingTranslocateData.x = ts->x = Spell.base_value[1]; + PendingTranslocateData.z = ts->z = Spell.base_value[2]; + PendingTranslocateData.heading = 0.0; + } + + ts->unknown008 = 0; + ts->Complete = 0; + + PendingTranslocate = true; + TranslocateTime = time(nullptr); + + QueuePacket(outapp); + safe_delete(outapp); + + return; +} +void Client::SendPickPocketResponse(Mob *from, uint32 amt, int type, const EQ::ItemData* item){ + auto outapp = new EQApplicationPacket(OP_PickPocket, sizeof(sPickPocket_Struct)); + sPickPocket_Struct *pick_out = (sPickPocket_Struct *)outapp->pBuffer; + pick_out->coin = amt; + pick_out->from = GetID(); + pick_out->to = from->GetID(); + pick_out->myskill = GetSkill(EQ::skills::SkillPickPockets); + + if ((type >= PickPocketPlatinum) && (type <= PickPocketCopper) && (amt == 0)) + type = PickPocketFailed; + + pick_out->type = type; + if (item) + strcpy(pick_out->itemname, item->Name); + else + pick_out->itemname[0] = '\0'; + // if we do not send this packet the client will lock up and require the player to relog. + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SetHoTT(uint32 mobid) { + auto outapp = new EQApplicationPacket(OP_TargetHoTT, sizeof(ClientTarget_Struct)); + ClientTarget_Struct *ct = (ClientTarget_Struct *) outapp->pBuffer; + ct->new_target = mobid; + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SendPopupToClient(const char *Title, const char *Text, uint32 PopupID, uint32 Buttons, uint32 Duration) +{ + auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); + + OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; + + if ((strlen(Title) > (sizeof(olms->Title) - 1)) || (strlen(Text) > (sizeof(olms->Text) - 1))) { + safe_delete(outapp); + return; + } + + strcpy(olms->Title, Title); + strcpy(olms->Text, Text); + + olms->Buttons = Buttons; + + if (Duration > 0) { + olms->Duration = Duration * 1000; + } + else { + olms->Duration = 0xffffffff; + } + + olms->PopupID = PopupID; + olms->NegativeID = 0; + + sprintf(olms->ButtonName0, "%s", "Yes"); + sprintf(olms->ButtonName1, "%s", "No"); + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SendFullPopup( + const char *Title, + const char *Text, + uint32 PopupID, + uint32 NegativeID, + uint32 Buttons, + uint32 Duration, + const char *ButtonName0, + const char *ButtonName1, + uint32 SoundControls +) +{ + auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); + + OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; + + if ((strlen(Text) > (sizeof(olms->Text) - 1)) || (strlen(Title) > (sizeof(olms->Title) - 1))) { + safe_delete(outapp); + return; + } + + if (ButtonName0 && ButtonName1 && ((strlen(ButtonName0) > (sizeof(olms->ButtonName0) - 1)) || + (strlen(ButtonName1) > (sizeof(olms->ButtonName1) - 1)))) { + safe_delete(outapp); + return; + } + + strcpy(olms->Title, Title); + strcpy(olms->Text, Text); + + olms->Buttons = Buttons; + + if (ButtonName0 == nullptr || ButtonName1 == nullptr) { + sprintf(olms->ButtonName0, "%s", "Yes"); + sprintf(olms->ButtonName1, "%s", "No"); + } + else { + strcpy(olms->ButtonName0, ButtonName0); + strcpy(olms->ButtonName1, ButtonName1); + } + + if (Duration > 0) { + olms->Duration = Duration * 1000; + } + else { + olms->Duration = 0xffffffff; + } + + olms->PopupID = PopupID; + olms->NegativeID = NegativeID; + olms->SoundControls = SoundControls; + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SendWindow( + uint32 button_one_id, + uint32 button_two_id, + uint32 button_type, + const char* button_one_text, + const char* button_two_text, + uint32 duration, + int title_type, + Mob* target, + const char* title, + const char* text, + ... +) +{ + va_list argptr; + char buffer[4096]; + + va_start(argptr, text); + vsnprintf(buffer, sizeof(buffer), text, argptr); + va_end(argptr); + + size_t len = strlen(buffer); + + auto app = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); + auto* olms = (OnLevelMessage_Struct *) app->pBuffer; + + if (strlen(text) > (sizeof(olms->Text) - 1)) { + safe_delete(app); + return; + } + + if (!target) { + title_type = 0; + } + + switch (title_type) { + case 1: { + char name[64] = ""; + strcpy(name, target->GetName()); + + if (strlen(target->GetLastName()) > 0) { + char last_name[64] = ""; + strcpy(last_name, target->GetLastName()); + strcat(name, " "); + strcat(name, last_name); + } + + strcpy(olms->Title, name); + break; + } + case 2: { + if (target->IsClient() && target->CastToClient()->GuildID()) { + auto guild_name = guild_mgr.GetGuildName(target->CastToClient()->GuildID()); + strn0cpy(olms->Title, guild_name, sizeof(olms->Title)); + } else { + strcpy(olms->Title, "No Guild"); + } + break; + } + default: { + strcpy(olms->Title, title); + break; + } + } + + memcpy(olms->Text, buffer, len + 1); + + olms->Buttons = button_type; + + strn0cpy(olms->ButtonName0, button_one_text, sizeof(olms->ButtonName0)); + strn0cpy(olms->ButtonName1, button_two_text, sizeof(olms->ButtonName1)); + + if (duration > 0) { + olms->Duration = duration * 1000; + } else { + olms->Duration = UINT32_MAX; + } + + olms->PopupID = button_one_id; + olms->NegativeID = button_two_id; + + FastQueuePacket(&app); +} + +void Client::KeyRingLoad() +{ + const auto &l = KeyringRepository::GetWhere( + database, + fmt::format( + "`char_id` = {} ORDER BY `item_id`", + character_id + ) + ); + + if (l.empty()) { + return; + } + + + for (const auto &e : l) { + keyring.emplace_back(e.item_id); + } +} + +void Client::KeyRingAdd(uint32 item_id) +{ + if (!item_id) { + return; + } + + const bool found = KeyRingCheck(item_id); + if (found) { + return; + } + + auto e = KeyringRepository::NewEntity(); + + e.char_id = CharacterID(); + e.item_id = item_id; + + e = KeyringRepository::InsertOne(database, e); + + if (!e.id) { + return; + } + + keyring.emplace_back(item_id); + + if (!RuleB(World, UseItemLinksForKeyRing)) { + Message(Chat::LightBlue, "Added to keyring."); + return; + } + + const std::string &item_link = database.CreateItemLink(item_id); + + Message( + Chat::LightBlue, + fmt::format( + "Added {} to keyring.", + item_link + ).c_str() + ); +} + +bool Client::KeyRingCheck(uint32 item_id) +{ + for (const auto &e : keyring) { + if (e == item_id) { + return true; + } + } + + return false; +} + +void Client::KeyRingList() +{ + Message(Chat::LightBlue, "Keys on Keyring:"); + + const EQ::ItemData *item = nullptr; + + for (const auto &e : keyring) { + item = database.GetItem(e); + if (item) { + const std::string &item_string = RuleB(World, UseItemLinksForKeyRing) ? database.CreateItemLink(e) : item->Name; + + Message(Chat::LightBlue, item_string.c_str()); + } + } } -bool Client::CheckIncreaseSkill(EQ::skills::SkillType skillid, Mob *against_who, int chancemodi) { - if (IsDead() || IsUnconscious()) { - return false; - } - - if (IsAIControlled()) { // no skillups while chamred =p - return false; - } - - if (against_who && against_who->IsCorpse()) { // no skillups on corpses - return false; - } - - if (skillid > EQ::skills::HIGHEST_SKILL) { - return false; - } - - auto skillval = GetRawSkill(skillid); - auto maxskill = GetMaxSkillAfterSpecializationRules(skillid, MaxSkill(skillid)); - - if (parse->PlayerHasQuestSub(EVENT_USE_SKILL)) { - const auto& export_string = fmt::format( - "{} {}", - skillid, - skillval - ); - - parse->EventPlayer(EVENT_USE_SKILL, this, export_string, 0); - } - - if (against_who) { - if ( - against_who->GetSpecialAbility(SpecialAbility::AggroImmunity) || - against_who->GetSpecialAbility(SpecialAbility::ClientAggroImmunity) || - against_who->IsClient() || - GetLevelCon(against_who->GetLevel()) == ConsiderColor::Gray - ) { - return false; - } - } - - // Make sure we're not already at skill cap - if (skillval < maxskill) - { - double Chance = 0; - if (RuleI(Character, SkillUpMaximumChancePercentage) + chancemodi - RuleI(Character, SkillUpMinimumChancePercentage) <= RuleI(Character, SkillUpMinimumChancePercentage)) { - Chance = RuleI(Character, SkillUpMinimumChancePercentage); - } - else { - // f(x) = (max - min + modification) * .99^skillval + min - // This results in a exponential decay where as you skill up, you lose a slight chance to skill up, ranging from your modified maximum to approaching your minimum - // This result is increased by the existing SkillUpModifier rule - double working_chance = (((RuleI(Character, SkillUpMaximumChancePercentage) - RuleI(Character, SkillUpMinimumChancePercentage) + chancemodi) * (pow(0.99, skillval))) + RuleI(Character, SkillUpMinimumChancePercentage)); - Chance = (working_chance * RuleI(Character, SkillUpModifier) / 100); - } - - if(zone->random.Real(0, 99) < Chance) - { - SetSkill(skillid, GetRawSkill(skillid) + 1); - - if (player_event_logs.IsEventEnabled(PlayerEvent::SKILL_UP)) { - auto e = PlayerEvent::SkillUpEvent{ - .skill_id = static_cast(skillid), - .value = static_cast((skillval + 1)), - .max_skill = static_cast(maxskill), - .against_who = (against_who) ? against_who->GetCleanName() : GetCleanName(), - }; - RecordPlayerEventLog(PlayerEvent::SKILL_UP, e); - } - - if (parse->PlayerHasQuestSub(EVENT_SKILL_UP)) { - const auto& export_string = fmt::format( - "{} {} {} {}", - skillid, - skillval + 1, - maxskill, - 0 - ); - - parse->EventPlayer(EVENT_SKILL_UP, this, export_string, 0); - } - - LogSkills("Skill [{}] at value [{}] successfully gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); - return true; - } else { - LogSkills("Skill [{}] at value [{}] failed to gain with [{}] chance (mod [{}])", skillid, skillval, Chance, chancemodi); - } - } else { - LogSkills("Skill [{}] at value [{}] cannot increase due to maxmum [{}]", skillid, skillval, maxskill); - } - return false; +bool Client::IsPetNameChangeAllowed() { + DataBucketKey k = GetScopedBucketKeys(); + k.key = "PetNameChangesAllowed"; + + auto b = DataBucket::GetData(k); + if (!b.value.empty()) { + return true; + } + + return false; +} + +void Client::InvokeChangePetName(bool immediate) { + if (!IsPetNameChangeAllowed()) { + return; + } + + auto packet_op = immediate ? OP_InvokeChangePetNameImmediate : OP_InvokeChangePetName; + + auto outapp = new EQApplicationPacket(packet_op, 0); + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::GrantPetNameChange() { + DataBucketKey k = GetScopedBucketKeys(); + k.key = "PetNameChangesAllowed"; + k.value = "true"; + DataBucket::SetData(k); + + InvokeChangePetName(true); +} + +void Client::ClearPetNameChange() { + DataBucketKey k = GetScopedBucketKeys(); + k.key = "PetNameChangesAllowed"; + + DataBucket::DeleteData(k); +} + +bool Client::ChangePetName(std::string new_name) { + if (new_name.empty()) { + return false; + } + + if (!IsPetNameChangeAllowed()) { + return false; + } + + if (GetPet()) { + std::string cur_name = GetPet()->GetName(); + + if (cur_name == new_name) { + return false; + } + } + + if (!database.CheckNameFilter(new_name) || database.IsNameUsed(new_name)) { + return false; + } + + CharacterPetNameRepository::ReplaceOne(database, { + .char_id = static_cast(CharacterID()), + .name = new_name + }); + + + if (GetPet()) { + GetPet()->TempName(new_name.c_str()); + } + + ClearPetNameChange(); + return true; +} + +bool Client::IsDiscovered(uint32 item_id) { + const auto& l = DiscoveredItemsRepository::GetWhere( + database, + fmt::format( + "item_id = {}", + item_id + ) + ); + if (l.empty()) { + return false; + } + + return true; +} + +void Client::DiscoverItem(uint32 item_id) { + auto e = DiscoveredItemsRepository::NewEntity(); + + e.account_status = Admin(); + e.char_name = GetCleanName(); + e.discovered_date = std::time(nullptr); + e.item_id = item_id; + + auto d = DiscoveredItemsRepository::InsertOne(database, e); + + if (player_event_logs.IsEventEnabled(PlayerEvent::DISCOVER_ITEM)) { + const auto* item = database.GetItem(item_id); + + auto e = PlayerEvent::DiscoverItemEvent{ + .item_id = item_id, + .item_name = item->Name, + }; + RecordPlayerEventLog(PlayerEvent::DISCOVER_ITEM, e); + + } + + if (parse->PlayerHasQuestSub(EVENT_DISCOVER_ITEM)) { + auto* item = database.CreateItem(item_id); + std::vector args = { item }; + + parse->EventPlayer(EVENT_DISCOVER_ITEM, this, "", item_id, &args); + } +} + +void Client::UpdateLFP() { + + Group *g = GetGroup(); + + if(g && !g->IsLeader(this)) { + database.SetLFP(CharacterID(), false); + worldserver.StopLFP(CharacterID()); + LFP = false; + return; + } + + GroupLFPMemberEntry LFPMembers[MAX_GROUP_MEMBERS]; + + for(unsigned int i=0; iGetZoneID(); + + if(g) { + // Fill the LFPMembers array with the rest of the group members, excluding ourself + // We don't fill in the class, level or zone, because we may not be able to determine + // them if the other group members are not in this zone. World will fill in this information + // for us, if it can. + int NextFreeSlot = 1; + for(unsigned int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if((g->membername[i][0] != '\0') && strcasecmp(g->membername[i], LFPMembers[0].Name)) + strcpy(LFPMembers[NextFreeSlot++].Name, g->membername[i]); + } + } + worldserver.UpdateLFP(CharacterID(), LFPMembers); +} + +bool Client::GroupFollow(Client* inviter) { + + if (inviter) { + isgrouped = true; + Raid* raid = entity_list.GetRaidByClient(inviter); + Raid* iraid = entity_list.GetRaidByClient(this); + + //inviter has a raid don't do group stuff instead do raid stuff! + if (raid) { + // Suspend the merc while in a raid (maybe a rule could be added for this) + if (GetMerc()) { + GetMerc()->Suspend(); + } + + uint32 groupToUse = 0xFFFFFFFF; + for (const auto& m : raid->members) { + if (m.member && m.member == inviter) { + groupToUse = m.group_number; + break; + } + } + if (iraid == raid) { + //both in same raid + uint32 ngid = raid->GetGroup(inviter->GetName()); + if (raid->GroupCount(ngid) < MAX_GROUP_MEMBERS) { + raid->MoveMember(GetName(), ngid); + raid->SendGroupDisband(this); + raid->GroupUpdate(ngid); + } + return false; + } + if (raid->RaidCount() < MAX_RAID_MEMBERS) + { + // okay, so we now have a single client (this) joining a group in a raid + // And they're not already in the raid (which is above and doesn't need xtarget shit) + if (!GetXTargetAutoMgr()->empty()) { + raid->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); + GetXTargetAutoMgr()->clear(); + RemoveAutoXTargets(); + } + + SetXTargetAutoMgr(raid->GetXTargetAutoMgr()); + if (!GetXTargetAutoMgr()->empty()) + SetDirtyAutoHaters(); + + if (raid->GroupCount(groupToUse) < MAX_GROUP_MEMBERS) + { + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->AddMember(this, groupToUse); + raid->SendBulkRaid(this); + //raid->SendRaidGroupAdd(GetName(), groupToUse); + //raid->SendGroupUpdate(this); + raid->GroupUpdate(groupToUse); //break + if (raid->IsLocked()) + { + raid->SendRaidLockTo(this); + } + return false; + } + else + { + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->AddMember(this); + raid->SendBulkRaid(this); + if (raid->IsLocked()) + { + raid->SendRaidLockTo(this); + } + return false; + } + } + } + + Group* group = entity_list.GetGroupByClient(inviter); + + if (!group) + { + //Make new group + group = new Group(inviter); + + if (!group) + { + return false; + } + + entity_list.AddGroup(group); + + if (group->GetID() == 0) + { + Message(Chat::Red, "Unable to get new group id. Cannot create group."); + inviter->Message(Chat::Red, "Unable to get new group id. Cannot create group."); + return false; + } + + //now we have a group id, can set inviter's id + group->AddToGroup(inviter); + database.SetGroupLeaderName(group->GetID(), inviter->GetName()); + group->UpdateGroupAAs(); + + //Invite the inviter into the group first.....dont ask + if (inviter->ClientVersion() < EQ::versions::ClientVersion::SoD) + { + auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupJoin_Struct)); + GroupJoin_Struct* outgj = (GroupJoin_Struct*)outapp->pBuffer; + strcpy(outgj->membername, inviter->GetName()); + strcpy(outgj->yourname, inviter->GetName()); + outgj->action = groupActInviteInitial; // 'You have formed the group'. + group->GetGroupAAs(&outgj->leader_aas); + inviter->QueuePacket(outapp); + safe_delete(outapp); + } + else + { + // SoD and later + inviter->SendGroupCreatePacket(); + inviter->SendGroupLeaderChangePacket(inviter->GetName()); + inviter->SendGroupJoinAcknowledge(); + } + group->GetXTargetAutoMgr()->merge(*inviter->GetXTargetAutoMgr()); + inviter->GetXTargetAutoMgr()->clear(); + inviter->SetXTargetAutoMgr(group->GetXTargetAutoMgr()); + } + + if (!group) + { + return false; + } + + // Remove merc from old group before adding client to the new one + if (GetMerc() && GetMerc()->HasGroup()) + { + GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); + } + + if (!group->AddMember(this)) + { + // If failed to add client to new group, regroup with merc + if (GetMerc()) + { + GetMerc()->MercJoinClientGroup(); + } + else + { + isgrouped = false; + } + return false; + } + + if (ClientVersion() >= EQ::versions::ClientVersion::SoD) + { + SendGroupJoinAcknowledge(); + } + + // Temporary hack for SoD, as things seem to work quite differently + if (inviter->IsClient() && inviter->ClientVersion() >= EQ::versions::ClientVersion::SoD) + { + database.RefreshGroupFromDB(inviter); + } + + // Add the merc back into the new group if possible + if (GetMerc()) + { + GetMerc()->MercJoinClientGroup(); + } + + if (inviter->IsLFP()) + { + // If the player who invited us to a group is LFP, have them update world now that we have joined their group. + inviter->UpdateLFP(); + } + + database.RefreshGroupFromDB(this); + group->SendHPManaEndPacketsTo(this); + //send updates to clients out of zone... + group->SendGroupJoinOOZ(this); + return true; + } + return false; +} + +uint16 Client::GetPrimarySkillValue() +{ + EQ::skills::SkillType skill = EQ::skills::HIGHEST_SKILL; //because nullptr == 0, which is 1H Slashing, & we want it to return 0 from GetSkill + bool equipped = m_inv.GetItem(EQ::invslot::slotPrimary); + + if (!equipped) + skill = EQ::skills::SkillHandtoHand; + + else { + + uint8 type = m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->ItemType; //is this the best way to do this? + + switch (type) { + case EQ::item::ItemType1HSlash: // 1H Slashing + skill = EQ::skills::Skill1HSlashing; + break; + case EQ::item::ItemType2HSlash: // 2H Slashing + skill = EQ::skills::Skill2HSlashing; + break; + case EQ::item::ItemType1HPiercing: // Piercing + skill = EQ::skills::Skill1HPiercing; + break; + case EQ::item::ItemType1HBlunt: // 1H Blunt + skill = EQ::skills::Skill1HBlunt; + break; + case EQ::item::ItemType2HBlunt: // 2H Blunt + skill = EQ::skills::Skill2HBlunt; + break; + case EQ::item::ItemType2HPiercing: // 2H Piercing + if (IsClient() && CastToClient()->ClientVersion() < EQ::versions::ClientVersion::RoF2) + skill = EQ::skills::Skill1HPiercing; + else + skill = EQ::skills::Skill2HPiercing; + break; + case EQ::item::ItemTypeMartial: // Hand to Hand + skill = EQ::skills::SkillHandtoHand; + break; + default: // All other types default to Hand to Hand + skill = EQ::skills::SkillHandtoHand; + break; + } + } + + return GetSkill(skill); +} + +uint32 Client::GetTotalATK() +{ + uint32 AttackRating = 0; + uint32 WornCap = itembonuses.ATK; + + if(IsClient()) { + AttackRating = ((WornCap * 1.342) + (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69)); + AttackRating += aabonuses.ATK + GroupLeadershipAAOffenseEnhancement(); + + if (AttackRating < 10) + AttackRating = 10; + } + else + AttackRating = GetATK(); + + AttackRating += spellbonuses.ATK; + + return AttackRating; +} + +uint32 Client::GetATKRating() +{ + uint32 AttackRating = 0; + if(IsClient()) { + AttackRating = (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69); + + if (AttackRating < 10) + AttackRating = 10; + } + return AttackRating; +} + +void Client::VoiceMacroReceived(uint32 Type, char *Target, uint32 MacroNumber) { + + uint32 GroupOrRaidID = 0; + + switch(Type) { + + case VoiceMacroGroup: { + + Group* g = GetGroup(); + + if(g) + GroupOrRaidID = g->GetID(); + else + return; + + break; + } + + case VoiceMacroRaid: { + + Raid* r = GetRaid(); + + if(r) + GroupOrRaidID = r->GetID(); + else + return; + + break; + } + } + + if(!worldserver.SendVoiceMacro(this, Type, Target, MacroNumber, GroupOrRaidID)) + Message(0, "Error: World server disconnected"); +} + +void Client::ClearGroupAAs() { + for(unsigned int i = 0; i < MAX_GROUP_LEADERSHIP_AA_ARRAY; i++) + m_pp.leader_abilities.ranks[i] = 0; + + m_pp.group_leadership_points = 0; + m_pp.raid_leadership_points = 0; + m_pp.group_leadership_exp = 0; + m_pp.raid_leadership_exp = 0; + + Save(); + database.SaveCharacterLeadershipAbilities(CharacterID(), &m_pp); +} + +void Client::UpdateGroupAAs(int32 points, uint32 type) { + switch(type) { + case 0: { m_pp.group_leadership_points += points; break; } + case 1: { m_pp.raid_leadership_points += points; break; } + } + SendLeadershipEXPUpdate(); +} + +bool Client::IsLeadershipEXPOn() { + + if(!m_pp.leadAAActive) + return false; + + Group *g = GetGroup(); + + if (g && g->IsLeader(this) && g->GroupCount() > 2) + return true; + + Raid *r = GetRaid(); + + if (!r) + return false; + + // raid leaders can only gain raid AA XP + if (r->IsLeader(this)) { + if (r->RaidCount() > 17) + return true; + else + return false; + } + + uint32 gid = r->GetGroup(this); + + if (gid > 11) // not in a group + return false; + + if (r->IsGroupLeader(GetName()) && r->GroupCount(gid) > 2) + return true; + + return false; + +} + +uint32 Client::GetAggroCount() { + return AggroCount; +} + +// we pass in for book keeping if RestRegen is enabled +void Client::IncrementAggroCount(bool raid_target) +{ + // This method is called when a client is added to a mob's hate list. It turns the clients aggro flag on so + // rest state regen is stopped, and for SoF, it sends the opcode to show the crossed swords in-combat indicator. + AggroCount++; + + if(!RuleB(Character, RestRegenEnabled)) + return; + + uint32 newtimer = raid_target ? RuleI(Character, RestRegenRaidTimeToActivate) : RuleI(Character, RestRegenTimeToActivate); + + // When our aggro count is 1 here, we are exiting rest state. We need to pause our current timer, if we have time remaining + // We should not actually have to do anything to the Timer object since the AggroCount counter blocks it from being checked + // and will have it's timer changed when we exit combat so let's not do any extra work + if (AggroCount == 1 && rest_timer.GetRemainingTime()) // the Client::rest_timer is never disabled, so don't need to check + m_pp.RestTimer = std::max(1u, rest_timer.GetRemainingTime() / 1000); // I guess round up? + + // save the new timer if it's higher + m_pp.RestTimer = std::max(m_pp.RestTimer, newtimer); + + // If we already had aggro before this method was called, the combat indicator should already be up for SoF clients, + // so we don't need to send it again. + // + if(AggroCount > 1) + return; + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + auto outapp = new EQApplicationPacket(OP_RestState, 1); + char *Buffer = (char *)outapp->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x01); + QueuePacket(outapp); + safe_delete(outapp); + } + +} + +void Client::DecrementAggroCount() +{ + // This should be called when a client is removed from a mob's hate list (it dies or is memblurred). + // It checks whether any other mob is aggro on the player, and if not, starts the rest timer. + // For SoF, the opcode to start the rest state countdown timer in the UI is sent. + + // If we didn't have aggro before, this method should not have been called. + if(!AggroCount) + return; + + AggroCount--; + + if(!RuleB(Character, RestRegenEnabled)) + return; + + // Something else is still aggro on us, can't rest yet. + if (AggroCount) + return; + + rest_timer.Start(m_pp.RestTimer * 1000); + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + auto outapp = new EQApplicationPacket(OP_RestState, 5); + char *Buffer = (char *)outapp->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, m_pp.RestTimer); + QueuePacket(outapp); + safe_delete(outapp); + } +} + +// when we cast a beneficial spell we need to steal our targets current timer +// That's what we use this for +void Client::UpdateRestTimer(uint32 new_timer) +{ + // their timer was 0, so we don't do anything + if (new_timer == 0) + return; + + if (!RuleB(Character, RestRegenEnabled)) + return; + + // so if we're currently on aggro, we check our saved timer + if (AggroCount) { + if (m_pp.RestTimer < new_timer) // our timer needs to be updated, don't need to update client here + m_pp.RestTimer = new_timer; + } else { // if we're not aggro, we need to check if current timer needs updating + if (rest_timer.GetRemainingTime() / 1000 < new_timer) { + rest_timer.Start(new_timer * 1000); + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + auto outapp = new EQApplicationPacket(OP_RestState, 5); + char *Buffer = (char *)outapp->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, new_timer); + QueuePacket(outapp); + safe_delete(outapp); + } + } + } +} + +void Client::SendPVPStats() +{ + // This sends the data to the client to populate the PVP Stats Window. + // + // When the PVP Stats window is opened, no opcode is sent. Therefore this method should be called + // from Client::CompleteConnect, and also when the player makes a PVP kill. + // + auto outapp = new EQApplicationPacket(OP_PVPStats, sizeof(PVPStats_Struct)); + PVPStats_Struct *pvps = (PVPStats_Struct *)outapp->pBuffer; + + pvps->Kills = m_pp.PVPKills; + pvps->Deaths = m_pp.PVPDeaths; + pvps->PVPPointsAvailable = m_pp.PVPCurrentPoints; + pvps->TotalPVPPoints = m_pp.PVPCareerPoints; + pvps->BestKillStreak = m_pp.PVPBestKillStreak; + pvps->WorstDeathStreak = m_pp.PVPWorstDeathStreak; + pvps->CurrentKillStreak = m_pp.PVPCurrentKillStreak; + + // TODO: Record and send other PVP Stats + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SendCrystalCounts() +{ + auto outapp = new EQApplicationPacket(OP_CrystalCountUpdate, sizeof(CrystalCountUpdate_Struct)); + CrystalCountUpdate_Struct *ccus = (CrystalCountUpdate_Struct *)outapp->pBuffer; + + ccus->CurrentRadiantCrystals = GetRadiantCrystals(); + ccus->CurrentEbonCrystals = GetEbonCrystals(); + ccus->CareerRadiantCrystals = m_pp.careerRadCrystals; + ccus->CareerEbonCrystals = m_pp.careerEbonCrystals; + + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SendDisciplineTimers() +{ + + auto outapp = new EQApplicationPacket(OP_DisciplineTimer, sizeof(DisciplineTimer_Struct)); + DisciplineTimer_Struct *dts = (DisciplineTimer_Struct *)outapp->pBuffer; + + for(unsigned int i = 0; i < MAX_DISCIPLINE_TIMERS; ++i) + { + uint32 RemainingTime = p_timers.GetRemainingTime(pTimerDisciplineReuseStart + i); + + if(RemainingTime > 0) + { + dts->TimerID = i; + dts->Duration = RemainingTime; + QueuePacket(outapp); + } + } + + safe_delete(outapp); +} + +void Client::SendRespawnBinds() +{ + // This sends the data to the client to populate the Respawn from Death Window. + // + // This should be sent after OP_Death for SoF clients + // Client will respond with a 4 byte packet that includes the number of the selection made + // + + //If no options have been given, default to Bind + Rez + if (respawn_options.empty()) + { + BindStruct* b = &m_pp.binds[0]; + RespawnOption opt; + opt.name = "Bind Location"; + opt.zone_id = b->zone_id; + opt.instance_id = b->instance_id; + opt.x = b->x; + opt.y = b->y; + opt.z = b->z; + opt.heading = b->heading; + respawn_options.push_front(opt); + } + //Rez is always added at the end + RespawnOption rez; + rez.name = "Resurrect"; + rez.zone_id = zone->GetZoneID(); + rez.instance_id = zone->GetInstanceID(); + rez.x = GetX(); + rez.y = GetY(); + rez.z = GetZ(); + rez.heading = GetHeading(); + respawn_options.push_back(rez); + + int num_options = respawn_options.size(); + uint32 PacketLength = 17 + (26 * num_options); //Header size + per-option invariant size + + std::list::iterator itr; + RespawnOption* opt = nullptr; + + //Find string size for each option + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + opt = &(*itr); + PacketLength += opt->name.size() + 1; //+1 for cstring + } + + auto outapp = new EQApplicationPacket(OP_RespawnWindow, PacketLength); + char* buffer = (char*)outapp->pBuffer; + + //Packet header + VARSTRUCT_ENCODE_TYPE(uint32, buffer, initial_respawn_selection); //initial selection (from 0) + VARSTRUCT_ENCODE_TYPE(uint32, buffer, RuleI(Character, RespawnFromHoverTimer) * 1000); + VARSTRUCT_ENCODE_TYPE(uint32, buffer, 0); //unknown + VARSTRUCT_ENCODE_TYPE(uint32, buffer, num_options); //number of options to display + + //Individual options + int count = 0; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + opt = &(*itr); + VARSTRUCT_ENCODE_TYPE(uint32, buffer, count++); //option num (from 0) + VARSTRUCT_ENCODE_TYPE(uint32, buffer, opt->zone_id); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->x); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->y); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->z); + VARSTRUCT_ENCODE_TYPE(float, buffer, opt->heading); + VARSTRUCT_ENCODE_STRING(buffer, opt->name.c_str()); + VARSTRUCT_ENCODE_TYPE(uint8, buffer, (count == num_options)); //is this one Rez (the last option)? + } + + QueuePacket(outapp); + safe_delete(outapp); + return; +} + +void Client::HandleLDoNOpen(NPC *target) +{ + if(target) + { + if(target->GetClass() != Class::LDoNTreasure) + { + LogDebug("[{}] tried to open [{}] but [{}] was not a treasure chest", + GetName(), target->GetName(), target->GetName()); + return; + } + + if (target->GetSpecialAbility(SpecialAbility::OpenImmunity)) + { + LogDebug("[{}] tried to open [{}] but it was immune", GetName(), target->GetName()); + return; + } + + if(DistanceSquaredNoZ(m_Position, target->GetPosition()) > RuleI(Adventure, LDoNTrapDistanceUse)) + { + LogDebug("[{}] tried to open [{}] but [{}] was out of range", + GetName(), target->GetName(), target->GetName()); + Message(Chat::Red, "Treasure chest out of range."); + return; + } + + if(target->IsLDoNTrapped()) + { + if(target->GetLDoNTrapSpellID() != 0) + { + MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + } + else + { + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + } + } + + if(target->IsLDoNLocked()) + { + MessageString(Chat::Skills, LDON_STILL_LOCKED, target->GetCleanName()); + return; + } + else + { + target->AddToHateList(this, 0, 500000, false, false, false); + if(target->GetLDoNTrapType() != 0) + { + if(GetRaid()) + { + GetRaid()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); + } + else if(GetGroup()) + { + GetGroup()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); + } + else + { + AddEXP(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, GetLevelCon(target->GetLevel())); + } + } + target->Death(this, 0, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); + } + } } -void Client::CheckLanguageSkillIncrease(uint8 language_id, uint8 teacher_skill) { - if (IsDead() || IsUnconscious()) { - return; - } +void Client::HandleLDoNSenseTraps(NPC *target, uint16 skill, uint8 type) +{ + if(target && target->GetClass() == Class::LDoNTreasure) + { + if(target->IsLDoNTrapped()) + { + if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) + { + MessageString(Chat::Skills, LDON_CANT_DETERMINE_TRAP, target->GetCleanName()); + return; + } - if (IsAIControlled()) { - return; - } + if(target->IsLDoNTrapDetected()) + { + MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); + } + else + { + int check = LDoNChest_SkillCheck(target, skill); + switch(check) + { + case -1: + case 0: + MessageString(Chat::Skills, LDON_DONT_KNOW_TRAPPED, target->GetCleanName()); + break; + case 1: + MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); + target->SetLDoNTrapDetected(true); + break; + default: + break; + } + } + } + else + { + MessageString(Chat::Skills, LDON_CERTAIN_NOT_TRAP, target->GetCleanName()); + } + } +} - if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { - return; - } +void Client::HandleLDoNDisarm(NPC *target, uint16 skill, uint8 type) +{ + if(target) + { + if(target->GetClass() == Class::LDoNTreasure) + { + if(!target->IsLDoNTrapped()) + { + MessageString(Chat::Skills, LDON_WAS_NOT_TRAPPED, target->GetCleanName()); + return; + } - const uint8 language_skill = m_pp.languages[language_id]; // get current language skill + if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) + { + MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); + return; + } - if (language_skill < Language::MaxValue) { // if the language isn't already maxed - int chance = 5 + ((teacher_skill - language_skill) / 10); // greater chance to learn if teacher's skill is much higher than yours - chance = (chance * RuleI(Character, SkillUpModifier) / 100); + int check = 0; + if(target->IsLDoNTrapDetected()) + { + check = LDoNChest_SkillCheck(target, skill); + } + else + { + check = LDoNChest_SkillCheck(target, skill*33/100); + } + switch(check) + { + case 1: + target->SetLDoNTrapDetected(false); + target->SetLDoNTrapped(false); + target->SetLDoNTrapSpellID(0); + MessageString(Chat::Skills, LDON_HAVE_DISARMED, target->GetCleanName()); + break; + case 0: + MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); + break; + case -1: + MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + break; + } + } + } +} - if (zone->random.Real(0, 100) < chance) { // if they make the roll - IncreaseLanguageSkill(language_id); +void Client::HandleLDoNPickLock(NPC *target, uint16 skill, uint8 type) +{ + if(target) + { + if(target->GetClass() == Class::LDoNTreasure) + { + if(target->IsLDoNTrapped()) + { + MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); + target->SetLDoNTrapSpellID(0); + target->SetLDoNTrapped(false); + target->SetLDoNTrapDetected(false); + } - if (parse->PlayerHasQuestSub(EVENT_LANGUAGE_SKILL_UP)) { - const auto &export_string = fmt::format( - "{} {} {}", - language_id, - language_skill + 1, - Language::MaxValue - ); + if(!target->IsLDoNLocked()) + { + MessageString(Chat::Skills, LDON_WAS_NOT_LOCKED, target->GetCleanName()); + return; + } - parse->EventPlayer(EVENT_LANGUAGE_SKILL_UP, this, export_string, 0); - } + if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) + { + Message(Chat::Skills, "You cannot unlock %s with this skill.", target->GetCleanName()); + return; + } - LogSkills("Language [{}] at value [{}] successfully gain with [{}] % chance", language_id, language_skill, chance); - } else { - LogSkills("Language [{}] at value [{}] failed to gain with [{}] % chance", language_id, language_skill, chance); - } - } + int check = LDoNChest_SkillCheck(target, skill); + + switch(check) + { + case 0: + case -1: + MessageString(Chat::Skills, LDON_PICKLOCK_FAILURE, target->GetCleanName()); + break; + case 1: + target->SetLDoNLocked(false); + MessageString(Chat::Skills, LDON_PICKLOCK_SUCCESS, target->GetCleanName()); + break; + } + } + } } -bool Client::HasSkill(EQ::skills::SkillType skill_id) const +int Client::LDoNChest_SkillCheck(NPC *target, int skill) { - return GetSkill(skill_id) > 0 && CanHaveSkill(skill_id); + if(!target) + return -1; + + int chest_difficulty = target->GetLDoNLockedSkill() == 0 ? (target->GetLevel() * 5) : target->GetLDoNLockedSkill(); + float base_difficulty = RuleR(Adventure, LDoNBaseTrapDifficulty); + + if(chest_difficulty == 0) + chest_difficulty = 5; + + float chance = ((100.0f - base_difficulty) * ((float)skill / (float)chest_difficulty)); + + if(chance > (100.0f - base_difficulty)) + { + chance = 100.0f - base_difficulty; + } + + float d100 = (float)zone->random.Real(0, 100); + + if(d100 <= chance) + return 1; + else + { + if(d100 > (chance + RuleR(Adventure, LDoNCriticalFailTrapThreshold))) + return -1; + } + + return 0; } -bool Client::CanHaveSkill(EQ::skills::SkillType skill_id) const +void Client::SummonAndRezzAllCorpses() { - if ( - ClientVersion() < EQ::versions::ClientVersion::RoF2 && - class_ == Class::Berserker && - skill_id == EQ::skills::Skill1HPiercing - ) { - skill_id = EQ::skills::Skill2HPiercing; - } + PendingRezzXP = -1; + + auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); + + ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; + + sdapcs->CharacterID = CharacterID(); + sdapcs->ZoneID = zone->GetZoneID(); + sdapcs->InstanceID = zone->GetInstanceID(); + + worldserver.SendPacket(Pack); + + safe_delete(Pack); + + entity_list.RemoveAllCorpsesByCharID(CharacterID()); + + int CorpseCount = database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), GetPosition()); + if(CorpseCount <= 0) + { + Message(Chat::Yellow, "You have no corpses to summnon."); + return; + } + + int RezzExp = entity_list.RezzAllCorpsesByCharID(CharacterID()); - return skill_caps.GetSkillCap(GetClass(), skill_id, RuleI(Character, MaxLevel)).cap > 0; + if(RezzExp > 0) + SetEXP(ExpSource::Resurrection, GetEXP() + RezzExp, GetAAXP(), true); + + Message(Chat::Yellow, "All your corpses have been summoned to your feet and have received a 100% resurrection."); } -uint16 Client::MaxSkill(EQ::skills::SkillType skill_id, uint8 class_id, uint8 level) const +void Client::SummonAllCorpses(const glm::vec4& position) { - if ( - ClientVersion() < EQ::versions::ClientVersion::RoF2 && - class_id == Class::Berserker && - skill_id == EQ::skills::Skill1HPiercing - ) { - skill_id = EQ::skills::Skill2HPiercing; - } + auto summonLocation = position; + if(IsOrigin(position) && position.w == 0.0f) + summonLocation = GetPosition(); + + auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); + + ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; + + sdapcs->CharacterID = CharacterID(); + sdapcs->ZoneID = zone->GetZoneID(); + sdapcs->InstanceID = zone->GetInstanceID(); + + worldserver.SendPacket(Pack); + + safe_delete(Pack); - return skill_caps.GetSkillCap(class_id, skill_id, level).cap; + entity_list.RemoveAllCorpsesByCharID(CharacterID()); + + database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), summonLocation); } -uint8 Client::GetSkillTrainLevel(EQ::skills::SkillType skill_id, uint8 class_id) +void Client::DepopAllCorpses() { - if ( - ClientVersion() < EQ::versions::ClientVersion::RoF2 && - class_id == Class::Berserker && - skill_id == EQ::skills::Skill1HPiercing - ) { - skill_id = EQ::skills::Skill2HPiercing; - } + auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); + + ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; - return skill_caps.GetSkillTrainLevel(class_id, skill_id, RuleI(Character, MaxLevel)); + sdapcs->CharacterID = CharacterID(); + sdapcs->ZoneID = zone->GetZoneID(); + sdapcs->InstanceID = zone->GetInstanceID(); + + worldserver.SendPacket(Pack); + + safe_delete(Pack); + + entity_list.RemoveAllCorpsesByCharID(CharacterID()); } -uint16 Client::GetMaxSkillAfterSpecializationRules(EQ::skills::SkillType skillid, uint16 maxSkill) +void Client::DepopPlayerCorpse(uint32 dbid) { - uint16 Result = maxSkill; - - uint16 PrimarySpecialization = 0, SecondaryForte = 0; - - uint16 PrimarySkillValue = 0, SecondarySkillValue = 0; - - uint16 MaxSpecializations = aabonuses.SecondaryForte ? 2 : 1; - - if (skillid >= EQ::skills::SkillSpecializeAbjure && skillid <= EQ::skills::SkillSpecializeEvocation) - { - bool HasPrimarySpecSkill = false; - - int NumberOfPrimarySpecSkills = 0; - - for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) - { - if(m_pp.skills[i] > 50) - { - HasPrimarySpecSkill = true; - NumberOfPrimarySpecSkills++; - } - if(m_pp.skills[i] > PrimarySkillValue) - { - if(PrimarySkillValue > SecondarySkillValue) - { - SecondarySkillValue = PrimarySkillValue; - SecondaryForte = PrimarySpecialization; - } - - PrimarySpecialization = i; - PrimarySkillValue = m_pp.skills[i]; - } - else if(m_pp.skills[i] > SecondarySkillValue) - { - SecondaryForte = i; - SecondarySkillValue = m_pp.skills[i]; - } - } - - if(SecondarySkillValue <=50) - SecondaryForte = 0; - - if(HasPrimarySpecSkill) - { - if(NumberOfPrimarySpecSkills <= MaxSpecializations) - { - if(MaxSpecializations == 1) - { - if(skillid != PrimarySpecialization) - { - Result = 50; - } - } - else - { - if((skillid != PrimarySpecialization) && ((skillid == SecondaryForte) || (SecondaryForte == 0))) - { - if((PrimarySkillValue > 100) || (!SecondaryForte)) - Result = 100; - } - else if(skillid != PrimarySpecialization) - { - Result = 50; - } - } - } - else - { - Message(Chat::Red, "Your spell casting specializations skills have been reset. " - "Only %i primary specialization skill is allowed.", MaxSpecializations); - - for (int i = EQ::skills::SkillSpecializeAbjure; i <= EQ::skills::SkillSpecializeEvocation; ++i) - SetSkill((EQ::skills::SkillType)i, 1); - - Save(); - - LogInfo("Reset [{}]'s caster specialization skills to 1" - "Too many specializations skills were above 50.", GetCleanName()); - } - - } - } - - Result += spellbonuses.RaiseSkillCap[skillid] + itembonuses.RaiseSkillCap[skillid] + aabonuses.RaiseSkillCap[skillid]; - - if (skillid == EQ::skills::SkillType::SkillForage) - Result += aabonuses.GrantForage; - - return Result; -} + auto Pack = new ServerPacket(ServerOP_DepopPlayerCorpse, sizeof(ServerDepopPlayerCorpse_Struct)); -void Client::SetPVP(bool toggle, bool message) { - m_pp.pvp = toggle ? 1 : 0; + ServerDepopPlayerCorpse_Struct *sdpcs = (ServerDepopPlayerCorpse_Struct*)Pack->pBuffer; - if (message) { - if(GetPVP()) { - MessageString(Chat::Shout, PVP_ON); - } else { - Message(Chat::Shout, "You now follow the ways of Order."); - } - } + sdpcs->DBID = dbid; + sdpcs->ZoneID = zone->GetZoneID(); + sdpcs->InstanceID = zone->GetInstanceID(); - SendAppearancePacket(AppearanceType::PVP, GetPVP()); - Save(); -} + worldserver.SendPacket(Pack); -void Client::Kick(const std::string &reason) { - client_state = CLIENT_KICKED; + safe_delete(Pack); - LogClientLogin("Client [{}] kicked, reason [{}]", GetCleanName(), reason.c_str()); + entity_list.RemoveCorpseByDBID(dbid); } -void Client::WorldKick() { - auto outapp = new EQApplicationPacket(OP_GMKick, sizeof(GMKick_Struct)); - GMKick_Struct* gmk = (GMKick_Struct *)outapp->pBuffer; - strcpy(gmk->name,GetName()); - QueuePacket(outapp); - safe_delete(outapp); - Kick("World kick issued"); +void Client::BuryPlayerCorpses() +{ + database.BuryAllCharacterCorpses(CharacterID()); } -void Client::GMKill() { - auto outapp = new EQApplicationPacket(OP_GMKill, sizeof(GMKill_Struct)); - GMKill_Struct* gmk = (GMKill_Struct *)outapp->pBuffer; - strcpy(gmk->name,GetName()); - QueuePacket(outapp); - safe_delete(outapp); -} +void Client::NotifyNewTitlesAvailable() +{ + auto outapp = new EQApplicationPacket(OP_NewTitlesAvailable, 0); -void Client::MemorizeSpell(uint32 slot, uint32 spell_id, uint32 scribing, uint32 reduction){ - if ( - !EQ::ValueWithin( - slot, - 0, - (EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize - 1) - ) - ) { - return; - } - - if ( - !EQ::ValueWithin( - spell_id, - 3, - EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellIdMax - ) && - spell_id != UINT32_MAX - ) { - return; - } - - auto outapp = new EQApplicationPacket(OP_MemorizeSpell, sizeof(MemorizeSpell_Struct)); - - auto* mss = (MemorizeSpell_Struct*) outapp->pBuffer; - - mss->scribing = scribing; - mss->slot = slot; - mss->spell_id = spell_id; - mss->reduction = reduction; - - outapp->priority = 5; - - if ( - parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) || - parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) || - parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) - ) { - const auto export_string = fmt::format("{} {}", slot, spell_id); - - if ( - scribing == ScribeSpellActions::Memorize && - parse->PlayerHasQuestSub(EVENT_MEMORIZE_SPELL) - ) { - parse->EventPlayer(EVENT_MEMORIZE_SPELL, this, export_string, 0); - } else if ( - scribing == ScribeSpellActions::Unmemorize && - parse->PlayerHasQuestSub(EVENT_UNMEMORIZE_SPELL) - ) { - parse->EventPlayer(EVENT_UNMEMORIZE_SPELL, this, export_string, 0); - } else if ( - scribing == ScribeSpellActions::Scribe && - parse->PlayerHasQuestSub(EVENT_SCRIBE_SPELL) - ) { - parse->EventPlayer(EVENT_SCRIBE_SPELL, this, export_string, 0); - } - } - - QueuePacket(outapp); - safe_delete(outapp); -} + QueuePacket(outapp); + + safe_delete(outapp); -void Client::Disarm(Client* disarmer, int chance) { - int16 slot = EQ::invslot::SLOT_INVALID; - const EQ::ItemInstance *inst = GetInv().GetItem(EQ::invslot::slotPrimary); - if (inst && inst->IsWeapon()) { - slot = EQ::invslot::slotPrimary; - } - else { - inst = GetInv().GetItem(EQ::invslot::slotSecondary); - if (inst && inst->IsWeapon()) - slot = EQ::invslot::slotSecondary; - } - if (slot != EQ::invslot::SLOT_INVALID && inst->IsClassCommon()) { - // We have an item that can be disarmed. - if (zone->random.Int(0, 1000) <= chance) { - // Find a free inventory slot - int16 slot_id = EQ::invslot::SLOT_INVALID; - slot_id = m_inv.FindFreeSlot(false, true, inst->GetItem()->Size, (inst->GetItem()->ItemType == EQ::item::ItemTypeArrow)); - if (slot_id != EQ::invslot::SLOT_INVALID) - { - EQ::ItemInstance *InvItem = m_inv.PopItem(slot); - if (InvItem) { // there should be no way it is not there, but check anyway - EQApplicationPacket* outapp = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); - MoveItem_Struct* mi = (MoveItem_Struct*)outapp->pBuffer; - mi->from_slot = slot; - mi->to_slot = 0xFFFFFFFF; - if (inst->IsStackable()) // it should not be stackable - mi->number_in_stack = inst->GetCharges(); - else - mi->number_in_stack = 0; - FastQueuePacket(&outapp); // this deletes item from the weapon slot on the client - if (PutItemInInventory(slot_id, *InvItem, true)) - database.SaveInventory(CharacterID(), NULL, slot); - auto matslot = (slot == EQ::invslot::slotPrimary ? EQ::textures::weaponPrimary : EQ::textures::weaponSecondary); - if (matslot != EQ::textures::materialInvalid) - SendWearChange(matslot); - } - MessageString(Chat::Skills, DISARMED); - if (disarmer != this) - disarmer->MessageString(Chat::Skills, DISARM_SUCCESS, GetCleanName()); - if (chance != 1000) - disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 4); - CalcBonuses(); - // CalcEnduranceWeightFactor(); - return; - } - disarmer->MessageString(Chat::Skills, DISARM_FAILED); - if (chance != 1000) - disarmer->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 2); - return; - } - } - disarmer->MessageString(Chat::Skills, DISARM_FAILED); } -bool Client::BindWound(Mob *bindmob, bool start, bool fail) +void Client::SetStartZone(uint32 zoneid, float x, float y, float z, float heading) { - EQApplicationPacket *outapp = nullptr; - if (!fail) { - outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); - BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; - // Start bind - if (!bindwound_timer.Enabled()) { - // make sure we actually have a bandage... and consume it. - int16 bslot = m_inv.HasItemByUse(EQ::item::ItemTypeBandage, 1, invWhereWorn | invWherePersonal); - if (bslot == INVALID_INDEX) { - bind_out->type = 3; - QueuePacket(outapp); - bind_out->type = 7; // this is the wrong message, dont know the right one. - QueuePacket(outapp); - safe_delete(outapp); - return (true); - } - DeleteItemInInventory(bslot, 1, true); // do we need client update? - - // start complete timer - bindwound_timer.Start(10000); - bindwound_target = bindmob; - - // Send client unlock - bind_out->type = 3; - QueuePacket(outapp); - bind_out->type = 0; - // Client Unlocked - if (!bindmob) { - // send "bindmob dead" to client - bind_out->type = 4; - QueuePacket(outapp); - bind_out->type = 0; - bindwound_timer.Disable(); - bindwound_target = 0; - } else { - // send bindmob "stand still" - if (!bindmob->IsAIControlled() && bindmob != this) { - bindmob->CastToClient()->MessageString(Chat::Yellow, - YOU_ARE_BEING_BANDAGED); - } else if (bindmob->IsAIControlled() && bindmob != this) { - ; // Tell IPC to stand still? - } else { - ; // Binding self - } - } - } else if (bindwound_timer.Check()) // Did the timer finish? - { - // finish bind - // disable complete timer - bindwound_timer.Disable(); - bindwound_target = 0; - if (!bindmob) { - // send "bindmob gone" to client - bind_out->type = 5; // not in zone - QueuePacket(outapp); - bind_out->type = 0; - } - - else { - if (!GetFeigned() && (DistanceSquared(bindmob->GetPosition(), m_Position) <= 400)) { - // send bindmob bind done - if (!bindmob->IsAIControlled() && bindmob != this) { - - } else if (bindmob->IsAIControlled() && bindmob != this) { - // Tell IPC to resume?? - } else { - // Binding self - } - // Send client bind done - - bind_out->type = 1; // Done - QueuePacket(outapp); - bind_out->type = 0; - CheckIncreaseSkill(EQ::skills::SkillBindWound, nullptr, 5); - - if (RuleB(Character, UseOldBindWound)) { - int maxHPBonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + - aabonuses.MaxBindWound; - - int max_percent = 50 + maxHPBonus; - - if (GetClass() == Class::Monk && GetSkill(EQ::skills::SkillBindWound) > 200) { - max_percent = 70 + maxHPBonus; - } - - int64 max_hp = bindmob->GetMaxHP() * max_percent / 100; - - // send bindmob new hp's - if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() <= (max_hp)-1) { - // 0.120 per skill point, 0.60 per skill level, minimum 3 max 30 - int bindhps = 3; - - if (GetSkill(EQ::skills::SkillBindWound) > 200) { - bindhps += GetSkill(EQ::skills::SkillBindWound) * 4 / 10; - } - else if (GetSkill(EQ::skills::SkillBindWound) >= 10) { - bindhps += GetSkill(EQ::skills::SkillBindWound) / 4; - } - - // Implementation of aaMithanielsBinding is a guess (the multiplier) - int bindBonus = spellbonuses.BindWound + itembonuses.BindWound + - aabonuses.BindWound; - - bindhps += bindhps * bindBonus / 100; - - // if the bind takes them above the max bindable - // cap it at that value. Dont know if live does it this way - // but it makes sense to me. - int chp = bindmob->GetHP() + bindhps; - if (chp > max_hp) - chp = max_hp; - - bindmob->SetHP(chp); - bindmob->SendHPUpdate(); - } - else { - // I dont have the real, live - Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", - max_percent); - if (bindmob != this && bindmob->IsClient()) - bindmob->CastToClient()->Message( - 15, - "You cannot have your wounds bound above %d%% hitpoints.", - max_percent); - // Too many hp message goes here. - } - } - else { - int percent_base = 50; - if (GetRawSkill(EQ::skills::SkillBindWound) > 200) { - if ((GetClass() == Class::Monk) || (GetClass() == Class::Beastlord)) - percent_base = 70; - else if ((GetLevel() > 50) && ((GetClass() == Class::Warrior) || (GetClass() == Class::Rogue) || (GetClass() == Class::Cleric))) - percent_base = 70; - } - - int percent_bonus = spellbonuses.MaxBindWound + itembonuses.MaxBindWound + aabonuses.MaxBindWound; - - int max_percent = percent_base + percent_bonus; - if (max_percent < 0) - max_percent = 0; - if (max_percent > 100) - max_percent = 100; - - int max_hp = (bindmob->GetMaxHP() * max_percent) / 100; - if (max_hp > bindmob->GetMaxHP()) - max_hp = bindmob->GetMaxHP(); - - if (bindmob->GetHP() < bindmob->GetMaxHP() && bindmob->GetHP() < max_hp) { - int bindhps = 3; // base bind hp - if (percent_base >= 70) - bindhps = (GetSkill(EQ::skills::SkillBindWound) * 4) / 10; // 8:5 skill-to-hp ratio - else if (GetSkill(EQ::skills::SkillBindWound) >= 12) - bindhps = GetSkill(EQ::skills::SkillBindWound) / 4; // 4:1 skill-to-hp ratio - - int bonus_hp_percent = spellbonuses.BindWound + itembonuses.BindWound + aabonuses.BindWound; - - bindhps += (bindhps * bonus_hp_percent) / 100; - - if (bindhps < 3) - bindhps = 3; - - bindhps += bindmob->GetHP(); - if (bindhps > max_hp) - bindhps = max_hp; - - bindmob->SetHP(bindhps); - bindmob->SendHPUpdate(); - } - else { - Message(Chat::Yellow, "You cannot bind wounds above %d%% hitpoints.", max_percent); - if (bindmob != this && bindmob->IsClient()) - bindmob->CastToClient()->Message(Chat::Yellow, "You cannot have your wounds bound above %d%% hitpoints.", max_percent); - } - } - } - else { - // Send client bind failed - if (bindmob != this) - bind_out->type = 6; // They moved - else - bind_out->type = 7; // Bandager moved - - QueuePacket(outapp); - bind_out->type = 0; - } - } - } - } else if (bindwound_timer.Enabled()) { - // You moved - outapp = new EQApplicationPacket(OP_Bind_Wound, sizeof(BindWound_Struct)); - BindWound_Struct *bind_out = (BindWound_Struct *)outapp->pBuffer; - bindwound_timer.Disable(); - bindwound_target = 0; - bind_out->type = 7; - QueuePacket(outapp); - bind_out->type = 3; - QueuePacket(outapp); - } - safe_delete(outapp); - return true; + // setting city to zero allows the player to use /setstartcity to set the city themselves + if(zoneid == 0) { + m_pp.binds[4].zone_id = 0; + Message(Chat::Yellow,"Your starting city has been reset. Use /setstartcity to choose a new one"); + return; + } + + // check to make sure the zone is valid + const char *target_zone_name = ZoneName(zoneid); + if(target_zone_name == nullptr) + return; + + m_pp.binds[4].zone_id = zoneid; + if(zone->GetInstanceID() != 0 && zone->IsInstancePersistent()) { + m_pp.binds[4].instance_id = zone->GetInstanceID(); + } + + if (x == 0 && y == 0 && z == 0) { + auto zd = GetZone(m_pp.binds[4].zone_id); + if (zd) { + m_pp.binds[4].x = zd->safe_x; + m_pp.binds[4].y = zd->safe_y; + m_pp.binds[4].z = zd->safe_z; + m_pp.binds[4].heading = zd->safe_heading; + } + } + else { + m_pp.binds[4].x = x; + m_pp.binds[4].y = y; + m_pp.binds[4].z = z; + m_pp.binds[4].heading = heading; + } } -void Client::SetMaterial(int16 in_slot, uint32 item_id) +uint32 Client::GetStartZone() { - const EQ::ItemData *item = database.GetItem(item_id); - if (item && item->IsClassCommon()) { - uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); - if (matslot != EQ::textures::materialInvalid) { - m_pp.item_material.Slot[matslot].Material = GetEquipmentMaterial(matslot); - } - } + return m_pp.binds[4].zone_id; } -void Client::ServerFilter(SetServerFilter_Struct* filter){ +void Client::ShowSkillsWindow() +{ + std::string popup_text; + std::map skills_map = EQ::skills::GetSkillTypeMap(); -/* this code helps figure out the filter IDs in the packet if needed - static SetServerFilter_Struct ssss; - int r; - uint32 *o = (uint32 *) &ssss; - uint32 *n = (uint32 *) filter; - for(r = 0; r < (sizeof(SetServerFilter_Struct)/4); r++) { - if(*o != *n) - LogFile->write(EQEMuLog::Debug, "Filter %d changed from %d to %d", r, *o, *n); - o++; n++; - } - memcpy(&ssss, filter, sizeof(SetServerFilter_Struct)); -*/ -#define Filter0(type) \ - if(filter->filters[type] == 1) \ - SetFilter(type, FilterShow); \ - else \ - SetFilter(type, FilterHide); -#define Filter1(type) \ - if(filter->filters[type] == 0) \ - SetFilter(type, FilterShow); \ - else \ - SetFilter(type, FilterHide); - - Filter0(FilterGuildChat); - Filter0(FilterSocials); - Filter0(FilterGroupChat); - Filter0(FilterShouts); - Filter0(FilterAuctions); - Filter0(FilterOOC); - Filter0(FilterBadWords); - - if (filter->filters[FilterPCSpells] == 0) { - SetFilter(FilterPCSpells, FilterShow); - } else if (filter->filters[FilterPCSpells] == 1) { - SetFilter(FilterPCSpells, FilterHide); - } else { - SetFilter(FilterPCSpells, FilterShowGroupOnly); - } - - Filter1(FilterNPCSpells); - - if (filter->filters[FilterBardSongs] == 0) { - SetFilter(FilterBardSongs, FilterShow); - } else if (filter->filters[FilterBardSongs] == 1) { - SetFilter(FilterBardSongs, FilterShowSelfOnly); - } else if (filter->filters[FilterBardSongs] == 2) { - SetFilter(FilterBardSongs, FilterShowGroupOnly); - } else { - SetFilter(FilterBardSongs, FilterHide); - } - - if (filter->filters[FilterSpellCrits] == 0) { - SetFilter(FilterSpellCrits, FilterShow); - } else if (filter->filters[FilterSpellCrits] == 1) { - SetFilter(FilterSpellCrits, FilterShowSelfOnly); - } else { - SetFilter(FilterSpellCrits, FilterHide); - } - - if (filter->filters[FilterMeleeCrits] == 0) { - SetFilter(FilterMeleeCrits, FilterShow); - } else if (filter->filters[FilterMeleeCrits] == 1) { - SetFilter(FilterMeleeCrits, FilterShowSelfOnly); - } else { - SetFilter(FilterMeleeCrits, FilterHide); - } - - if (filter->filters[FilterSpellDamage] == 0) { - SetFilter(FilterSpellDamage, FilterShow); - } else if (filter->filters[FilterSpellDamage] == 1) { - SetFilter(FilterSpellDamage, FilterShowSelfOnly); - } else { - SetFilter(FilterSpellDamage, FilterHide); - } - - Filter0(FilterMyMisses); - Filter0(FilterOthersMiss); - Filter0(FilterOthersHit); - Filter0(FilterMissedMe); - Filter1(FilterDamageShields); - - if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { - if (filter->filters[FilterDOT] == 0) { - SetFilter(FilterDOT, FilterShow); - } else if (filter->filters[FilterDOT] == 1) { - SetFilter(FilterDOT, FilterShowSelfOnly); - } else if (filter->filters[FilterDOT] == 2) { - SetFilter(FilterDOT, FilterShowGroupOnly); - } else { - SetFilter(FilterDOT, FilterHide); - } - } else { - if (filter->filters[FilterDOT] == 0) { // show functions as self only - SetFilter(FilterDOT, FilterShowSelfOnly); - } else { - SetFilter(FilterDOT, FilterHide); - } - } - - Filter1(FilterPetHits); - Filter1(FilterPetMisses); - Filter1(FilterFocusEffects); - Filter1(FilterPetSpells); - - if (ClientVersionBit() & EQ::versions::maskSoDAndLater) { - if (filter->filters[FilterHealOverTime] == 0) { - SetFilter(FilterHealOverTime, FilterShow); - } else if (filter->filters[FilterHealOverTime] == 1) { - SetFilter(FilterHealOverTime, FilterShowSelfOnly); - } else { - SetFilter(FilterHealOverTime, FilterHide); - } - } else { // these clients don't have a 'self only' filter - Filter1(FilterHealOverTime); - } - - Filter1(FilterItemSpeech); - Filter1(FilterStrikethrough); - Filter1(FilterStuns); - Filter1(FilterBardSongsOnPets); -} + if (ClientVersion() < EQ::versions::ClientVersion::RoF2) { + skills_map[EQ::skills::Skill1HPiercing] = "Piercing"; + } -// this version is for messages with no parameters -void Client::MessageString(uint32 type, uint32 string_id, uint32 distance) -{ - if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) - return; - if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... - return; - if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) - return; - auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); - SimpleMessage_Struct* sms = (SimpleMessage_Struct*)outapp->pBuffer; - sms->color=type; - sms->string_id=string_id; + // Table Start + popup_text += ""; + + for (const auto& skill : skills_map) { + auto skill_id = skill.first; + auto skill_name = skill.second; + auto can_have_skill = CanHaveSkill(skill_id); + auto current_skill = GetSkill(skill_id); + auto max_skill = MaxSkill(skill_id); + auto skill_maxed = current_skill >= max_skill; + if ( + skill_id == EQ::skills::Skill2HPiercing && + ClientVersion() < EQ::versions::ClientVersion::RoF2 + ) { + continue; + } - sms->unknown8=0; + if ( + !can_have_skill || + !current_skill || + !max_skill + ) { + continue; + } - if(distance>0) - entity_list.QueueCloseClients(this,outapp,false,distance); - else - QueuePacket(outapp); - safe_delete(outapp); -} + // Row Start + popup_text += ""; -// -// this list of 9 args isn't how I want to do it, but to use va_arg -// you have to know how many args you're expecting, and to do that we have -// to load the eqstr file and count them in the string. -// This hack sucks but it's gonna work for now. -// -void Client::MessageString(uint32 type, uint32 string_id, const char* message1, - const char* message2,const char* message3,const char* message4, - const char* message5,const char* message6,const char* message7, - const char* message8,const char* message9, uint32 distance) -{ - if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) - return; - if (GetFilter(FilterMeleeCrits) == FilterHide && type == Chat::MeleeCrit) //98 is self... - return; - if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) - return; - if (GetFilter(FilterDamageShields) == FilterHide && type == Chat::DamageShield) - return; - if (GetFilter(FilterFocusEffects) == FilterHide && type == Chat::FocusEffect) - return; - - if (type == Chat::Emote) - type = 4; - - if (!message1) { - MessageString(type, string_id); // use the simple message instead - return; - } - - const char *message_arg[] = { - message1, message2, message3, message4, message5, - message6, message7, message8, message9 - }; - - SerializeBuffer buf(20); - buf.WriteInt32(0); // unknown - buf.WriteInt32(string_id); - buf.WriteInt32(type); - for (auto &m : message_arg) { - if (m == nullptr) - break; - buf.WriteString(m); - } - - buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime - - auto outapp = std::make_unique(OP_FormattedMessage, buf); - - if (distance > 0) - entity_list.QueueCloseClients(this, outapp.get(), false, distance); - else - QueuePacket(outapp.get()); + // Skill Name + popup_text += fmt::format( + "", + skill_name + ); + + // Current Skill Level out of Max Skill Level or a Check Mark for Maxed + popup_text += fmt::format( + "", + ( + skill_maxed ? + "Max" : + fmt::format( + "{}/{}", + current_skill, + max_skill + ) + ) + ); + + // Row End + popup_text += ""; + } + + // Table End + popup_text += "
{}{}
"; + + SendPopupToClient( + "Skills", + popup_text.c_str() + ); } -void Client::MessageString(const CZClientMessageString_Struct* msg) +void Client::Signal(int signal_id) { - if (msg) - { - if (msg->args_size == 0) - { - MessageString(msg->chat_type, msg->string_id); - } - else - { - uint32_t outsize = sizeof(FormattedMessage_Struct) + msg->args_size; - auto outapp = std::make_unique(OP_FormattedMessage, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->string_id = msg->string_id; - outbuf->type = msg->chat_type; - memcpy(outbuf->message, msg->args, msg->args_size); - QueuePacket(outapp.get()); - } - } + if (parse->PlayerHasQuestSub(EVENT_SIGNAL)) { + parse->EventPlayer(EVENT_SIGNAL, this, std::to_string(signal_id), 0); + } } -// helper function, returns true if we should see the message -bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) +void Client::SendPayload(int payload_id, std::string payload_value) { - eqFilterMode mode = GetFilter(filter); - // easy ones first - if (mode == FilterShow) { - return true; - } else if (mode == FilterHide) { - return false; - } - - if (sender != this && mode == FilterShowSelfOnly) { - return false; - } else if (sender) { - if (mode == FilterShowGroupOnly) { - auto g = GetGroup(); - auto r = GetRaid(); - if (g) { - if (g->IsGroupMember(sender)) { - return true; - } - } else if (r && sender->IsClient()) { - auto rgid1 = r->GetGroup(this); - auto rgid2 = r->GetGroup(sender->CastToClient()); - if (rgid1 != RAID_GROUPLESS && rgid1 == rgid2) { - return true; - } - } else { - return false; - } - } - } - - // we passed our checks - return true; + if (parse->PlayerHasQuestSub(EVENT_PAYLOAD)) { + const auto& export_string = fmt::format("{} {}", payload_id, payload_value); + + parse->EventPlayer(EVENT_PAYLOAD, this, export_string, 0); + } } -void Client::FilteredMessageString(Mob *sender, uint32 type, - eqFilterType filter, uint32 string_id) +void Client::SendRewards() { - if (!FilteredMessageCheck(sender, filter)) - return; + std::vector rewards; + std::string query = StringFormat("SELECT reward_id, amount " + "FROM account_rewards " + "WHERE account_id = %i " + "ORDER BY reward_id", AccountID()); + auto results = database.QueryDatabase(query); + if (!results.Success()) { + return; + } - auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); - SimpleMessage_Struct *sms = (SimpleMessage_Struct *)outapp->pBuffer; - sms->color = type; - sms->string_id = string_id; + for (auto row = results.begin(); row != results.end(); ++row) { + ClientReward cr; + cr.id = Strings::ToInt(row[0]); + cr.amount = Strings::ToInt(row[1]); + rewards.push_back(cr); + } - sms->unknown8 = 0; + if(rewards.empty()) + return; + + auto vetapp = new EQApplicationPacket(OP_VetRewardsAvaliable, (sizeof(InternalVeteranReward) * rewards.size())); + uchar *data = vetapp->pBuffer; + for(int i = 0; i < rewards.size(); ++i) { + InternalVeteranReward *ivr = (InternalVeteranReward*)data; + ivr->claim_id = rewards[i].id; + ivr->number_available = rewards[i].amount; + auto iter = zone->VeteranRewards.begin(); + for (;iter != zone->VeteranRewards.end(); ++iter) + if((*iter).claim_id == rewards[i].id) + break; + + if(iter != zone->VeteranRewards.end()) { + InternalVeteranReward ivro = (*iter); + ivr->claim_count = ivro.claim_count; + for(int x = 0; x < ivro.claim_count; ++x) { + ivr->items[x].item_id = ivro.items[x].item_id; + ivr->items[x].charges = ivro.items[x].charges; + strcpy(ivr->items[x].item_name, ivro.items[x].item_name); + } + } - QueuePacket(outapp); - safe_delete(outapp); + data += sizeof(InternalVeteranReward); + } - return; + FastQueuePacket(&vetapp); } -void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id, - const char *message1, const char *message2, const char *message3, - const char *message4, const char *message5, const char *message6, - const char *message7, const char *message8, const char *message9) +bool Client::TryReward(uint32 claim_id) { - if (!FilteredMessageCheck(sender, filter)) - return; + // Make sure we have an open spot + // Make sure we have it in our acct and count > 0 + // Make sure the entry was found + // If we meet all the criteria: + // Decrement our count by 1 if it > 1 delete if it == 1 + // Create our item in bag if necessary at the free inv slot + // save + uint32 free_slot = 0xFFFFFFFF; + + for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; ++i) { + EQ::ItemInstance *item = GetInv().GetItem(i); + if (!item) { + free_slot = i; + break; + } + } + + if (free_slot == 0xFFFFFFFF) + return false; + + std::string query = StringFormat("SELECT amount FROM account_rewards " + "WHERE account_id = %i AND reward_id = %i", + AccountID(), claim_id); + auto results = database.QueryDatabase(query); + if (!results.Success()) + return false; + + if (results.RowCount() == 0) + return false; + + auto row = results.begin(); + + uint32 amt = Strings::ToInt(row[0]); + if (amt == 0) + return false; + + auto iter = std::find_if(zone->VeteranRewards.begin(), zone->VeteranRewards.end(), + [claim_id](const InternalVeteranReward &a) { return a.claim_id == claim_id; }); + + if (iter == zone->VeteranRewards.end()) + return false; + + if (amt == 1) { + query = StringFormat("DELETE FROM account_rewards " + "WHERE account_id = %i AND reward_id = %i", + AccountID(), claim_id); + auto results = database.QueryDatabase(query); + } else { + query = StringFormat("UPDATE account_rewards SET amount = (amount-1) " + "WHERE account_id = %i AND reward_id = %i", + AccountID(), claim_id); + auto results = database.QueryDatabase(query); + } - if (type == Chat::Emote) - type = 4; + auto &ivr = (*iter); + EQ::ItemInstance *claim = database.CreateItem(ivr.items[0].item_id, ivr.items[0].charges); + if (!claim) { + Save(); + return true; + } - if (!message1) { - FilteredMessageString(sender, type, filter, string_id); // use the simple message instead - return; - } + bool lore_conflict = CheckLoreConflict(claim->GetItem()); - const char *message_arg[] = { - message1, message2, message3, message4, message5, - message6, message7, message8, message9 - }; + for (int y = 1; y < 8; y++) + if (ivr.items[y].item_id && claim->GetItem()->ItemClass == 1) { + EQ::ItemInstance *item_temp = database.CreateItem(ivr.items[y].item_id, ivr.items[y].charges); + if (item_temp) { + if (CheckLoreConflict(item_temp->GetItem())) { + lore_conflict = true; + DuplicateLoreMessage(ivr.items[y].item_id); + } + claim->PutItem(y - 1, *item_temp); + safe_delete(item_temp); + } + } - SerializeBuffer buf(20); - buf.WriteInt32(0); // unknown - buf.WriteInt32(string_id); - buf.WriteInt32(type); - for (auto &m : message_arg) { - if (m == nullptr) - break; - buf.WriteString(m); - } + if (lore_conflict) { + safe_delete(claim); + return true; + } - buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime + PutItemInInventory(free_slot, *claim); + SendItemPacket(free_slot, claim, ItemPacketTrade); + safe_delete(claim); - auto outapp = std::make_unique(OP_FormattedMessage, buf); + Save(); + return true; +} - QueuePacket(outapp.get()); +uint32 Client::GetLDoNPointsTheme(uint32 t) +{ + switch(t) + { + case LDoNTheme::GUK: + return m_pp.ldon_points_guk; + case LDoNTheme::MIR: + return m_pp.ldon_points_mir; + case LDoNTheme::MMC: + return m_pp.ldon_points_mmc; + case LDoNTheme::RUJ: + return m_pp.ldon_points_ruj; + case LDoNTheme::TAK: + return m_pp.ldon_points_tak; + default: + return 0; + } } -void Client::Tell_StringID(uint32 string_id, const char *who, const char *message) +uint32 Client::GetLDoNWinsTheme(uint32 t) { - char string_id_str[10]; - snprintf(string_id_str, 10, "%d", string_id); + switch(t) + { + case LDoNTheme::GUK: + return m_pp.ldon_wins_guk; + case LDoNTheme::MIR: + return m_pp.ldon_wins_mir; + case LDoNTheme::MMC: + return m_pp.ldon_wins_mmc; + case LDoNTheme::RUJ: + return m_pp.ldon_wins_ruj; + case LDoNTheme::TAK: + return m_pp.ldon_wins_tak; + default: + return 0; + } +} - MessageString(Chat::EchoTell, TELL_QUEUED_MESSAGE, who, string_id_str, message); +uint32 Client::GetLDoNLossesTheme(uint32 t) +{ + switch(t) + { + case LDoNTheme::GUK: + return m_pp.ldon_losses_guk; + case LDoNTheme::MIR: + return m_pp.ldon_losses_mir; + case LDoNTheme::MMC: + return m_pp.ldon_losses_mmc; + case LDoNTheme::RUJ: + return m_pp.ldon_losses_ruj; + case LDoNTheme::TAK: + return m_pp.ldon_losses_tak; + default: + return 0; + } } -void Client::SetTint(int16 in_slot, uint32 color) { - EQ::textures::Tint_Struct new_color; - new_color.Color = color; - SetTint(in_slot, new_color); - database.SaveCharacterMaterialColor(CharacterID(), in_slot, color); +void Client::UpdateLDoNWinLoss(uint32 theme_id, bool win, bool remove) { + switch (theme_id) { + case LDoNTheme::GUK: + if (win) { + m_pp.ldon_wins_guk += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_guk += (remove ? -1 : 1); + } + break; + case LDoNTheme::MIR: + if (win) { + m_pp.ldon_wins_mir += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_mir += (remove ? -1 : 1); + } + break; + case LDoNTheme::MMC: + if (win) { + m_pp.ldon_wins_mmc += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_mmc += (remove ? -1 : 1); + } + break; + case LDoNTheme::RUJ: + if (win) { + m_pp.ldon_wins_ruj += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_ruj += (remove ? -1 : 1); + } + break; + case LDoNTheme::TAK: + if (win) { + m_pp.ldon_wins_tak += (remove ? -1 : 1); + } else { + m_pp.ldon_losses_tak += (remove ? -1 : 1); + } + break; + default: + return; + } + database.UpdateAdventureStatsEntry(CharacterID(), theme_id, win, remove); } -// Still need to reconcile bracer01 versus bracer02 -void Client::SetTint(int16 in_slot, EQ::textures::Tint_Struct& color) { - uint8 matslot = EQ::InventoryProfile::CalcMaterialFromSlot(in_slot); - if (matslot != EQ::textures::materialInvalid) - { - m_pp.item_tint.Slot[matslot].Color = color.Color; - database.SaveCharacterMaterialColor(CharacterID(), in_slot, color.Color); - } +void Client::SuspendMinion(int value) +{ + /* + SPA 151 Allows an extra pet to be saved and resummoned later. + Casting with a pet but without a suspended pet will suspend the pet + Casting without a pet and with a suspended pet will unsuspend the pet + effect value 0 = save pet with no buffs or equipment + effect value 1 = save pet with buffs and equipment + effect value 2 = unknown + Note: SPA 308 allows for suspended pets to be resummoned after zoning. + */ + + NPC *CurrentPet = GetPet()->CastToNPC(); + + if(!CurrentPet) + { + if(m_suspendedminion.SpellID > 0) + { + if (m_suspendedminion.SpellID >= SPDAT_RECORDS) { + Message(Chat::Red, "Invalid suspended minion spell id (%u).", m_suspendedminion.SpellID); + memset(&m_suspendedminion, 0, sizeof(PetInfo)); + return; + } + + MakePoweredPet(m_suspendedminion.SpellID, spells[m_suspendedminion.SpellID].teleport_zone, + m_suspendedminion.petpower, m_suspendedminion.Name, m_suspendedminion.size); -} + CurrentPet = GetPet()->CastToNPC(); -void Client::SetHideMe(bool flag) -{ - EQApplicationPacket app; + if(!CurrentPet) + { + Message(Chat::Red, "Failed to recall suspended minion."); + return; + } + + if(value >= 1) + { + CurrentPet->SetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items); + + CurrentPet->SendPetBuffsToClient(); + } + CurrentPet->CalcBonuses(); + + CurrentPet->SetHP(m_suspendedminion.HP); + + CurrentPet->SetMana(m_suspendedminion.Mana); + + CurrentPet->SetTaunting(m_suspendedminion.taunting); + + MessageString(Chat::Magenta, SUSPEND_MINION_UNSUSPEND, CurrentPet->GetCleanName()); + + memset(&m_suspendedminion, 0, sizeof(struct PetInfo)); + // TODO: These pet command states need to be synced ... + // Will just fix them for now + if (m_ClientVersionBit & EQ::versions::maskUFAndLater) { + SetPetCommandState(PET_BUTTON_SIT, 0); + SetPetCommandState(PET_BUTTON_STOP, 0); + SetPetCommandState(PET_BUTTON_REGROUP, 0); + SetPetCommandState(PET_BUTTON_FOLLOW, 1); + SetPetCommandState(PET_BUTTON_GUARD, 0); + // Taunt saved on client side for logging on with pet + // In our db for when we zone. + SetPetCommandState(PET_BUTTON_HOLD, 0); + SetPetCommandState(PET_BUTTON_GHOLD, 0); + SetPetCommandState(PET_BUTTON_FOCUS, 0); + SetPetCommandState(PET_BUTTON_SPELLHOLD, 0); + } + } + else + return; + + } + else + { + uint16 SpellID = CurrentPet->GetPetSpellID(); + + if(SpellID) + { + if(m_suspendedminion.SpellID > 0) + { + MessageString(Chat::Red,ONLY_ONE_PET); + + return; + } + else if(CurrentPet->IsEngaged()) + { + MessageString(Chat::Red,SUSPEND_MINION_FIGHTING); + + return; + } + else if(entity_list.Fighting(CurrentPet)) + { + MessageString(Chat::Blue,SUSPEND_MINION_HAS_AGGRO); + } + else + { + m_suspendedminion.SpellID = SpellID; + + m_suspendedminion.HP = CurrentPet->GetHP();; + + m_suspendedminion.Mana = CurrentPet->GetMana(); + m_suspendedminion.petpower = CurrentPet->GetPetPower(); + m_suspendedminion.size = CurrentPet->GetSize(); - gm_hide_me = flag; + if(value >= 1) + CurrentPet->GetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items, m_suspendedminion.Name); + else + strn0cpy(m_suspendedminion.Name, CurrentPet->GetName(), 64); // Name stays even at rank 1 - if (gm_hide_me) { - database.SetHideMe(AccountID(), true); - CreateDespawnPacket(&app, false); - entity_list.RemoveFromTargets(this); - trackable = false; - if (RuleB(Command, HideMeCommandDisablesTells)) { - tellsoff = true; - } - } else { - database.SetHideMe(AccountID(), false); - CreateSpawnPacket(&app); - trackable = true; - tellsoff = false; - } + MessageString(Chat::Magenta, SUSPEND_MINION_SUSPEND, CurrentPet->GetCleanName()); - entity_list.QueueClientsStatus(this, &app, true, 0, Admin() - 1); - UpdateWho(); + CurrentPet->Depop(false); + + SetPetID(0); + } + } + else + { + MessageString(Chat::Red, ONLY_SUMMONED_PETS); + + return; + } + } } -void Client::SetLanguageSkill(uint8 language_id, uint8 language_skill) +void Client::AddPVPPoints(uint32 Points) { - if (!EQ::ValueWithin(language_id, Language::CommonTongue, Language::Unknown27)) { - return; - } + m_pp.PVPCurrentPoints += Points; + m_pp.PVPCareerPoints += Points; - if (language_skill > Language::MaxValue) { - language_skill = Language::MaxValue; - } + Save(); - m_pp.languages[language_id] = language_skill; + SendPVPStats(); +} + +void Client::AddEbonCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentEbonCrystals += amount; + m_pp.careerEbonCrystals += amount; + + SaveCurrency(); + SendCrystalCounts(); + + MessageString( + Chat::Yellow, + YOU_RECEIVE, + fmt::format( + "{} {}", + amount, + database.CreateItemLink(RuleI(Zone, EbonCrystalItemID)) + ).c_str() + ); + + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { + const std::string &export_string = fmt::format( + "{} 0 {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); + } +} - database.SaveCharacterLanguage(CharacterID(), language_id, language_skill); +void Client::AddRadiantCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentRadCrystals += amount; + m_pp.careerRadCrystals += amount; + + SaveCurrency(); + SendCrystalCounts(); + + MessageString( + Chat::Yellow, + YOU_RECEIVE, + fmt::format( + "{} {}", + amount, + database.CreateItemLink(RuleI(Zone, RadiantCrystalItemID)) + ).c_str() + ); - auto outapp = new EQApplicationPacket(OP_SkillUpdate, sizeof(SkillUpdate_Struct)); - auto* s = (SkillUpdate_Struct*) outapp->pBuffer; + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { + const std::string &export_string = fmt::format( + "0 {} {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); + } +} - s->skillId = 100 + language_id; - s->value = m_pp.languages[language_id]; +void Client::RemoveEbonCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentEbonCrystals -= amount; - QueuePacket(outapp); - safe_delete(outapp); + SaveCurrency(); + SendCrystalCounts(); - MessageString(Chat::Skills, LANG_SKILL_IMPROVED); + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { + const std::string &export_string = fmt::format( + "{} 0 {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); + } } -void Client::LinkDead() -{ - if (GetGroup()) - { - entity_list.MessageGroup(this,true,15,"%s has gone linkdead.",GetName()); - GetGroup()->DelMember(this); - if (GetMerc()) - { - GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); - } - } - Raid *raid = entity_list.GetRaidByClient(this); - if(raid){ - raid->MemberZoned(this); - } - - SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::LinkDead); +void Client::RemoveRadiantCrystals(uint32 amount, bool is_reclaim) { + m_pp.currentRadCrystals -= amount; -// save_timer.Start(2500); - linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); - SendAppearancePacket(AppearanceType::Linkdead, 1); - client_state = CLIENT_LINKDEAD; - AI_Start(CLIENT_LD_TIMEOUT); + SaveCurrency(); + SendCrystalCounts(); + + if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { + const std::string &export_string = fmt::format( + "0 {} {}", + amount, + is_reclaim ? 1 : 0 + ); + parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); + } } -uint8 Client::SlotConvert(uint8 slot,bool bracer){ - uint8 slot2 = 0; // why are we returning MainCharm instead of INVALID_INDEX? (must be a pre-charm segment...) - if(bracer) - return EQ::invslot::slotWrist2; - switch(slot) { - case EQ::textures::armorHead: - slot2 = EQ::invslot::slotHead; - break; - case EQ::textures::armorChest: - slot2 = EQ::invslot::slotChest; - break; - case EQ::textures::armorArms: - slot2 = EQ::invslot::slotArms; - break; - case EQ::textures::armorWrist: - slot2 = EQ::invslot::slotWrist1; - break; - case EQ::textures::armorHands: - slot2 = EQ::invslot::slotHands; - break; - case EQ::textures::armorLegs: - slot2 = EQ::invslot::slotLegs; - break; - case EQ::textures::armorFeet: - slot2 = EQ::invslot::slotFeet; - break; - } - return slot2; +void Client::SetEbonCrystals(uint32 value) { + m_pp.currentEbonCrystals = value; + SaveCurrency(); + SendCrystalCounts(); } -uint8 Client::SlotConvert2(uint8 slot){ - uint8 slot2 = 0; // same as above... - switch(slot){ - case EQ::invslot::slotHead: - slot2 = EQ::textures::armorHead; - break; - case EQ::invslot::slotChest: - slot2 = EQ::textures::armorChest; - break; - case EQ::invslot::slotArms: - slot2 = EQ::textures::armorArms; - break; - case EQ::invslot::slotWrist1: - slot2 = EQ::textures::armorWrist; - break; - case EQ::invslot::slotHands: - slot2 = EQ::textures::armorHands; - break; - case EQ::invslot::slotLegs: - slot2 = EQ::textures::armorLegs; - break; - case EQ::invslot::slotFeet: - slot2 = EQ::textures::armorFeet; - break; - } - return slot2; +void Client::SetRadiantCrystals(uint32 value) { + m_pp.currentRadCrystals = value; + SaveCurrency(); + SendCrystalCounts(); +} + +// Processes a client request to inspect a SoF+ client's equipment. +void Client::ProcessInspectRequest(Client *requestee, Client *requester) +{ + if (requestee && requester) { + auto outapp = new EQApplicationPacket(OP_InspectAnswer, sizeof(InspectResponse_Struct)); + auto insr = (InspectResponse_Struct *) outapp->pBuffer; + + insr->TargetID = requester->GetID(); + insr->playerid = requestee->GetID(); + + const EQ::ItemData *item = nullptr; + const EQ::ItemInstance *inst = nullptr; + + for (int16 L = EQ::invslot::EQUIPMENT_BEGIN; L <= EQ::invslot::EQUIPMENT_END; L++) { + inst = requestee->GetInv().GetItem(L); + + if (inst) { + item = inst->GetItem(); + if (item) { + strcpy(insr->itemnames[L], item->Name); + + const EQ::ItemData *augment_item = nullptr; + const auto augment = inst->GetOrnamentationAugment(); + + if (augment) { + augment_item = augment->GetItem(); + } + + if (augment_item) { + insr->itemicons[L] = augment_item->Icon; + } else if (inst->GetOrnamentationIcon()) { + insr->itemicons[L] = inst->GetOrnamentationIcon(); + } else { + insr->itemicons[L] = item->Icon; + } + } else { + insr->itemnames[L][0] = '\0'; + insr->itemicons[L] = 0xFFFFFFFF; + } + } else { + insr->itemnames[L][0] = '\0'; + insr->itemicons[L] = 0xFFFFFFFF; + } + } + + strcpy(insr->text, requestee->GetInspectMessage().text); + + // There could be an OP for this..or not... (Ti clients are not processed here..this message is generated client-side) + if (requestee->IsClient() && requestee != requester) { + requestee->Message( + Chat::White, + fmt::format( + "{} is looking at your equipment...", + requester->GetName() + ).c_str() + ); + } + + requester->QueuePacket(outapp); // Send answer to requester + safe_delete(outapp); + } } -void Client::Escape() +void Client::GuildBankAck() { - entity_list.RemoveFromTargets(this, true); - SetInvisible(Invisibility::Invisible); - MessageString(Chat::Skills, ESCAPE); -} + auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankAck_Struct)); -float Client::CalcClassicPriceMod(Mob* other, bool reverse) { - float price_multiplier = 0.8f; - - if (other && other->IsNPC()) { - FACTION_VALUE faction_level = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); - int32 cha = GetCHA(); - - if (faction_level <= FACTION_AMIABLY) { - cha += 11; // amiable faction grants a defacto 11 charisma bonus - } - - uint8 greed = other->CastToNPC()->GetGreedPercent(); - - // Sony's precise algorithm is unknown, but this produces output that is virtually identical - if (faction_level <= FACTION_INDIFFERENTLY) { - if (cha > 75) { - if (greed) { - // this is derived from curve fitting to a lot of price data - price_multiplier = -0.2487768 + (1.599635 - -0.2487768) / (1 + pow((cha / 135.1495), 1.001983)); - price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' - price_multiplier = 1.0f / price_multiplier; - } - else { - // non-greedy merchants use a linear scale - price_multiplier = 1.0f - ((115.0f - cha) * 0.004f); - } - } - else if (cha > 60) { - price_multiplier = 1.0f / (1.25f + (greed / 100.0f)); - } - else { - price_multiplier = 1.0f / ((1.0f - (cha - 120.0f) / 220.0f) + (greed / 100.0f)); - } - } - else { // apprehensive - if (cha > 75) { - if (greed) { - // this is derived from curve fitting to a lot of price data - price_multiplier = -0.25f + (1.823662 - -0.25f) / (1 + (cha / 135.0f)); - price_multiplier += (greed + 25u) / 100.0f; // default vendor markup is 25%; anything above that is 'greedy' - price_multiplier = 1.0f / price_multiplier; - } - else { - price_multiplier = (100.0f - (145.0f - cha) / 2.8f) / 100.0f; - } - } - else if (cha > 60) { - price_multiplier = 1.0f / (1.4f + greed / 100.0f); - } - else { - price_multiplier = 1.0f / ((1.0f + (143.574 - cha) / 196.434) + (greed / 100.0f)); - } - } - - float maxResult = 1.0f / 1.05; // price reduction caps at this amount - if (price_multiplier > maxResult) { - price_multiplier = maxResult; - } - - if (!reverse) { - price_multiplier = 1.0f / price_multiplier; - } - } - - LogMerchants( - "[{}] [{}] items at [{}] price multiplier [{}] [{}]", - other->GetName(), - reverse ? "buys" : "sells", - price_multiplier, - reverse ? "from" : "to", - GetName() - ); - - return price_multiplier; -} + GuildBankAck_Struct *gbas = (GuildBankAck_Struct*) outapp->pBuffer; -float Client::CalcNewPriceMod(Mob* other, bool reverse) -{ - float chaformula = 0; - if (other) - { - int factionlvl = GetFactionLevel(CharacterID(), other->CastToNPC()->GetNPCTypeID(), GetFactionRace(), GetClass(), GetDeity(), other->CastToNPC()->GetPrimaryFaction(), other); - if (factionlvl >= FACTION_APPREHENSIVELY) // Apprehensive or worse. - { - if (GetCHA() > 103) - { - chaformula = (GetCHA() - 103)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. - if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) - chaformula = -1*(RuleI(Merchant, PriceBonusPct)); - } - else if (GetCHA() < 103) - { - chaformula = (103 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // This will bottom out price penalty. - if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) - chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); - } - } - if (factionlvl <= FACTION_INDIFFERENTLY) // Indifferent or better. - { - if (GetCHA() > 75) - { - chaformula = (GetCHA() - 75)*((-(RuleR(Merchant, ChaBonusMod))/100)*(RuleI(Merchant, PriceBonusPct))); // This will max out price bonus. - if (chaformula < -1*(RuleI(Merchant, PriceBonusPct))) - chaformula = -1*(RuleI(Merchant, PriceBonusPct)); - } - else if (GetCHA() < 75) - { - chaformula = (75 - GetCHA())*(((RuleR(Merchant, ChaPenaltyMod))/100)*(RuleI(Merchant, PricePenaltyPct))); // Faction modifier keeps up from reaching bottom price penalty. - if (chaformula > 1*(RuleI(Merchant, PricePenaltyPct))) - chaformula = 1*(RuleI(Merchant, PricePenaltyPct)); - } - } - } - - if (reverse) - chaformula *= -1; //For selling - //Now we have, for example, 10 - chaformula /= 100; //Convert to 0.10 - chaformula += 1; //Convert to 1.10; - return chaformula; //Returns 1.10, expensive stuff! + gbas->Action = GuildBankAcknowledge; + + FastQueuePacket(&outapp); } -float Client::CalcPriceMod(Mob* other, bool reverse) +void Client::GuildBankDepositAck(bool Fail, int8 action) { - float price_mod = CalcNewPriceMod(other, reverse); - if (RuleB(Merchant, UseClassicPriceMod)) { - price_mod = CalcClassicPriceMod(other, reverse); - } + auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankDepositAck_Struct)); - return price_mod; -} + GuildBankDepositAck_Struct *gbdas = (GuildBankDepositAck_Struct*) outapp->pBuffer; -void Client::GetGroupAAs(GroupLeadershipAA_Struct *into) const { - memcpy(into, &m_pp.leader_abilities.group, sizeof(GroupLeadershipAA_Struct)); -} + gbdas->Action = action; -void Client::GetRaidAAs(RaidLeadershipAA_Struct *into) const { - memcpy(into, &m_pp.leader_abilities.raid, sizeof(RaidLeadershipAA_Struct)); + gbdas->Fail = Fail ? 1 : 0; + + FastQueuePacket(&outapp); } -void Client::EnteringMessages(Client* client) +void Client::ClearGuildBank() { - std::string rules = RuleS(World, Rules); + auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankClear_Struct)); - if (!rules.empty() || database.GetVariable("Rules", rules)) { - const uint8 flag = database.GetAgreementFlag(client->AccountID()); - if (!flag) { - const std::string& rules_link = Saylink::Silent("#serverrules", "rules"); + GuildBankClear_Struct *gbcs = (GuildBankClear_Struct*) outapp->pBuffer; - client->Message( - Chat::White, - fmt::format( - "You must agree to the {} before you can move.", - rules_link - ).c_str() - ); + gbcs->Action = GuildBankBulkItems; + gbcs->DepositAreaCount = 0; + gbcs->MainAreaCount = 0; - client->SendAppearancePacket(AppearanceType::Animation, Animation::Freeze); - } - } + FastQueuePacket(&outapp); } -void Client::SendRules() +void Client::SendGroupCreatePacket() { - std::string rules = RuleS(World, Rules); + // For SoD and later clients, this is sent the Group Leader upon initial creation of the group + // + auto outapp = new EQApplicationPacket(OP_GroupUpdateB, 32 + strlen(GetName())); - if (rules.empty() && !database.GetVariable("Rules", rules)) { - return; - } + char *Buffer = (char *)outapp->pBuffer; + // Header + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // group ID probably + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 1); // count of members in packet + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // Null Leader name, shouldn't be null besides this case - auto lines = Strings::Split(rules, "|"); - auto line_number = 1; - for (auto&& line : lines) { - Message( - Chat::White, - fmt::format( - "{}. {}", - line_number, - line - ).c_str() - ); - line_number++; - } + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // Member 0, index + VARSTRUCT_ENCODE_STRING(Buffer, GetName()); // group member name + VARSTRUCT_ENCODE_TYPE(uint16, Buffer, 0); // merc flag + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // owner name (if merc) + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, GetLevel()); // level + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group tank flag + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group assist flag + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group puller flag + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // offline flag + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // timestamp + + FastQueuePacket(&outapp); } -void Client::SetEndurance(int32 newEnd) +void Client::SendGroupLeaderChangePacket(const char *LeaderName) { - /*Endurance can't be less than 0 or greater than max*/ - if(newEnd < 0) - newEnd = 0; - else if(newEnd > GetMaxEndurance()){ - newEnd = GetMaxEndurance(); - } + // For SoD and later, send name of Group Leader to this client + + auto outapp = new EQApplicationPacket(OP_GroupLeaderChange, sizeof(GroupLeaderChange_Struct)); - current_endurance = newEnd; - CheckManaEndUpdate(); + GroupLeaderChange_Struct *glcs = (GroupLeaderChange_Struct*)outapp->pBuffer; + + strn0cpy(glcs->LeaderName, LeaderName, sizeof(glcs->LeaderName)); + + FastQueuePacket(&outapp); } -void Client::SacrificeConfirm(Mob *caster) +void Client::SendGroupJoinAcknowledge() { - auto outapp = new EQApplicationPacket(OP_Sacrifice, sizeof(Sacrifice_Struct)); - Sacrifice_Struct *ss = (Sacrifice_Struct *)outapp->pBuffer; - - if (!caster || PendingSacrifice) { - safe_delete(outapp); - return; - } - - if (GetLevel() < RuleI(Spells, SacrificeMinLevel)) { - caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. - safe_delete(outapp); - return; - } - - if (GetLevel() > RuleI(Spells, SacrificeMaxLevel)) { - caster->MessageString(Chat::Red, SAC_TOO_HIGH); - safe_delete(outapp); - return; - } - - ss->CasterID = caster->GetID(); - ss->TargetID = GetID(); - ss->Confirm = 0; - QueuePacket(outapp); - safe_delete(outapp); - // We store the Caster's id, because when the packet comes back, it only has the victim's entityID in it, - // not the caster. - sacrifice_caster_id = caster->GetID(); - PendingSacrifice = true; + // For SoD and later, This produces the 'You have joined the group' message. + auto outapp = new EQApplicationPacket(OP_GroupAcknowledge, 4); + FastQueuePacket(&outapp); } -//Essentially a special case death function -void Client::Sacrifice(Mob *caster) +void Client::SendAdventureError(const char *error) { - if (GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)) { - int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000); - if (exploss < GetEXP()) { - SetEXP(ExpSource::Sacrifice, GetEXP() - exploss, GetAAXP(), false); - SendLogoutPackets(); - - // make our become corpse packet, and queue to ourself before OP_Death. - EQApplicationPacket app2(OP_BecomeCorpse, sizeof(BecomeCorpse_Struct)); - BecomeCorpse_Struct *bc = (BecomeCorpse_Struct *)app2.pBuffer; - bc->spawn_id = GetID(); - bc->x = GetX(); - bc->y = GetY(); - bc->z = GetZ(); - QueuePacket(&app2); - - // make death packet - EQApplicationPacket app(OP_Death, sizeof(Death_Struct)); - Death_Struct *d = (Death_Struct *)app.pBuffer; - d->spawn_id = GetID(); - d->killer_id = caster ? caster->GetID() : 0; - d->bindzoneid = GetPP().binds[0].zone_id; - d->spell_id = SPELL_UNKNOWN; - d->attack_skill = 0xe7; - d->damage = 0; - app.priority = 6; - entity_list.QueueClients(this, &app); - - BuffFadeAll(); - UnmemSpellAll(); - Group *g = GetGroup(); - if (g) { - g->MemberZoned(this); - } - Raid *r = entity_list.GetRaidByClient(this); - if (r) { - r->MemberZoned(this); - } - ClearAllProximities(); - if (RuleB(Character, LeaveCorpses)) { - auto new_corpse = new Corpse(this, 0); - entity_list.AddCorpse(new_corpse, GetID()); - SetID(0); - entity_list.QueueClients(this, &app2, true); - } - Save(); - GoToDeath(); - if (caster && caster->IsClient()) { - caster->CastToClient()->SummonItem(RuleI(Spells, SacrificeItemID)); - } else if (caster && caster->IsNPC()) { - caster->CastToNPC()->AddItem(RuleI(Spells, SacrificeItemID), 1, false); - } - } - } else { - caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice. - } + size_t error_size = strlen(error); + auto outapp = new EQApplicationPacket(OP_AdventureInfo, (error_size + 2)); + strn0cpy((char*)outapp->pBuffer, error, error_size); + FastQueuePacket(&outapp); } -void Client::SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID) { - - if(!Caster || PendingTranslocate) - return; - - const SPDat_Spell_Struct &Spell = spells[SpellID]; - - auto outapp = new EQApplicationPacket(OP_Translocate, sizeof(Translocate_Struct)); - Translocate_Struct *ts = (Translocate_Struct*)outapp->pBuffer; - - strcpy(ts->Caster, Caster->GetName()); - PendingTranslocateData.spell_id = ts->SpellID = SpellID; - - if((SpellID == 1422) || (SpellID == 1334) || (SpellID == 3243)) { - PendingTranslocateData.zone_id = ts->ZoneID = m_pp.binds[0].zone_id; - PendingTranslocateData.instance_id = m_pp.binds[0].instance_id; - PendingTranslocateData.x = ts->x = m_pp.binds[0].x; - PendingTranslocateData.y = ts->y = m_pp.binds[0].y; - PendingTranslocateData.z = ts->z = m_pp.binds[0].z; - PendingTranslocateData.heading = m_pp.binds[0].heading; - } - else { - PendingTranslocateData.zone_id = ts->ZoneID = ZoneID(Spell.teleport_zone); - PendingTranslocateData.instance_id = 0; - PendingTranslocateData.y = ts->y = Spell.base_value[0]; - PendingTranslocateData.x = ts->x = Spell.base_value[1]; - PendingTranslocateData.z = ts->z = Spell.base_value[2]; - PendingTranslocateData.heading = 0.0; - } - - ts->unknown008 = 0; - ts->Complete = 0; - - PendingTranslocate = true; - TranslocateTime = time(nullptr); +void Client::SendAdventureDetails() +{ + if(adv_data) + { + ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; + auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); + AdventureRequestResponse_Struct *arr = (AdventureRequestResponse_Struct*)outapp->pBuffer; + arr->unknown000 = 0xBFC40100; + arr->unknown2080 = 0x0A; + arr->risk = ad->risk; + strcpy(arr->text, ad->text); + + if(ad->time_to_enter != 0) + { + arr->timetoenter = ad->time_to_enter; + } + else + { + arr->timeleft = ad->time_left; + } - QueuePacket(outapp); - safe_delete(outapp); + if(ad->zone_in_id == zone->GetZoneID()) + { + arr->y = ad->x; + arr->x = ad->y; + arr->showcompass = 1; + } + FastQueuePacket(&outapp); - return; -} -void Client::SendPickPocketResponse(Mob *from, uint32 amt, int type, const EQ::ItemData* item){ - auto outapp = new EQApplicationPacket(OP_PickPocket, sizeof(sPickPocket_Struct)); - sPickPocket_Struct *pick_out = (sPickPocket_Struct *)outapp->pBuffer; - pick_out->coin = amt; - pick_out->from = GetID(); - pick_out->to = from->GetID(); - pick_out->myskill = GetSkill(EQ::skills::SkillPickPockets); - - if ((type >= PickPocketPlatinum) && (type <= PickPocketCopper) && (amt == 0)) - type = PickPocketFailed; - - pick_out->type = type; - if (item) - strcpy(pick_out->itemname, item->Name); - else - pick_out->itemname[0] = '\0'; - // if we do not send this packet the client will lock up and require the player to relog. - QueuePacket(outapp); - safe_delete(outapp); + SendAdventureCount(ad->count, ad->total); + } + else + { + ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; + auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); + FastQueuePacket(&outapp); + } } -void Client::SetHoTT(uint32 mobid) { - auto outapp = new EQApplicationPacket(OP_TargetHoTT, sizeof(ClientTarget_Struct)); - ClientTarget_Struct *ct = (ClientTarget_Struct *) outapp->pBuffer; - ct->new_target = mobid; - QueuePacket(outapp); - safe_delete(outapp); +void Client::SendAdventureCount(uint32 count, uint32 total) +{ + auto outapp = new EQApplicationPacket(OP_AdventureUpdate, sizeof(AdventureCountUpdate_Struct)); + AdventureCountUpdate_Struct *acu = (AdventureCountUpdate_Struct*)outapp->pBuffer; + acu->current = count; + acu->total = total; + FastQueuePacket(&outapp); } -void Client::SendPopupToClient(const char *Title, const char *Text, uint32 PopupID, uint32 Buttons, uint32 Duration) +void Client::NewAdventure(int id, int theme, const char *text, int member_count, const char *members) { - auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); + size_t text_size = strlen(text); + auto outapp = new EQApplicationPacket(OP_AdventureDetails, text_size + 2); + strn0cpy((char*)outapp->pBuffer, text, text_size); + FastQueuePacket(&outapp); - OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; + adv_requested_id = id; + adv_requested_theme = theme; + safe_delete_array(adv_requested_data); + adv_requested_member_count = member_count; + adv_requested_data = new char[64 * member_count]; + memcpy(adv_requested_data, members, (64 * member_count)); +} - if ((strlen(Title) > (sizeof(olms->Title) - 1)) || (strlen(Text) > (sizeof(olms->Text) - 1))) { - safe_delete(outapp); - return; - } +void Client::ClearPendingAdventureData() +{ + adv_requested_id = 0; + adv_requested_theme = LDoNTheme::Unused; + safe_delete_array(adv_requested_data); + adv_requested_member_count = 0; +} - strcpy(olms->Title, Title); - strcpy(olms->Text, Text); +bool Client::IsOnAdventure() +{ + if(adv_data) + { + ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; + if(ad->zone_in_id == 0) + { + return false; + } + else + { + return true; + } + } + return false; +} - olms->Buttons = Buttons; +void Client::LeaveAdventure() +{ + if(!GetPendingAdventureLeave()) + { + PendingAdventureLeave(); + auto pack = new ServerPacket(ServerOP_AdventureLeave, 64); + strcpy((char*)pack->pBuffer, GetName()); + worldserver.SendPacket(pack); + delete pack; + } +} - if (Duration > 0) { - olms->Duration = Duration * 1000; - } - else { - olms->Duration = 0xffffffff; - } +void Client::ClearCurrentAdventure() +{ + if(adv_data) + { + ServerSendAdventureData_Struct* ds = (ServerSendAdventureData_Struct*)adv_data; + if(ds->finished_adventures > 0) + { + ds->instance_id = 0; + ds->risk = 0; + memset(ds->text, 0, 512); + ds->time_left = 0; + ds->time_to_enter = 0; + ds->x = 0; + ds->y = 0; + ds->zone_in_id = 0; + ds->zone_in_object = 0; + } + else + { + safe_delete(adv_data); + } - olms->PopupID = PopupID; - olms->NegativeID = 0; + SendAdventureError("You are not currently assigned to an adventure."); + } +} - sprintf(olms->ButtonName0, "%s", "Yes"); - sprintf(olms->ButtonName1, "%s", "No"); - QueuePacket(outapp); - safe_delete(outapp); +void Client::AdventureFinish(bool win, int theme, int points) +{ + UpdateLDoNPoints(theme, points); + auto outapp = new EQApplicationPacket(OP_AdventureFinish, sizeof(AdventureFinish_Struct)); + AdventureFinish_Struct *af = (AdventureFinish_Struct*)outapp->pBuffer; + af->win_lose = win ? 1 : 0; + af->points = points; + FastQueuePacket(&outapp); } -void Client::SendFullPopup( - const char *Title, - const char *Text, - uint32 PopupID, - uint32 NegativeID, - uint32 Buttons, - uint32 Duration, - const char *ButtonName0, - const char *ButtonName1, - uint32 SoundControls -) +void Client::CheckLDoNHail(NPC* n) { - auto outapp = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); + if (!zone->adv_data || !n || n->GetOwnerID()) { + return; + } - OnLevelMessage_Struct *olms = (OnLevelMessage_Struct *) outapp->pBuffer; + auto* ds = (ServerZoneAdventureDataReply_Struct*) zone->adv_data; + if (ds->type != Adventure_Rescue || ds->data_id != n->GetNPCTypeID()) { + return; + } - if ((strlen(Text) > (sizeof(olms->Text) - 1)) || (strlen(Title) > (sizeof(olms->Title) - 1))) { - safe_delete(outapp); - return; - } + if (entity_list.CheckNPCsClose(n)) { + n->Say( + "You're here to save me? I couldn't possibly risk leaving yet. There are " + "far too many of those horrid things out there waiting to recapture me! Please get " + "rid of some more of those vermin and then we can try to leave." + ); + return; + } - if (ButtonName0 && ButtonName1 && ((strlen(ButtonName0) > (sizeof(olms->ButtonName0) - 1)) || - (strlen(ButtonName1) > (sizeof(olms->ButtonName1) - 1)))) { - safe_delete(outapp); - return; - } + auto pet = GetPet(); + if (pet) { + if (pet->GetPetType() == petCharmed) { + pet->BuffFadeByEffect(SE_Charm); + } else if (pet->GetPetType() == petNPCFollow) { + pet->SetOwnerID(0); + } else { + pet->Depop(); + } + } - strcpy(olms->Title, Title); - strcpy(olms->Text, Text); + SetPet(n); + n->SetOwnerID(GetID()); + n->Say( + "Wonderful! Someone to set me free! I feared for my life for so long, " + "never knowing when they might choose to end my life. Now that you're here though " + "I can rest easy. Please help me find my way out of here as soon as you can " + "I'll stay close behind you!" + ); +} - olms->Buttons = Buttons; +void Client::CheckEmoteHail(NPC* n, const char* message) +{ + if (!Strings::BeginsWith(Strings::ToLower(message), "hail")) { + return; + } - if (ButtonName0 == nullptr || ButtonName1 == nullptr) { - sprintf(olms->ButtonName0, "%s", "Yes"); - sprintf(olms->ButtonName1, "%s", "No"); - } - else { - strcpy(olms->ButtonName0, ButtonName0); - strcpy(olms->ButtonName1, ButtonName1); - } + if (!n || n->GetOwnerID()) { + return; + } - if (Duration > 0) { - olms->Duration = Duration * 1000; - } - else { - olms->Duration = 0xffffffff; - } + const uint32 emote_id = n->GetEmoteID(); + if (emote_id) { + n->DoNPCEmote(EQ::constants::EmoteEventTypes::Hailed, emote_id, this); + } +} - olms->PopupID = PopupID; - olms->NegativeID = NegativeID; - olms->SoundControls = SoundControls; +void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count) +{ + m_has_quest_compass = (count != 0); + m_quest_compass.x = in_x; + m_quest_compass.y = in_y; + m_quest_compass.z = in_z; - QueuePacket(outapp); - safe_delete(outapp); + SendDzCompassUpdate(); } -void Client::SendWindow( - uint32 button_one_id, - uint32 button_two_id, - uint32 button_type, - const char* button_one_text, - const char* button_two_text, - uint32 duration, - int title_type, - Mob* target, - const char* title, - const char* text, - ... -) +void Client::SendZonePoints() { - va_list argptr; - char buffer[4096]; + int count = 0; + LinkedListIterator iterator(zone->zone_point_list); + iterator.Reset(); + while (iterator.MoreElements()) { + ZonePoint *data = iterator.GetData(); - va_start(argptr, text); - vsnprintf(buffer, sizeof(buffer), text, argptr); - va_end(argptr); + if (ClientVersionBit() & data->client_version_mask) { + count++; + } - size_t len = strlen(buffer); + iterator.Advance(); + } - auto app = new EQApplicationPacket(OP_OnLevelMessage, sizeof(OnLevelMessage_Struct)); - auto* olms = (OnLevelMessage_Struct *) app->pBuffer; + uint32 zpsize = sizeof(ZonePoints) + ((count + 1) * sizeof(ZonePoint_Entry)); + auto outapp = new EQApplicationPacket(OP_SendZonepoints, zpsize); + ZonePoints* zp = (ZonePoints*)outapp->pBuffer; + zp->count = count; - if (strlen(text) > (sizeof(olms->Text) - 1)) { - safe_delete(app); - return; - } + int i = 0; + iterator.Reset(); + while(iterator.MoreElements()) + { + ZonePoint* data = iterator.GetData(); - if (!target) { - title_type = 0; - } + LogZonePoints( + "Sending zone point to client [{}] mask [{}] x [{}] y [{}] z [{}] number [{}]", + GetCleanName(), + ClientVersionBit() & data->client_version_mask ? "true" : "false", + data->x, + data->y, + data->z, + data->number + ); - switch (title_type) { - case 1: { - char name[64] = ""; - strcpy(name, target->GetName()); + if(ClientVersionBit() & data->client_version_mask) + { + zp->zpe[i].iterator = data->number; + zp->zpe[i].x = data->target_x; + zp->zpe[i].y = data->target_y; + zp->zpe[i].z = data->target_z; + zp->zpe[i].heading = data->target_heading; + zp->zpe[i].zoneid = data->target_zone_id; + + // if the target zone is the same as the current zone, use the instance of the current zone + // if we don't use the same instance_id that the client was sent, the client will forcefully + // issue a zone change request when they should be simply moving to a different point in the same zone + // because the client will think the zone point target is different from the current instance + auto target_instance = data->target_zone_instance; + if (data->target_zone_id == zone->GetZoneID() && data->target_zone_instance == 0) { + target_instance = zone->GetInstanceID(); + } - if (strlen(target->GetLastName()) > 0) { - char last_name[64] = ""; - strcpy(last_name, target->GetLastName()); - strcat(name, " "); - strcat(name, last_name); - } + zp->zpe[i].zoneinstance = target_instance; + i++; + } + iterator.Advance(); + } - strcpy(olms->Title, name); - break; - } - case 2: { - if (target->IsClient() && target->CastToClient()->GuildID()) { - auto guild_name = guild_mgr.GetGuildName(target->CastToClient()->GuildID()); - strn0cpy(olms->Title, guild_name, sizeof(olms->Title)); - } else { - strcpy(olms->Title, "No Guild"); - } - break; - } - default: { - strcpy(olms->Title, title); - break; - } - } + FastQueuePacket(&outapp); +} - memcpy(olms->Text, buffer, len + 1); +void Client::SendTargetCommand(uint32 EntityID) +{ + auto outapp = new EQApplicationPacket(OP_TargetCommand, sizeof(ClientTarget_Struct)); + ClientTarget_Struct *cts = (ClientTarget_Struct*)outapp->pBuffer; + cts->new_target = EntityID; + FastQueuePacket(&outapp); +} - olms->Buttons = button_type; +void Client::LocateCorpse() +{ + Corpse *ClosestCorpse = nullptr; + if(!GetTarget()) + ClosestCorpse = entity_list.GetClosestCorpse(this, nullptr); + else if(GetTarget()->IsCorpse()) + ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->CastToCorpse()->GetOwnerName()); + else + ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->GetCleanName()); + + if(ClosestCorpse) + { + MessageString(Chat::Spells, SENSE_CORPSE_DIRECTION); + SetHeading(CalculateHeadingToTarget(ClosestCorpse->GetX(), ClosestCorpse->GetY())); + SetTarget(ClosestCorpse); + SendTargetCommand(ClosestCorpse->GetID()); + SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0, true); + } + else if(!GetTarget()) + MessageString(Chat::Red, SENSE_CORPSE_NONE); + else + MessageString(Chat::Red, SENSE_CORPSE_NOT_NAME); +} - strn0cpy(olms->ButtonName0, button_one_text, sizeof(olms->ButtonName0)); - strn0cpy(olms->ButtonName1, button_two_text, sizeof(olms->ButtonName1)); +void Client::NPCSpawn(NPC *target_npc, const char *identifier, uint32 extra) +{ + if (!target_npc || !identifier) { + return; + } - if (duration > 0) { - olms->Duration = duration * 1000; - } else { - olms->Duration = UINT32_MAX; - } + std::string spawn_type = Strings::ToLower(identifier); + bool is_add = spawn_type.find("add") != std::string::npos; + bool is_create = spawn_type.find("create") != std::string::npos; + bool is_delete = spawn_type.find("delete") != std::string::npos; + bool is_remove = spawn_type.find("remove") != std::string::npos; + bool is_update = spawn_type.find("update") != std::string::npos; + bool is_clone = spawn_type.find("clone") != std::string::npos; + if (is_add || is_create) { + // extra sets the Respawn Timer for add/create + content_db.NPCSpawnDB( + is_add ? NPCSpawnTypes::AddNewSpawngroup : NPCSpawnTypes::CreateNewSpawn, + zone->GetShortName(), + zone->GetInstanceVersion(), + this, + target_npc->CastToNPC(), + extra + ); + } else if (is_delete || is_remove || is_update) { + uint8 spawn_update_type = ( + is_delete ? + NPCSpawnTypes::DeleteSpawn : + ( + is_remove ? + NPCSpawnTypes::RemoveSpawn : + NPCSpawnTypes::UpdateAppearance + ) + ); + content_db.NPCSpawnDB( + spawn_update_type, + zone->GetShortName(), + zone->GetInstanceVersion(), + this, + target_npc->CastToNPC(), + extra + ); + } else if (is_clone) { + content_db.NPCSpawnDB( + NPCSpawnTypes::AddSpawnFromSpawngroup, + zone->GetShortName(), + zone->GetInstanceVersion(), + this, + target_npc->CastToNPC(), + extra + ); + } +} - olms->PopupID = button_one_id; - olms->NegativeID = button_two_id; +bool Client::IsDraggingCorpse(uint16 CorpseID) +{ + for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { + if (It->second == CorpseID) + return true; + } - FastQueuePacket(&app); + return false; } -void Client::KeyRingLoad() +void Client::DragCorpses() { - const auto &l = KeyringRepository::GetWhere( - database, - fmt::format( - "`char_id` = {} ORDER BY `item_id`", - character_id - ) - ); + for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { + Mob *corpse = entity_list.GetMob(It->second); - if (l.empty()) { - return; - } + if (corpse && corpse->IsPlayerCorpse() && + (DistanceSquaredNoZ(m_Position, corpse->GetPosition()) <= RuleR(Character, DragCorpseDistance))) + continue; - - for (const auto &e : l) { - keyring.emplace_back(e.item_id); - } + if (!corpse || !corpse->IsPlayerCorpse() || + corpse->CastToCorpse()->IsBeingLooted() || + !corpse->CastToCorpse()->Summon(this, false, false)) { + MessageString(Chat::DefaultText, CORPSEDRAG_STOP); + It = DraggedCorpses.erase(It); + if (It == DraggedCorpses.end()) + break; + } + } } -void Client::KeyRingAdd(uint32 item_id) +void Client::ConsentCorpses(std::string consent_name, bool deny) { - if (!item_id) { - return; - } - - const bool found = KeyRingCheck(item_id); - if (found) { - return; - } + if (strcasecmp(consent_name.c_str(), GetName()) == 0) { + MessageString(Chat::Red, CONSENT_YOURSELF); + } + else if (!consent_throttle_timer.Check()) { + MessageString(Chat::Red, CONSENT_WAIT); + } + else { + auto pack = new ServerPacket(ServerOP_Consent, sizeof(ServerOP_Consent_Struct)); + ServerOP_Consent_Struct* scs = (ServerOP_Consent_Struct*)pack->pBuffer; + strn0cpy(scs->grantname, consent_name.c_str(), sizeof(scs->grantname)); + strn0cpy(scs->ownername, GetName(), sizeof(scs->ownername)); + strn0cpy(scs->zonename, "Unknown", sizeof(scs->zonename)); + scs->permission = deny ? 0 : 1; + scs->zone_id = zone->GetZoneID(); + scs->instance_id = zone->GetInstanceID(); + scs->consent_type = EQ::consent::Normal; + scs->consent_id = 0; + if (strcasecmp(scs->grantname, "group") == 0) { + if (!deny) { + Group* grp = GetGroup(); + scs->consent_id = grp ? grp->GetID() : 0; + } + scs->consent_type = EQ::consent::Group; + } + else if (strcasecmp(scs->grantname, "raid") == 0) { + if (!deny) { + Raid* raid = GetRaid(); + scs->consent_id = raid ? raid->GetID() : 0; + } + scs->consent_type = EQ::consent::Raid; + } + else if (strcasecmp(scs->grantname, "guild") == 0) { + if (!deny) { + scs->consent_id = GuildID(); + } + scs->consent_type = EQ::consent::Guild; + // update all corpses in db so buried/unloaded corpses see new consent id + database.UpdateCharacterCorpseConsent(CharacterID(), scs->consent_id); + } + worldserver.SendPacket(pack); + safe_delete(pack); + } +} - auto e = KeyringRepository::NewEntity(); +void Client::Doppelganger(uint16 spell_id, Mob *target, const char *name_override, int pet_count, int pet_duration) +{ + if(!target || !IsValidSpell(spell_id) || GetID() == target->GetID()) + return; - e.char_id = CharacterID(); - e.item_id = item_id; + PetRecord record; + if(!database.GetPetEntry(spells[spell_id].teleport_zone, &record)) + { + LogError("Unknown doppelganger spell id: [{}], check pets table", spell_id); + Message(Chat::Red, "Unable to find data for pet %s", spells[spell_id].teleport_zone); + return; + } - e = KeyringRepository::InsertOne(database, e); + SwarmPet_Struct pet; + pet.count = pet_count; + pet.duration = pet_duration; + pet.npc_id = record.npc_type; - if (!e.id) { - return; - } + NPCType *made_npc = nullptr; - keyring.emplace_back(item_id); + const NPCType *npc_type = content_db.LoadNPCTypesData(pet.npc_id); + if(npc_type == nullptr) { + LogError("Unknown npc type for doppelganger spell id: [{}]", spell_id); + Message(0,"Unable to find pet!"); + return; + } + // make a custom NPC type for this + made_npc = new NPCType; + memcpy(made_npc, npc_type, sizeof(NPCType)); + + strcpy(made_npc->name, name_override); + made_npc->level = GetLevel(); + made_npc->race = GetRace(); + made_npc->gender = GetGender(); + made_npc->size = GetSize(); + made_npc->AC = GetAC(); + made_npc->STR = GetSTR(); + made_npc->STA = GetSTA(); + made_npc->DEX = GetDEX(); + made_npc->AGI = GetAGI(); + made_npc->MR = GetMR(); + made_npc->FR = GetFR(); + made_npc->CR = GetCR(); + made_npc->DR = GetDR(); + made_npc->PR = GetPR(); + made_npc->Corrup = GetCorrup(); + made_npc->PhR = GetPhR(); + // looks + made_npc->texture = GetEquipmentMaterial(EQ::textures::armorChest); + made_npc->helmtexture = GetEquipmentMaterial(EQ::textures::armorHead); + made_npc->haircolor = GetHairColor(); + made_npc->beardcolor = GetBeardColor(); + made_npc->eyecolor1 = GetEyeColor1(); + made_npc->eyecolor2 = GetEyeColor2(); + made_npc->hairstyle = GetHairStyle(); + made_npc->luclinface = GetLuclinFace(); + made_npc->beard = GetBeard(); + made_npc->drakkin_heritage = GetDrakkinHeritage(); + made_npc->drakkin_tattoo = GetDrakkinTattoo(); + made_npc->drakkin_details = GetDrakkinDetails(); + made_npc->d_melee_texture1 = GetEquipmentMaterial(EQ::textures::weaponPrimary); + made_npc->d_melee_texture2 = GetEquipmentMaterial(EQ::textures::weaponSecondary); + for (int i = EQ::textures::textureBegin; i <= EQ::textures::LastTexture; i++) { + made_npc->armor_tint.Slot[i].Color = GetEquipmentColor(i); + } + made_npc->loottable_id = 0; - if (!RuleB(World, UseItemLinksForKeyRing)) { - Message(Chat::LightBlue, "Added to keyring."); - return; - } + int summon_count = pet.count; - const std::string &item_link = database.CreateItemLink(item_id); + if(summon_count > MAX_SWARM_PETS) + summon_count = MAX_SWARM_PETS; - Message( - Chat::LightBlue, - fmt::format( - "Added {} to keyring.", - item_link - ).c_str() - ); -} + static const glm::vec2 swarmPetLocations[MAX_SWARM_PETS] = { + glm::vec2(5, 5), glm::vec2(-5, 5), glm::vec2(5, -5), glm::vec2(-5, -5), + glm::vec2(10, 10), glm::vec2(-10, 10), glm::vec2(10, -10), glm::vec2(-10, -10), + glm::vec2(8, 8), glm::vec2(-8, 8), glm::vec2(8, -8), glm::vec2(-8, -8) + }; -bool Client::KeyRingCheck(uint32 item_id) -{ - for (const auto &e : keyring) { - if (e == item_id) { - return true; - } - } + while(summon_count > 0) { + auto npc_type_copy = new NPCType; + memcpy(npc_type_copy, made_npc, sizeof(NPCType)); - return false; -} + NPC* swarm_pet_npc = new NPC( + npc_type_copy, + 0, + GetPosition() + glm::vec4(swarmPetLocations[summon_count - 1], 0.0f, 0.0f), + GravityBehavior::Water); -void Client::KeyRingList() -{ - Message(Chat::LightBlue, "Keys on Keyring:"); + if(!swarm_pet_npc->GetSwarmInfo()){ + auto nSI = new SwarmPet; + swarm_pet_npc->SetSwarmInfo(nSI); + swarm_pet_npc->GetSwarmInfo()->duration = new Timer(pet_duration*1000); + } + else{ + swarm_pet_npc->GetSwarmInfo()->duration->Start(pet_duration*1000); + } - const EQ::ItemData *item = nullptr; + swarm_pet_npc->StartSwarmTimer(pet_duration * 1000); - for (const auto &e : keyring) { - item = database.GetItem(e); - if (item) { - const std::string &item_string = RuleB(World, UseItemLinksForKeyRing) ? database.CreateItemLink(e) : item->Name; + swarm_pet_npc->GetSwarmInfo()->owner_id = GetID(); + swarm_pet_npc->SetFollowID(GetID()); - Message(Chat::LightBlue, item_string.c_str()); - } - } -} + // Give the pets alittle more agro than the caster and then agro them on the target + target->AddToHateList(swarm_pet_npc, (target->GetHateAmount(this) + 100), (target->GetDamageAmount(this) + 100)); + swarm_pet_npc->AddToHateList(target, 1000, 1000); + swarm_pet_npc->GetSwarmInfo()->target = 0; -bool Client::IsPetNameChangeAllowed() { - DataBucketKey k = GetScopedBucketKeys(); - k.key = "PetNameChangesAllowed"; + //we allocated a new NPC type object, give the NPC ownership of that memory + swarm_pet_npc->GiveNPCTypeData(npc_type_copy); - auto b = DataBucket::GetData(k); - if (!b.value.empty()) { - return true; - } + entity_list.AddNPC(swarm_pet_npc); + summon_count--; + } - return false; + safe_delete(made_npc); } -void Client::InvokeChangePetName(bool immediate) { - if (!IsPetNameChangeAllowed()) { - return; - } - - auto packet_op = immediate ? OP_InvokeChangePetNameImmediate : OP_InvokeChangePetName; +void Client::AssignToInstance(uint16 instance_id) +{ + database.AddClientToInstance(instance_id, CharacterID()); +} - auto outapp = new EQApplicationPacket(packet_op, 0); - QueuePacket(outapp); - safe_delete(outapp); +void Client::RemoveFromInstance(uint16 instance_id) +{ + database.RemoveClientFromInstance(instance_id, CharacterID()); } -void Client::GrantPetNameChange() { - DataBucketKey k = GetScopedBucketKeys(); - k.key = "PetNameChangesAllowed"; - k.value = "true"; - DataBucket::SetData(k); +void Client::SendAltCurrencies() { + if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + const uint32 currency_count = zone->AlternateCurrencies.size(); + if (!currency_count) { + return; + } - InvokeChangePetName(true); -} + auto outapp = new EQApplicationPacket( + OP_AltCurrency, + sizeof(AltCurrencyPopulate_Struct) + + sizeof(AltCurrencyPopulateEntry_Struct) * currency_count + ); -void Client::ClearPetNameChange() { - DataBucketKey k = GetScopedBucketKeys(); - k.key = "PetNameChangesAllowed"; + auto a = (AltCurrencyPopulate_Struct*) outapp->pBuffer; - DataBucket::DeleteData(k); -} + a->opcode = AlternateCurrencyMode::Populate; + a->count = currency_count; -bool Client::ChangePetName(std::string new_name) { - if (new_name.empty()) { - return false; - } + uint32 currency_id = 0; + for (const auto& c : zone->AlternateCurrencies) { + const auto* item = database.GetItem(c.item_id); - if (!IsPetNameChangeAllowed()) { - return false; - } + a->entries[currency_id].currency_number = c.id; + a->entries[currency_id].unknown00 = 1; + a->entries[currency_id].currency_number2 = c.id; + a->entries[currency_id].item_id = c.item_id; + a->entries[currency_id].item_icon = item ? item->Icon : 1000; + a->entries[currency_id].stack_size = item ? item->StackSize : 1000; - if (GetPet()) { - std::string cur_name = GetPet()->GetName(); + currency_id++; + } - if (new_name == cur_name) { - return false; - } - } + FastQueuePacket(&outapp); + } +} - if (!database.CheckNameFilter(new_name) || database.IsNameUsed(new_name)) { - return false; - } +void Client::SetAlternateCurrencyValue(uint32 currency_id, uint32 new_amount) +{ + if (!zone->DoesAlternateCurrencyExist(currency_id)) { + return; + } - CharacterPetNameRepository::ReplaceOne(database, { - .char_id = static_cast(CharacterID()), - .name = new_name - }); + const uint32 current_amount = alternate_currency[currency_id]; + const bool is_gain = new_amount > current_amount; - if (GetPet()) { - GetPet()->TempName(new_name.c_str()); - } + const uint32 change_amount = is_gain ? (new_amount - current_amount) : (current_amount - new_amount); - ClearPetNameChange(); - return true; -} + if (!change_amount) { + return; + } -bool Client::IsDiscovered(uint32 item_id) { - const auto& l = DiscoveredItemsRepository::GetWhere( - database, - fmt::format( - "item_id = {}", - item_id - ) - ); - if (l.empty()) { - return false; - } + alternate_currency[currency_id] = new_amount; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); + SendAlternateCurrencyValue(currency_id); + + QuestEventID event_id = is_gain ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; + if (parse->PlayerHasQuestSub(event_id)) { + const std::string &export_string = fmt::format( + "{} {} {}", + currency_id, + change_amount, + new_amount + ); - return true; + parse->EventPlayer(event_id, this, export_string, 0); + } } -void Client::DiscoverItem(uint32 item_id) { - auto e = DiscoveredItemsRepository::NewEntity(); - - e.account_status = Admin(); - e.char_name = GetCleanName(); - e.discovered_date = std::time(nullptr); - e.item_id = item_id; +bool Client::RemoveAlternateCurrencyValue(uint32 currency_id, uint32 amount) +{ + if (!amount || !zone->DoesAlternateCurrencyExist(currency_id)) { + return false; + } - auto d = DiscoveredItemsRepository::InsertOne(database, e); + const uint32 current_amount = alternate_currency[currency_id]; + if (current_amount < amount) { + return false; + } - if (player_event_logs.IsEventEnabled(PlayerEvent::DISCOVER_ITEM)) { - const auto* item = database.GetItem(item_id); + const uint32 new_amount = (current_amount - amount); - auto e = PlayerEvent::DiscoverItemEvent{ - .item_id = item_id, - .item_name = item->Name, - }; - RecordPlayerEventLog(PlayerEvent::DISCOVER_ITEM, e); + alternate_currency[currency_id] = new_amount; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); + SendAlternateCurrencyValue(currency_id); - } + if (parse->PlayerHasQuestSub(EVENT_ALT_CURRENCY_LOSS)) { + const std::string &export_string = fmt::format( + "{} {} {}", + currency_id, + amount, + new_amount + ); - if (parse->PlayerHasQuestSub(EVENT_DISCOVER_ITEM)) { - auto* item = database.CreateItem(item_id); - std::vector args = { item }; + parse->EventPlayer(EVENT_ALT_CURRENCY_LOSS, this, export_string, 0); + } - parse->EventPlayer(EVENT_DISCOVER_ITEM, this, "", item_id, &args); - } + return true; } -void Client::UpdateLFP() { +int Client::AddAlternateCurrencyValue(uint32 currency_id, int amount, bool is_scripted) +{ + if (!zone->DoesAlternateCurrencyExist(currency_id)) { + return 0; + } - Group *g = GetGroup(); - - if(g && !g->IsLeader(this)) { - database.SetLFP(CharacterID(), false); - worldserver.StopLFP(CharacterID()); - LFP = false; - return; - } - - GroupLFPMemberEntry LFPMembers[MAX_GROUP_MEMBERS]; - - for(unsigned int i=0; iGetZoneID(); - - if(g) { - // Fill the LFPMembers array with the rest of the group members, excluding ourself - // We don't fill in the class, level or zone, because we may not be able to determine - // them if the other group members are not in this zone. World will fill in this information - // for us, if it can. - int NextFreeSlot = 1; - for(unsigned int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if((g->membername[i][0] != '\0') && strcasecmp(g->membername[i], LFPMembers[0].Name)) - strcpy(LFPMembers[NextFreeSlot++].Name, g->membername[i]); - } - } - worldserver.UpdateLFP(CharacterID(), LFPMembers); -} + /* Added via Quest, rest of the logging methods may be done inline due to information available in that area of the code */ + if (is_scripted) { + /* QS: PlayerLogAlternateCurrencyTransactions :: Cursor to Item Storage */ + if (RuleB(QueryServ, PlayerLogAlternateCurrencyTransactions)){ + std::string event_desc = StringFormat("Added via Quest :: Cursor to Item :: alt_currency_id:%i amount:%i in zoneid:%i instid:%i", currency_id, GetZoneID(), GetInstanceID()); + QServ->PlayerLogEvent(Player_Log_Alternate_Currency_Transactions, CharacterID(), event_desc); + } + } -bool Client::GroupFollow(Client* inviter) { + if (!amount) { + return 0; + } - if (inviter) { - isgrouped = true; - Raid* raid = entity_list.GetRaidByClient(inviter); - Raid* iraid = entity_list.GetRaidByClient(this); - - //inviter has a raid don't do group stuff instead do raid stuff! - if (raid) { - // Suspend the merc while in a raid (maybe a rule could be added for this) - if (GetMerc()) { - GetMerc()->Suspend(); - } - - uint32 groupToUse = 0xFFFFFFFF; - for (const auto& m : raid->members) { - if (m.member && m.member == inviter) { - groupToUse = m.group_number; - break; - } - } - if (iraid == raid) { - //both in same raid - uint32 ngid = raid->GetGroup(inviter->GetName()); - if (raid->GroupCount(ngid) < MAX_GROUP_MEMBERS) { - raid->MoveMember(GetName(), ngid); - raid->SendGroupDisband(this); - raid->GroupUpdate(ngid); - } - return false; - } - if (raid->RaidCount() < MAX_RAID_MEMBERS) - { - // okay, so we now have a single client (this) joining a group in a raid - // And they're not already in the raid (which is above and doesn't need xtarget shit) - if (!GetXTargetAutoMgr()->empty()) { - raid->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); - GetXTargetAutoMgr()->clear(); - RemoveAutoXTargets(); - } - - SetXTargetAutoMgr(raid->GetXTargetAutoMgr()); - if (!GetXTargetAutoMgr()->empty()) - SetDirtyAutoHaters(); - - if (raid->GroupCount(groupToUse) < MAX_GROUP_MEMBERS) - { - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->AddMember(this, groupToUse); - raid->SendBulkRaid(this); - //raid->SendRaidGroupAdd(GetName(), groupToUse); - //raid->SendGroupUpdate(this); - raid->GroupUpdate(groupToUse); //break - if (raid->IsLocked()) - { - raid->SendRaidLockTo(this); - } - return false; - } - else - { - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->AddMember(this); - raid->SendBulkRaid(this); - if (raid->IsLocked()) - { - raid->SendRaidLockTo(this); - } - return false; - } - } - } - - Group* group = entity_list.GetGroupByClient(inviter); - - if (!group) - { - //Make new group - group = new Group(inviter); - - if (!group) - { - return false; - } - - entity_list.AddGroup(group); - - if (group->GetID() == 0) - { - Message(Chat::Red, "Unable to get new group id. Cannot create group."); - inviter->Message(Chat::Red, "Unable to get new group id. Cannot create group."); - return false; - } - - //now we have a group id, can set inviter's id - group->AddToGroup(inviter); - database.SetGroupLeaderName(group->GetID(), inviter->GetName()); - group->UpdateGroupAAs(); - - //Invite the inviter into the group first.....dont ask - if (inviter->ClientVersion() < EQ::versions::ClientVersion::SoD) - { - auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupJoin_Struct)); - GroupJoin_Struct* outgj = (GroupJoin_Struct*)outapp->pBuffer; - strcpy(outgj->membername, inviter->GetName()); - strcpy(outgj->yourname, inviter->GetName()); - outgj->action = groupActInviteInitial; // 'You have formed the group'. - group->GetGroupAAs(&outgj->leader_aas); - inviter->QueuePacket(outapp); - safe_delete(outapp); - } - else - { - // SoD and later - inviter->SendGroupCreatePacket(); - inviter->SendGroupLeaderChangePacket(inviter->GetName()); - inviter->SendGroupJoinAcknowledge(); - } - group->GetXTargetAutoMgr()->merge(*inviter->GetXTargetAutoMgr()); - inviter->GetXTargetAutoMgr()->clear(); - inviter->SetXTargetAutoMgr(group->GetXTargetAutoMgr()); - } - - if (!group) - { - return false; - } - - // Remove merc from old group before adding client to the new one - if (GetMerc() && GetMerc()->HasGroup()) - { - GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); - } - - if (!group->AddMember(this)) - { - // If failed to add client to new group, regroup with merc - if (GetMerc()) - { - GetMerc()->MercJoinClientGroup(); - } - else - { - isgrouped = false; - } - return false; - } - - if (ClientVersion() >= EQ::versions::ClientVersion::SoD) - { - SendGroupJoinAcknowledge(); - } - - // Temporary hack for SoD, as things seem to work quite differently - if (inviter->IsClient() && inviter->ClientVersion() >= EQ::versions::ClientVersion::SoD) - { - database.RefreshGroupFromDB(inviter); - } - - // Add the merc back into the new group if possible - if (GetMerc()) - { - GetMerc()->MercJoinClientGroup(); - } - - if (inviter->IsLFP()) - { - // If the player who invited us to a group is LFP, have them update world now that we have joined their group. - inviter->UpdateLFP(); - } - - database.RefreshGroupFromDB(this); - group->SendHPManaEndPacketsTo(this); - //send updates to clients out of zone... - group->SendGroupJoinOOZ(this); - return true; - } - return false; -} + if (!alternate_currency_loaded) { + alternate_currency_queued_operations.push(std::make_pair(currency_id, amount)); + return 0; + } -uint16 Client::GetPrimarySkillValue() -{ - EQ::skills::SkillType skill = EQ::skills::HIGHEST_SKILL; //because nullptr == 0, which is 1H Slashing, & we want it to return 0 from GetSkill - bool equipped = m_inv.GetItem(EQ::invslot::slotPrimary); - - if (!equipped) - skill = EQ::skills::SkillHandtoHand; - - else { - - uint8 type = m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->ItemType; //is this the best way to do this? - - switch (type) { - case EQ::item::ItemType1HSlash: // 1H Slashing - skill = EQ::skills::Skill1HSlashing; - break; - case EQ::item::ItemType2HSlash: // 2H Slashing - skill = EQ::skills::Skill2HSlashing; - break; - case EQ::item::ItemType1HPiercing: // Piercing - skill = EQ::skills::Skill1HPiercing; - break; - case EQ::item::ItemType1HBlunt: // 1H Blunt - skill = EQ::skills::Skill1HBlunt; - break; - case EQ::item::ItemType2HBlunt: // 2H Blunt - skill = EQ::skills::Skill2HBlunt; - break; - case EQ::item::ItemType2HPiercing: // 2H Piercing - if (IsClient() && CastToClient()->ClientVersion() < EQ::versions::ClientVersion::RoF2) - skill = EQ::skills::Skill1HPiercing; - else - skill = EQ::skills::Skill2HPiercing; - break; - case EQ::item::ItemTypeMartial: // Hand to Hand - skill = EQ::skills::SkillHandtoHand; - break; - default: // All other types default to Hand to Hand - skill = EQ::skills::SkillHandtoHand; - break; - } - } - - return GetSkill(skill); -} + int new_value = 0; + auto iter = alternate_currency.find(currency_id); + if (iter == alternate_currency.end()) { + new_value = amount; + } else { + new_value = (*iter).second + amount; + } -uint32 Client::GetTotalATK() -{ - uint32 AttackRating = 0; - uint32 WornCap = itembonuses.ATK; + if (new_value < 0) { + new_value = 0; + alternate_currency[currency_id] = 0; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, 0); + } else { + alternate_currency[currency_id] = new_value; + database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_value); + } - if(IsClient()) { - AttackRating = ((WornCap * 1.342) + (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69)); - AttackRating += aabonuses.ATK + GroupLeadershipAAOffenseEnhancement(); + SendAlternateCurrencyValue(currency_id); - if (AttackRating < 10) - AttackRating = 10; - } - else - AttackRating = GetATK(); + QuestEventID event_id = amount > 0 ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; + if (parse->PlayerHasQuestSub(event_id)) { + const std::string &export_string = fmt::format( + "{} {} {}", + currency_id, + std::abs(amount), + new_value + ); - AttackRating += spellbonuses.ATK; + parse->EventPlayer(event_id, this, export_string, 0); + } - return AttackRating; + return new_value; } -uint32 Client::GetATKRating() +void Client::SendAlternateCurrencyValues() { - uint32 AttackRating = 0; - if(IsClient()) { - AttackRating = (GetSkill(EQ::skills::SkillOffense) * 1.345) + ((GetSTR() - 66) * 0.9) + (GetPrimarySkillValue() * 2.69); - - if (AttackRating < 10) - AttackRating = 10; - } - return AttackRating; + for (const auto& alternate_currency : zone->AlternateCurrencies) { + SendAlternateCurrencyValue(alternate_currency.id, false); + } } -void Client::VoiceMacroReceived(uint32 Type, char *Target, uint32 MacroNumber) { - - uint32 GroupOrRaidID = 0; - - switch(Type) { - - case VoiceMacroGroup: { +void Client::SendAlternateCurrencyValue(uint32 currency_id, bool send_if_null) +{ + const auto value = GetAlternateCurrencyValue(currency_id); + if (value > 0 || send_if_null) { + auto outapp = new EQApplicationPacket(OP_AltCurrency, sizeof(AltCurrencyUpdate_Struct)); + auto update = (AltCurrencyUpdate_Struct *) outapp->pBuffer; + update->opcode = 7; + update->currency_number = currency_id; + update->amount = value; + update->unknown072 = 1; - Group* g = GetGroup(); + strn0cpy(update->name, GetName(), sizeof(update->name)); - if(g) - GroupOrRaidID = g->GetID(); - else - return; + FastQueuePacket(&outapp); + } +} - break; - } +uint32 Client::GetAlternateCurrencyValue(uint32 currency_id) const +{ + if (!zone->DoesAlternateCurrencyExist(currency_id)) { + return 0; + } - case VoiceMacroRaid: { + auto iter = alternate_currency.find(currency_id); - Raid* r = GetRaid(); + return iter == alternate_currency.end() ? 0 : (*iter).second; +} - if(r) - GroupOrRaidID = r->GetID(); - else - return; +void Client::ProcessAlternateCurrencyQueue() { + while(!alternate_currency_queued_operations.empty()) { + std::pair op = alternate_currency_queued_operations.front(); - break; - } - } + AddAlternateCurrencyValue(op.first, op.second); - if(!worldserver.SendVoiceMacro(this, Type, Target, MacroNumber, GroupOrRaidID)) - Message(0, "Error: World server disconnected"); + alternate_currency_queued_operations.pop(); + } } -void Client::ClearGroupAAs() { - for(unsigned int i = 0; i < MAX_GROUP_LEADERSHIP_AA_ARRAY; i++) - m_pp.leader_abilities.ranks[i] = 0; +void Client::OpenLFGuildWindow() +{ + auto outapp = new EQApplicationPacket(OP_LFGuild, 8); - m_pp.group_leadership_points = 0; - m_pp.raid_leadership_points = 0; - m_pp.group_leadership_exp = 0; - m_pp.raid_leadership_exp = 0; + outapp->WriteUInt32(6); - Save(); - database.SaveCharacterLeadershipAbilities(CharacterID(), &m_pp); + FastQueuePacket(&outapp); } -void Client::UpdateGroupAAs(int32 points, uint32 type) { - switch(type) { - case 0: { m_pp.group_leadership_points += points; break; } - case 1: { m_pp.raid_leadership_points += points; break; } - } - SendLeadershipEXPUpdate(); -} +bool Client::IsXTarget(const Mob *m) const +{ + if(!XTargettingAvailable() || !m || (m->GetID() == 0)) + return false; -bool Client::IsLeadershipEXPOn() { + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(XTargets[i].ID == m->GetID()) + return true; + } + return false; +} - if(!m_pp.leadAAActive) - return false; +bool Client::IsClientXTarget(const Client *c) const +{ + if(!XTargettingAvailable() || !c) + return false; - Group *g = GetGroup(); + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(!strcasecmp(XTargets[i].Name, c->GetName())) + return true; + } + return false; +} - if (g && g->IsLeader(this) && g->GroupCount() > 2) - return true; - Raid *r = GetRaid(); +void Client::UpdateClientXTarget(Client *c) +{ + if(!XTargettingAvailable() || !c) + return; - if (!r) - return false; + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(!strcasecmp(XTargets[i].Name, c->GetName())) + { + XTargets[i].ID = c->GetID(); + SendXTargetPacket(i, c); + } + } +} - // raid leaders can only gain raid AA XP - if (r->IsLeader(this)) { - if (r->RaidCount() > 17) - return true; - else - return false; - } +// IT IS NOT SAFE TO CALL THIS IF IT'S NOT INITIAL AGGRO +void Client::AddAutoXTarget(Mob *m, bool send) +{ + if (m->IsBot() || ((m->IsPet() || m->IsTempPet()) && m->IsPetOwnerBot())) { + return; + } - uint32 gid = r->GetGroup(this); + m_activeautohatermgr->increment_count(m); - if (gid > 11) // not in a group - return false; + if (!XTargettingAvailable() || !XTargetAutoAddHaters || IsXTarget(m)) { + return; + } - if (r->IsGroupLeader(GetName()) && r->GroupCount(gid) > 2) - return true; + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type == Auto && XTargets[i].ID == 0) { + XTargets[i].ID = m->GetID(); - return false; + if (send) { // if we don't send we're bulk sending updates later on + SendXTargetPacket(i, m); + } else { + XTargets[i].dirty = true; + } -} + break; + } + } -uint32 Client::GetAggroCount() { - return AggroCount; + LogXTargets( + "Adding [{}] to [{}] ({}) XTargets", + m->GetCleanName(), + GetCleanName(), + GetID() + ); } -// we pass in for book keeping if RestRegen is enabled -void Client::IncrementAggroCount(bool raid_target) +void Client::RemoveXTarget(Mob *m, bool OnlyAutoSlots) { - // This method is called when a client is added to a mob's hate list. It turns the clients aggro flag on so - // rest state regen is stopped, and for SoF, it sends the opcode to show the crossed swords in-combat indicator. - AggroCount++; + if (!XTargettingAvailable() || !m || !m_activeautohatermgr) { + return; + } - if(!RuleB(Character, RestRegenEnabled)) - return; + m_activeautohatermgr->decrement_count(m); + // now we may need to clean up our CurrentTargetNPC entries + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type == CurrentTargetNPC && XTargets[i].ID == m->GetID()) { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].dirty = true; + } + } - uint32 newtimer = raid_target ? RuleI(Character, RestRegenRaidTimeToActivate) : RuleI(Character, RestRegenTimeToActivate); + auto r = GetRaid(); + if (r) { + r->UpdateRaidXTargets(); + } - // When our aggro count is 1 here, we are exiting rest state. We need to pause our current timer, if we have time remaining - // We should not actually have to do anything to the Timer object since the AggroCount counter blocks it from being checked - // and will have it's timer changed when we exit combat so let's not do any extra work - if (AggroCount == 1 && rest_timer.GetRemainingTime()) // the Client::rest_timer is never disabled, so don't need to check - m_pp.RestTimer = std::max(1u, rest_timer.GetRemainingTime() / 1000); // I guess round up? + LogXTargets( + "Removing [{}] from [{}] ({}) XTargets", + m->GetCleanName(), + GetCleanName(), + GetID() + ); +} - // save the new timer if it's higher - m_pp.RestTimer = std::max(m_pp.RestTimer, newtimer); +void Client::UpdateXTargetType(XTargetType Type, Mob *m, const char *Name) +{ + if (!XTargettingAvailable()) { + return; + } - // If we already had aggro before this method was called, the combat indicator should already be up for SoF clients, - // so we don't need to send it again. - // - if(AggroCount > 1) - return; + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type == Type) { + if (m) { + XTargets[i].ID = m->GetID(); + } + else { + XTargets[i].ID = 0; + } - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - auto outapp = new EQApplicationPacket(OP_RestState, 1); - char *Buffer = (char *)outapp->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x01); - QueuePacket(outapp); - safe_delete(outapp); - } + if (Name) { + strncpy(XTargets[i].Name, Name, 64); + } + SendXTargetPacket(i, m); + } + } } -void Client::DecrementAggroCount() +void Client::SendXTargetPacket(uint32 Slot, Mob *m) { - // This should be called when a client is removed from a mob's hate list (it dies or is memblurred). - // It checks whether any other mob is aggro on the player, and if not, starts the rest timer. - // For SoF, the opcode to start the rest state countdown timer in the UI is sent. + if(!XTargettingAvailable()) + return; - // If we didn't have aggro before, this method should not have been called. - if(!AggroCount) - return; + uint32 PacketSize = 18; - AggroCount--; + if(m) + PacketSize += strlen(m->GetCleanName()); + else + { + PacketSize += strlen(XTargets[Slot].Name); + } - if(!RuleB(Character, RestRegenEnabled)) - return; + auto outapp = new EQApplicationPacket(OP_XTargetResponse, PacketSize); + outapp->WriteUInt32(GetMaxXTargets()); + outapp->WriteUInt32(1); + outapp->WriteUInt32(Slot); + if(m) + { + outapp->WriteUInt8(1); + } + else + { + if (strlen(XTargets[Slot].Name) && ((XTargets[Slot].Type == CurrentTargetPC) || + (XTargets[Slot].Type == GroupTank) || + (XTargets[Slot].Type == GroupAssist) || + (XTargets[Slot].Type == Puller))) + { + outapp->WriteUInt8(2); + } + else + { + outapp->WriteUInt8(0); + } + } + outapp->WriteUInt32(XTargets[Slot].ID); + outapp->WriteString(m ? m->GetCleanName() : XTargets[Slot].Name); + FastQueuePacket(&outapp); +} - // Something else is still aggro on us, can't rest yet. - if (AggroCount) - return; +// This is a bulk packet, we use it when we remove something since we need to reorder the xtargets and maybe +// add new mobs! Currently doesn't check if there is a dirty flag set, so it should only be called when there is +void Client::SendXTargetUpdates() +{ + if (!XTargettingAvailable()) + return; + + int count = 0; + // header is 4 bytes max xtargets, 4 bytes count + // entry is 4 bytes slot, 1 byte unknown, 4 bytes ID, 65 char name + auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8 + 74 * GetMaxXTargets()); // fuck it max size + outapp->WriteUInt32(GetMaxXTargets()); + outapp->WriteUInt32(1); // we will correct this later + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].dirty) { + outapp->WriteUInt32(i); + // MQ2 checks this for valid mobs, so 0 is bad here at least ... + outapp->WriteUInt8(XTargets[i].ID ? 1 : 0); + outapp->WriteUInt32(XTargets[i].ID); + outapp->WriteString(XTargets[i].Name); + count++; + XTargets[i].dirty = false; + } + } - rest_timer.Start(m_pp.RestTimer * 1000); + // RemoveXTarget probably got called with a mob not on our xtargets + if (count == 0) { + safe_delete(outapp); + return; + } - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - auto outapp = new EQApplicationPacket(OP_RestState, 5); - char *Buffer = (char *)outapp->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, m_pp.RestTimer); - QueuePacket(outapp); - safe_delete(outapp); - } + auto newbuff = new uchar[outapp->GetWritePosition()]; + memcpy(newbuff, outapp->pBuffer, outapp->GetWritePosition()); + safe_delete_array(outapp->pBuffer); + outapp->pBuffer = newbuff; + outapp->size = outapp->GetWritePosition(); + outapp->SetWritePosition(4); + outapp->WriteUInt32(count); + FastQueuePacket(&outapp); } -// when we cast a beneficial spell we need to steal our targets current timer -// That's what we use this for -void Client::UpdateRestTimer(uint32 new_timer) +void Client::RemoveGroupXTargets() { - // their timer was 0, so we don't do anything - if (new_timer == 0) - return; - - if (!RuleB(Character, RestRegenEnabled)) - return; - - // so if we're currently on aggro, we check our saved timer - if (AggroCount) { - if (m_pp.RestTimer < new_timer) // our timer needs to be updated, don't need to update client here - m_pp.RestTimer = new_timer; - } else { // if we're not aggro, we need to check if current timer needs updating - if (rest_timer.GetRemainingTime() / 1000 < new_timer) { - rest_timer.Start(new_timer * 1000); - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - auto outapp = new EQApplicationPacket(OP_RestState, 5); - char *Buffer = (char *)outapp->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0x00); - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, new_timer); - QueuePacket(outapp); - safe_delete(outapp); - } - } - } + if(!XTargettingAvailable()) + return; + + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if ((XTargets[i].Type == GroupTank) || + (XTargets[i].Type == GroupAssist) || + (XTargets[i].Type == Puller)) + { + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + SendXTargetPacket(i, nullptr); + } + } } -void Client::SendPVPStats() +void Client::RemoveAutoXTargets() { - // This sends the data to the client to populate the PVP Stats Window. - // - // When the PVP Stats window is opened, no opcode is sent. Therefore this method should be called - // from Client::CompleteConnect, and also when the player makes a PVP kill. - // - auto outapp = new EQApplicationPacket(OP_PVPStats, sizeof(PVPStats_Struct)); - PVPStats_Struct *pvps = (PVPStats_Struct *)outapp->pBuffer; + if(!XTargettingAvailable()) + return; - pvps->Kills = m_pp.PVPKills; - pvps->Deaths = m_pp.PVPDeaths; - pvps->PVPPointsAvailable = m_pp.PVPCurrentPoints; - pvps->TotalPVPPoints = m_pp.PVPCareerPoints; - pvps->BestKillStreak = m_pp.PVPBestKillStreak; - pvps->WorstDeathStreak = m_pp.PVPWorstDeathStreak; - pvps->CurrentKillStreak = m_pp.PVPCurrentKillStreak; - - // TODO: Record and send other PVP Stats - - QueuePacket(outapp); - safe_delete(outapp); + for(int i = 0; i < GetMaxXTargets(); ++i) + { + if(XTargets[i].Type == Auto) + { + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + SendXTargetPacket(i, nullptr); + } + } } -void Client::SendCrystalCounts() +void Client::ShowXTargets(Client *c) { - auto outapp = new EQApplicationPacket(OP_CrystalCountUpdate, sizeof(CrystalCountUpdate_Struct)); - CrystalCountUpdate_Struct *ccus = (CrystalCountUpdate_Struct *)outapp->pBuffer; + if (!c) { + return; + } + + auto xtarget_count = 0; + + for (int i = 0; i < GetMaxXTargets(); ++i) { + c->Message( + Chat::White, + fmt::format( + "xtarget slot [{}] type [{}] ID [{}] name [{}]", + i, + static_cast(XTargets[i].Type), + XTargets[i].ID, + strlen(XTargets[i].Name) ? XTargets[i].Name : "No Name" + ).c_str() + ); + + xtarget_count++; + } + + auto &list = GetXTargetAutoMgr()->get_list(); + // yeah, I kept having to do something for debugging to tell if managers were the same object or not :P + // so lets use the address as an "ID" + c->Message( + Chat::White, + fmt::format( + "XTargetAutoMgr ID [{}] size [{}]", + fmt::ptr(GetXTargetAutoMgr()), + list.size() + ).c_str() + ); - ccus->CurrentRadiantCrystals = GetRadiantCrystals(); - ccus->CurrentEbonCrystals = GetEbonCrystals(); - ccus->CareerRadiantCrystals = m_pp.careerRadCrystals; - ccus->CareerEbonCrystals = m_pp.careerEbonCrystals; + int count = 0; + for (auto &e : list) { + c->Message( + Chat::White, + fmt::format( + "Spawn ID: {} Count: {}", + e.spawn_id, + e.count + ).c_str() + ); + count++; - QueuePacket(outapp); - safe_delete(outapp); + if (count == 20) { + break; + } + } } -void Client::SendDisciplineTimers() +void Client::ProcessXTargetAutoHaters() { + if (!XTargettingAvailable()) + return; - auto outapp = new EQApplicationPacket(OP_DisciplineTimer, sizeof(DisciplineTimer_Struct)); - DisciplineTimer_Struct *dts = (DisciplineTimer_Struct *)outapp->pBuffer; + // move shit up! If the removed NPC was in a CurrentTargetNPC slot it becomes Auto + // and we need to potentially fill it + std::queue empty_slots; + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].Type != Auto) + continue; + + if (XTargets[i].ID != 0 && !GetXTargetAutoMgr()->contains_mob(XTargets[i].ID)) { + XTargets[i].ID = 0; + XTargets[i].dirty = true; + } - for(unsigned int i = 0; i < MAX_DISCIPLINE_TIMERS; ++i) - { - uint32 RemainingTime = p_timers.GetRemainingTime(pTimerDisciplineReuseStart + i); + if (XTargets[i].ID == 0) { + empty_slots.push(i); + continue; + } - if(RemainingTime > 0) - { - dts->TimerID = i; - dts->Duration = RemainingTime; - QueuePacket(outapp); - } - } + if (XTargets[i].ID != 0 && !empty_slots.empty()) { + int temp = empty_slots.front(); + std::swap(XTargets[i], XTargets[temp]); + XTargets[i].dirty = XTargets[temp].dirty = true; + empty_slots.pop(); + empty_slots.push(i); + } + } + // okay, now we need to check if we have any empty slots and if we have aggro + // We make the assumption that if we shuffled the NPCs up that they're still on the aggro + // list in the same order. We could probably do this better and try to calc if + // there are new NPCs for our empty slots on the manager, but ahhh fuck it. + if (!empty_slots.empty() && !GetXTargetAutoMgr()->empty() && XTargetAutoAddHaters) { + auto &haters = GetXTargetAutoMgr()->get_list(); + for (auto &e : haters) { + auto *mob = entity_list.GetMob(e.spawn_id); + if (mob && !IsXTarget(mob)) { + auto slot = empty_slots.front(); + empty_slots.pop(); + XTargets[slot].dirty = true; + XTargets[slot].ID = mob->GetID(); + strn0cpy(XTargets[slot].Name, mob->GetCleanName(), 64); + } + if (empty_slots.empty()) + break; + } + } - safe_delete(outapp); + m_dirtyautohaters = false; + SendXTargetUpdates(); } -void Client::SendRespawnBinds() +// This function is called when a client is added to a group +// Group leader joining isn't handled by this function +void Client::JoinGroupXTargets(Group *g) { - // This sends the data to the client to populate the Respawn from Death Window. - // - // This should be sent after OP_Death for SoF clients - // Client will respond with a 4 byte packet that includes the number of the selection made - // - - //If no options have been given, default to Bind + Rez - if (respawn_options.empty()) - { - BindStruct* b = &m_pp.binds[0]; - RespawnOption opt; - opt.name = "Bind Location"; - opt.zone_id = b->zone_id; - opt.instance_id = b->instance_id; - opt.x = b->x; - opt.y = b->y; - opt.z = b->z; - opt.heading = b->heading; - respawn_options.push_front(opt); - } - //Rez is always added at the end - RespawnOption rez; - rez.name = "Resurrect"; - rez.zone_id = zone->GetZoneID(); - rez.instance_id = zone->GetInstanceID(); - rez.x = GetX(); - rez.y = GetY(); - rez.z = GetZ(); - rez.heading = GetHeading(); - respawn_options.push_back(rez); - - int num_options = respawn_options.size(); - uint32 PacketLength = 17 + (26 * num_options); //Header size + per-option invariant size - - std::list::iterator itr; - RespawnOption* opt = nullptr; - - //Find string size for each option - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - opt = &(*itr); - PacketLength += opt->name.size() + 1; //+1 for cstring - } - - auto outapp = new EQApplicationPacket(OP_RespawnWindow, PacketLength); - char* buffer = (char*)outapp->pBuffer; - - //Packet header - VARSTRUCT_ENCODE_TYPE(uint32, buffer, initial_respawn_selection); //initial selection (from 0) - VARSTRUCT_ENCODE_TYPE(uint32, buffer, RuleI(Character, RespawnFromHoverTimer) * 1000); - VARSTRUCT_ENCODE_TYPE(uint32, buffer, 0); //unknown - VARSTRUCT_ENCODE_TYPE(uint32, buffer, num_options); //number of options to display - - //Individual options - int count = 0; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - opt = &(*itr); - VARSTRUCT_ENCODE_TYPE(uint32, buffer, count++); //option num (from 0) - VARSTRUCT_ENCODE_TYPE(uint32, buffer, opt->zone_id); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->x); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->y); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->z); - VARSTRUCT_ENCODE_TYPE(float, buffer, opt->heading); - VARSTRUCT_ENCODE_STRING(buffer, opt->name.c_str()); - VARSTRUCT_ENCODE_TYPE(uint8, buffer, (count == num_options)); //is this one Rez (the last option)? - } - - QueuePacket(outapp); - safe_delete(outapp); - return; -} + if (!g) + return; -void Client::HandleLDoNOpen(NPC *target) -{ - if(target) - { - if(target->GetClass() != Class::LDoNTreasure) - { - LogDebug("[{}] tried to open [{}] but [{}] was not a treasure chest", - GetName(), target->GetName(), target->GetName()); - return; - } - - if (target->GetSpecialAbility(SpecialAbility::OpenImmunity)) - { - LogDebug("[{}] tried to open [{}] but it was immune", GetName(), target->GetName()); - return; - } - - if(DistanceSquaredNoZ(m_Position, target->GetPosition()) > RuleI(Adventure, LDoNTrapDistanceUse)) - { - LogDebug("[{}] tried to open [{}] but [{}] was out of range", - GetName(), target->GetName(), target->GetName()); - Message(Chat::Red, "Treasure chest out of range."); - return; - } - - if(target->IsLDoNTrapped()) - { - if(target->GetLDoNTrapSpellID() != 0) - { - MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - } - else - { - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - } - } - - if(target->IsLDoNLocked()) - { - MessageString(Chat::Skills, LDON_STILL_LOCKED, target->GetCleanName()); - return; - } - else - { - target->AddToHateList(this, 0, 500000, false, false, false); - if(target->GetLDoNTrapType() != 0) - { - if(GetRaid()) - { - GetRaid()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); - } - else if(GetGroup()) - { - GetGroup()->SplitExp(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, target); - } - else - { - AddEXP(ExpSource::LDoNChest, target->GetLevel()*target->GetLevel()*2625/10, GetLevelCon(target->GetLevel())); - } - } - target->Death(this, 0, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); - } - } -} + if (!GetXTargetAutoMgr()->empty()) { + g->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); + GetXTargetAutoMgr()->clear(); + RemoveAutoXTargets(); + } -void Client::HandleLDoNSenseTraps(NPC *target, uint16 skill, uint8 type) -{ - if(target && target->GetClass() == Class::LDoNTreasure) - { - if(target->IsLDoNTrapped()) - { - if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) - { - MessageString(Chat::Skills, LDON_CANT_DETERMINE_TRAP, target->GetCleanName()); - return; - } - - if(target->IsLDoNTrapDetected()) - { - MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); - } - else - { - int check = LDoNChest_SkillCheck(target, skill); - switch(check) - { - case -1: - case 0: - MessageString(Chat::Skills, LDON_DONT_KNOW_TRAPPED, target->GetCleanName()); - break; - case 1: - MessageString(Chat::Skills, LDON_CERTAIN_TRAP, target->GetCleanName()); - target->SetLDoNTrapDetected(true); - break; - default: - break; - } - } - } - else - { - MessageString(Chat::Skills, LDON_CERTAIN_NOT_TRAP, target->GetCleanName()); - } - } + SetXTargetAutoMgr(g->GetXTargetAutoMgr()); + + if (!GetXTargetAutoMgr()->empty()) + SetDirtyAutoHaters(); } -void Client::HandleLDoNDisarm(NPC *target, uint16 skill, uint8 type) +// This function is called when a client leaves a group +void Client::LeaveGroupXTargets(Group *g) { - if(target) - { - if(target->GetClass() == Class::LDoNTreasure) - { - if(!target->IsLDoNTrapped()) - { - MessageString(Chat::Skills, LDON_WAS_NOT_TRAPPED, target->GetCleanName()); - return; - } - - if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) - { - MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); - return; - } - - int check = 0; - if(target->IsLDoNTrapDetected()) - { - check = LDoNChest_SkillCheck(target, skill); - } - else - { - check = LDoNChest_SkillCheck(target, skill*33/100); - } - switch(check) - { - case 1: - target->SetLDoNTrapDetected(false); - target->SetLDoNTrapped(false); - target->SetLDoNTrapSpellID(0); - MessageString(Chat::Skills, LDON_HAVE_DISARMED, target->GetCleanName()); - break; - case 0: - MessageString(Chat::Skills, LDON_HAVE_NOT_DISARMED, target->GetCleanName()); - break; - case -1: - MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - break; - } - } - } + if (!g) + return; + + SetXTargetAutoMgr(nullptr); // this will set it back to our manager + RemoveAutoXTargets(); + entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever + // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups + if (!GetXTargetAutoMgr()->empty()) { + GetXTargetAutoMgr()->demerge(*g->GetXTargetAutoMgr()); // this will remove entries where we only had aggro + SetDirtyAutoHaters(); + } } -void Client::HandleLDoNPickLock(NPC *target, uint16 skill, uint8 type) +// This function is called when a client leaves a group +void Client::LeaveRaidXTargets(Raid *r) { - if(target) - { - if(target->GetClass() == Class::LDoNTreasure) - { - if(target->IsLDoNTrapped()) - { - MessageString(Chat::Red, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQ::spells::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].resist_difficulty); - target->SetLDoNTrapSpellID(0); - target->SetLDoNTrapped(false); - target->SetLDoNTrapDetected(false); - } - - if(!target->IsLDoNLocked()) - { - MessageString(Chat::Skills, LDON_WAS_NOT_LOCKED, target->GetCleanName()); - return; - } - - if((target->GetLDoNTrapType() == LDoNTypeCursed || target->GetLDoNTrapType() == LDoNTypeMagical) && type != target->GetLDoNTrapType()) - { - Message(Chat::Skills, "You cannot unlock %s with this skill.", target->GetCleanName()); - return; - } - - int check = LDoNChest_SkillCheck(target, skill); - - switch(check) - { - case 0: - case -1: - MessageString(Chat::Skills, LDON_PICKLOCK_FAILURE, target->GetCleanName()); - break; - case 1: - target->SetLDoNLocked(false); - MessageString(Chat::Skills, LDON_PICKLOCK_SUCCESS, target->GetCleanName()); - break; - } - } - } + if (!r) + return; + + SetXTargetAutoMgr(nullptr); // this will set it back to our manager + RemoveAutoXTargets(); + entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever + // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups + if (!GetXTargetAutoMgr()->empty()) { + GetXTargetAutoMgr()->demerge(*r->GetXTargetAutoMgr()); // this will remove entries where we only had aggro + SetDirtyAutoHaters(); + } } -int Client::LDoNChest_SkillCheck(NPC *target, int skill) +void Client::SetMaxXTargets(uint8 NewMax) { - if(!target) - return -1; + if(!XTargettingAvailable()) + return; - int chest_difficulty = target->GetLDoNLockedSkill() == 0 ? (target->GetLevel() * 5) : target->GetLDoNLockedSkill(); - float base_difficulty = RuleR(Adventure, LDoNBaseTrapDifficulty); + if(NewMax > XTARGET_HARDCAP) + return; - if(chest_difficulty == 0) - chest_difficulty = 5; + MaxXTargets = NewMax; - float chance = ((100.0f - base_difficulty) * ((float)skill / (float)chest_difficulty)); + Save(0); - if(chance > (100.0f - base_difficulty)) - { - chance = 100.0f - base_difficulty; - } + for(int i = MaxXTargets; i < XTARGET_HARDCAP; ++i) + { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + } - float d100 = (float)zone->random.Real(0, 100); + auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8); + outapp->WriteUInt32(GetMaxXTargets()); + outapp->WriteUInt32(0); + FastQueuePacket(&outapp); +} - if(d100 <= chance) - return 1; - else - { - if(d100 > (chance + RuleR(Adventure, LDoNCriticalFailTrapThreshold))) - return -1; - } +void Client::SendWebLink(const char *website) +{ + if (website) { + size_t len = strlen(website) + 1; + if (len > 1) + { + auto outapp = new EQApplicationPacket(OP_Weblink, sizeof(Weblink_Struct) + len); + Weblink_Struct* wl = (Weblink_Struct*)outapp->pBuffer; + memcpy(wl->weblink, website, len); + wl->weblink[len] = '\0'; - return 0; + FastQueuePacket(&outapp); + } + } } -void Client::SummonAndRezzAllCorpses() +void Client::SendMercPersonalInfo() { - PendingRezzXP = -1; + uint32 mercTypeCount = 1; + uint32 mercCount = 1; //TODO: Un-hardcode this and support multiple mercs like in later clients than SoD. + uint32 i = 0; + uint32 altCurrentType = 19; //TODO: Implement alternate currency purchases involving mercs! - auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); + MercTemplate *mercData = &zone->merc_templates[GetMercInfo().MercTemplateID]; + + int stancecount = 0; + stancecount += zone->merc_stance_list[GetMercInfo().MercTemplateID].size(); + if(stancecount > MAX_MERC_STANCES || mercCount > MAX_MERC || mercTypeCount > MAX_MERC_GRADES) + { + Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo canceled: (%i) (%i) (%i) for %s", stancecount, mercCount, mercTypeCount, GetName()); + SendMercMerchantResponsePacket(0); + return; + } - ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(MercenaryDataUpdate_Struct)); + auto mdus = (MercenaryDataUpdate_Struct *) outapp->pBuffer; + + mdus->MercStatus = 0; + mdus->MercCount = mercCount; + mdus->MercData[i].MercID = mercData->MercTemplateID; + mdus->MercData[i].MercType = mercData->MercType; + mdus->MercData[i].MercSubType = mercData->MercSubType; + mdus->MercData[i].PurchaseCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0); + mdus->MercData[i].UpkeepCost = Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0); + mdus->MercData[i].Status = 0; + mdus->MercData[i].AltCurrencyCost = Merc::CalcPurchaseCost( + mercData->MercTemplateID, + GetLevel(), + altCurrentType + ); + mdus->MercData[i].AltCurrencyUpkeep = Merc::CalcPurchaseCost( + mercData->MercTemplateID, + GetLevel(), + altCurrentType + ); + mdus->MercData[i].AltCurrencyType = altCurrentType; + mdus->MercData[i].MercUnk01 = 0; + mdus->MercData[i].TimeLeft = GetMercInfo().MercTimerRemaining; //GetMercTimer().GetRemainingTime(); + mdus->MercData[i].MerchantSlot = i + 1; + mdus->MercData[i].MercUnk02 = 1; + mdus->MercData[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); + mdus->MercData[i].MercUnk03 = 0; + mdus->MercData[i].MercUnk04 = 1; + + strn0cpy(mdus->MercData[i].MercName, GetMercInfo().merc_name, sizeof(mdus->MercData[i].MercName)); + + uint32 stanceindex = 0; + if (mdus->MercData[i].StanceCount != 0) { + auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); + while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { + mdus->MercData[i].Stances[stanceindex].StanceIndex = stanceindex; + mdus->MercData[i].Stances[stanceindex].Stance = (iter->StanceID); + stanceindex++; + ++iter; + } + } - sdapcs->CharacterID = CharacterID(); - sdapcs->ZoneID = zone->GetZoneID(); - sdapcs->InstanceID = zone->GetInstanceID(); + mdus->MercData[i].MercUnk05 = 1; + FastQueuePacket(&outapp); + safe_delete(outapp); + return; + } else { + auto outapp = new EQApplicationPacket(OP_MercenaryDataResponse, sizeof(MercenaryMerchantList_Struct)); + auto mml = (MercenaryMerchantList_Struct *) outapp->pBuffer; + + mml->MercTypeCount = mercTypeCount; //We should only have one merc entry. + mml->MercGrades[i] = 1; + + mml->MercCount = mercCount; + mml->Mercs[i].MercID = mercData->MercTemplateID; + mml->Mercs[i].MercType = mercData->MercType; + mml->Mercs[i].MercSubType = mercData->MercSubType; + mml->Mercs[i].PurchaseCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0) : 0; + mml->Mercs[i].UpkeepCost = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0) : 0; + mml->Mercs[i].Status = 0; + mml->Mercs[i].AltCurrencyCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; + mml->Mercs[i].AltCurrencyUpkeep = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; + mml->Mercs[i].AltCurrencyType = altCurrentType; + mml->Mercs[i].MercUnk01 = 0; + mml->Mercs[i].TimeLeft = GetMercInfo().MercTimerRemaining; + mml->Mercs[i].MerchantSlot = i + 1; + mml->Mercs[i].MercUnk02 = 1; + mml->Mercs[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); + mml->Mercs[i].MercUnk03 = 0; + mml->Mercs[i].MercUnk04 = 1; + + strn0cpy(mml->Mercs[i].MercName, GetMercInfo().merc_name, sizeof(mml->Mercs[i].MercName)); + + int stanceindex = 0; + if (mml->Mercs[i].StanceCount != 0) { + auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); + while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { + mml->Mercs[i].Stances[stanceindex].StanceIndex = stanceindex; + mml->Mercs[i].Stances[stanceindex].Stance = (iter->StanceID); + stanceindex++; + ++iter; + } + } - worldserver.SendPacket(Pack); + FastQueuePacket(&outapp); + safe_delete(outapp); + return; + } +} - safe_delete(Pack); +void Client::SendClearMercInfo() +{ + auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(NoMercenaryHired_Struct)); + NoMercenaryHired_Struct *nmhs = (NoMercenaryHired_Struct*)outapp->pBuffer; + nmhs->MercStatus = -1; + nmhs->MercCount = 0; + nmhs->MercID = 1; + FastQueuePacket(&outapp); +} - entity_list.RemoveAllCorpsesByCharID(CharacterID()); - int CorpseCount = database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), GetPosition()); - if(CorpseCount <= 0) - { - Message(Chat::Yellow, "You have no corpses to summnon."); - return; - } +void Client::DuplicateLoreMessage(uint32 ItemID) +{ + if (!(m_ClientVersionBit & EQ::versions::maskRoFAndLater)) + { + MessageString(Chat::White, PICK_LORE); + return; + } - int RezzExp = entity_list.RezzAllCorpsesByCharID(CharacterID()); + const EQ::ItemData *item = database.GetItem(ItemID); - if(RezzExp > 0) - SetEXP(ExpSource::Resurrection, GetEXP() + RezzExp, GetAAXP(), true); + if(!item) + return; - Message(Chat::Yellow, "All your corpses have been summoned to your feet and have received a 100% resurrection."); + MessageString(Chat::White, PICK_LORE, item->Name); } -void Client::SummonAllCorpses(const glm::vec4& position) +void Client::GarbleMessage(char *message, uint8 variance) { - auto summonLocation = position; - if(IsOrigin(position) && position.w == 0.0f) - summonLocation = GetPosition(); + // Garble message by variance% + const char alpha_list[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // only change alpha characters for now + const char delimiter = 0x12; + int delimiter_count = 0; - auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); + // Don't garble # commands + if (message[0] == COMMAND_CHAR || message[0] == BOT_COMMAND_CHAR) { + return; + } + + for (size_t i = 0; i < strlen(message); i++) { + // Client expects hex values inside of a text link body + if (message[i] == delimiter) { + if (!(delimiter_count & 1)) { i += EQ::constants::SAY_LINK_BODY_SIZE; } + ++delimiter_count; + continue; + } - ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; + uint8 chance = (uint8)zone->random.Int(0, 115); // variation just over worst possible scrambling + if (isalpha((unsigned char)message[i]) && (chance <= variance)) { + uint8 rand_char = (uint8)zone->random.Int(0,51); // choose a random character from the alpha list + message[i] = alpha_list[rand_char]; + } + } +} - sdapcs->CharacterID = CharacterID(); - sdapcs->ZoneID = zone->GetZoneID(); - sdapcs->InstanceID = zone->GetInstanceID(); +// returns what Other thinks of this +FACTION_VALUE Client::GetReverseFactionCon(Mob* iOther) { + if (GetOwnerID()) { + return GetOwnerOrSelf()->GetReverseFactionCon(iOther); + } - worldserver.SendPacket(Pack); + iOther = iOther->GetOwnerOrSelf(); - safe_delete(Pack); + if (iOther->GetPrimaryFaction() < 0) + return GetSpecialFactionCon(iOther); - entity_list.RemoveAllCorpsesByCharID(CharacterID()); + if (iOther->GetPrimaryFaction() == 0) + return FACTION_INDIFFERENTLY; - database.SummonAllCharacterCorpses(CharacterID(), zone->GetZoneID(), zone->GetInstanceID(), summonLocation); + return GetFactionLevel(CharacterID(), 0, GetFactionRace(), GetClass(), GetDeity(), iOther->GetPrimaryFaction(), iOther); } -void Client::DepopAllCorpses() +bool Client::ReloadCharacterFaction(Client *c, uint32 facid, uint32 charid) { - auto Pack = new ServerPacket(ServerOP_DepopAllPlayersCorpses, sizeof(ServerDepopAllPlayersCorpses_Struct)); + if (database.SetCharacterFactionLevel(charid, facid, 0, 0, factionvalues)) + return true; + else + return false; +} - ServerDepopAllPlayersCorpses_Struct *sdapcs = (ServerDepopAllPlayersCorpses_Struct*)Pack->pBuffer; +//o-------------------------------------------------------------- +//| Name: GetFactionLevel; Dec. 16, 2001 +//o-------------------------------------------------------------- +//| Notes: Gets the characters faction standing with the specified NPC. +//| Will return Indifferent on failure. +//o-------------------------------------------------------------- +FACTION_VALUE Client::GetFactionLevel(uint32 char_id, uint32 npc_id, uint32 p_race, uint32 p_class, uint32 p_deity, int32 pFaction, Mob* tnpc) +{ + if (pFaction < 0) + return GetSpecialFactionCon(tnpc); + FACTION_VALUE fac = FACTION_INDIFFERENTLY; + int32 tmpFactionValue; + FactionMods fmods; + + // few optimizations + if (GetFeigned()) + return FACTION_INDIFFERENTLY; + if(!zone->CanDoCombat()) + return FACTION_INDIFFERENTLY; + if (invisible_undead && tnpc && !tnpc->SeeInvisibleUndead()) + return FACTION_INDIFFERENTLY; + if (IsInvisible(tnpc)) + return FACTION_INDIFFERENTLY; + if (tnpc && tnpc->GetOwnerID() != 0) // pets con amiably to owner and indiff to rest + { + if (char_id == tnpc->GetOwner()->CastToClient()->CharacterID()) + return FACTION_AMIABLY; + else + return FACTION_INDIFFERENTLY; + } - sdapcs->CharacterID = CharacterID(); - sdapcs->ZoneID = zone->GetZoneID(); - sdapcs->InstanceID = zone->GetInstanceID(); + //First get the NPC's Primary faction + if(pFaction > 0) + { + //Get the faction data from the database + if(content_db.GetFactionData(&fmods, p_class, p_race, p_deity, pFaction)) + { + //Get the players current faction with pFaction + tmpFactionValue = GetCharacterFactionLevel(pFaction); + //Tack on any bonuses from Alliance type spell effects + tmpFactionValue += GetFactionBonus(pFaction); + tmpFactionValue += GetItemFactionBonus(pFaction); + //Return the faction to the client + fac = CalculateFaction(&fmods, tmpFactionValue); + } + } + else + { + return(FACTION_INDIFFERENTLY); + } - worldserver.SendPacket(Pack); + // merchant fix + if (tnpc && tnpc->IsNPC() && tnpc->CastToNPC()->MerchantType && (fac == FACTION_THREATENINGLY || fac == FACTION_SCOWLS)) + fac = FACTION_DUBIOUSLY; - safe_delete(Pack); + if (tnpc != 0 && fac != FACTION_SCOWLS && tnpc->CastToNPC()->CheckAggro(this)) + fac = FACTION_THREATENINGLY; - entity_list.RemoveAllCorpsesByCharID(CharacterID()); + return fac; } -void Client::DepopPlayerCorpse(uint32 dbid) +//Sets the characters faction standing with the specified NPC. +void Client::SetFactionLevel( + uint32 character_id, + uint32 npc_faction_id, + uint8 class_id, + uint8 race_id, + uint8 deity_id, + bool is_quest +) { - auto Pack = new ServerPacket(ServerOP_DepopPlayerCorpse, sizeof(ServerDepopPlayerCorpse_Struct)); + auto l = zone->GetNPCFactionEntries(npc_faction_id); - ServerDepopPlayerCorpse_Struct *sdpcs = (ServerDepopPlayerCorpse_Struct*)Pack->pBuffer; + if (l.empty()) { + return; + } - sdpcs->DBID = dbid; - sdpcs->ZoneID = zone->GetZoneID(); - sdpcs->InstanceID = zone->GetInstanceID(); + int current_value; - worldserver.SendPacket(Pack); + for (auto& e : l) { + if (e.faction_id <= 0 || e.value == 0) { + continue; + } - safe_delete(Pack); + int faction_before; + int faction_minimum; + int faction_maximum; - entity_list.RemoveCorpseByDBID(dbid); -} + FactionMods faction_modifiers; -void Client::BuryPlayerCorpses() -{ - database.BuryAllCharacterCorpses(CharacterID()); -} + content_db.GetFactionData(&faction_modifiers, class_id, race_id, deity_id, e.faction_id); -void Client::NotifyNewTitlesAvailable() -{ - auto outapp = new EQApplicationPacket(OP_NewTitlesAvailable, 0); + if (is_quest) { + if (e.value > 0) { + e.value = -std::abs(e.value); + } else if (e.value < 0) { + e.value = std::abs(e.value); + } + } - QueuePacket(outapp); + // Adjust the amount you can go up or down so the resulting range + // is PERSONAL_MAX - PERSONAL_MIN + // + // Adjust these values for cases where starting faction is below + // min or above max by not allowing any earn in those directions. + faction_minimum = faction_modifiers.min - faction_modifiers.base; + faction_minimum = std::min(0, faction_minimum); - safe_delete(outapp); + faction_maximum = faction_modifiers.max - faction_modifiers.base; + faction_maximum = std::max(0, faction_maximum); -} + // Get the characters current value with that faction + current_value = GetCharacterFactionLevel(e.faction_id); + faction_before = current_value; -void Client::SetStartZone(uint32 zoneid, float x, float y, float z, float heading) -{ - // setting city to zero allows the player to use /setstartcity to set the city themselves - if(zoneid == 0) { - m_pp.binds[4].zone_id = 0; - Message(Chat::Yellow,"Your starting city has been reset. Use /setstartcity to choose a new one"); - return; - } - - // check to make sure the zone is valid - const char *target_zone_name = ZoneName(zoneid); - if(target_zone_name == nullptr) - return; - - m_pp.binds[4].zone_id = zoneid; - if(zone->GetInstanceID() != 0 && zone->IsInstancePersistent()) { - m_pp.binds[4].instance_id = zone->GetInstanceID(); - } - - if (x == 0 && y == 0 && z == 0) { - auto zd = GetZone(m_pp.binds[4].zone_id); - if (zd) { - m_pp.binds[4].x = zd->safe_x; - m_pp.binds[4].y = zd->safe_y; - m_pp.binds[4].z = zd->safe_z; - m_pp.binds[4].heading = zd->safe_heading; - } - } - else { - m_pp.binds[4].x = x; - m_pp.binds[4].y = y; - m_pp.binds[4].z = z; - m_pp.binds[4].heading = heading; - } -} +#ifdef LUA_EQEMU + int32 lua_ret = 0; + bool ignore_default = false; + lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, e.value, e.faction_id, current_value, e.temp, faction_minimum, faction_maximum, ignore_default); -uint32 Client::GetStartZone() -{ - return m_pp.binds[4].zone_id; -} + if (ignore_default) { + e.value = lua_ret; + } +#endif -void Client::ShowSkillsWindow() -{ - std::string popup_text; - std::map skills_map = EQ::skills::GetSkillTypeMap(); - - if (ClientVersion() < EQ::versions::ClientVersion::RoF2) { - skills_map[EQ::skills::Skill1HPiercing] = "Piercing"; - } - - // Table Start - popup_text += ""; - - for (const auto& skill : skills_map) { - auto skill_id = skill.first; - auto skill_name = skill.second; - auto can_have_skill = CanHaveSkill(skill_id); - auto current_skill = GetSkill(skill_id); - auto max_skill = MaxSkill(skill_id); - auto skill_maxed = current_skill >= max_skill; - if ( - skill_id == EQ::skills::Skill2HPiercing && - ClientVersion() < EQ::versions::ClientVersion::RoF2 - ) { - continue; - } - - if ( - !can_have_skill || - !current_skill || - !max_skill - ) { - continue; - } - - // Row Start - popup_text += ""; - - // Skill Name - popup_text += fmt::format( - "", - skill_name - ); - - // Current Skill Level out of Max Skill Level or a Check Mark for Maxed - popup_text += fmt::format( - "", - ( - skill_maxed ? - "Max" : - fmt::format( - "{}/{}", - current_skill, - max_skill - ) - ) - ); - - // Row End - popup_text += ""; - } - - // Table End - popup_text += "
{}{}
"; - - SendPopupToClient( - "Skills", - popup_text.c_str() - ); -} + UpdatePersonalFaction( + character_id, + e.value, + e.faction_id, + ¤t_value, + e.temp, + faction_minimum, + faction_maximum + ); -void Client::Signal(int signal_id) -{ - if (parse->PlayerHasQuestSub(EVENT_SIGNAL)) { - parse->EventPlayer(EVENT_SIGNAL, this, std::to_string(signal_id), 0); - } + SendFactionMessage( + e.value, + e.faction_id, + faction_before, + current_value, + e.temp, + faction_minimum, + faction_maximum + ); + } } -void Client::SendPayload(int payload_id, std::string payload_value) +void Client::SetFactionLevel2(uint32 char_id, int32 faction_id, uint8 char_class, uint8 char_race, uint8 char_deity, int32 value, uint8 temp) { - if (parse->PlayerHasQuestSub(EVENT_PAYLOAD)) { - const auto& export_string = fmt::format("{} {}", payload_id, payload_value); + int32 current_value; + + //Get the npc faction list + if(faction_id > 0 && value != 0) { + int32 faction_before_hit; + FactionMods fm; + int32 this_faction_max; + int32 this_faction_min; + + // Find out starting faction for this faction + // It needs to be used to adj max and min personal + // The range is still the same, 1200-3000(4200), but adjusted for base + content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), + faction_id); + + // Adjust the amount you can go up or down so the resulting range + // is PERSONAL_MAX - PERSONAL_MIN + // + // Adjust these values for cases where starting faction is below + // min or above max by not allowing any earn/loss in those directions. + // At least one faction starts out way below min, so we don't want + // to allow loses in those cases, just massive gains. + this_faction_min = fm.min - fm.base; + this_faction_min = std::min(0, this_faction_min); + this_faction_max = fm.max - fm.base; + this_faction_max = std::max(0, this_faction_max); + + //Get the faction modifiers + current_value = GetCharacterFactionLevel(faction_id); + faction_before_hit = current_value; - parse->EventPlayer(EVENT_PAYLOAD, this, export_string, 0); - } -} +#ifdef LUA_EQEMU + int32 lua_ret = 0; + bool ignore_default = false; + lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, value, faction_id, current_value, temp, this_faction_min, this_faction_max, ignore_default); -void Client::SendRewards() -{ - std::vector rewards; - std::string query = StringFormat("SELECT reward_id, amount " - "FROM account_rewards " - "WHERE account_id = %i " - "ORDER BY reward_id", AccountID()); - auto results = database.QueryDatabase(query); - if (!results.Success()) { - return; - } - - for (auto row = results.begin(); row != results.end(); ++row) { - ClientReward cr; - cr.id = Strings::ToInt(row[0]); - cr.amount = Strings::ToInt(row[1]); - rewards.push_back(cr); - } - - if(rewards.empty()) - return; - - auto vetapp = new EQApplicationPacket(OP_VetRewardsAvaliable, (sizeof(InternalVeteranReward) * rewards.size())); - uchar *data = vetapp->pBuffer; - for(int i = 0; i < rewards.size(); ++i) { - InternalVeteranReward *ivr = (InternalVeteranReward*)data; - ivr->claim_id = rewards[i].id; - ivr->number_available = rewards[i].amount; - auto iter = zone->VeteranRewards.begin(); - for (;iter != zone->VeteranRewards.end(); ++iter) - if((*iter).claim_id == rewards[i].id) - break; - - if(iter != zone->VeteranRewards.end()) { - InternalVeteranReward ivro = (*iter); - ivr->claim_count = ivro.claim_count; - for(int x = 0; x < ivro.claim_count; ++x) { - ivr->items[x].item_id = ivro.items[x].item_id; - ivr->items[x].charges = ivro.items[x].charges; - strcpy(ivr->items[x].item_name, ivro.items[x].item_name); - } - } - - data += sizeof(InternalVeteranReward); - } - - FastQueuePacket(&vetapp); -} + if (ignore_default) { + value = lua_ret; + } +#endif -bool Client::TryReward(uint32 claim_id) -{ - // Make sure we have an open spot - // Make sure we have it in our acct and count > 0 - // Make sure the entry was found - // If we meet all the criteria: - // Decrement our count by 1 if it > 1 delete if it == 1 - // Create our item in bag if necessary at the free inv slot - // save - uint32 free_slot = 0xFFFFFFFF; - - for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; ++i) { - EQ::ItemInstance *item = GetInv().GetItem(i); - if (!item) { - free_slot = i; - break; - } - } - - if (free_slot == 0xFFFFFFFF) - return false; - - std::string query = StringFormat("SELECT amount FROM account_rewards " - "WHERE account_id = %i AND reward_id = %i", - AccountID(), claim_id); - auto results = database.QueryDatabase(query); - if (!results.Success()) - return false; - - if (results.RowCount() == 0) - return false; - - auto row = results.begin(); - - uint32 amt = Strings::ToInt(row[0]); - if (amt == 0) - return false; - - auto iter = std::find_if(zone->VeteranRewards.begin(), zone->VeteranRewards.end(), - [claim_id](const InternalVeteranReward &a) { return a.claim_id == claim_id; }); - - if (iter == zone->VeteranRewards.end()) - return false; - - if (amt == 1) { - query = StringFormat("DELETE FROM account_rewards " - "WHERE account_id = %i AND reward_id = %i", - AccountID(), claim_id); - auto results = database.QueryDatabase(query); - } else { - query = StringFormat("UPDATE account_rewards SET amount = (amount-1) " - "WHERE account_id = %i AND reward_id = %i", - AccountID(), claim_id); - auto results = database.QueryDatabase(query); - } - - auto &ivr = (*iter); - EQ::ItemInstance *claim = database.CreateItem(ivr.items[0].item_id, ivr.items[0].charges); - if (!claim) { - Save(); - return true; - } - - bool lore_conflict = CheckLoreConflict(claim->GetItem()); - - for (int y = 1; y < 8; y++) - if (ivr.items[y].item_id && claim->GetItem()->ItemClass == 1) { - EQ::ItemInstance *item_temp = database.CreateItem(ivr.items[y].item_id, ivr.items[y].charges); - if (item_temp) { - if (CheckLoreConflict(item_temp->GetItem())) { - lore_conflict = true; - DuplicateLoreMessage(ivr.items[y].item_id); - } - claim->PutItem(y - 1, *item_temp); - safe_delete(item_temp); - } - } - - if (lore_conflict) { - safe_delete(claim); - return true; - } - - PutItemInInventory(free_slot, *claim); - SendItemPacket(free_slot, claim, ItemPacketTrade); - safe_delete(claim); - - Save(); - return true; -} + UpdatePersonalFaction(char_id, value, faction_id, ¤t_value, temp, this_faction_min, this_faction_max); -uint32 Client::GetLDoNPointsTheme(uint32 t) -{ - switch(t) - { - case LDoNTheme::GUK: - return m_pp.ldon_points_guk; - case LDoNTheme::MIR: - return m_pp.ldon_points_mir; - case LDoNTheme::MMC: - return m_pp.ldon_points_mmc; - case LDoNTheme::RUJ: - return m_pp.ldon_points_ruj; - case LDoNTheme::TAK: - return m_pp.ldon_points_tak; - default: - return 0; - } -} + //Message(Chat::Lime, "Min(%d) Max(%d) Before(%d), After(%d)\n", this_faction_min, this_faction_max, faction_before_hit, current_value); -uint32 Client::GetLDoNWinsTheme(uint32 t) -{ - switch(t) - { - case LDoNTheme::GUK: - return m_pp.ldon_wins_guk; - case LDoNTheme::MIR: - return m_pp.ldon_wins_mir; - case LDoNTheme::MMC: - return m_pp.ldon_wins_mmc; - case LDoNTheme::RUJ: - return m_pp.ldon_wins_ruj; - case LDoNTheme::TAK: - return m_pp.ldon_wins_tak; - default: - return 0; - } -} + SendFactionMessage(value, faction_id, faction_before_hit, current_value, temp, this_faction_min, this_faction_max); + } -uint32 Client::GetLDoNLossesTheme(uint32 t) -{ - switch(t) - { - case LDoNTheme::GUK: - return m_pp.ldon_losses_guk; - case LDoNTheme::MIR: - return m_pp.ldon_losses_mir; - case LDoNTheme::MMC: - return m_pp.ldon_losses_mmc; - case LDoNTheme::RUJ: - return m_pp.ldon_losses_ruj; - case LDoNTheme::TAK: - return m_pp.ldon_losses_tak; - default: - return 0; - } + return; } -void Client::UpdateLDoNWinLoss(uint32 theme_id, bool win, bool remove) { - switch (theme_id) { - case LDoNTheme::GUK: - if (win) { - m_pp.ldon_wins_guk += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_guk += (remove ? -1 : 1); - } - break; - case LDoNTheme::MIR: - if (win) { - m_pp.ldon_wins_mir += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_mir += (remove ? -1 : 1); - } - break; - case LDoNTheme::MMC: - if (win) { - m_pp.ldon_wins_mmc += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_mmc += (remove ? -1 : 1); - } - break; - case LDoNTheme::RUJ: - if (win) { - m_pp.ldon_wins_ruj += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_ruj += (remove ? -1 : 1); - } - break; - case LDoNTheme::TAK: - if (win) { - m_pp.ldon_wins_tak += (remove ? -1 : 1); - } else { - m_pp.ldon_losses_tak += (remove ? -1 : 1); - } - break; - default: - return; - } - database.UpdateAdventureStatsEntry(CharacterID(), theme_id, win, remove); +int32 Client::GetCharacterFactionLevel(int32 faction_id) +{ + if (faction_id <= 0) + return 0; + faction_map::iterator res; + res = factionvalues.find(faction_id); + if (res == factionvalues.end()) + return 0; + return res->second; } +// Common code to set faction level. +// Applies HeroicCHA is it applies +// Checks for bottom out and max faction and old faction db entries +// Updates the faction if we are not minned, maxed or we need to repair -void Client::SuspendMinion(int value) +void Client::UpdatePersonalFaction(int32 char_id, int32 npc_value, int32 faction_id, int32 *current_value, int32 temp, int32 this_faction_min, int32 this_faction_max) { - /* - SPA 151 Allows an extra pet to be saved and resummoned later. - Casting with a pet but without a suspended pet will suspend the pet - Casting without a pet and with a suspended pet will unsuspend the pet - effect value 0 = save pet with no buffs or equipment - effect value 1 = save pet with buffs and equipment - effect value 2 = unknown - Note: SPA 308 allows for suspended pets to be resummoned after zoning. - */ - - NPC *CurrentPet = GetPet()->CastToNPC(); - - if(!CurrentPet) - { - if(m_suspendedminion.SpellID > 0) - { - if (m_suspendedminion.SpellID >= SPDAT_RECORDS) { - Message(Chat::Red, "Invalid suspended minion spell id (%u).", m_suspendedminion.SpellID); - memset(&m_suspendedminion, 0, sizeof(PetInfo)); - return; - } - - MakePoweredPet(m_suspendedminion.SpellID, spells[m_suspendedminion.SpellID].teleport_zone, - m_suspendedminion.petpower, m_suspendedminion.Name, m_suspendedminion.size); - - CurrentPet = GetPet()->CastToNPC(); - - if(!CurrentPet) - { - Message(Chat::Red, "Failed to recall suspended minion."); - return; - } - - if(value >= 1) - { - CurrentPet->SetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items); - - CurrentPet->SendPetBuffsToClient(); - } - CurrentPet->CalcBonuses(); - - CurrentPet->SetHP(m_suspendedminion.HP); - - CurrentPet->SetMana(m_suspendedminion.Mana); - - CurrentPet->SetTaunting(m_suspendedminion.taunting); - - MessageString(Chat::Magenta, SUSPEND_MINION_UNSUSPEND, CurrentPet->GetCleanName()); - - memset(&m_suspendedminion, 0, sizeof(struct PetInfo)); - // TODO: These pet command states need to be synced ... - // Will just fix them for now - if (m_ClientVersionBit & EQ::versions::maskUFAndLater) { - SetPetCommandState(PET_BUTTON_SIT, 0); - SetPetCommandState(PET_BUTTON_STOP, 0); - SetPetCommandState(PET_BUTTON_REGROUP, 0); - SetPetCommandState(PET_BUTTON_FOLLOW, 1); - SetPetCommandState(PET_BUTTON_GUARD, 0); - // Taunt saved on client side for logging on with pet - // In our db for when we zone. - SetPetCommandState(PET_BUTTON_HOLD, 0); - SetPetCommandState(PET_BUTTON_GHOLD, 0); - SetPetCommandState(PET_BUTTON_FOCUS, 0); - SetPetCommandState(PET_BUTTON_SPELLHOLD, 0); - } - } - else - return; - - } - else - { - uint16 SpellID = CurrentPet->GetPetSpellID(); - - if(SpellID) - { - if(m_suspendedminion.SpellID > 0) - { - MessageString(Chat::Red,ONLY_ONE_PET); - - return; - } - else if(CurrentPet->IsEngaged()) - { - MessageString(Chat::Red,SUSPEND_MINION_FIGHTING); - - return; - } - else if(entity_list.Fighting(CurrentPet)) - { - MessageString(Chat::Blue,SUSPEND_MINION_HAS_AGGRO); - } - else - { - m_suspendedminion.SpellID = SpellID; - - m_suspendedminion.HP = CurrentPet->GetHP();; - - m_suspendedminion.Mana = CurrentPet->GetMana(); - m_suspendedminion.petpower = CurrentPet->GetPetPower(); - m_suspendedminion.size = CurrentPet->GetSize(); - - if(value >= 1) - CurrentPet->GetPetState(m_suspendedminion.Buffs, m_suspendedminion.Items, m_suspendedminion.Name); - else - strn0cpy(m_suspendedminion.Name, CurrentPet->GetName(), 64); // Name stays even at rank 1 - - MessageString(Chat::Magenta, SUSPEND_MINION_SUSPEND, CurrentPet->GetCleanName()); - - CurrentPet->Depop(false); - - SetPetID(0); - } - } - else - { - MessageString(Chat::Red, ONLY_SUMMONED_PETS); - - return; - } - } -} + bool repair = false; + bool change = false; + + if (itembonuses.HeroicCHA) + { + int faction_mod = itembonuses.HeroicCHA / 5; + // If our result isn't truncated, then just do that + if (npc_value * faction_mod / 100 != 0) + npc_value += npc_value * faction_mod / 100; + // If our result is truncated, then double a mob's value every once and a while to equal what they would have got + else + { + if (zone->random.Int(0, 100) < faction_mod) + npc_value *= 2; + } + } -void Client::AddPVPPoints(uint32 Points) -{ - m_pp.PVPCurrentPoints += Points; - m_pp.PVPCareerPoints += Points; + // Set flag when to update db + // Repair needed, as db changes could modify a base value for a faction + // and we need to auto correct when that happens. + if (*current_value > this_faction_max) + { + *current_value = this_faction_max; + repair = true; + } + else if (*current_value < this_faction_min) + { + *current_value = this_faction_min; + repair = true; + } + else if ((m_pp.gm != 1) && (npc_value != 0) && + ((npc_value > 0 && *current_value != this_faction_max) || + ((npc_value < 0 && *current_value != this_faction_min)))) + change = true; - Save(); + if (change || repair) + { + *current_value += npc_value; - SendPVPStats(); -} + if (*current_value > this_faction_max) + *current_value = this_faction_max; + else if (*current_value < this_faction_min) + *current_value = this_faction_min; -void Client::AddEbonCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentEbonCrystals += amount; - m_pp.careerEbonCrystals += amount; - - SaveCurrency(); - SendCrystalCounts(); - - MessageString( - Chat::Yellow, - YOU_RECEIVE, - fmt::format( - "{} {}", - amount, - database.CreateItemLink(RuleI(Zone, EbonCrystalItemID)) - ).c_str() - ); - - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { - const std::string &export_string = fmt::format( - "{} 0 {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); - } -} + database.SetCharacterFactionLevel(char_id, faction_id, *current_value, temp, factionvalues); + } -void Client::AddRadiantCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentRadCrystals += amount; - m_pp.careerRadCrystals += amount; - - SaveCurrency(); - SendCrystalCounts(); - - MessageString( - Chat::Yellow, - YOU_RECEIVE, - fmt::format( - "{} {}", - amount, - database.CreateItemLink(RuleI(Zone, RadiantCrystalItemID)) - ).c_str() - ); - - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_GAIN)) { - const std::string &export_string = fmt::format( - "0 {} {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_GAIN, this, export_string, 0); - } +return; } -void Client::RemoveEbonCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentEbonCrystals -= amount; +// returns the character's faction level, adjusted for racial, class, and deity modifiers +int32 Client::GetModCharacterFactionLevel(int32 faction_id) { + int32 Modded = GetCharacterFactionLevel(faction_id); + FactionMods fm; + if (content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), faction_id)) + { + Modded += fm.base + fm.class_mod + fm.race_mod + fm.deity_mod; + + //Tack on any bonuses from Alliance type spell effects + Modded += GetFactionBonus(faction_id); + Modded += GetItemFactionBonus(faction_id); + } - SaveCurrency(); - SendCrystalCounts(); + return Modded; +} - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { - const std::string &export_string = fmt::format( - "{} 0 {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); - } +void Client::MerchantRejectMessage(Mob *merchant, int primaryfaction) +{ + int messageid = 0; + int32 tmpFactionValue = 0; + int32 lowestvalue = 0; + FactionMods fmod; + + // If a faction is involved, get the data. + if (primaryfaction > 0) { + if (content_db.GetFactionData(&fmod, GetClass(), GetFactionRace(), GetDeity(), primaryfaction)) { + tmpFactionValue = GetCharacterFactionLevel(primaryfaction); + lowestvalue = std::min(std::min(tmpFactionValue, fmod.deity_mod), + std::min(fmod.class_mod, fmod.race_mod)); + } + } + // If no primary faction or biggest influence is your faction hit + if (primaryfaction <= 0 || lowestvalue == tmpFactionValue) { + merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS6)); + } else if (lowestvalue == fmod.race_mod) { // race biggest + // Non-standard race (ex. illusioned to wolf) + if (GetRace() > PLAYER_RACE_COUNT) { + messageid = zone->random.Int(1, 3); // these aren't sequential StringIDs :( + switch (messageid) { + case 1: + messageid = WONT_SELL_NONSTDRACE1; + break; + case 2: + messageid = WONT_SELL_NONSTDRACE2; + break; + case 3: + messageid = WONT_SELL_NONSTDRACE3; + break; + default: // w/e should never happen + messageid = WONT_SELL_NONSTDRACE1; + break; + } + merchant->SayString(messageid); + } else { // normal player races + messageid = zone->random.Int(1, 4); + switch (messageid) { + case 1: + messageid = WONT_SELL_RACE1; + break; + case 2: + messageid = WONT_SELL_RACE2; + break; + case 3: + messageid = WONT_SELL_RACE3; + break; + case 4: + messageid = WONT_SELL_RACE4; + break; + default: // w/e should never happen + messageid = WONT_SELL_RACE1; + break; + } + merchant->SayString(messageid, itoa(GetRace())); + } + } else if (lowestvalue == fmod.class_mod) { + merchant->SayString(zone->random.Int(WONT_SELL_CLASS1, WONT_SELL_CLASS5), itoa(GetClass())); + } else { + // Must be deity - these two sound the best for that. + // Can't use a message with a field, GUI wants class/race names. + // for those message IDs. These are straight text. + merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS2)); + } + return; } -void Client::RemoveRadiantCrystals(uint32 amount, bool is_reclaim) { - m_pp.currentRadCrystals -= amount; +//o-------------------------------------------------------------- +//| Name: SendFactionMessage +//o-------------------------------------------------------------- +//| Purpose: Send faction change message to client +//o-------------------------------------------------------------- +void Client::SendFactionMessage(int32 tmpvalue, int32 faction_id, int32 faction_before_hit, int32 totalvalue, uint8 temp, int32 this_faction_min, int32 this_faction_max) +{ + char name[50]; + int32 faction_value; + + // If we're dropping from MAX or raising from MIN or repairing, + // we should base the message on the new updated value so we don't show + // a min MAX message + // + // If we're changing any other place, we use the value before the + // hit. For example, if we go from 1199 to 1200 which is the MAX + // we still want to say faction got better this time around. + + if (!EQ::ValueWithin(faction_before_hit, this_faction_min, this_faction_max)) { + faction_value = totalvalue; + } else { + faction_value = faction_before_hit; + } - SaveCurrency(); - SendCrystalCounts(); + // default to Faction# if we couldn't get the name from the ID + if (!content_db.GetFactionName(faction_id, name, sizeof(name))) { + snprintf(name, sizeof(name), "Faction%i", faction_id); + } - if (parse->PlayerHasQuestSub(EVENT_CRYSTAL_LOSS)) { - const std::string &export_string = fmt::format( - "0 {} {}", - amount, - is_reclaim ? 1 : 0 - ); - parse->EventPlayer(EVENT_CRYSTAL_LOSS, this, export_string, 0); - } + if (tmpvalue == 0 || temp == 1 || temp == 2) { + return; + } else if (faction_value >= this_faction_max) { + MessageString(Chat::Yellow, FACTION_BEST, name); + } else if (faction_value <= this_faction_min) { + MessageString(Chat::Yellow, FACTION_WORST, name); + } else if (tmpvalue > 0 && !RuleB(Client, UseLiveFactionMessage)) { + MessageString(Chat::Yellow, FACTION_BETTER, name); + } else if (tmpvalue < 0 && !RuleB(Client, UseLiveFactionMessage)) { + MessageString(Chat::Yellow, FACTION_WORSE, name); + } else if (RuleB(Client, UseLiveFactionMessage)) { + Message( + Chat::Yellow, + fmt::format( + "Your faction standing with {} has been adjusted by {}.", + name, + tmpvalue + ).c_str() + ); + } //New Live faction message (14261) } -void Client::SetEbonCrystals(uint32 value) { - m_pp.currentEbonCrystals = value; - SaveCurrency(); - SendCrystalCounts(); -} +void Client::LoadAccountFlags() +{ + accountflags.clear(); -void Client::SetRadiantCrystals(uint32 value) { - m_pp.currentRadCrystals = value; - SaveCurrency(); - SendCrystalCounts(); -} + const auto& l = AccountFlagsRepository::GetWhere(database, fmt::format("p_accid = {}", account_id)); + if (l.empty()) { + return; + } -// Processes a client request to inspect a SoF+ client's equipment. -void Client::ProcessInspectRequest(Client *requestee, Client *requester) -{ - if (requestee && requester) { - auto outapp = new EQApplicationPacket(OP_InspectAnswer, sizeof(InspectResponse_Struct)); - auto insr = (InspectResponse_Struct *) outapp->pBuffer; - - insr->TargetID = requester->GetID(); - insr->playerid = requestee->GetID(); - - const EQ::ItemData *item = nullptr; - const EQ::ItemInstance *inst = nullptr; - - for (int16 L = EQ::invslot::EQUIPMENT_BEGIN; L <= EQ::invslot::EQUIPMENT_END; L++) { - inst = requestee->GetInv().GetItem(L); - - if (inst) { - item = inst->GetItem(); - if (item) { - strcpy(insr->itemnames[L], item->Name); - - const EQ::ItemData *augment_item = nullptr; - const auto augment = inst->GetOrnamentationAugment(); - - if (augment) { - augment_item = augment->GetItem(); - } - - if (augment_item) { - insr->itemicons[L] = augment_item->Icon; - } else if (inst->GetOrnamentationIcon()) { - insr->itemicons[L] = inst->GetOrnamentationIcon(); - } else { - insr->itemicons[L] = item->Icon; - } - } else { - insr->itemnames[L][0] = '\0'; - insr->itemicons[L] = 0xFFFFFFFF; - } - } else { - insr->itemnames[L][0] = '\0'; - insr->itemicons[L] = 0xFFFFFFFF; - } - } - - strcpy(insr->text, requestee->GetInspectMessage().text); - - // There could be an OP for this..or not... (Ti clients are not processed here..this message is generated client-side) - if (requestee->IsClient() && requestee != requester) { - requestee->Message( - Chat::White, - fmt::format( - "{} is looking at your equipment...", - requester->GetName() - ).c_str() - ); - } - - requester->QueuePacket(outapp); // Send answer to requester - safe_delete(outapp); - } + for (const auto& e : l) { + accountflags[e.p_flag] = e.p_value; + } } -void Client::GuildBankAck() +void Client::ClearAccountFlag(const std::string& flag) { - auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankAck_Struct)); + auto e = AccountFlagsRepository::NewEntity(); - GuildBankAck_Struct *gbas = (GuildBankAck_Struct*) outapp->pBuffer; + e.p_accid = account_id; + e.p_flag = flag; - gbas->Action = GuildBankAcknowledge; - - FastQueuePacket(&outapp); + AccountFlagsRepository::ClearFlag(database, e); } -void Client::GuildBankDepositAck(bool Fail, int8 action) +void Client::SetAccountFlag(const std::string& flag, const std::string& value) { + auto e = AccountFlagsRepository::NewEntity(); - auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankDepositAck_Struct)); - - GuildBankDepositAck_Struct *gbdas = (GuildBankDepositAck_Struct*) outapp->pBuffer; + e.p_accid = account_id; + e.p_flag = flag; + e.p_value = value; - gbdas->Action = action; + AccountFlagsRepository::ReplaceFlag(database, e); - gbdas->Fail = Fail ? 1 : 0; + accountflags[flag] = value; +} - FastQueuePacket(&outapp); +std::string Client::GetAccountFlag(const std::string& flag) +{ + return accountflags[flag]; } -void Client::ClearGuildBank() +std::vector Client::GetAccountFlags() { - auto outapp = new EQApplicationPacket(OP_GuildBank, sizeof(GuildBankClear_Struct)); + std::vector l; - GuildBankClear_Struct *gbcs = (GuildBankClear_Struct*) outapp->pBuffer; + l.reserve(accountflags.size()); - gbcs->Action = GuildBankBulkItems; - gbcs->DepositAreaCount = 0; - gbcs->MainAreaCount = 0; + for (const auto& e : accountflags) { + l.emplace_back(e.first); + } - FastQueuePacket(&outapp); + return l; } -void Client::SendGroupCreatePacket() +void Client::ItemTimerCheck() { - // For SoD and later clients, this is sent the Group Leader upon initial creation of the group - // - auto outapp = new EQApplicationPacket(OP_GroupUpdateB, 32 + strlen(GetName())); - - char *Buffer = (char *)outapp->pBuffer; - // Header - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // group ID probably - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 1); // count of members in packet - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // Null Leader name, shouldn't be null besides this case - - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // Member 0, index - VARSTRUCT_ENCODE_STRING(Buffer, GetName()); // group member name - VARSTRUCT_ENCODE_TYPE(uint16, Buffer, 0); // merc flag - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // owner name (if merc) - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, GetLevel()); // level - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group tank flag - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group assist flag - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // group puller flag - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // offline flag - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // timestamp - - FastQueuePacket(&outapp); + int i; + for (i = EQ::invslot::POSSESSIONS_BEGIN; i <= EQ::invslot::POSSESSIONS_END; i++) + { + TryItemTimer(i); + } + for (i = EQ::invbag::GENERAL_BAGS_BEGIN; i <= EQ::invbag::CURSOR_BAG_END; i++) + { + TryItemTimer(i); + } } -void Client::SendGroupLeaderChangePacket(const char *LeaderName) +void Client::TryItemTimer(int slot) { - // For SoD and later, send name of Group Leader to this client + EQ::ItemInstance* inst = m_inv.GetItem(slot); + if(!inst) { + return; + } - auto outapp = new EQApplicationPacket(OP_GroupLeaderChange, sizeof(GroupLeaderChange_Struct)); + auto item_timers = inst->GetTimers(); + auto it_iter = item_timers.begin(); + while(it_iter != item_timers.end()) { + if(it_iter->second.Check()) { + if (parse->ItemHasQuestSub(inst, EVENT_TIMER)) { + parse->EventItem(EVENT_TIMER, this, inst, nullptr, it_iter->first, 0); + } + } + ++it_iter; + } - GroupLeaderChange_Struct *glcs = (GroupLeaderChange_Struct*)outapp->pBuffer; + if (slot > EQ::invslot::EQUIPMENT_END) { + return; + } - strn0cpy(glcs->LeaderName, LeaderName, sizeof(glcs->LeaderName)); + for (int x = EQ::invaug::SOCKET_BEGIN; x <= EQ::invaug::SOCKET_END; ++x) + { + EQ::ItemInstance * a_inst = inst->GetAugment(x); + if(!a_inst) { + continue; + } - FastQueuePacket(&outapp); + auto& item_timers = a_inst->GetTimers(); + auto it_iter = item_timers.begin(); + while(it_iter != item_timers.end()) { + if(it_iter->second.Check()) { + if (parse->ItemHasQuestSub(a_inst, EVENT_TIMER)) { + parse->EventItem(EVENT_TIMER, this, a_inst, nullptr, it_iter->first, 0); + } + } + ++it_iter; + } + } } -void Client::SendGroupJoinAcknowledge() -{ - // For SoD and later, This produces the 'You have joined the group' message. - auto outapp = new EQApplicationPacket(OP_GroupAcknowledge, 4); - FastQueuePacket(&outapp); +void Client::SendItemScale(EQ::ItemInstance *inst) { + int slot = m_inv.GetSlotByItemInst(inst); + if(slot != -1) { + inst->ScaleItem(); + SendItemPacket(slot, inst, ItemPacketCharmUpdate); + CalcBonuses(); + } } -void Client::SendAdventureError(const char *error) +void Client::AddRespawnOption(std::string option_name, uint32 zoneid, uint16 instance_id, float x, float y, float z, float heading, bool initial_selection, int8 position) { - size_t error_size = strlen(error); - auto outapp = new EQApplicationPacket(OP_AdventureInfo, (error_size + 2)); - strn0cpy((char*)outapp->pBuffer, error, error_size); - FastQueuePacket(&outapp); + //If respawn window is already open, any changes would create an inconsistency with the client + if (IsHoveringForRespawn()) { return; } + + if (zoneid == 0) + zoneid = zone->GetZoneID(); + + //Create respawn option + RespawnOption res_opt; + res_opt.name = option_name; + res_opt.zone_id = zoneid; + res_opt.instance_id = instance_id; + res_opt.x = x; + res_opt.y = y; + res_opt.z = z; + res_opt.heading = heading; + + if (position == -1 || position >= respawn_options.size()) + { + //No position specified, or specified beyond the end, simply append + respawn_options.push_back(res_opt); + //Make this option the initial selection for the window if desired + if (initial_selection) + initial_respawn_selection = static_cast(respawn_options.size()) - 1; + } + else if (position == 0) + { + respawn_options.push_front(res_opt); + if (initial_selection) + initial_respawn_selection = 0; + } + else + { + //Insert new option between existing options + std::list::iterator itr; + uint8 pos = 0; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + if (pos++ == position) + { + respawn_options.insert(itr,res_opt); + //Make this option the initial selection for the window if desired + if (initial_selection) + initial_respawn_selection = pos; + return; + } + } + } } -void Client::SendAdventureDetails() +bool Client::RemoveRespawnOption(std::string option_name) { - if(adv_data) - { - ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; - auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); - AdventureRequestResponse_Struct *arr = (AdventureRequestResponse_Struct*)outapp->pBuffer; - arr->unknown000 = 0xBFC40100; - arr->unknown2080 = 0x0A; - arr->risk = ad->risk; - strcpy(arr->text, ad->text); - - if(ad->time_to_enter != 0) - { - arr->timetoenter = ad->time_to_enter; - } - else - { - arr->timeleft = ad->time_left; - } - - if(ad->zone_in_id == zone->GetZoneID()) - { - arr->y = ad->x; - arr->x = ad->y; - arr->showcompass = 1; - } - FastQueuePacket(&outapp); - - SendAdventureCount(ad->count, ad->total); - } - else - { - ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; - auto outapp = new EQApplicationPacket(OP_AdventureData, sizeof(AdventureRequestResponse_Struct)); - FastQueuePacket(&outapp); - } + //If respawn window is already open, any changes would create an inconsistency with the client + if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } + + bool had = false; + RespawnOption* opt = nullptr; + std::list::iterator itr; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + opt = &(*itr); + if (opt->name.compare(option_name) == 0) + { + itr = respawn_options.erase(itr); + had = true; + //could be more with the same name, so keep going... + } + } + return had; } -void Client::SendAdventureCount(uint32 count, uint32 total) +bool Client::RemoveRespawnOption(uint8 position) { - auto outapp = new EQApplicationPacket(OP_AdventureUpdate, sizeof(AdventureCountUpdate_Struct)); - AdventureCountUpdate_Struct *acu = (AdventureCountUpdate_Struct*)outapp->pBuffer; - acu->current = count; - acu->total = total; - FastQueuePacket(&outapp); -} + //If respawn window is already open, any changes would create an inconsistency with the client + if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } -void Client::NewAdventure(int id, int theme, const char *text, int member_count, const char *members) -{ - size_t text_size = strlen(text); - auto outapp = new EQApplicationPacket(OP_AdventureDetails, text_size + 2); - strn0cpy((char*)outapp->pBuffer, text, text_size); - FastQueuePacket(&outapp); + //Easy cases first... + if (position == 0) + { + respawn_options.pop_front(); + return true; + } + else if (position == (respawn_options.size() - 1)) + { + respawn_options.pop_back(); + return true; + } - adv_requested_id = id; - adv_requested_theme = theme; - safe_delete_array(adv_requested_data); - adv_requested_member_count = member_count; - adv_requested_data = new char[64 * member_count]; - memcpy(adv_requested_data, members, (64 * member_count)); + std::list::iterator itr; + uint8 pos = 0; + for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) + { + if (pos++ == position) + { + respawn_options.erase(itr); + return true; + } + } + return false; } -void Client::ClearPendingAdventureData() +void Client::SetHunger(int32 in_hunger) { - adv_requested_id = 0; - adv_requested_theme = LDoNTheme::Unused; - safe_delete_array(adv_requested_data); - adv_requested_member_count = 0; -} + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); + Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; + sta->food = in_hunger; + sta->water = m_pp.thirst_level > 6000 ? 6000 : m_pp.thirst_level; -bool Client::IsOnAdventure() -{ - if(adv_data) - { - ServerSendAdventureData_Struct *ad = (ServerSendAdventureData_Struct*)adv_data; - if(ad->zone_in_id == 0) - { - return false; - } - else - { - return true; - } - } - return false; -} + m_pp.hunger_level = in_hunger; -void Client::LeaveAdventure() -{ - if(!GetPendingAdventureLeave()) - { - PendingAdventureLeave(); - auto pack = new ServerPacket(ServerOP_AdventureLeave, 64); - strcpy((char*)pack->pBuffer, GetName()); - worldserver.SendPacket(pack); - delete pack; - } + QueuePacket(outapp); + safe_delete(outapp); } -void Client::ClearCurrentAdventure() +void Client::SetThirst(int32 in_thirst) { - if(adv_data) - { - ServerSendAdventureData_Struct* ds = (ServerSendAdventureData_Struct*)adv_data; - if(ds->finished_adventures > 0) - { - ds->instance_id = 0; - ds->risk = 0; - memset(ds->text, 0, 512); - ds->time_left = 0; - ds->time_to_enter = 0; - ds->x = 0; - ds->y = 0; - ds->zone_in_id = 0; - ds->zone_in_object = 0; - } - else - { - safe_delete(adv_data); - } - - SendAdventureError("You are not currently assigned to an adventure."); - } -} + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); + Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; + sta->food = m_pp.hunger_level > 6000 ? 6000 : m_pp.hunger_level; + sta->water = in_thirst; -void Client::AdventureFinish(bool win, int theme, int points) -{ - UpdateLDoNPoints(theme, points); - auto outapp = new EQApplicationPacket(OP_AdventureFinish, sizeof(AdventureFinish_Struct)); - AdventureFinish_Struct *af = (AdventureFinish_Struct*)outapp->pBuffer; - af->win_lose = win ? 1 : 0; - af->points = points; - FastQueuePacket(&outapp); + m_pp.thirst_level = in_thirst; + + QueuePacket(outapp); + safe_delete(outapp); } -void Client::CheckLDoNHail(NPC* n) +void Client::SetIntoxication(int32 in_intoxication) { - if (!zone->adv_data || !n || n->GetOwnerID()) { - return; - } - - auto* ds = (ServerZoneAdventureDataReply_Struct*) zone->adv_data; - if (ds->type != Adventure_Rescue || ds->data_id != n->GetNPCTypeID()) { - return; - } - - if (entity_list.CheckNPCsClose(n)) { - n->Say( - "You're here to save me? I couldn't possibly risk leaving yet. There are " - "far too many of those horrid things out there waiting to recapture me! Please get " - "rid of some more of those vermin and then we can try to leave." - ); - return; - } - - auto pet = GetPet(); - if (pet) { - if (pet->GetPetType() == petCharmed) { - pet->BuffFadeByEffect(SE_Charm); - } else if (pet->GetPetType() == petNPCFollow) { - pet->SetOwnerID(0); - } else { - pet->Depop(); - } - } - - SetPet(n); - n->SetOwnerID(GetID()); - n->Say( - "Wonderful! Someone to set me free! I feared for my life for so long, " - "never knowing when they might choose to end my life. Now that you're here though " - "I can rest easy. Please help me find my way out of here as soon as you can " - "I'll stay close behind you!" - ); + m_pp.intoxication = EQ::Clamp(in_intoxication, 0, 200); } -void Client::CheckEmoteHail(NPC* n, const char* message) +void Client::SetConsumption(int32 in_hunger, int32 in_thirst) { - if (!Strings::BeginsWith(Strings::ToLower(message), "hail")) { - return; - } + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); + Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; + sta->food = in_hunger; + sta->water = in_thirst; - if (!n || n->GetOwnerID()) { - return; - } + m_pp.hunger_level = in_hunger; + m_pp.thirst_level = in_thirst; - const uint32 emote_id = n->GetEmoteID(); - if (emote_id) { - n->DoNPCEmote(EQ::constants::EmoteEventTypes::Hailed, emote_id, this); - } + QueuePacket(outapp); + safe_delete(outapp); } -void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count) +void Client::Consume(const EQ::ItemData *item, uint8 type, int16 slot, bool auto_consume) { - m_has_quest_compass = (count != 0); - m_quest_compass.x = in_x; - m_quest_compass.y = in_y; - m_quest_compass.z = in_z; + if (!item) + return; - SendDzCompassUpdate(); -} + int increase = item->CastTime_ * 100; + if (!auto_consume) // force feeding is half as effective + increase /= 2; -void Client::SendZonePoints() -{ - int count = 0; - LinkedListIterator iterator(zone->zone_point_list); - iterator.Reset(); - while (iterator.MoreElements()) { - ZonePoint *data = iterator.GetData(); - - if (ClientVersionBit() & data->client_version_mask) { - count++; - } - - iterator.Advance(); - } - - uint32 zpsize = sizeof(ZonePoints) + ((count + 1) * sizeof(ZonePoint_Entry)); - auto outapp = new EQApplicationPacket(OP_SendZonepoints, zpsize); - ZonePoints* zp = (ZonePoints*)outapp->pBuffer; - zp->count = count; - - int i = 0; - iterator.Reset(); - while(iterator.MoreElements()) - { - ZonePoint* data = iterator.GetData(); - - LogZonePoints( - "Sending zone point to client [{}] mask [{}] x [{}] y [{}] z [{}] number [{}]", - GetCleanName(), - ClientVersionBit() & data->client_version_mask ? "true" : "false", - data->x, - data->y, - data->z, - data->number - ); - - if(ClientVersionBit() & data->client_version_mask) - { - zp->zpe[i].iterator = data->number; - zp->zpe[i].x = data->target_x; - zp->zpe[i].y = data->target_y; - zp->zpe[i].z = data->target_z; - zp->zpe[i].heading = data->target_heading; - zp->zpe[i].zoneid = data->target_zone_id; - - // if the target zone is the same as the current zone, use the instance of the current zone - // if we don't use the same instance_id that the client was sent, the client will forcefully - // issue a zone change request when they should be simply moving to a different point in the same zone - // because the client will think the zone point target is different from the current instance - auto target_instance = data->target_zone_instance; - if (data->target_zone_id == zone->GetZoneID() && data->target_zone_instance == 0) { - target_instance = zone->GetInstanceID(); - } - - zp->zpe[i].zoneinstance = target_instance; - i++; - } - iterator.Advance(); - } - - FastQueuePacket(&outapp); -} + if (increase < 0) // wasn't food? oh well + return; -void Client::SendTargetCommand(uint32 EntityID) -{ - auto outapp = new EQApplicationPacket(OP_TargetCommand, sizeof(ClientTarget_Struct)); - ClientTarget_Struct *cts = (ClientTarget_Struct*)outapp->pBuffer; - cts->new_target = EntityID; - FastQueuePacket(&outapp); -} + if (type == EQ::item::ItemTypeFood) { + m_pp.hunger_level += increase; -void Client::LocateCorpse() -{ - Corpse *ClosestCorpse = nullptr; - if(!GetTarget()) - ClosestCorpse = entity_list.GetClosestCorpse(this, nullptr); - else if(GetTarget()->IsCorpse()) - ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->CastToCorpse()->GetOwnerName()); - else - ClosestCorpse = entity_list.GetClosestCorpse(this, GetTarget()->GetCleanName()); - - if(ClosestCorpse) - { - MessageString(Chat::Spells, SENSE_CORPSE_DIRECTION); - SetHeading(CalculateHeadingToTarget(ClosestCorpse->GetX(), ClosestCorpse->GetY())); - SetTarget(ClosestCorpse); - SendTargetCommand(ClosestCorpse->GetID()); - SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0, true); - } - else if(!GetTarget()) - MessageString(Chat::Red, SENSE_CORPSE_NONE); - else - MessageString(Chat::Red, SENSE_CORPSE_NOT_NAME); -} + LogFood("Consuming food, points added to hunger_level: [{}] - current_hunger: [{}]", increase, m_pp.hunger_level); -void Client::NPCSpawn(NPC *target_npc, const char *identifier, uint32 extra) -{ - if (!target_npc || !identifier) { - return; - } - - std::string spawn_type = Strings::ToLower(identifier); - bool is_add = spawn_type.find("add") != std::string::npos; - bool is_create = spawn_type.find("create") != std::string::npos; - bool is_delete = spawn_type.find("delete") != std::string::npos; - bool is_remove = spawn_type.find("remove") != std::string::npos; - bool is_update = spawn_type.find("update") != std::string::npos; - bool is_clone = spawn_type.find("clone") != std::string::npos; - if (is_add || is_create) { - // extra sets the Respawn Timer for add/create - content_db.NPCSpawnDB( - is_add ? NPCSpawnTypes::AddNewSpawngroup : NPCSpawnTypes::CreateNewSpawn, - zone->GetShortName(), - zone->GetInstanceVersion(), - this, - target_npc->CastToNPC(), - extra - ); - } else if (is_delete || is_remove || is_update) { - uint8 spawn_update_type = ( - is_delete ? - NPCSpawnTypes::DeleteSpawn : - ( - is_remove ? - NPCSpawnTypes::RemoveSpawn : - NPCSpawnTypes::UpdateAppearance - ) - ); - content_db.NPCSpawnDB( - spawn_update_type, - zone->GetShortName(), - zone->GetInstanceVersion(), - this, - target_npc->CastToNPC(), - extra - ); - } else if (is_clone) { - content_db.NPCSpawnDB( - NPCSpawnTypes::AddSpawnFromSpawngroup, - zone->GetShortName(), - zone->GetInstanceVersion(), - this, - target_npc->CastToNPC(), - extra - ); - } -} + DeleteItemInInventory(slot, 1); -bool Client::IsDraggingCorpse(uint16 CorpseID) -{ - for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { - if (It->second == CorpseID) - return true; - } + if (!auto_consume) // no message if the client consumed for us + entity_list.MessageCloseString(this, true, 50, 0, EATING_MESSAGE, GetName(), item->Name); - return false; -} + LogFood("Eating from slot: [{}]", (int)slot); -void Client::DragCorpses() -{ - for (auto It = DraggedCorpses.begin(); It != DraggedCorpses.end(); ++It) { - Mob *corpse = entity_list.GetMob(It->second); + } else { + m_pp.thirst_level += increase; - if (corpse && corpse->IsPlayerCorpse() && - (DistanceSquaredNoZ(m_Position, corpse->GetPosition()) <= RuleR(Character, DragCorpseDistance))) - continue; + DeleteItemInInventory(slot, 1); - if (!corpse || !corpse->IsPlayerCorpse() || - corpse->CastToCorpse()->IsBeingLooted() || - !corpse->CastToCorpse()->Summon(this, false, false)) { - MessageString(Chat::DefaultText, CORPSEDRAG_STOP); - It = DraggedCorpses.erase(It); - if (It == DraggedCorpses.end()) - break; - } - } -} + LogFood("Consuming drink, points added to thirst_level: [{}] current_thirst: [{}]", increase, m_pp.thirst_level); -void Client::ConsentCorpses(std::string consent_name, bool deny) -{ - if (strcasecmp(consent_name.c_str(), GetName()) == 0) { - MessageString(Chat::Red, CONSENT_YOURSELF); - } - else if (!consent_throttle_timer.Check()) { - MessageString(Chat::Red, CONSENT_WAIT); - } - else { - auto pack = new ServerPacket(ServerOP_Consent, sizeof(ServerOP_Consent_Struct)); - ServerOP_Consent_Struct* scs = (ServerOP_Consent_Struct*)pack->pBuffer; - strn0cpy(scs->grantname, consent_name.c_str(), sizeof(scs->grantname)); - strn0cpy(scs->ownername, GetName(), sizeof(scs->ownername)); - strn0cpy(scs->zonename, "Unknown", sizeof(scs->zonename)); - scs->permission = deny ? 0 : 1; - scs->zone_id = zone->GetZoneID(); - scs->instance_id = zone->GetInstanceID(); - scs->consent_type = EQ::consent::Normal; - scs->consent_id = 0; - if (strcasecmp(scs->grantname, "group") == 0) { - if (!deny) { - Group* grp = GetGroup(); - scs->consent_id = grp ? grp->GetID() : 0; - } - scs->consent_type = EQ::consent::Group; - } - else if (strcasecmp(scs->grantname, "raid") == 0) { - if (!deny) { - Raid* raid = GetRaid(); - scs->consent_id = raid ? raid->GetID() : 0; - } - scs->consent_type = EQ::consent::Raid; - } - else if (strcasecmp(scs->grantname, "guild") == 0) { - if (!deny) { - scs->consent_id = GuildID(); - } - scs->consent_type = EQ::consent::Guild; - // update all corpses in db so buried/unloaded corpses see new consent id - database.UpdateCharacterCorpseConsent(CharacterID(), scs->consent_id); - } - worldserver.SendPacket(pack); - safe_delete(pack); - } + if (!auto_consume) // no message if the client consumed for us + entity_list.MessageCloseString(this, true, 50, 0, DRINKING_MESSAGE, GetName(), item->Name); + + LogFood("Drinking from slot: [{}]", (int)slot); + } } -void Client::Doppelganger(uint16 spell_id, Mob *target, const char *name_override, int pet_count, int pet_duration) +void Client::SendMarqueeMessage(uint32 type, std::string message, uint32 duration) { - if(!target || !IsValidSpell(spell_id) || GetID() == target->GetID()) - return; - - PetRecord record; - if(!database.GetPetEntry(spells[spell_id].teleport_zone, &record)) - { - LogError("Unknown doppelganger spell id: [{}], check pets table", spell_id); - Message(Chat::Red, "Unable to find data for pet %s", spells[spell_id].teleport_zone); - return; - } - - SwarmPet_Struct pet; - pet.count = pet_count; - pet.duration = pet_duration; - pet.npc_id = record.npc_type; - - NPCType *made_npc = nullptr; - - const NPCType *npc_type = content_db.LoadNPCTypesData(pet.npc_id); - if(npc_type == nullptr) { - LogError("Unknown npc type for doppelganger spell id: [{}]", spell_id); - Message(0,"Unable to find pet!"); - return; - } - // make a custom NPC type for this - made_npc = new NPCType; - memcpy(made_npc, npc_type, sizeof(NPCType)); - - strcpy(made_npc->name, name_override); - made_npc->level = GetLevel(); - made_npc->race = GetRace(); - made_npc->gender = GetGender(); - made_npc->size = GetSize(); - made_npc->AC = GetAC(); - made_npc->STR = GetSTR(); - made_npc->STA = GetSTA(); - made_npc->DEX = GetDEX(); - made_npc->AGI = GetAGI(); - made_npc->MR = GetMR(); - made_npc->FR = GetFR(); - made_npc->CR = GetCR(); - made_npc->DR = GetDR(); - made_npc->PR = GetPR(); - made_npc->Corrup = GetCorrup(); - made_npc->PhR = GetPhR(); - // looks - made_npc->texture = GetEquipmentMaterial(EQ::textures::armorChest); - made_npc->helmtexture = GetEquipmentMaterial(EQ::textures::armorHead); - made_npc->haircolor = GetHairColor(); - made_npc->beardcolor = GetBeardColor(); - made_npc->eyecolor1 = GetEyeColor1(); - made_npc->eyecolor2 = GetEyeColor2(); - made_npc->hairstyle = GetHairStyle(); - made_npc->luclinface = GetLuclinFace(); - made_npc->beard = GetBeard(); - made_npc->drakkin_heritage = GetDrakkinHeritage(); - made_npc->drakkin_tattoo = GetDrakkinTattoo(); - made_npc->drakkin_details = GetDrakkinDetails(); - made_npc->d_melee_texture1 = GetEquipmentMaterial(EQ::textures::weaponPrimary); - made_npc->d_melee_texture2 = GetEquipmentMaterial(EQ::textures::weaponSecondary); - for (int i = EQ::textures::textureBegin; i <= EQ::textures::LastTexture; i++) { - made_npc->armor_tint.Slot[i].Color = GetEquipmentColor(i); - } - made_npc->loottable_id = 0; - - int summon_count = pet.count; - - if(summon_count > MAX_SWARM_PETS) - summon_count = MAX_SWARM_PETS; - - static const glm::vec2 swarmPetLocations[MAX_SWARM_PETS] = { - glm::vec2(5, 5), glm::vec2(-5, 5), glm::vec2(5, -5), glm::vec2(-5, -5), - glm::vec2(10, 10), glm::vec2(-10, 10), glm::vec2(10, -10), glm::vec2(-10, -10), - glm::vec2(8, 8), glm::vec2(-8, 8), glm::vec2(8, -8), glm::vec2(-8, -8) - }; - - while(summon_count > 0) { - auto npc_type_copy = new NPCType; - memcpy(npc_type_copy, made_npc, sizeof(NPCType)); - - NPC* swarm_pet_npc = new NPC( - npc_type_copy, - 0, - GetPosition() + glm::vec4(swarmPetLocations[summon_count - 1], 0.0f, 0.0f), - GravityBehavior::Water); - - if(!swarm_pet_npc->GetSwarmInfo()){ - auto nSI = new SwarmPet; - swarm_pet_npc->SetSwarmInfo(nSI); - swarm_pet_npc->GetSwarmInfo()->duration = new Timer(pet_duration*1000); - } - else{ - swarm_pet_npc->GetSwarmInfo()->duration->Start(pet_duration*1000); - } - - swarm_pet_npc->StartSwarmTimer(pet_duration * 1000); - - swarm_pet_npc->GetSwarmInfo()->owner_id = GetID(); - swarm_pet_npc->SetFollowID(GetID()); - - // Give the pets alittle more agro than the caster and then agro them on the target - target->AddToHateList(swarm_pet_npc, (target->GetHateAmount(this) + 100), (target->GetDamageAmount(this) + 100)); - swarm_pet_npc->AddToHateList(target, 1000, 1000); - swarm_pet_npc->GetSwarmInfo()->target = 0; - - //we allocated a new NPC type object, give the NPC ownership of that memory - swarm_pet_npc->GiveNPCTypeData(npc_type_copy); - - entity_list.AddNPC(swarm_pet_npc); - summon_count--; - } - - safe_delete(made_npc); + if (!duration || !message.length()) { + return; + } + + EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); + ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; + + cms->type = type; + cms->unk04 = 10; + cms->priority = 510; + cms->fade_in_time = 0; + cms->fade_out_time = 3000; + cms->duration = duration; + + strcpy(cms->msg, message.c_str()); + + QueuePacket(&outapp); } -void Client::AssignToInstance(uint16 instance_id) +void Client::SendMarqueeMessage(uint32 type, uint32 priority, uint32 fade_in, uint32 fade_out, uint32 duration, std::string message) { - database.AddClientToInstance(instance_id, CharacterID()); + if (!duration || !message.length()) { + return; + } + + EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); + ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; + + cms->type = type; + cms->unk04 = 10; + cms->priority = priority; + cms->fade_in_time = fade_in; + cms->fade_out_time = fade_out; + cms->duration = duration; + + strcpy(cms->msg, message.c_str()); + + QueuePacket(&outapp); } -void Client::RemoveFromInstance(uint16 instance_id) +void Client::PlayMP3(const char* fname) { - database.RemoveClientFromInstance(instance_id, CharacterID()); + std::string filename = fname; + auto outapp = new EQApplicationPacket(OP_PlayMP3, filename.length() + 1); + PlayMP3_Struct* buf = (PlayMP3_Struct*)outapp->pBuffer; + strncpy(buf->filename, fname, filename.length()); + QueuePacket(outapp); + safe_delete(outapp); } -void Client::SendAltCurrencies() { - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - const uint32 currency_count = zone->AlternateCurrencies.size(); - if (!currency_count) { - return; - } +void Client::ExpeditionSay(const char *str, int ExpID) { - auto outapp = new EQApplicationPacket( - OP_AltCurrency, - sizeof(AltCurrencyPopulate_Struct) + - sizeof(AltCurrencyPopulateEntry_Struct) * currency_count - ); + std::string query = StringFormat("SELECT `player_name` FROM `cust_inst_players` " + "WHERE `inst_id` = %i", ExpID); + auto results = database.QueryDatabase(query); + if (!results.Success()) + return; - auto a = (AltCurrencyPopulate_Struct*) outapp->pBuffer; + if(results.RowCount() == 0) { + Message(Chat::Lime, "You say to the expedition, '%s'", str); + return; + } - a->opcode = AlternateCurrencyMode::Populate; - a->count = currency_count; + for(auto row = results.begin(); row != results.end(); ++row) { + const char* charName = row[0]; + if(strcmp(charName, GetCleanName()) != 0) { + worldserver.SendEmoteMessage( + charName, + 0, + AccountStatus::Player, + Chat::Lime, + fmt::format( + "{} says to the expedition, '{}'", + GetCleanName(), + str + ).c_str() + ); + } + // ChannelList->CreateChannel(ChannelName, ChannelOwner, ChannelPassword, true, Strings::ToInt(row[3])); + } - uint32 currency_id = 0; - for (const auto& c : zone->AlternateCurrencies) { - const auto* item = database.GetItem(c.item_id); - a->entries[currency_id].currency_number = c.id; - a->entries[currency_id].unknown00 = 1; - a->entries[currency_id].currency_number2 = c.id; - a->entries[currency_id].item_id = c.item_id; - a->entries[currency_id].item_icon = item ? item->Icon : 1000; - a->entries[currency_id].stack_size = item ? item->StackSize : 1000; +} - currency_id++; - } +int Client::GetQuiverHaste(int delay) +{ + const EQ::ItemInstance *pi = nullptr; + for (int r = EQ::invslot::GENERAL_BEGIN; r <= EQ::invslot::GENERAL_END; r++) { + pi = GetInv().GetItem(r); + if (pi && pi->IsClassBag() && pi->GetItem()->BagType == EQ::item::BagTypeQuiver && + pi->GetItem()->BagWR > 0) + break; + if (r == EQ::invslot::GENERAL_END) + // we will get here if we don't find a valid quiver + return 0; + } + return (pi->GetItem()->BagWR * 0.0025f * delay) + 1; +} - FastQueuePacket(&outapp); - } +void Client::SendColoredText(uint32 color, std::string message) +{ + // arbitrary size limit + if (message.size() > 512) // live does send this with empty strings sometimes ... + return; + auto outapp = new EQApplicationPacket(OP_ColoredText, sizeof(ColoredText_Struct) + message.size()); + ColoredText_Struct *cts = (ColoredText_Struct *)outapp->pBuffer; + cts->color = color; + strcpy(cts->msg, message.c_str()); + QueuePacket(outapp); + safe_delete(outapp); } -void Client::SetAlternateCurrencyValue(uint32 currency_id, uint32 new_amount) + +void Client::QuestReward(Mob* target, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, uint32 itemid, uint32 exp, bool faction) { - if (!zone->DoesAlternateCurrencyExist(currency_id)) { - return; - } - const uint32 current_amount = alternate_currency[currency_id]; + auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); + memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); + QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; - const bool is_gain = new_amount > current_amount; + qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name + qr->target_id = GetID(); // The Client ID (this) + qr->copper = copper; + qr->silver = silver; + qr->gold = gold; + qr->platinum = platinum; + qr->item_id[0] = itemid; + qr->exp_reward = exp; - const uint32 change_amount = is_gain ? (new_amount - current_amount) : (current_amount - new_amount); + if (copper > 0 || silver > 0 || gold > 0 || platinum > 0) { + AddMoneyToPP(copper, silver, gold, platinum); + } - if (!change_amount) { - return; - } + if (itemid > 0) { + SummonItemIntoInventory(itemid, -1, 0, 0, 0, 0, 0, 0, false); + } - alternate_currency[currency_id] = new_amount; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); - SendAlternateCurrencyValue(currency_id); + if (faction) { + if (target && target->IsNPC() && !target->IsCharmed()) { + int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); + SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); + qr->faction = target->CastToNPC()->GetPrimaryFaction(); + qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. + } + } - QuestEventID event_id = is_gain ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; - if (parse->PlayerHasQuestSub(event_id)) { - const std::string &export_string = fmt::format( - "{} {} {}", - currency_id, - change_amount, - new_amount - ); + if (exp > 0) { + AddEXP(ExpSource::Quest, exp); + } - parse->EventPlayer(event_id, this, export_string, 0); - } + QueuePacket(outapp, true, Client::CLIENT_CONNECTED); + safe_delete(outapp); } -bool Client::RemoveAlternateCurrencyValue(uint32 currency_id, uint32 amount) +void Client::QuestReward(Mob* target, const QuestReward_Struct &reward, bool faction) { - if (!amount || !zone->DoesAlternateCurrencyExist(currency_id)) { - return false; - } + auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); + memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); + QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; - const uint32 current_amount = alternate_currency[currency_id]; - if (current_amount < amount) { - return false; - } + memcpy(qr, &reward, sizeof(QuestReward_Struct)); - const uint32 new_amount = (current_amount - amount); + // not set in caller because reasons + qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name, tasks won't set this - alternate_currency[currency_id] = new_amount; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_amount); - SendAlternateCurrencyValue(currency_id); + if (reward.copper > 0 || reward.silver > 0 || reward.gold > 0 || reward.platinum > 0) { + AddMoneyToPP(reward.copper, reward.silver, reward.gold, reward.platinum); + } - if (parse->PlayerHasQuestSub(EVENT_ALT_CURRENCY_LOSS)) { - const std::string &export_string = fmt::format( - "{} {} {}", - currency_id, - amount, - new_amount - ); + for (int i = 0; i < QUESTREWARD_COUNT; ++i) { + if (reward.item_id[i] > 0) { + SummonItemIntoInventory(reward.item_id[i], -1, 0, 0, 0, 0, 0, 0, false); + } + } - parse->EventPlayer(EVENT_ALT_CURRENCY_LOSS, this, export_string, 0); - } + // only process if both are valid + // if we don't have a target here, we want to just reward, but if there is a target, need to check charm + if (reward.faction && reward.faction_mod && (target == nullptr || !target->IsCharmed())) { + RewardFaction(reward.faction, reward.faction_mod); + } - return true; -} + // legacy support + if (faction) { + if (target && target->IsNPC() && !target->IsCharmed()) { + int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); + SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); + qr->faction = target->CastToNPC()->GetPrimaryFaction(); + qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. + } + } -int Client::AddAlternateCurrencyValue(uint32 currency_id, int amount, bool is_scripted) -{ - if (!zone->DoesAlternateCurrencyExist(currency_id)) { - return 0; - } - - /* Added via Quest, rest of the logging methods may be done inline due to information available in that area of the code */ - if (is_scripted) { - /* QS: PlayerLogAlternateCurrencyTransactions :: Cursor to Item Storage */ - if (RuleB(QueryServ, PlayerLogAlternateCurrencyTransactions)){ - std::string event_desc = StringFormat("Added via Quest :: Cursor to Item :: alt_currency_id:%i amount:%i in zoneid:%i instid:%i", currency_id, GetZoneID(), GetInstanceID()); - QServ->PlayerLogEvent(Player_Log_Alternate_Currency_Transactions, CharacterID(), event_desc); - } - } - - if (!amount) { - return 0; - } - - if (!alternate_currency_loaded) { - alternate_currency_queued_operations.push(std::make_pair(currency_id, amount)); - return 0; - } - - int new_value = 0; - auto iter = alternate_currency.find(currency_id); - if (iter == alternate_currency.end()) { - new_value = amount; - } else { - new_value = (*iter).second + amount; - } - - if (new_value < 0) { - new_value = 0; - alternate_currency[currency_id] = 0; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, 0); - } else { - alternate_currency[currency_id] = new_value; - database.UpdateAltCurrencyValue(CharacterID(), currency_id, new_value); - } - - SendAlternateCurrencyValue(currency_id); - - QuestEventID event_id = amount > 0 ? EVENT_ALT_CURRENCY_GAIN : EVENT_ALT_CURRENCY_LOSS; - if (parse->PlayerHasQuestSub(event_id)) { - const std::string &export_string = fmt::format( - "{} {} {}", - currency_id, - std::abs(amount), - new_value - ); - - parse->EventPlayer(event_id, this, export_string, 0); - } - - return new_value; -} + if (reward.exp_reward > 0) { + AddEXP(ExpSource::Quest, reward.exp_reward); + } -void Client::SendAlternateCurrencyValues() -{ - for (const auto& alternate_currency : zone->AlternateCurrencies) { - SendAlternateCurrencyValue(alternate_currency.id, false); - } + QueuePacket(outapp, true, Client::CLIENT_CONNECTED); + safe_delete(outapp); } -void Client::SendAlternateCurrencyValue(uint32 currency_id, bool send_if_null) +void Client::CashReward(uint32 copper, uint32 silver, uint32 gold, uint32 platinum) { - const auto value = GetAlternateCurrencyValue(currency_id); - if (value > 0 || send_if_null) { - auto outapp = new EQApplicationPacket(OP_AltCurrency, sizeof(AltCurrencyUpdate_Struct)); - auto update = (AltCurrencyUpdate_Struct *) outapp->pBuffer; - update->opcode = 7; - update->currency_number = currency_id; - update->amount = value; - update->unknown072 = 1; + auto outapp = std::make_unique(OP_CashReward, sizeof(CashReward_Struct)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->copper = copper; + outbuf->silver = silver; + outbuf->gold = gold; + outbuf->platinum = platinum; - strn0cpy(update->name, GetName(), sizeof(update->name)); + AddMoneyToPP(copper, silver, gold, platinum); - FastQueuePacket(&outapp); - } + QueuePacket(outapp.get()); } -uint32 Client::GetAlternateCurrencyValue(uint32 currency_id) const +void Client::RewardFaction(int faction_id, int amount) { - if (!zone->DoesAlternateCurrencyExist(currency_id)) { - return 0; - } + SetFactionLevel2(CharacterID(), faction_id, GetClass(), GetBaseRace(), GetDeity(), amount, false); - auto iter = alternate_currency.find(currency_id); + auto f = zone->GetFactionAssociation(faction_id); + if (!f) { + return; + } - return iter == alternate_currency.end() ? 0 : (*iter).second; + std::vector faction_ids = { + f->id_1, + f->id_2, + f->id_3, + f->id_4, + f->id_5, + f->id_6, + f->id_7, + f->id_8, + f->id_9, + f->id_10 + }; + + std::vector faction_modifiers = { + f->mod_1, + f->mod_2, + f->mod_3, + f->mod_4, + f->mod_5, + f->mod_6, + f->mod_7, + f->mod_8, + f->mod_9, + f->mod_10 + }; + + std::vector temporary_values = { + static_cast(faction_modifiers[0] * amount), + static_cast(faction_modifiers[1] * amount), + static_cast(faction_modifiers[2] * amount), + static_cast(faction_modifiers[3] * amount), + static_cast(faction_modifiers[4] * amount), + static_cast(faction_modifiers[5] * amount), + static_cast(faction_modifiers[6] * amount), + static_cast(faction_modifiers[7] * amount), + static_cast(faction_modifiers[8] * amount), + static_cast(faction_modifiers[9] * amount) + }; + + std::vector signs = { + temporary_values[0] < 0.0f ? -1 : 1, + temporary_values[1] < 0.0f ? -1 : 1, + temporary_values[2] < 0.0f ? -1 : 1, + temporary_values[3] < 0.0f ? -1 : 1, + temporary_values[4] < 0.0f ? -1 : 1, + temporary_values[5] < 0.0f ? -1 : 1, + temporary_values[6] < 0.0f ? -1 : 1, + temporary_values[7] < 0.0f ? -1 : 1, + temporary_values[8] < 0.0f ? -1 : 1, + temporary_values[9] < 0.0f ? -1 : 1 + }; + + std::vector new_values = { + std::max(1, static_cast(std::abs(temporary_values[0]))) * signs[0], + std::max(1, static_cast(std::abs(temporary_values[1]))) * signs[1], + std::max(1, static_cast(std::abs(temporary_values[2]))) * signs[2], + std::max(1, static_cast(std::abs(temporary_values[3]))) * signs[3], + std::max(1, static_cast(std::abs(temporary_values[4]))) * signs[4], + std::max(1, static_cast(std::abs(temporary_values[5]))) * signs[5], + std::max(1, static_cast(std::abs(temporary_values[6]))) * signs[6], + std::max(1, static_cast(std::abs(temporary_values[7]))) * signs[7], + std::max(1, static_cast(std::abs(temporary_values[8]))) * signs[8], + std::max(1, static_cast(std::abs(temporary_values[9]))) * signs[9] + }; + + for (uint16 slot_id = 0; slot_id < faction_ids.size(); slot_id++) { + if (faction_ids[slot_id] > 0) { + SetFactionLevel2( + CharacterID(), + faction_ids[slot_id], + GetClass(), + GetBaseRace(), + GetDeity(), + new_values[slot_id], + false + ); + } + } } -void Client::ProcessAlternateCurrencyQueue() { - while(!alternate_currency_queued_operations.empty()) { - std::pair op = alternate_currency_queued_operations.front(); +void Client::SendHPUpdateMarquee(){ + if (!IsClient() || !current_hp || !max_hp) { + return; + } - AddAlternateCurrencyValue(op.first, op.second); + /* Health Update Marquee Display: Custom*/ + const auto health_percentage = static_cast(current_hp * 100 / max_hp); + if (health_percentage >= 100) { + return; + } - alternate_currency_queued_operations.pop(); - } + const auto health_update_notification = fmt::format("Health: {}%%", health_percentage); + SendMarqueeMessage(Chat::Yellow, 510, 0, 3000, 3000, health_update_notification); } -void Client::OpenLFGuildWindow() -{ - auto outapp = new EQApplicationPacket(OP_LFGuild, 8); +uint32 Client::GetMoney(uint8 type, uint8 subtype) { + uint32 value = 0; + + switch (type) { + case MoneyTypes::Copper: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.copper); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.copper_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.copper_cursor); + break; + default: + break; + } + break; + } + case MoneyTypes::Silver: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.silver); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.silver_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.silver_cursor); + break; + default: + break; + } + break; + } + case MoneyTypes::Gold: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.gold); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.gold_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.gold_cursor); + break; + default: + break; + } + break; + } + case MoneyTypes::Platinum: { + switch (subtype) { + case MoneySubtypes::Personal: + value = static_cast(m_pp.platinum); + break; + case MoneySubtypes::Bank: + value = static_cast(m_pp.platinum_bank); + break; + case MoneySubtypes::Cursor: + value = static_cast(m_pp.platinum_cursor); + break; + case MoneySubtypes::SharedBank: + value = static_cast(m_pp.platinum_shared); + break; + default: + break; + } + break; + } + default: + break; + } - outapp->WriteUInt32(6); + return value; +} - FastQueuePacket(&outapp); +int Client::GetAccountAge() { + return (time(nullptr) - GetAccountCreation()); } -bool Client::IsXTarget(const Mob *m) const +void Client::CheckRegionTypeChanges() { - if(!XTargettingAvailable() || !m || (m->GetID() == 0)) - return false; + if (!zone->HasWaterMap()) { + return; + } - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(XTargets[i].ID == m->GetID()) - return true; - } - return false; -} + auto new_region = zone->watermap->ReturnRegionType(glm::vec3(m_Position)); -bool Client::IsClientXTarget(const Client *c) const -{ - if(!XTargettingAvailable() || !c) - return false; + // still same region, do nothing + if (last_region_type == new_region) { + return; + } - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(!strcasecmp(XTargets[i].Name, c->GetName())) - return true; - } - return false; -} + // If we got out of water clear any water aggro for water only npcs + if (last_region_type == RegionTypeWater) { + entity_list.ClearWaterAggro(this); + } + // region type changed + last_region_type = new_region; -void Client::UpdateClientXTarget(Client *c) -{ - if(!XTargettingAvailable() || !c) - return; + // PVP is the only state we need to keep track of, so we can just return now for PVP servers + if (RuleI(World, PVPSettings) > 0) { + return; + } - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(!strcasecmp(XTargets[i].Name, c->GetName())) - { - XTargets[i].ID = c->GetID(); - SendXTargetPacket(i, c); - } - } + if (last_region_type == RegionTypePVP && RuleB(World, EnablePVPRegions)) { + temp_pvp = true; + } else if (temp_pvp) { + temp_pvp = false; + } } -// IT IS NOT SAFE TO CALL THIS IF IT'S NOT INITIAL AGGRO -void Client::AddAutoXTarget(Mob *m, bool send) +void Client::ProcessAggroMeter() { - if (m->IsBot() || ((m->IsPet() || m->IsTempPet()) && m->IsPetOwnerBot())) { - return; - } + if (!AggroMeterAvailable()) { + aggro_meter_timer.Disable(); + return; + } + + // we need to decide if we need to send OP_AggroMeterTargetInfo now + // This packet sends the current lock target ID and the current target ID + // target ID will be either our target or our target of target when we're targeting a PC + bool send_targetinfo = false; + auto cur_tar = GetTarget(); + + // probably should have PVP rules ... + if (cur_tar && cur_tar != this) { + if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerClient() && cur_tar->GetID() != m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(cur_tar->GetID()); + send_targetinfo = true; + } else if ((cur_tar->IsPetOwnerClient() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(cur_tar->GetTarget()->GetID()); + send_targetinfo = true; + } + } else if (m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(0); + send_targetinfo = true; + } - m_activeautohatermgr->increment_count(m); + if (m_aggrometer.update_lock()) + send_targetinfo = true; + + if (send_targetinfo) { + auto app = new EQApplicationPacket(OP_AggroMeterTargetInfo, sizeof(uint32) * 2); + app->WriteUInt32(m_aggrometer.get_lock_id()); + app->WriteUInt32(m_aggrometer.get_target_id()); + FastQueuePacket(&app); + } - if (!XTargettingAvailable() || !XTargetAutoAddHaters || IsXTarget(m)) { - return; - } + // we could just calculate how big the packet would need to be ... but it's easier this way :P should be 87 bytes + auto app = new EQApplicationPacket(OP_AggroMeterUpdate, m_aggrometer.max_packet_size()); + + cur_tar = entity_list.GetMob(m_aggrometer.get_target_id()); + + // first we must check the secondary + // TODO: lock target should affect secondary as well + bool send = false; + Mob *secondary = nullptr; + bool has_aggro = false; + if (cur_tar) { + if (cur_tar->GetTarget() == this) {// we got aggro + secondary = cur_tar->GetSecondaryHate(this); + has_aggro = true; + } else { + secondary = cur_tar->CheckAggro(cur_tar->GetTarget()) ? cur_tar->GetTarget() : nullptr; // make sure they are targeting for aggro reasons + } + } - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type == Auto && XTargets[i].ID == 0) { - XTargets[i].ID = m->GetID(); + if (secondary && secondary->GetID() != m_aggrometer.get_secondary_id()) { + m_aggrometer.set_secondary_id(secondary->GetID()); + app->WriteUInt8(1); + app->WriteUInt32(m_aggrometer.get_secondary_id()); + send = true; + } else if (!secondary && m_aggrometer.get_secondary_id()) { + m_aggrometer.set_secondary_id(0); + app->WriteUInt8(1); + app->WriteUInt32(0); + send = true; + } else { // might not need to send in this case + app->WriteUInt8(0); + } - if (send) { // if we don't send we're bulk sending updates later on - SendXTargetPacket(i, m); - } else { - XTargets[i].dirty = true; - } + auto count_offset = app->GetWritePosition(); + app->WriteUInt8(0); + + int count = 0; + auto add_entry = [&app, &count, this](AggroMeter::AggroTypes i) { + count++; + app->WriteUInt8(i); + app->WriteUInt16(m_aggrometer.get_pct(i)); + }; + // TODO: Player entry should either be lock or yourself, ignoring lock for now + // player, secondary, and group depend on your target/lock + if (cur_tar) { + if (m_aggrometer.set_pct(AggroMeter::AT_Player, cur_tar->GetHateRatio(cur_tar->GetTarget(), this))) + add_entry(AggroMeter::AT_Player); + + if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, has_aggro ? cur_tar->GetHateRatio(this, secondary) : secondary ? 100 : 0)) + add_entry(AggroMeter::AT_Secondary); + + if (IsRaidGrouped()) { + auto raid = GetRaid(); + if (raid) { + auto gid = raid->GetGroup(this); + if (gid < MAX_RAID_GROUPS) { + int at_id = AggroMeter::AT_Group1; + for (const auto& m : raid->members) { + if (m.member && m.member != this && m.group_number == gid) { + if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), m.member))) + add_entry(static_cast(at_id)); + at_id++; + if (at_id > AggroMeter::AT_Group5) + break; + } + } + } + } + } else if (IsGrouped()) { + auto group = GetGroup(); + if (group) { + int at_id = AggroMeter::AT_Group1; + for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { + if (group->members[i] && group->members[i] != this) { + if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), group->members[i]))) + add_entry(static_cast(at_id)); + at_id++; + } + } + } + } + } else { // we might need to clear out some data now + if (m_aggrometer.set_pct(AggroMeter::AT_Player, 0)) + add_entry(AggroMeter::AT_Player); + if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, 0)) + add_entry(AggroMeter::AT_Secondary); + if (m_aggrometer.set_pct(AggroMeter::AT_Group1, 0)) + add_entry(AggroMeter::AT_Group1); + if (m_aggrometer.set_pct(AggroMeter::AT_Group2, 0)) + add_entry(AggroMeter::AT_Group2); + if (m_aggrometer.set_pct(AggroMeter::AT_Group3, 0)) + add_entry(AggroMeter::AT_Group3); + if (m_aggrometer.set_pct(AggroMeter::AT_Group4, 0)) + add_entry(AggroMeter::AT_Group4); + if (m_aggrometer.set_pct(AggroMeter::AT_Group5, 0)) + add_entry(AggroMeter::AT_Group5); + } - break; - } - } + // now to go over our xtargets + // if the entry is an NPC it's our hate relative to the NPCs current tank + // if it's a PC, it's their hate relative to our current target + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].ID) { + auto mob = entity_list.GetMob(XTargets[i].ID); + if (mob) { + int ratio = 0; + if (mob->IsNPC()) + ratio = mob->GetHateRatio(mob->GetTarget(), this); + else if (cur_tar) + ratio = cur_tar->GetHateRatio(cur_tar->GetTarget(), mob); + if (m_aggrometer.set_pct(static_cast(AggroMeter::AT_XTarget1 + i), ratio)) + add_entry(static_cast(AggroMeter::AT_XTarget1 + i)); + } + } + } - LogXTargets( - "Adding [{}] to [{}] ({}) XTargets", - m->GetCleanName(), - GetCleanName(), - GetID() - ); + if (send || count) { + app->size = app->GetWritePosition(); // this should be safe, although not recommended + // but this way we can have a smaller buffer created for the packet dispatched to the client w/o resizing this one + app->SetWritePosition(count_offset); + app->WriteUInt8(count); + FastQueuePacket(&app); + } else { + safe_delete(app); + } } -void Client::RemoveXTarget(Mob *m, bool OnlyAutoSlots) +void Client::SetPetCommandState(int button, int state) { - if (!XTargettingAvailable() || !m || !m_activeautohatermgr) { - return; - } - - m_activeautohatermgr->decrement_count(m); - // now we may need to clean up our CurrentTargetNPC entries - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type == CurrentTargetNPC && XTargets[i].ID == m->GetID()) { - XTargets[i].Type = Auto; - XTargets[i].ID = 0; - XTargets[i].dirty = true; - } - } - - auto r = GetRaid(); - if (r) { - r->UpdateRaidXTargets(); - } - - LogXTargets( - "Removing [{}] from [{}] ({}) XTargets", - m->GetCleanName(), - GetCleanName(), - GetID() - ); + auto app = new EQApplicationPacket(OP_PetCommandState, sizeof(PetCommandState_Struct)); + auto pcs = (PetCommandState_Struct *)app->pBuffer; + pcs->button_id = button; + pcs->state = state; + FastQueuePacket(&app); } -void Client::UpdateXTargetType(XTargetType Type, Mob *m, const char *Name) +bool Client::CanMedOnHorse() { - if (!XTargettingAvailable()) { - return; - } + // no horse is false + if (GetHorseId() == 0) + return false; - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type == Type) { - if (m) { - XTargets[i].ID = m->GetID(); - } - else { - XTargets[i].ID = 0; - } + // can't med while attacking + if (auto_attack) + return false; - if (Name) { - strncpy(XTargets[i].Name, Name, 64); - } + return animation == 0 && m_Delta.x == 0.0f && m_Delta.y == 0.0f; // TODO: animation is SpeedRun +} - SendXTargetPacket(i, m); - } - } +void Client::EnableAreaHPRegen(int value) +{ + AreaHPRegen = value * 0.001f; + SendAppearancePacket(AppearanceType::AreaHealthRegen, value, false); } -void Client::SendXTargetPacket(uint32 Slot, Mob *m) +void Client::DisableAreaHPRegen() { - if(!XTargettingAvailable()) - return; - - uint32 PacketSize = 18; - - if(m) - PacketSize += strlen(m->GetCleanName()); - else - { - PacketSize += strlen(XTargets[Slot].Name); - } - - auto outapp = new EQApplicationPacket(OP_XTargetResponse, PacketSize); - outapp->WriteUInt32(GetMaxXTargets()); - outapp->WriteUInt32(1); - outapp->WriteUInt32(Slot); - if(m) - { - outapp->WriteUInt8(1); - } - else - { - if (strlen(XTargets[Slot].Name) && ((XTargets[Slot].Type == CurrentTargetPC) || - (XTargets[Slot].Type == GroupTank) || - (XTargets[Slot].Type == GroupAssist) || - (XTargets[Slot].Type == Puller))) - { - outapp->WriteUInt8(2); - } - else - { - outapp->WriteUInt8(0); - } - } - outapp->WriteUInt32(XTargets[Slot].ID); - outapp->WriteString(m ? m->GetCleanName() : XTargets[Slot].Name); - FastQueuePacket(&outapp); + AreaHPRegen = 1.0f; + SendAppearancePacket(AppearanceType::AreaHealthRegen, 1000, false); } -// This is a bulk packet, we use it when we remove something since we need to reorder the xtargets and maybe -// add new mobs! Currently doesn't check if there is a dirty flag set, so it should only be called when there is -void Client::SendXTargetUpdates() +void Client::EnableAreaManaRegen(int value) { - if (!XTargettingAvailable()) - return; - - int count = 0; - // header is 4 bytes max xtargets, 4 bytes count - // entry is 4 bytes slot, 1 byte unknown, 4 bytes ID, 65 char name - auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8 + 74 * GetMaxXTargets()); // fuck it max size - outapp->WriteUInt32(GetMaxXTargets()); - outapp->WriteUInt32(1); // we will correct this later - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].dirty) { - outapp->WriteUInt32(i); - // MQ2 checks this for valid mobs, so 0 is bad here at least ... - outapp->WriteUInt8(XTargets[i].ID ? 1 : 0); - outapp->WriteUInt32(XTargets[i].ID); - outapp->WriteString(XTargets[i].Name); - count++; - XTargets[i].dirty = false; - } - } - - // RemoveXTarget probably got called with a mob not on our xtargets - if (count == 0) { - safe_delete(outapp); - return; - } - - auto newbuff = new uchar[outapp->GetWritePosition()]; - memcpy(newbuff, outapp->pBuffer, outapp->GetWritePosition()); - safe_delete_array(outapp->pBuffer); - outapp->pBuffer = newbuff; - outapp->size = outapp->GetWritePosition(); - outapp->SetWritePosition(4); - outapp->WriteUInt32(count); - FastQueuePacket(&outapp); + AreaManaRegen = value * 0.001f; + SendAppearancePacket(AppearanceType::AreaManaRegen, value, false); } -void Client::RemoveGroupXTargets() +void Client::DisableAreaManaRegen() { - if(!XTargettingAvailable()) - return; + AreaManaRegen = 1.0f; + SendAppearancePacket(AppearanceType::AreaManaRegen, 1000, false); +} - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if ((XTargets[i].Type == GroupTank) || - (XTargets[i].Type == GroupAssist) || - (XTargets[i].Type == Puller)) - { - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - SendXTargetPacket(i, nullptr); - } - } +void Client::EnableAreaEndRegen(int value) +{ + AreaEndRegen = value * 0.001f; + SendAppearancePacket(AppearanceType::AreaEnduranceRegen, value, false); } -void Client::RemoveAutoXTargets() +void Client::DisableAreaEndRegen() { - if(!XTargettingAvailable()) - return; + AreaEndRegen = 1.0f; + SendAppearancePacket(AppearanceType::AreaEnduranceRegen, 1000, false); +} - for(int i = 0; i < GetMaxXTargets(); ++i) - { - if(XTargets[i].Type == Auto) - { - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - SendXTargetPacket(i, nullptr); - } - } +void Client::EnableAreaRegens(int value) +{ + EnableAreaHPRegen(value); + EnableAreaManaRegen(value); + EnableAreaEndRegen(value); } -void Client::ShowXTargets(Client *c) +void Client::DisableAreaRegens() { - if (!c) { - return; - } - - auto xtarget_count = 0; - - for (int i = 0; i < GetMaxXTargets(); ++i) { - c->Message( - Chat::White, - fmt::format( - "xtarget slot [{}] type [{}] ID [{}] name [{}]", - i, - static_cast(XTargets[i].Type), - XTargets[i].ID, - strlen(XTargets[i].Name) ? XTargets[i].Name : "No Name" - ).c_str() - ); - - xtarget_count++; - } - - auto &list = GetXTargetAutoMgr()->get_list(); - // yeah, I kept having to do something for debugging to tell if managers were the same object or not :P - // so lets use the address as an "ID" - c->Message( - Chat::White, - fmt::format( - "XTargetAutoMgr ID [{}] size [{}]", - fmt::ptr(GetXTargetAutoMgr()), - list.size() - ).c_str() - ); - - int count = 0; - for (auto &e : list) { - c->Message( - Chat::White, - fmt::format( - "Spawn ID: {} Count: {}", - e.spawn_id, - e.count - ).c_str() - ); - - count++; - - if (count == 20) { - break; - } - } + DisableAreaHPRegen(); + DisableAreaManaRegen(); + DisableAreaEndRegen(); } -void Client::ProcessXTargetAutoHaters() +void Client::InitInnates() +{ + // this function on the client also inits the level one innate skills (like swimming, hide, etc) + // we won't do that here, lets just do the InnateSkills for now. Basically translation of what the client is doing + // A lot of these we could probably have ignored because they have no known use or are 100% client side + // but I figured just in case we'll do them all out + // + // The client calls this in a few places. When you remove a vision buff and in SetHeights, which is called in + // illusions, mounts, and a bunch of other cases. All of the calls to InitInnates are wrapped in restoring regen + // besides the call initializing the first time + auto race = GetRace(); + auto class_ = GetClass(); + + for (int i = 0; i < InnateSkillMax; ++i) { + m_pp.InnateSkills[i] = InnateDisabled; + } + + m_pp.InnateSkills[InnateInspect] = InnateEnabled; + m_pp.InnateSkills[InnateOpen] = InnateEnabled; + + if (race >= Race::Froglok2) { + if (race == Race::Skeleton2 || race == Race::Froglok2) { + m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; + } else { + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + } + } + + switch (race) { + case Race::Barbarian: + case Race::HalasCitizen: + m_pp.InnateSkills[InnateSlam] = InnateEnabled; + break; + case Race::Erudite: + case Race::EruditeCitizen: + m_pp.InnateSkills[InnateLore] = InnateEnabled; + break; + case Race::WoodElf: + case Race::Fayguard: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Gnome: + case Race::HighElf: + case Race::Felguard: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + m_pp.InnateSkills[InnateLore] = InnateEnabled; + break; + case Race::Troll: + case Race::GrobbCitizen: + m_pp.InnateSkills[InnateRegen] = InnateEnabled; + m_pp.InnateSkills[InnateSlam] = InnateEnabled; + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Dwarf: + case Race::KaladimCitizen: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Ogre: + case Race::OggokCitizen: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + m_pp.InnateSkills[InnateSlam] = InnateEnabled; + m_pp.InnateSkills[InnateNoBash] = InnateEnabled; + m_pp.InnateSkills[InnateBashDoor] = InnateEnabled; + break; + case Race::Halfling: + case Race::RivervaleCitizen: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::Iksar: + m_pp.InnateSkills[InnateRegen] = InnateEnabled; + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::VahShir: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + case Race::DarkElf: + case Race::NeriakCitizen: + case Race::ElfVampire: + case Race::FroglokGhoul: + case Race::Ghost: + case Race::Ghoul: + case Race::Skeleton: + case Race::Vampire: + case Race::Wisp: + case Race::Zombie: + case Race::Spectre: + case Race::DwarfGhost: + case Race::EruditeGhost: + case Race::DragonSkeleton: + case Race::Innoruuk: + m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; + break; + case Race::Human: + case Race::FreeportGuard: + case Race::HumanBeggar: + case Race::HighpassCitizen: + case Race::QeynosCitizen: + case Race::Froglok2: // client does froglok weird, but this should work out fine + break; + default: + m_pp.InnateSkills[InnateInfravision] = InnateEnabled; + break; + } + + switch (class_) { + case Class::Druid: + m_pp.InnateSkills[InnateHarmony] = InnateEnabled; + break; + case Class::Bard: + m_pp.InnateSkills[InnateReveal] = InnateEnabled; + break; + case Class::Rogue: + m_pp.InnateSkills[InnateSurprise] = InnateEnabled; + m_pp.InnateSkills[InnateReveal] = InnateEnabled; + break; + case Class::Ranger: + m_pp.InnateSkills[InnateAwareness] = InnateEnabled; + break; + case Class::Monk: + m_pp.InnateSkills[InnateSurprise] = InnateEnabled; + m_pp.InnateSkills[InnateAwareness] = InnateEnabled; + default: + break; + } +} + +bool Client::GetDisplayMobInfoWindow() const +{ + return display_mob_info_window; +} + +void Client::SetDisplayMobInfoWindow(bool display_mob_info_window) +{ + Client::display_mob_info_window = display_mob_info_window; +} + +bool Client::IsDevToolsEnabled() const { - if (!XTargettingAvailable()) - return; - - // move shit up! If the removed NPC was in a CurrentTargetNPC slot it becomes Auto - // and we need to potentially fill it - std::queue empty_slots; - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].Type != Auto) - continue; - - if (XTargets[i].ID != 0 && !GetXTargetAutoMgr()->contains_mob(XTargets[i].ID)) { - XTargets[i].ID = 0; - XTargets[i].dirty = true; - } - - if (XTargets[i].ID == 0) { - empty_slots.push(i); - continue; - } - - if (XTargets[i].ID != 0 && !empty_slots.empty()) { - int temp = empty_slots.front(); - std::swap(XTargets[i], XTargets[temp]); - XTargets[i].dirty = XTargets[temp].dirty = true; - empty_slots.pop(); - empty_slots.push(i); - } - } - // okay, now we need to check if we have any empty slots and if we have aggro - // We make the assumption that if we shuffled the NPCs up that they're still on the aggro - // list in the same order. We could probably do this better and try to calc if - // there are new NPCs for our empty slots on the manager, but ahhh fuck it. - if (!empty_slots.empty() && !GetXTargetAutoMgr()->empty() && XTargetAutoAddHaters) { - auto &haters = GetXTargetAutoMgr()->get_list(); - for (auto &e : haters) { - auto *mob = entity_list.GetMob(e.spawn_id); - if (mob && !IsXTarget(mob)) { - auto slot = empty_slots.front(); - empty_slots.pop(); - XTargets[slot].dirty = true; - XTargets[slot].ID = mob->GetID(); - strn0cpy(XTargets[slot].Name, mob->GetCleanName(), 64); - } - if (empty_slots.empty()) - break; - } - } - - m_dirtyautohaters = false; - SendXTargetUpdates(); + return dev_tools_enabled && GetGM() && RuleB(World, EnableDevTools); } -// This function is called when a client is added to a group -// Group leader joining isn't handled by this function -void Client::JoinGroupXTargets(Group *g) +void Client::SetDevToolsEnabled(bool in_dev_tools_enabled) { - if (!g) - return; + const auto dev_tools_key = fmt::format("{}-dev-tools-disabled", AccountID()); - if (!GetXTargetAutoMgr()->empty()) { - g->GetXTargetAutoMgr()->merge(*GetXTargetAutoMgr()); - GetXTargetAutoMgr()->clear(); - RemoveAutoXTargets(); - } + if (in_dev_tools_enabled) { + DataBucket::DeleteData(dev_tools_key); + } else { + DataBucket::SetData(dev_tools_key, "true"); + } - SetXTargetAutoMgr(g->GetXTargetAutoMgr()); + Client::dev_tools_enabled = in_dev_tools_enabled; +} - if (!GetXTargetAutoMgr()->empty()) - SetDirtyAutoHaters(); +bool Client::IsEXPEnabled() const { + return m_exp_enabled; } -// This function is called when a client leaves a group -void Client::LeaveGroupXTargets(Group *g) +void Client::SetEXPEnabled(bool is_exp_enabled) { - if (!g) - return; + auto c = CharacterDataRepository::FindOne(database, CharacterID()); - SetXTargetAutoMgr(nullptr); // this will set it back to our manager - RemoveAutoXTargets(); - entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever - // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups - if (!GetXTargetAutoMgr()->empty()) { - GetXTargetAutoMgr()->demerge(*g->GetXTargetAutoMgr()); // this will remove entries where we only had aggro - SetDirtyAutoHaters(); - } -} + c.exp_enabled = is_exp_enabled; -// This function is called when a client leaves a group -void Client::LeaveRaidXTargets(Raid *r) -{ - if (!r) - return; + auto updated = CharacterDataRepository::UpdateOne(database, c); + + if (!updated) { + return; + } - SetXTargetAutoMgr(nullptr); // this will set it back to our manager - RemoveAutoXTargets(); - entity_list.RefreshAutoXTargets(this); // this will probably break the temporal ordering, but whatever - // We now have a rebuilt, valid auto hater manager, so we need to demerge from the groups - if (!GetXTargetAutoMgr()->empty()) { - GetXTargetAutoMgr()->demerge(*r->GetXTargetAutoMgr()); // this will remove entries where we only had aggro - SetDirtyAutoHaters(); - } + m_exp_enabled = is_exp_enabled; } -void Client::SetMaxXTargets(uint8 NewMax) +void Client::SetPrimaryWeaponOrnamentation(uint32 model_id) { - if(!XTargettingAvailable()) - return; + auto primary_item = m_inv.GetItem(EQ::invslot::slotPrimary); + if (primary_item) { + auto l = InventoryRepository::GetWhere( + database, + fmt::format( + "`charid` = {} AND `slotid` = {}", + character_id, + EQ::invslot::slotPrimary + ) + ); - if(NewMax > XTARGET_HARDCAP) - return; + if (l.empty()) { + return; + } + + auto e = l.front(); - MaxXTargets = NewMax; + e.ornamentidfile = model_id; - Save(0); + const int updated = InventoryRepository::UpdateOne(database, e); - for(int i = MaxXTargets; i < XTARGET_HARDCAP; ++i) - { - XTargets[i].Type = Auto; - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - } + if (updated) { + primary_item->SetOrnamentationIDFile(model_id); + SendItemPacket(EQ::invslot::slotPrimary, primary_item, ItemPacketTrade); + WearChange(EQ::textures::weaponPrimary, model_id, 0); - auto outapp = new EQApplicationPacket(OP_XTargetResponse, 8); - outapp->WriteUInt32(GetMaxXTargets()); - outapp->WriteUInt32(0); - FastQueuePacket(&outapp); + Message(Chat::Yellow, "Your primary weapon appearance has been modified."); + } + } } -void Client::SendWebLink(const char *website) +void Client::SetSecondaryWeaponOrnamentation(uint32 model_id) { - if (website) { - size_t len = strlen(website) + 1; - if (len > 1) - { - auto outapp = new EQApplicationPacket(OP_Weblink, sizeof(Weblink_Struct) + len); - Weblink_Struct* wl = (Weblink_Struct*)outapp->pBuffer; - memcpy(wl->weblink, website, len); - wl->weblink[len] = '\0'; + auto secondary_item = m_inv.GetItem(EQ::invslot::slotSecondary); + if (secondary_item) { + auto l = InventoryRepository::GetWhere( + database, + fmt::format( + "`charid` = {} AND `slotid` = {}", + character_id, + EQ::invslot::slotSecondary + ) + ); - FastQueuePacket(&outapp); - } - } -} + if (l.empty()) { + return; + } -void Client::SendMercPersonalInfo() -{ - uint32 mercTypeCount = 1; - uint32 mercCount = 1; //TODO: Un-hardcode this and support multiple mercs like in later clients than SoD. - uint32 i = 0; - uint32 altCurrentType = 19; //TODO: Implement alternate currency purchases involving mercs! - - MercTemplate *mercData = &zone->merc_templates[GetMercInfo().MercTemplateID]; - - int stancecount = 0; - stancecount += zone->merc_stance_list[GetMercInfo().MercTemplateID].size(); - if(stancecount > MAX_MERC_STANCES || mercCount > MAX_MERC || mercTypeCount > MAX_MERC_GRADES) - { - Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo canceled: (%i) (%i) (%i) for %s", stancecount, mercCount, mercTypeCount, GetName()); - SendMercMerchantResponsePacket(0); - return; - } - - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { - auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(MercenaryDataUpdate_Struct)); - auto mdus = (MercenaryDataUpdate_Struct *) outapp->pBuffer; - - mdus->MercStatus = 0; - mdus->MercCount = mercCount; - mdus->MercData[i].MercID = mercData->MercTemplateID; - mdus->MercData[i].MercType = mercData->MercType; - mdus->MercData[i].MercSubType = mercData->MercSubType; - mdus->MercData[i].PurchaseCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0); - mdus->MercData[i].UpkeepCost = Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0); - mdus->MercData[i].Status = 0; - mdus->MercData[i].AltCurrencyCost = Merc::CalcPurchaseCost( - mercData->MercTemplateID, - GetLevel(), - altCurrentType - ); - mdus->MercData[i].AltCurrencyUpkeep = Merc::CalcPurchaseCost( - mercData->MercTemplateID, - GetLevel(), - altCurrentType - ); - mdus->MercData[i].AltCurrencyType = altCurrentType; - mdus->MercData[i].MercUnk01 = 0; - mdus->MercData[i].TimeLeft = GetMercInfo().MercTimerRemaining; //GetMercTimer().GetRemainingTime(); - mdus->MercData[i].MerchantSlot = i + 1; - mdus->MercData[i].MercUnk02 = 1; - mdus->MercData[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); - mdus->MercData[i].MercUnk03 = 0; - mdus->MercData[i].MercUnk04 = 1; - - strn0cpy(mdus->MercData[i].MercName, GetMercInfo().merc_name, sizeof(mdus->MercData[i].MercName)); - - uint32 stanceindex = 0; - if (mdus->MercData[i].StanceCount != 0) { - auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); - while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { - mdus->MercData[i].Stances[stanceindex].StanceIndex = stanceindex; - mdus->MercData[i].Stances[stanceindex].Stance = (iter->StanceID); - stanceindex++; - ++iter; - } - } - - mdus->MercData[i].MercUnk05 = 1; - FastQueuePacket(&outapp); - safe_delete(outapp); - return; - } else { - auto outapp = new EQApplicationPacket(OP_MercenaryDataResponse, sizeof(MercenaryMerchantList_Struct)); - auto mml = (MercenaryMerchantList_Struct *) outapp->pBuffer; - - mml->MercTypeCount = mercTypeCount; //We should only have one merc entry. - mml->MercGrades[i] = 1; - - mml->MercCount = mercCount; - mml->Mercs[i].MercID = mercData->MercTemplateID; - mml->Mercs[i].MercType = mercData->MercType; - mml->Mercs[i].MercSubType = mercData->MercSubType; - mml->Mercs[i].PurchaseCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0) : 0; - mml->Mercs[i].UpkeepCost = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0) : 0; - mml->Mercs[i].Status = 0; - mml->Mercs[i].AltCurrencyCost = RuleB(Mercs, ChargeMercPurchaseCost) ? Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; - mml->Mercs[i].AltCurrencyUpkeep = RuleB(Mercs, ChargeMercUpkeepCost) ? Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), altCurrentType) : 0; - mml->Mercs[i].AltCurrencyType = altCurrentType; - mml->Mercs[i].MercUnk01 = 0; - mml->Mercs[i].TimeLeft = GetMercInfo().MercTimerRemaining; - mml->Mercs[i].MerchantSlot = i + 1; - mml->Mercs[i].MercUnk02 = 1; - mml->Mercs[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); - mml->Mercs[i].MercUnk03 = 0; - mml->Mercs[i].MercUnk04 = 1; - - strn0cpy(mml->Mercs[i].MercName, GetMercInfo().merc_name, sizeof(mml->Mercs[i].MercName)); - - int stanceindex = 0; - if (mml->Mercs[i].StanceCount != 0) { - auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); - while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { - mml->Mercs[i].Stances[stanceindex].StanceIndex = stanceindex; - mml->Mercs[i].Stances[stanceindex].Stance = (iter->StanceID); - stanceindex++; - ++iter; - } - } - - FastQueuePacket(&outapp); - safe_delete(outapp); - return; - } + auto e = l.front(); + + e.ornamentidfile = model_id; + + const int updated = InventoryRepository::UpdateOne(database, e); + + if (updated) { + secondary_item->SetOrnamentationIDFile(model_id); + SendItemPacket(EQ::invslot::slotSecondary, secondary_item, ItemPacketTrade); + WearChange(EQ::textures::weaponSecondary, model_id, 0); + + Message(Chat::Yellow, "Your secondary weapon appearance has been modified."); + } + } } -void Client::SendClearMercInfo() +/** + * Used in #goto + * + * @param player_name + */ +bool Client::GotoPlayer(const std::string& player_name) { - auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(NoMercenaryHired_Struct)); - NoMercenaryHired_Struct *nmhs = (NoMercenaryHired_Struct*)outapp->pBuffer; - nmhs->MercStatus = -1; - nmhs->MercCount = 0; - nmhs->MercID = 1; - FastQueuePacket(&outapp); -} + const auto& l = CharacterDataRepository::GetWhere( + database, + fmt::format( + "name = '{}' AND last_login > (UNIX_TIMESTAMP() - 600) LIMIT 1", + Strings::Escape(player_name) + ) + ); + if (l.empty()) { + return false; + } -void Client::DuplicateLoreMessage(uint32 ItemID) -{ - if (!(m_ClientVersionBit & EQ::versions::maskRoFAndLater)) - { - MessageString(Chat::White, PICK_LORE); - return; - } + const auto& e = l.front(); + + if (e.zone_instance > 0 && !database.CheckInstanceExists(e.zone_instance)) { + Message(Chat::Yellow, "Instance no longer exists..."); + return false; + } - const EQ::ItemData *item = database.GetItem(ItemID); + if (e.zone_instance > 0) { + database.AddClientToInstance(e.zone_instance, CharacterID()); + } - if(!item) - return; + MovePC(e.zone_id, e.zone_instance, e.x, e.y, e.z, e.heading); - MessageString(Chat::White, PICK_LORE, item->Name); + return true; } -void Client::GarbleMessage(char *message, uint8 variance) +bool Client::GotoPlayerGroup(const std::string& player_name) { - // Garble message by variance% - const char alpha_list[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // only change alpha characters for now - const char delimiter = 0x12; - int delimiter_count = 0; + if (!GetGroup()) { + return GotoPlayer(player_name); + } + + for (auto &m: GetGroup()->members) { + if (m && m->IsClient()) { + auto c = m->CastToClient(); + if (!c->GotoPlayer(player_name)) { + return false; + } + } + } + + return true; +} - // Don't garble # commands - if (message[0] == COMMAND_CHAR || message[0] == BOT_COMMAND_CHAR) { - return; - } +bool Client::GotoPlayerRaid(const std::string& player_name) +{ + if (!GetRaid()) { + return GotoPlayer(player_name); + } - for (size_t i = 0; i < strlen(message); i++) { - // Client expects hex values inside of a text link body - if (message[i] == delimiter) { - if (!(delimiter_count & 1)) { i += EQ::constants::SAY_LINK_BODY_SIZE; } - ++delimiter_count; - continue; - } + for (auto &m: GetRaid()->members) { + if (m.member && m.member->IsClient()) { + auto c = m.member->CastToClient(); + if (!c->GotoPlayer(player_name)) { + return false; + } + } + } - uint8 chance = (uint8)zone->random.Int(0, 115); // variation just over worst possible scrambling - if (isalpha((unsigned char)message[i]) && (chance <= variance)) { - uint8 rand_char = (uint8)zone->random.Int(0,51); // choose a random character from the alpha list - message[i] = alpha_list[rand_char]; - } - } + return true; } -// returns what Other thinks of this -FACTION_VALUE Client::GetReverseFactionCon(Mob* iOther) { - if (GetOwnerID()) { - return GetOwnerOrSelf()->GetReverseFactionCon(iOther); - } +void Client::SendToGuildHall() +{ + std::string zone_short_name = "guildhall"; + uint32 zone_id = ZoneID(zone_short_name.c_str()); + if (zone_id == 0) { + return; + } - iOther = iOther->GetOwnerOrSelf(); + uint32 expiration_time = (RuleI(Instances, GuildHallExpirationDays) * 86400); + uint16 instance_id = 0; + std::string guild_hall_instance_key = fmt::format("guild-hall-instance-{}", GuildID()); + std::string instance_data = DataBucket::GetData(guild_hall_instance_key); + if (!instance_data.empty() && Strings::ToInt(instance_data) > 0) { + instance_id = Strings::ToInt(instance_data); + } - if (iOther->GetPrimaryFaction() < 0) - return GetSpecialFactionCon(iOther); + if (instance_id <= 0) { + if (!database.GetUnusedInstanceID(instance_id)) { + Message(Chat::Red, "Server was unable to find a free instance id."); + return; + } - if (iOther->GetPrimaryFaction() == 0) - return FACTION_INDIFFERENTLY; + if (!database.CreateInstance(instance_id, zone_id, 1, expiration_time)) { + Message(Chat::Red, "Server was unable to create a new instance."); + return; + } - return GetFactionLevel(CharacterID(), 0, GetFactionRace(), GetClass(), GetDeity(), iOther->GetPrimaryFaction(), iOther); -} + DataBucket::SetData( + guild_hall_instance_key, + std::to_string(instance_id), + std::to_string(expiration_time) + ); + } -bool Client::ReloadCharacterFaction(Client *c, uint32 facid, uint32 charid) -{ - if (database.SetCharacterFactionLevel(charid, facid, 0, 0, factionvalues)) - return true; - else - return false; + AssignToInstance(instance_id); + MovePC(345, instance_id, -1.00, -1.00, 3.34, 0, 1); } -//o-------------------------------------------------------------- -//| Name: GetFactionLevel; Dec. 16, 2001 -//o-------------------------------------------------------------- -//| Notes: Gets the characters faction standing with the specified NPC. -//| Will return Indifferent on failure. -//o-------------------------------------------------------------- -FACTION_VALUE Client::GetFactionLevel(uint32 char_id, uint32 npc_id, uint32 p_race, uint32 p_class, uint32 p_deity, int32 pFaction, Mob* tnpc) +void Client::CheckVirtualZoneLines() { - if (pFaction < 0) - return GetSpecialFactionCon(tnpc); - FACTION_VALUE fac = FACTION_INDIFFERENTLY; - int32 tmpFactionValue; - FactionMods fmods; - - // few optimizations - if (GetFeigned()) - return FACTION_INDIFFERENTLY; - if(!zone->CanDoCombat()) - return FACTION_INDIFFERENTLY; - if (invisible_undead && tnpc && !tnpc->SeeInvisibleUndead()) - return FACTION_INDIFFERENTLY; - if (IsInvisible(tnpc)) - return FACTION_INDIFFERENTLY; - if (tnpc && tnpc->GetOwnerID() != 0) // pets con amiably to owner and indiff to rest - { - if (char_id == tnpc->GetOwner()->CastToClient()->CharacterID()) - return FACTION_AMIABLY; - else - return FACTION_INDIFFERENTLY; - } - - //First get the NPC's Primary faction - if(pFaction > 0) - { - //Get the faction data from the database - if(content_db.GetFactionData(&fmods, p_class, p_race, p_deity, pFaction)) - { - //Get the players current faction with pFaction - tmpFactionValue = GetCharacterFactionLevel(pFaction); - //Tack on any bonuses from Alliance type spell effects - tmpFactionValue += GetFactionBonus(pFaction); - tmpFactionValue += GetItemFactionBonus(pFaction); - //Return the faction to the client - fac = CalculateFaction(&fmods, tmpFactionValue); - } - } - else - { - return(FACTION_INDIFFERENTLY); - } - - // merchant fix - if (tnpc && tnpc->IsNPC() && tnpc->CastToNPC()->MerchantType && (fac == FACTION_THREATENINGLY || fac == FACTION_SCOWLS)) - fac = FACTION_DUBIOUSLY; - - if (tnpc != 0 && fac != FACTION_SCOWLS && tnpc->CastToNPC()->CheckAggro(this)) - fac = FACTION_THREATENINGLY; - - return fac; + for (auto &virtual_zone_point : zone->virtual_zone_point_list) { + float half_width = ((float) virtual_zone_point.width / 2); + + if ( + GetX() > (virtual_zone_point.x - half_width) && + GetX() < (virtual_zone_point.x + half_width) && + GetY() > (virtual_zone_point.y - half_width) && + GetY() < (virtual_zone_point.y + half_width) && + GetZ() >= (virtual_zone_point.z - 10) && + GetZ() < (virtual_zone_point.z + (float) virtual_zone_point.height) + ) { + + MovePC( + virtual_zone_point.target_zone_id, + virtual_zone_point.target_instance, + virtual_zone_point.target_x, + virtual_zone_point.target_y, + virtual_zone_point.target_z, + virtual_zone_point.target_heading + ); + + LogZonePoints( + "Virtual Zone Box Sending player [{}] to [{}]", + GetCleanName(), + ZoneLongName(virtual_zone_point.target_zone_id) + ); + } + } } -//Sets the characters faction standing with the specified NPC. -void Client::SetFactionLevel( - uint32 character_id, - uint32 npc_faction_id, - uint8 class_id, - uint8 race_id, - uint8 deity_id, - bool is_quest -) +void Client::ShowDevToolsMenu() { - auto l = zone->GetNPCFactionEntries(npc_faction_id); + std::string menu_search; + std::string menu_show; + std::string menu_reload_one; + std::string menu_reload_two; + std::string menu_reload_three; + std::string menu_reload_four; + std::string menu_reload_five; + std::string menu_reload_six; + std::string menu_reload_seven; + std::string menu_reload_eight; + std::string menu_reload_nine; + std::string menu_toggle; + std::string window_toggle; + + /** + * Search entity commands + */ + menu_search += Saylink::Silent("#list corpses", "Corpses"); + menu_search += " | " + Saylink::Silent("#list doors", "Doors"); + menu_search += " | " + Saylink::Silent("#finditem", "Items"); + menu_search += " | " + Saylink::Silent("#list npcs", "NPC"); + menu_search += " | " + Saylink::Silent("#list objects", "Objects"); + menu_search += " | " + Saylink::Silent("#list players", "Players"); + menu_search += " | " + Saylink::Silent("#findzone", "Zones"); + + /** + * Show + */ + menu_show += Saylink::Silent("#showzonepoints", "Zone Points"); + menu_show += " | " + Saylink::Silent("#showzonegloballoot", "Zone Global Loot"); + menu_show += " | " + Saylink::Silent("#show content_flags", "Content Flags"); + + /** + * Reload + */ + menu_reload_one += Saylink::Silent("#reload aa", "AAs"); + menu_reload_one += " | " + Saylink::Silent("#reload alternate_currencies", "Alternate Currencies"); + menu_reload_one += " | " + Saylink::Silent("#reload base_data", "Base Data"); + menu_reload_one += " | " + Saylink::Silent("#reload blocked_spells", "Blocked Spells"); + + menu_reload_two += Saylink::Silent("#reload commands", "Commands"); + menu_reload_two += " | " + Saylink::Silent("#reload content_flags", "Content Flags"); + + menu_reload_three += Saylink::Silent("#reload data_buckets_cache", "Databuckets"); + menu_reload_three += " | " + Saylink::Silent("#reload doors", "Doors"); + menu_reload_three += " | " + Saylink::Silent("#reload factions", "Factions"); + menu_reload_three += " | " + Saylink::Silent("#reload ground_spawns", "Ground Spawns"); + + menu_reload_four += Saylink::Silent("#reload logs", "Level Based Experience Modifiers"); + menu_reload_four += " | " + Saylink::Silent("#reload logs", "Log Settings"); + menu_reload_four += " | " + Saylink::Silent("#reload Loot", "Loot"); + + menu_reload_five += Saylink::Silent("#reload merchants", "Merchants"); + menu_reload_five += " | " + Saylink::Silent("#reload npc_emotes", "NPC Emotes"); + menu_reload_five += " | " + Saylink::Silent("#reload npc_spells", "NPC Spells"); + menu_reload_five += " | " + Saylink::Silent("#reload objects", "Objects"); + menu_reload_five += " | " + Saylink::Silent("#reload opcodes", "Opcodes"); + + menu_reload_six += Saylink::Silent("#reload perl_export", "Perl Event Export Settings"); + menu_reload_six += " | " + Saylink::Silent("#reload quest", "Quests"); + + menu_reload_seven += Saylink::Silent("#reload rules", "Rules"); + menu_reload_seven += " | " + Saylink::Silent("#reload skill_caps", "Skill Caps"); + menu_reload_seven += " | " + Saylink::Silent("#reload static", "Static Zone Data"); + menu_reload_seven += " | " + Saylink::Silent("#reload tasks", "Tasks"); + + menu_reload_eight += Saylink::Silent("#reload titles", "Titles"); + menu_reload_eight += " | " + Saylink::Silent("#reload traps 1", "Traps"); + menu_reload_eight += " | " + Saylink::Silent("#reload variables", "Variables"); + menu_reload_eight += " | " + Saylink::Silent("#reload veteran_rewards", "Veteran Rewards"); + + menu_reload_nine += Saylink::Silent("#reload world", "World"); + menu_reload_nine += " | " + Saylink::Silent("#reload zone", "Zone"); + menu_reload_nine += " | " + Saylink::Silent("#reload zone_points", "Zone Points"); + + /** + * Show window status + */ + menu_toggle = Saylink::Silent("#devtools menu enable", "Enable"); + if (IsDevToolsEnabled()) { + menu_toggle = Saylink::Silent("#devtools menu disable", "Disable"); + } - if (l.empty()) { - return; - } + window_toggle = Saylink::Silent("#devtools window enable", "Enable"); + if (GetDisplayMobInfoWindow()) { + window_toggle = Saylink::Silent("#devtools window disable", "Disable"); + } - int current_value; + /** + * Print menu + */ + SendChatLineBreak(); - for (auto& e : l) { - if (e.faction_id <= 0 || e.value == 0) { - continue; - } + Message(Chat::White, "Developer Tools Menu"); - int faction_before; - int faction_minimum; - int faction_maximum; + Message( + Chat::White, + fmt::format( + "Show Menu | {}", + Saylink::Silent("#dev") + ).c_str() + ); - FactionMods faction_modifiers; + Message( + Chat::White, + fmt::format( + "Toggle Menu | {}", + menu_toggle + ).c_str() + ); - content_db.GetFactionData(&faction_modifiers, class_id, race_id, deity_id, e.faction_id); + Message( + Chat::White, + fmt::format( + "Toggle Window | {}", + window_toggle + ).c_str() + ); - if (is_quest) { - if (e.value > 0) { - e.value = -std::abs(e.value); - } else if (e.value < 0) { - e.value = std::abs(e.value); - } - } + Message( + Chat::White, + fmt::format( + "Search | {}", + menu_search + ).c_str() + ); - // Adjust the amount you can go up or down so the resulting range - // is PERSONAL_MAX - PERSONAL_MIN - // - // Adjust these values for cases where starting faction is below - // min or above max by not allowing any earn in those directions. - faction_minimum = faction_modifiers.min - faction_modifiers.base; - faction_minimum = std::min(0, faction_minimum); + Message( + Chat::White, + fmt::format( + "Show | {}", + menu_show + ).c_str() + ); - faction_maximum = faction_modifiers.max - faction_modifiers.base; - faction_maximum = std::max(0, faction_maximum); + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_one + ).c_str() + ); - // Get the characters current value with that faction - current_value = GetCharacterFactionLevel(e.faction_id); - faction_before = current_value; + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_two + ).c_str() + ); -#ifdef LUA_EQEMU - int32 lua_ret = 0; - bool ignore_default = false; - lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, e.value, e.faction_id, current_value, e.temp, faction_minimum, faction_maximum, ignore_default); + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_three + ).c_str() + ); - if (ignore_default) { - e.value = lua_ret; - } -#endif + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_four + ).c_str() + ); - UpdatePersonalFaction( - character_id, - e.value, - e.faction_id, - ¤t_value, - e.temp, - faction_minimum, - faction_maximum - ); - - SendFactionMessage( - e.value, - e.faction_id, - faction_before, - current_value, - e.temp, - faction_minimum, - faction_maximum - ); - } -} + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_five + ).c_str() + ); -void Client::SetFactionLevel2(uint32 char_id, int32 faction_id, uint8 char_class, uint8 char_race, uint8 char_deity, int32 value, uint8 temp) -{ - int32 current_value; - - //Get the npc faction list - if(faction_id > 0 && value != 0) { - int32 faction_before_hit; - FactionMods fm; - int32 this_faction_max; - int32 this_faction_min; - - // Find out starting faction for this faction - // It needs to be used to adj max and min personal - // The range is still the same, 1200-3000(4200), but adjusted for base - content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), - faction_id); - - // Adjust the amount you can go up or down so the resulting range - // is PERSONAL_MAX - PERSONAL_MIN - // - // Adjust these values for cases where starting faction is below - // min or above max by not allowing any earn/loss in those directions. - // At least one faction starts out way below min, so we don't want - // to allow loses in those cases, just massive gains. - this_faction_min = fm.min - fm.base; - this_faction_min = std::min(0, this_faction_min); - this_faction_max = fm.max - fm.base; - this_faction_max = std::max(0, this_faction_max); - - //Get the faction modifiers - current_value = GetCharacterFactionLevel(faction_id); - faction_before_hit = current_value; + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_six + ).c_str() + ); -#ifdef LUA_EQEMU - int32 lua_ret = 0; - bool ignore_default = false; - lua_ret = LuaParser::Instance()->UpdatePersonalFaction(this, value, faction_id, current_value, temp, this_faction_min, this_faction_max, ignore_default); + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_seven + ).c_str() + ); - if (ignore_default) { - value = lua_ret; - } -#endif + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_eight + ).c_str() + ); - UpdatePersonalFaction(char_id, value, faction_id, ¤t_value, temp, this_faction_min, this_faction_max); + Message( + Chat::White, + fmt::format( + "Reload | {}", + menu_reload_nine + ).c_str() + ); - //Message(Chat::Lime, "Min(%d) Max(%d) Before(%d), After(%d)\n", this_faction_min, this_faction_max, faction_before_hit, current_value); + auto help_link = Saylink::Silent("#help"); - SendFactionMessage(value, faction_id, faction_before_hit, current_value, temp, this_faction_min, this_faction_max); - } + Message( + Chat::White, + fmt::format( + "Note: You can search for commands with {} [Search String]", + help_link + ).c_str() + ); - return; -} + SendChatLineBreak(); -int32 Client::GetCharacterFactionLevel(int32 faction_id) -{ - if (faction_id <= 0) - return 0; - faction_map::iterator res; - res = factionvalues.find(faction_id); - if (res == factionvalues.end()) - return 0; - return res->second; -} + Message( + Chat::White, + fmt::format( + "Current Expansion | {} ({})", + content_service.GetCurrentExpansionName(), + content_service.GetCurrentExpansion() + ).c_str() + ); -// Common code to set faction level. -// Applies HeroicCHA is it applies -// Checks for bottom out and max faction and old faction db entries -// Updates the faction if we are not minned, maxed or we need to repair -void Client::UpdatePersonalFaction(int32 char_id, int32 npc_value, int32 faction_id, int32 *current_value, int32 temp, int32 this_faction_min, int32 this_faction_max) -{ - bool repair = false; - bool change = false; - - if (itembonuses.HeroicCHA) - { - int faction_mod = itembonuses.HeroicCHA / 5; - // If our result isn't truncated, then just do that - if (npc_value * faction_mod / 100 != 0) - npc_value += npc_value * faction_mod / 100; - // If our result is truncated, then double a mob's value every once and a while to equal what they would have got - else - { - if (zone->random.Int(0, 100) < faction_mod) - npc_value *= 2; - } - } - - // Set flag when to update db - // Repair needed, as db changes could modify a base value for a faction - // and we need to auto correct when that happens. - if (*current_value > this_faction_max) - { - *current_value = this_faction_max; - repair = true; - } - else if (*current_value < this_faction_min) - { - *current_value = this_faction_min; - repair = true; - } - else if ((m_pp.gm != 1) && (npc_value != 0) && - ((npc_value > 0 && *current_value != this_faction_max) || - ((npc_value < 0 && *current_value != this_faction_min)))) - change = true; - - if (change || repair) - { - *current_value += npc_value; - - if (*current_value > this_faction_max) - *current_value = this_faction_max; - else if (*current_value < this_faction_min) - *current_value = this_faction_min; - - database.SetCharacterFactionLevel(char_id, faction_id, *current_value, temp, factionvalues); - } + auto z = GetZoneVersionWithFallback(zone->GetZoneID(), zone->GetInstanceVersion()); + + if (z) { + Message( + Chat::White, + fmt::format( + "Current Zone | [{}] ({}) version [{}] instance_id [{}] min/max expansion ({}/{}) content_flags [{}]", + z->short_name, + z->long_name, + z->version, + zone->GetInstanceID(), + z->min_expansion, + z->max_expansion, + z->content_flags + ).c_str() + ); + } -return; + SendChatLineBreak(); } -// returns the character's faction level, adjusted for racial, class, and deity modifiers -int32 Client::GetModCharacterFactionLevel(int32 faction_id) { - int32 Modded = GetCharacterFactionLevel(faction_id); - FactionMods fm; - if (content_db.GetFactionData(&fm, GetClass(), GetFactionRace(), GetDeity(), faction_id)) - { - Modded += fm.base + fm.class_mod + fm.race_mod + fm.deity_mod; - - //Tack on any bonuses from Alliance type spell effects - Modded += GetFactionBonus(faction_id); - Modded += GetItemFactionBonus(faction_id); - } - - return Modded; +void Client::SendChatLineBreak(uint16 color) { + Message(color, "------------------------------------------------"); } -void Client::MerchantRejectMessage(Mob *merchant, int primaryfaction) +void Client::SendCrossZoneMessage( + Client* client, const std::string& character_name, uint16_t chat_type, const std::string& message) { - int messageid = 0; - int32 tmpFactionValue = 0; - int32 lowestvalue = 0; - FactionMods fmod; - - // If a faction is involved, get the data. - if (primaryfaction > 0) { - if (content_db.GetFactionData(&fmod, GetClass(), GetFactionRace(), GetDeity(), primaryfaction)) { - tmpFactionValue = GetCharacterFactionLevel(primaryfaction); - lowestvalue = std::min(std::min(tmpFactionValue, fmod.deity_mod), - std::min(fmod.class_mod, fmod.race_mod)); - } - } - // If no primary faction or biggest influence is your faction hit - if (primaryfaction <= 0 || lowestvalue == tmpFactionValue) { - merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS6)); - } else if (lowestvalue == fmod.race_mod) { // race biggest - // Non-standard race (ex. illusioned to wolf) - if (GetRace() > PLAYER_RACE_COUNT) { - messageid = zone->random.Int(1, 3); // these aren't sequential StringIDs :( - switch (messageid) { - case 1: - messageid = WONT_SELL_NONSTDRACE1; - break; - case 2: - messageid = WONT_SELL_NONSTDRACE2; - break; - case 3: - messageid = WONT_SELL_NONSTDRACE3; - break; - default: // w/e should never happen - messageid = WONT_SELL_NONSTDRACE1; - break; - } - merchant->SayString(messageid); - } else { // normal player races - messageid = zone->random.Int(1, 4); - switch (messageid) { - case 1: - messageid = WONT_SELL_RACE1; - break; - case 2: - messageid = WONT_SELL_RACE2; - break; - case 3: - messageid = WONT_SELL_RACE3; - break; - case 4: - messageid = WONT_SELL_RACE4; - break; - default: // w/e should never happen - messageid = WONT_SELL_RACE1; - break; - } - merchant->SayString(messageid, itoa(GetRace())); - } - } else if (lowestvalue == fmod.class_mod) { - merchant->SayString(zone->random.Int(WONT_SELL_CLASS1, WONT_SELL_CLASS5), itoa(GetClass())); - } else { - // Must be deity - these two sound the best for that. - // Can't use a message with a field, GUI wants class/race names. - // for those message IDs. These are straight text. - merchant->SayString(zone->random.Int(WONT_SELL_DEEDS1, WONT_SELL_DEEDS2)); - } - return; -} + // if client is null, falls back to sending a cross zone message by name + if (!client && !character_name.empty()) + { + client = entity_list.GetClientByName(character_name.c_str()); + } -//o-------------------------------------------------------------- -//| Name: SendFactionMessage -//o-------------------------------------------------------------- -//| Purpose: Send faction change message to client -//o-------------------------------------------------------------- -void Client::SendFactionMessage(int32 tmpvalue, int32 faction_id, int32 faction_before_hit, int32 totalvalue, uint8 temp, int32 this_faction_min, int32 this_faction_max) -{ - char name[50]; - int32 faction_value; - - // If we're dropping from MAX or raising from MIN or repairing, - // we should base the message on the new updated value so we don't show - // a min MAX message - // - // If we're changing any other place, we use the value before the - // hit. For example, if we go from 1199 to 1200 which is the MAX - // we still want to say faction got better this time around. - - if (!EQ::ValueWithin(faction_before_hit, this_faction_min, this_faction_max)) { - faction_value = totalvalue; - } else { - faction_value = faction_before_hit; - } - - // default to Faction# if we couldn't get the name from the ID - if (!content_db.GetFactionName(faction_id, name, sizeof(name))) { - snprintf(name, sizeof(name), "Faction%i", faction_id); - } - - if (tmpvalue == 0 || temp == 1 || temp == 2) { - return; - } else if (faction_value >= this_faction_max) { - MessageString(Chat::Yellow, FACTION_BEST, name); - } else if (faction_value <= this_faction_min) { - MessageString(Chat::Yellow, FACTION_WORST, name); - } else if (tmpvalue > 0 && !RuleB(Client, UseLiveFactionMessage)) { - MessageString(Chat::Yellow, FACTION_BETTER, name); - } else if (tmpvalue < 0 && !RuleB(Client, UseLiveFactionMessage)) { - MessageString(Chat::Yellow, FACTION_WORSE, name); - } else if (RuleB(Client, UseLiveFactionMessage)) { - Message( - Chat::Yellow, - fmt::format( - "Your faction standing with {} has been adjusted by {}.", - name, - tmpvalue - ).c_str() - ); - } //New Live faction message (14261) + if (client) + { + client->Message(chat_type, message.c_str()); + } + else if (!character_name.empty() && !message.empty()) + { + uint32_t pack_size = sizeof(CZMessage_Struct); + auto pack = std::make_unique(ServerOP_CZMessage, pack_size); + auto buf = reinterpret_cast(pack->pBuffer); + uint8 update_type = CZUpdateType_Character; + int update_identifier = 0; + buf->update_type = update_type; + buf->update_identifier = update_identifier; + buf->type = chat_type; + strn0cpy(buf->message, message.c_str(), sizeof(buf->message)); + strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); + + worldserver.SendPacket(pack.get()); + } } -void Client::LoadAccountFlags() +void Client::SendCrossZoneMessageString( + Client* client, const std::string& character_name, uint16_t chat_type, + uint32_t string_id, const std::initializer_list& arguments) { - accountflags.clear(); + // if client is null, falls back to sending a cross zone message by name + if (!client && !character_name.empty()) // double check client isn't in this zone + { + client = entity_list.GetClientByName(character_name.c_str()); + } - const auto& l = AccountFlagsRepository::GetWhere(database, fmt::format("p_accid = {}", account_id)); - if (l.empty()) { - return; - } + if (!client && character_name.empty()) + { + return; + } + + SerializeBuffer argument_buffer; + for (const auto& argument : arguments) + { + argument_buffer.WriteString(argument); + } - for (const auto& e : l) { - accountflags[e.p_flag] = e.p_value; - } + uint32_t args_size = static_cast(argument_buffer.size()); + uint32_t pack_size = sizeof(CZClientMessageString_Struct) + args_size; + auto pack = std::make_unique(ServerOP_CZClientMessageString, pack_size); + auto buf = reinterpret_cast(pack->pBuffer); + buf->string_id = string_id; + buf->chat_type = chat_type; + strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); + buf->args_size = args_size; + memcpy(buf->args, argument_buffer.buffer(), argument_buffer.size()); + + if (client) + { + client->MessageString(buf); + } + else + { + worldserver.SendPacket(pack.get()); + } } -void Client::ClearAccountFlag(const std::string& flag) +void Client::SendDynamicZoneUpdates() { - auto e = AccountFlagsRepository::NewEntity(); - - e.p_accid = account_id; - e.p_flag = flag; + // bit inefficient since each do lookups but it avoids duplicating code here + SendDzCompassUpdate(); + SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Online); - AccountFlagsRepository::ClearFlag(database, e); -} + m_expedition_lockouts = ExpeditionDatabase::LoadCharacterLockouts(CharacterID()); -void Client::SetAccountFlag(const std::string& flag, const std::string& value) -{ - auto e = AccountFlagsRepository::NewEntity(); + // expeditions are the only dz type that keep the window updated + auto expedition = GetExpedition(); + if (expedition) + { + expedition->GetDynamicZone()->SendClientWindowUpdate(this); - e.p_accid = account_id; - e.p_flag = flag; - e.p_value = value; + // live synchronizes lockouts obtained during the active expedition to + // members once they zone into the expedition's dynamic zone instance + if (expedition->GetDynamicZone()->IsCurrentZoneDzInstance()) + { + expedition->SyncCharacterLockouts(CharacterID(), m_expedition_lockouts); + } + } - AccountFlagsRepository::ReplaceFlag(database, e); + SendExpeditionLockoutTimers(); - accountflags[flag] = value; + // ask world for any pending invite we saved from a previous zone + RequestPendingExpeditionInvite(); } -std::string Client::GetAccountFlag(const std::string& flag) +Expedition* Client::CreateExpedition(DynamicZone& dz, bool disable_messages) { - return accountflags[flag]; + return Expedition::TryCreate(this, dz, disable_messages); } -std::vector Client::GetAccountFlags() +Expedition* Client::CreateExpedition( + const std::string& zone_name, uint32 version, uint32 duration, const std::string& expedition_name, + uint32 min_players, uint32 max_players, bool disable_messages) { - std::vector l; - - l.reserve(accountflags.size()); + DynamicZone dz{ ZoneID(zone_name), version, duration, DynamicZoneType::Expedition }; + dz.SetName(expedition_name); + dz.SetMinPlayers(min_players); + dz.SetMaxPlayers(max_players); - for (const auto& e : accountflags) { - l.emplace_back(e.first); - } - - return l; + return Expedition::TryCreate(this, dz, disable_messages); } -void Client::ItemTimerCheck() +Expedition* Client::CreateExpeditionFromTemplate(uint32_t dz_template_id) { - int i; - for (i = EQ::invslot::POSSESSIONS_BEGIN; i <= EQ::invslot::POSSESSIONS_END; i++) - { - TryItemTimer(i); - } - for (i = EQ::invbag::GENERAL_BAGS_BEGIN; i <= EQ::invbag::CURSOR_BAG_END; i++) - { - TryItemTimer(i); - } + Expedition* expedition = nullptr; + auto it = zone->dz_template_cache.find(dz_template_id); + if (it != zone->dz_template_cache.end()) + { + DynamicZone dz(DynamicZoneType::Expedition); + dz.LoadTemplate(it->second); + expedition = Expedition::TryCreate(this, dz, false); + } + return expedition; } -void Client::TryItemTimer(int slot) +void Client::CreateTaskDynamicZone(int task_id, DynamicZone& dz_request) { - EQ::ItemInstance* inst = m_inv.GetItem(slot); - if(!inst) { - return; - } - - auto item_timers = inst->GetTimers(); - auto it_iter = item_timers.begin(); - while(it_iter != item_timers.end()) { - if(it_iter->second.Check()) { - if (parse->ItemHasQuestSub(inst, EVENT_TIMER)) { - parse->EventItem(EVENT_TIMER, this, inst, nullptr, it_iter->first, 0); - } - } - ++it_iter; - } - - if (slot > EQ::invslot::EQUIPMENT_END) { - return; - } - - for (int x = EQ::invaug::SOCKET_BEGIN; x <= EQ::invaug::SOCKET_END; ++x) - { - EQ::ItemInstance * a_inst = inst->GetAugment(x); - if(!a_inst) { - continue; - } - - auto& item_timers = a_inst->GetTimers(); - auto it_iter = item_timers.begin(); - while(it_iter != item_timers.end()) { - if(it_iter->second.Check()) { - if (parse->ItemHasQuestSub(a_inst, EVENT_TIMER)) { - parse->EventItem(EVENT_TIMER, this, a_inst, nullptr, it_iter->first, 0); - } - } - ++it_iter; - } - } + if (task_state) + { + task_state->CreateTaskDynamicZone(this, task_id, dz_request); + } } -void Client::SendItemScale(EQ::ItemInstance *inst) { - int slot = m_inv.GetSlotByItemInst(inst); - if(slot != -1) { - inst->ScaleItem(); - SendItemPacket(slot, inst, ItemPacketCharmUpdate); - CalcBonuses(); - } +Expedition* Client::GetExpedition() const +{ + if (zone && m_expedition_id) + { + auto expedition_cache_iter = zone->expedition_cache.find(m_expedition_id); + if (expedition_cache_iter != zone->expedition_cache.end()) + { + return expedition_cache_iter->second.get(); + } + } + return nullptr; } -void Client::AddRespawnOption(std::string option_name, uint32 zoneid, uint16 instance_id, float x, float y, float z, float heading, bool initial_selection, int8 position) +void Client::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db) { - //If respawn window is already open, any changes would create an inconsistency with the client - if (IsHoveringForRespawn()) { return; } - - if (zoneid == 0) - zoneid = zone->GetZoneID(); - - //Create respawn option - RespawnOption res_opt; - res_opt.name = option_name; - res_opt.zone_id = zoneid; - res_opt.instance_id = instance_id; - res_opt.x = x; - res_opt.y = y; - res_opt.z = z; - res_opt.heading = heading; - - if (position == -1 || position >= respawn_options.size()) - { - //No position specified, or specified beyond the end, simply append - respawn_options.push_back(res_opt); - //Make this option the initial selection for the window if desired - if (initial_selection) - initial_respawn_selection = static_cast(respawn_options.size()) - 1; - } - else if (position == 0) - { - respawn_options.push_front(res_opt); - if (initial_selection) - initial_respawn_selection = 0; - } - else - { - //Insert new option between existing options - std::list::iterator itr; - uint8 pos = 0; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - if (pos++ == position) - { - respawn_options.insert(itr,res_opt); - //Make this option the initial selection for the window if desired - if (initial_selection) - initial_respawn_selection = pos; - return; - } - } - } + // todo: support for account based lockouts like live AoC expeditions + + // if client already has this lockout, we're replacing it with the new one + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& existing_lockout) { + return existing_lockout.IsSameLockout(lockout); + } + ), m_expedition_lockouts.end()); + + m_expedition_lockouts.emplace_back(lockout); + + if (update_db) // for quest api + { + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { lockout }); + } + + SendExpeditionLockoutTimers(); } -bool Client::RemoveRespawnOption(std::string option_name) +void Client::AddNewExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, uint32_t seconds, std::string uuid) { - //If respawn window is already open, any changes would create an inconsistency with the client - if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } - - bool had = false; - RespawnOption* opt = nullptr; - std::list::iterator itr; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - opt = &(*itr); - if (opt->name.compare(option_name) == 0) - { - itr = respawn_options.erase(itr); - had = true; - //could be more with the same name, so keep going... - } - } - return had; + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + AddExpeditionLockout(lockout, true); } -bool Client::RemoveRespawnOption(uint8 position) +void Client::AddExpeditionLockoutDuration( + const std::string& expedition_name, const std::string& event_name, int seconds, + const std::string& uuid, bool update_db) { - //If respawn window is already open, any changes would create an inconsistency with the client - if (IsHoveringForRespawn() || respawn_options.empty()) { return false; } - - //Easy cases first... - if (position == 0) - { - respawn_options.pop_front(); - return true; - } - else if (position == (respawn_options.size() - 1)) - { - respawn_options.pop_back(); - return true; - } - - std::list::iterator itr; - uint8 pos = 0; - for (itr = respawn_options.begin(); itr != respawn_options.end(); ++itr) - { - if (pos++ == position) - { - respawn_options.erase(itr); - return true; - } - } - return false; + auto it = std::find_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + }); + + if (it != m_expedition_lockouts.end()) + { + it->AddLockoutTime(seconds); + + if (!uuid.empty()) + { + it->SetUUID(uuid); + } + + if (update_db) + { + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { *it }); + } + + SendExpeditionLockoutTimers(); + } + else if (seconds > 0) // missing lockouts inserted for reductions would be instantly expired + { + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + AddExpeditionLockout(lockout, update_db); + } } -void Client::SetHunger(int32 in_hunger) +void Client::RemoveExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool update_db) { - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); - Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; - sta->food = in_hunger; - sta->water = m_pp.thirst_level > 6000 ? 6000 : m_pp.thirst_level; + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + } + ), m_expedition_lockouts.end()); - m_pp.hunger_level = in_hunger; + if (update_db) // for quest api + { + ExpeditionDatabase::DeleteCharacterLockout(CharacterID(), expedition_name, event_name); + } - QueuePacket(outapp); - safe_delete(outapp); + SendExpeditionLockoutTimers(); } -void Client::SetThirst(int32 in_thirst) +void Client::RemoveAllExpeditionLockouts(const std::string& expedition_name, bool update_db) { - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); - Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; - sta->food = m_pp.hunger_level > 6000 ? 6000 : m_pp.hunger_level; - sta->water = in_thirst; + if (expedition_name.empty()) + { + if (update_db) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID()); + } + m_expedition_lockouts.clear(); + } + else + { + if (update_db) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID(), expedition_name); + } + + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.GetExpeditionName() == expedition_name; + } + ), m_expedition_lockouts.end()); + } - m_pp.thirst_level = in_thirst; + SendExpeditionLockoutTimers(); +} - QueuePacket(outapp); - safe_delete(outapp); +const ExpeditionLockoutTimer* Client::GetExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) const +{ + for (const auto& expedition_lockout : m_expedition_lockouts) + { + if ((include_expired || !expedition_lockout.IsExpired()) && + expedition_lockout.IsSameLockout(expedition_name, event_name)) + { + return &expedition_lockout; + } + } + return nullptr; } -void Client::SetIntoxication(int32 in_intoxication) +std::vector Client::GetExpeditionLockouts( + const std::string& expedition_name, bool include_expired) +{ + std::vector lockouts; + for (const auto& lockout : m_expedition_lockouts) + { + if ((include_expired || !lockout.IsExpired()) && + lockout.GetExpeditionName() == expedition_name) + { + lockouts.emplace_back(lockout); + } + } + return lockouts; +} + +bool Client::HasExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) { - m_pp.intoxication = EQ::Clamp(in_intoxication, 0, 200); + return (GetExpeditionLockout(expedition_name, event_name, include_expired) != nullptr); } -void Client::SetConsumption(int32 in_hunger, int32 in_thirst) +void Client::SendExpeditionLockoutTimers() { - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_Stamina, sizeof(Stamina_Struct)); - Stamina_Struct* sta = (Stamina_Struct*)outapp->pBuffer; - sta->food = in_hunger; - sta->water = in_thirst; + std::vector lockout_entries; + + // client displays lockouts rounded down to nearest minute, send lockouts + // with 60s offset added to compensate (live does this too) + constexpr uint32_t rounding_seconds = 60; - m_pp.hunger_level = in_hunger; - m_pp.thirst_level = in_thirst; + // erases expired lockouts while building lockout timer list + for (auto it = m_expedition_lockouts.begin(); it != m_expedition_lockouts.end();) + { + uint32_t seconds_remaining = it->GetSecondsRemaining(); + if (seconds_remaining == 0) + { + it = m_expedition_lockouts.erase(it); + } + else + { + ExpeditionLockoutTimerEntry_Struct lockout; + strn0cpy(lockout.expedition_name, it->GetExpeditionName().c_str(), sizeof(lockout.expedition_name)); + lockout.seconds_remaining = seconds_remaining + rounding_seconds; + lockout.event_type = it->IsReplayTimer() ? Expedition::REPLAY_TIMER_ID : Expedition::EVENT_TIMER_ID; + strn0cpy(lockout.event_name, it->GetEventName().c_str(), sizeof(lockout.event_name)); + + lockout_entries.emplace_back(lockout); + ++it; + } + } - QueuePacket(outapp); - safe_delete(outapp); + uint32_t lockout_count = static_cast(lockout_entries.size()); + uint32_t lockout_entries_size = sizeof(ExpeditionLockoutTimerEntry_Struct) * lockout_count; + uint32_t outsize = sizeof(ExpeditionLockoutTimers_Struct) + lockout_entries_size; + auto outapp = std::make_unique(OP_DzExpeditionLockoutTimers, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = lockout_count; + if (!lockout_entries.empty()) + { + memcpy(outbuf->timers, lockout_entries.data(), lockout_entries_size); + } + QueuePacket(outapp.get()); } -void Client::Consume(const EQ::ItemData *item, uint8 type, int16 slot, bool auto_consume) +void Client::RequestPendingExpeditionInvite() { - if (!item) - return; + uint32_t packsize = sizeof(ServerExpeditionCharacterID_Struct); + auto pack = std::make_unique(ServerOP_ExpeditionRequestInvite, packsize); + auto packbuf = reinterpret_cast(pack->pBuffer); + packbuf->character_id = CharacterID(); + worldserver.SendPacket(pack.get()); +} - int increase = item->CastTime_ * 100; - if (!auto_consume) // force feeding is half as effective - increase /= 2; +void Client::DzListTimers() +{ + // only lists player's current replay timer lockouts, not all event lockouts + bool found = false; + for (const auto& lockout : m_expedition_lockouts) + { + if (lockout.IsReplayTimer()) + { + found = true; + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + MessageString( + Chat::Yellow, DZLIST_REPLAY_TIMER, + time_remaining.days.c_str(), + time_remaining.hours.c_str(), + time_remaining.mins.c_str(), + lockout.GetExpeditionName().c_str() + ); + } + } - if (increase < 0) // wasn't food? oh well - return; + if (!found) + { + MessageString(Chat::Yellow, EXPEDITION_NO_TIMERS); + } +} - if (type == EQ::item::ItemTypeFood) { - m_pp.hunger_level += increase; +void Client::SetDzRemovalTimer(bool enable_timer) +{ + uint32_t timer_ms = RuleI(DynamicZone, ClientRemovalDelayMS); - LogFood("Consuming food, points added to hunger_level: [{}] - current_hunger: [{}]", increase, m_pp.hunger_level); + LogDynamicZones( + "Character [{}] instance [{}] removal timer enabled: [{}] delay (ms): [{}]", + CharacterID(), zone ? zone->GetInstanceID() : 0, enable_timer, timer_ms + ); - DeleteItemInInventory(slot, 1); + if (enable_timer) + { + dynamiczone_removal_timer.Start(timer_ms); + } + else + { + dynamiczone_removal_timer.Disable(); + } +} - if (!auto_consume) // no message if the client consumed for us - entity_list.MessageCloseString(this, true, 50, 0, EATING_MESSAGE, GetName(), item->Name); +void Client::SendDzCompassUpdate() +{ + // client may be associated with multiple dynamic zone compasses in this zone + std::vector compass_entries; - LogFood("Eating from slot: [{}]", (int)slot); + // need to sort by local doorid in case multiple have same dz switch id (live only sends first) + // todo: just store zone's door list ordered and ditch this + std::vector switches; + switches.reserve(entity_list.GetDoorsList().size()); + for (const auto& door_pair : entity_list.GetDoorsList()) + { + switches.push_back(door_pair.second); + } + std::sort(switches.begin(), switches.end(), + [](Doors* lhs, Doors* rhs) { return lhs->GetDoorID() < rhs->GetDoorID(); }); + + for (const auto& client_dz : GetDynamicZones()) + { + auto& compass = client_dz->GetCompassLocation(); + if (zone && zone->IsZone(compass.zone_id, 0)) + { + DynamicZoneCompassEntry_Struct entry{}; + entry.dz_zone_id = client_dz->GetZoneID(); + entry.dz_instance_id = client_dz->GetInstanceID(); + entry.dz_type = static_cast(client_dz->GetType()); + entry.x = compass.x; + entry.y = compass.y; + entry.z = compass.z; + + compass_entries.emplace_back(entry); + } - } else { - m_pp.thirst_level += increase; + // if client has a dz with a switch id add compass to any switch locs that share it + if (client_dz->GetSwitchID() != 0) + { + // live only sends one if multiple in zone have the same switch id + auto it = std::find_if(switches.begin(), switches.end(), + [&](const auto& eqswitch) { + return eqswitch->GetDzSwitchID() == client_dz->GetSwitchID(); + }); + + if (it != switches.end()) + { + DynamicZoneCompassEntry_Struct entry{}; + entry.dz_zone_id = client_dz->GetZoneID(); + entry.dz_instance_id = client_dz->GetInstanceID(); + entry.dz_type = static_cast(client_dz->GetType()); + entry.dz_switch_id = client_dz->GetSwitchID(); + entry.x = (*it)->GetX(); + entry.y = (*it)->GetY(); + entry.z = (*it)->GetZ(); + + compass_entries.emplace_back(entry); + } + } + } - DeleteItemInInventory(slot, 1); + // compass set via MarkSingleCompassLocation() + if (m_has_quest_compass) + { + DynamicZoneCompassEntry_Struct entry{}; + entry.dz_zone_id = 0; + entry.dz_instance_id = 0; + entry.dz_type = 0; + entry.x = m_quest_compass.x; + entry.y = m_quest_compass.y; + entry.z = m_quest_compass.z; + + compass_entries.emplace_back(entry); + } - LogFood("Consuming drink, points added to thirst_level: [{}] current_thirst: [{}]", increase, m_pp.thirst_level); + QueuePacket(CreateCompassPacket(compass_entries).get()); +} - if (!auto_consume) // no message if the client consumed for us - entity_list.MessageCloseString(this, true, 50, 0, DRINKING_MESSAGE, GetName(), item->Name); +std::unique_ptr Client::CreateCompassPacket( + const std::vector& compass_entries) +{ + uint32 count = static_cast(compass_entries.size()); + uint32 entries_size = sizeof(DynamicZoneCompassEntry_Struct) * count; + uint32 outsize = sizeof(DynamicZoneCompass_Struct) + entries_size; + auto outapp = std::make_unique(OP_DzCompass, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = count; + memcpy(outbuf->entries, compass_entries.data(), entries_size); - LogFood("Drinking from slot: [{}]", (int)slot); - } + return outapp; } -void Client::SendMarqueeMessage(uint32 type, std::string message, uint32 duration) +void Client::GoToDzSafeReturnOrBind(const DynamicZone* dynamic_zone) { - if (!duration || !message.length()) { - return; - } - - EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); - ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; + if (dynamic_zone) + { + auto safereturn = dynamic_zone->GetSafeReturnLocation(); + if (safereturn.zone_id != 0) + { + LogDynamicZonesDetail("Sending [{}] to safereturn zone [{}]", CharacterID(), safereturn.zone_id); + MovePC(safereturn.zone_id, 0, safereturn.x, safereturn.y, safereturn.z, safereturn.heading); + return; + } + } - cms->type = type; - cms->unk04 = 10; - cms->priority = 510; - cms->fade_in_time = 0; - cms->fade_out_time = 3000; - cms->duration = duration; + GoToBind(); +} - strcpy(cms->msg, message.c_str()); +void Client::AddDynamicZoneID(uint32_t dz_id) +{ + auto it = std::find_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), + [&](uint32_t current_dz_id) { return current_dz_id == dz_id; }); - QueuePacket(&outapp); + if (it == m_dynamic_zone_ids.end()) + { + LogDynamicZonesDetail("Adding dz [{}] to client [{}]", dz_id, GetName()); + m_dynamic_zone_ids.push_back(dz_id); + } } -void Client::SendMarqueeMessage(uint32 type, uint32 priority, uint32 fade_in, uint32 fade_out, uint32 duration, std::string message) +void Client::RemoveDynamicZoneID(uint32_t dz_id) { - if (!duration || !message.length()) { - return; - } - - EQApplicationPacket outapp(OP_Marquee, sizeof(ClientMarqueeMessage_Struct) + message.length()); - ClientMarqueeMessage_Struct* cms = (ClientMarqueeMessage_Struct*) outapp.pBuffer; + LogDynamicZonesDetail("Removing dz [{}] from client [{}]", dz_id, GetName()); + m_dynamic_zone_ids.erase(std::remove_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), + [&](uint32_t current_dz_id) { return current_dz_id == dz_id; } + ), m_dynamic_zone_ids.end()); +} - cms->type = type; - cms->unk04 = 10; - cms->priority = priority; - cms->fade_in_time = fade_in; - cms->fade_out_time = fade_out; - cms->duration = duration; +std::vector Client::GetDynamicZones(uint32_t zone_id, int zone_version) +{ + std::vector client_dzs; - strcpy(cms->msg, message.c_str()); + for (uint32_t dz_id : m_dynamic_zone_ids) + { + auto dz = DynamicZone::FindDynamicZoneByID(dz_id); + if (dz && + (zone_id == 0 || dz->GetZoneID() == zone_id) && + (zone_version < 0 || dz->GetZoneVersion() == zone_version)) + { + client_dzs.emplace_back(dz); + } + } - QueuePacket(&outapp); + return client_dzs; } -void Client::PlayMP3(const char* fname) +void Client::SetDynamicZoneMemberStatus(DynamicZoneMemberStatus status) { - std::string filename = fname; - auto outapp = new EQApplicationPacket(OP_PlayMP3, filename.length() + 1); - PlayMP3_Struct* buf = (PlayMP3_Struct*)outapp->pBuffer; - strncpy(buf->filename, fname, filename.length()); - QueuePacket(outapp); - safe_delete(outapp); + // sets status on all associated dzs client may have. if client is online + // inside a dz, only that dz has the "In Dynamic Zone" status set + for (auto& dz : GetDynamicZones()) + { + // the rule to disable this status is handled internally by the dz + if (status == DynamicZoneMemberStatus::Online && dz->IsCurrentZoneDzInstance()) + { + status = DynamicZoneMemberStatus::InDynamicZone; + } + dz->SetMemberStatus(CharacterID(), status); + } } -void Client::ExpeditionSay(const char *str, int ExpID) { +void Client::MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid) +{ + if (zone_id == 0) + { + return; + } - std::string query = StringFormat("SELECT `player_name` FROM `cust_inst_players` " - "WHERE `inst_id` = %i", ExpID); - auto results = database.QueryDatabase(query); - if (!results.Success()) - return; - - if(results.RowCount() == 0) { - Message(Chat::Lime, "You say to the expedition, '%s'", str); - return; - } - - for(auto row = results.begin(); row != results.end(); ++row) { - const char* charName = row[0]; - if(strcmp(charName, GetCleanName()) != 0) { - worldserver.SendEmoteMessage( - charName, - 0, - AccountStatus::Player, - Chat::Lime, - fmt::format( - "{} says to the expedition, '{}'", - GetCleanName(), - str - ).c_str() - ); - } - // ChannelList->CreateChannel(ChannelName, ChannelOwner, ChannelPassword, true, Strings::ToInt(row[3])); - } + auto client_dzs = GetDynamicZones(zone_id, zone_version); + if (client_dzs.empty()) + { + if (msg_if_invalid) + { + MessageString(Chat::Red, DYNAMICZONE_WAY_IS_BLOCKED); // unconfirmed message + } + } + else if (client_dzs.size() == 1) + { + auto dz = client_dzs.front(); + DynamicZoneLocation zonein = dz->GetZoneInLocation(); + ZoneMode zone_mode = dz->HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords; + MovePC(zone_id, dz->GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode); + } + else + { + LogDynamicZonesDetail("Sending DzSwitchListWnd to [{}] for zone [{}] with [{}] dynamic zone(s)", + CharacterID(), zone_id, client_dzs.size()); + // client has more than one dz for this zone, send out the switchlist window + QueuePacket(CreateDzSwitchListPacket(client_dzs).get()); + } } -int Client::GetQuiverHaste(int delay) +bool Client::TryMovePCDynamicZoneSwitch(int dz_switch_id) { - const EQ::ItemInstance *pi = nullptr; - for (int r = EQ::invslot::GENERAL_BEGIN; r <= EQ::invslot::GENERAL_END; r++) { - pi = GetInv().GetItem(r); - if (pi && pi->IsClassBag() && pi->GetItem()->BagType == EQ::item::BagTypeQuiver && - pi->GetItem()->BagWR > 0) - break; - if (r == EQ::invslot::GENERAL_END) - // we will get here if we don't find a valid quiver - return 0; - } - return (pi->GetItem()->BagWR * 0.0025f * delay) + 1; -} + auto client_dzs = GetDynamicZones(); -void Client::SendColoredText(uint32 color, std::string message) -{ - // arbitrary size limit - if (message.size() > 512) // live does send this with empty strings sometimes ... - return; - auto outapp = new EQApplicationPacket(OP_ColoredText, sizeof(ColoredText_Struct) + message.size()); - ColoredText_Struct *cts = (ColoredText_Struct *)outapp->pBuffer; - cts->color = color; - strcpy(cts->msg, message.c_str()); - QueuePacket(outapp); - safe_delete(outapp); + std::vector switch_dzs; + auto it = std::copy_if(client_dzs.begin(), client_dzs.end(), std::back_inserter(switch_dzs), + [&](const DynamicZone* dz) { return dz->GetSwitchID() == dz_switch_id; }); + + if (switch_dzs.size() == 1) + { + LogDynamicZonesDetail("Moving client [{}] to dz with switch id [{}]", GetName(), dz_switch_id); + switch_dzs.front()->MovePCInto(this, true); + } + else if (switch_dzs.size() > 1) + { + QueuePacket(CreateDzSwitchListPacket(switch_dzs).get()); + } + + return !switch_dzs.empty(); } +std::unique_ptr Client::CreateDzSwitchListPacket( + const std::vector& client_dzs) +{ + uint32 count = static_cast(client_dzs.size()); + uint32 entries_size = sizeof(DynamicZoneChooseZoneEntry_Struct) * count; + uint32 outsize = sizeof(DynamicZoneChooseZone_Struct) + entries_size; + auto outapp = std::make_unique(OP_DzChooseZone, outsize); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = count; + for (int i = 0; i < client_dzs.size(); ++i) + { + outbuf->choices[i].dz_zone_id = client_dzs[i]->GetZoneID(); + outbuf->choices[i].dz_instance_id = client_dzs[i]->GetInstanceID(); + outbuf->choices[i].dz_type = static_cast(client_dzs[i]->GetType()); + strn0cpy(outbuf->choices[i].description, client_dzs[i]->GetName().c_str(), sizeof(outbuf->choices[i].description)); + strn0cpy(outbuf->choices[i].leader_name, client_dzs[i]->GetLeaderName().c_str(), sizeof(outbuf->choices[i].leader_name)); + } + return outapp; +} -void Client::QuestReward(Mob* target, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, uint32 itemid, uint32 exp, bool faction) +void Client::MovePCDynamicZone(const std::string& zone_name, int zone_version, bool msg_if_invalid) { + auto zone_id = ZoneID(zone_name.c_str()); + MovePCDynamicZone(zone_id, zone_version, msg_if_invalid); +} - auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); - memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); - QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; - - qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name - qr->target_id = GetID(); // The Client ID (this) - qr->copper = copper; - qr->silver = silver; - qr->gold = gold; - qr->platinum = platinum; - qr->item_id[0] = itemid; - qr->exp_reward = exp; +void Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clip_through_walls, bool calculate_speed) { + BuffFadeByEffect(SE_Levitate); + if (CheckLosFN(target_x, target_y, target_z, 6.0f) || ignore_los) { + auto p = new EQApplicationPacket(OP_Fling, sizeof(fling_struct)); + auto* f = (fling_struct*) p->pBuffer; + + if (!calculate_speed) { + f->speed_z = value; + } else { + auto speed = 1.0f; + const auto distance = CalculateDistance(target_x, target_y, target_z); + + auto z_diff = target_z - GetZ(); + if (z_diff != 0.0f) { + speed += std::abs(z_diff) / 12.0f; + } - if (copper > 0 || silver > 0 || gold > 0 || platinum > 0) { - AddMoneyToPP(copper, silver, gold, platinum); - } + speed += distance / 200.0f; - if (itemid > 0) { - SummonItemIntoInventory(itemid, -1, 0, 0, 0, 0, 0, 0, false); - } + speed++; - if (faction) { - if (target && target->IsNPC() && !target->IsCharmed()) { - int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); - SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); - qr->faction = target->CastToNPC()->GetPrimaryFaction(); - qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. - } - } + speed = std::abs(speed); - if (exp > 0) { - AddEXP(ExpSource::Quest, exp); - } + f->speed_z = speed; + } - QueuePacket(outapp, true, Client::CLIENT_CONNECTED); - safe_delete(outapp); + f->collision = clip_through_walls ? 0 : -1; + f->travel_time = -1; + f->unk3 = 1; + f->disable_fall_damage = 1; + f->new_y = target_y; + f->new_x = target_x; + f->new_z = target_z; + p->priority = 6; + FastQueuePacket(&p); + } } -void Client::QuestReward(Mob* target, const QuestReward_Struct &reward, bool faction) -{ - auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); - memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); - QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; - - memcpy(qr, &reward, sizeof(QuestReward_Struct)); +std::vector Client::GetLearnableDisciplines(uint8 min_level, uint8 max_level) { + std::vector learnable_disciplines; + for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { + bool learnable = true; + if (!IsValidSpell(spell_id)) { + continue; + } - // not set in caller because reasons - qr->mob_id = target ? target->GetID() : 0; // Entity ID for the from mob name, tasks won't set this + if (!IsDiscipline(spell_id)) { + continue; + } - if (reward.copper > 0 || reward.silver > 0 || reward.gold > 0 || reward.platinum > 0) { - AddMoneyToPP(reward.copper, reward.silver, reward.gold, reward.platinum); - } + if (spells[spell_id].classes[Class::Warrior] == 0) { + continue; + } - for (int i = 0; i < QUESTREWARD_COUNT; ++i) { - if (reward.item_id[i] > 0) { - SummonItemIntoInventory(reward.item_id[i], -1, 0, 0, 0, 0, 0, 0, false); - } - } + if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { + continue; + } - // only process if both are valid - // if we don't have a target here, we want to just reward, but if there is a target, need to check charm - if (reward.faction && reward.faction_mod && (target == nullptr || !target->IsCharmed())) { - RewardFaction(reward.faction, reward.faction_mod); - } + if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { + continue; + } - // legacy support - if (faction) { - if (target && target->IsNPC() && !target->IsCharmed()) { - int32 nfl_id = target->CastToNPC()->GetNPCFactionID(); - SetFactionLevel(CharacterID(), nfl_id, GetBaseClass(), GetBaseRace(), GetDeity(), true); - qr->faction = target->CastToNPC()->GetPrimaryFaction(); - qr->faction_mod = 1; // Too lazy to get real value, not even used by client anyhow. - } - } + if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { + continue; + } - if (reward.exp_reward > 0) { - AddEXP(ExpSource::Quest, reward.exp_reward); - } + if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { + continue; + } - QueuePacket(outapp, true, Client::CLIENT_CONNECTED); - safe_delete(outapp); -} + if (HasDisciplineLearned(spell_id)) { + continue; + } -void Client::CashReward(uint32 copper, uint32 silver, uint32 gold, uint32 platinum) -{ - auto outapp = std::make_unique(OP_CashReward, sizeof(CashReward_Struct)); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->copper = copper; - outbuf->silver = silver; - outbuf->gold = gold; - outbuf->platinum = platinum; + if (RuleB(Spells, EnableSpellGlobals) && !SpellGlobalCheck(spell_id, CharacterID())) { + learnable = false; + } else if (RuleB(Spells, EnableSpellBuckets) && !SpellBucketCheck(spell_id, CharacterID())) { + learnable = false; + } - AddMoneyToPP(copper, silver, gold, platinum); + if (learnable) { + learnable_disciplines.push_back(spell_id); + } + } + return learnable_disciplines; +} - QueuePacket(outapp.get()); +std::vector Client::GetLearnedDisciplines() { + std::vector learned_disciplines; + for (int index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (IsValidSpell(m_pp.disciplines.values[index])) { + learned_disciplines.push_back(m_pp.disciplines.values[index]); + } + } + return learned_disciplines; } -void Client::RewardFaction(int faction_id, int amount) -{ - SetFactionLevel2(CharacterID(), faction_id, GetClass(), GetBaseRace(), GetDeity(), amount, false); - - auto f = zone->GetFactionAssociation(faction_id); - if (!f) { - return; - } - - std::vector faction_ids = { - f->id_1, - f->id_2, - f->id_3, - f->id_4, - f->id_5, - f->id_6, - f->id_7, - f->id_8, - f->id_9, - f->id_10 - }; - - std::vector faction_modifiers = { - f->mod_1, - f->mod_2, - f->mod_3, - f->mod_4, - f->mod_5, - f->mod_6, - f->mod_7, - f->mod_8, - f->mod_9, - f->mod_10 - }; - - std::vector temporary_values = { - static_cast(faction_modifiers[0] * amount), - static_cast(faction_modifiers[1] * amount), - static_cast(faction_modifiers[2] * amount), - static_cast(faction_modifiers[3] * amount), - static_cast(faction_modifiers[4] * amount), - static_cast(faction_modifiers[5] * amount), - static_cast(faction_modifiers[6] * amount), - static_cast(faction_modifiers[7] * amount), - static_cast(faction_modifiers[8] * amount), - static_cast(faction_modifiers[9] * amount) - }; - - std::vector signs = { - temporary_values[0] < 0.0f ? -1 : 1, - temporary_values[1] < 0.0f ? -1 : 1, - temporary_values[2] < 0.0f ? -1 : 1, - temporary_values[3] < 0.0f ? -1 : 1, - temporary_values[4] < 0.0f ? -1 : 1, - temporary_values[5] < 0.0f ? -1 : 1, - temporary_values[6] < 0.0f ? -1 : 1, - temporary_values[7] < 0.0f ? -1 : 1, - temporary_values[8] < 0.0f ? -1 : 1, - temporary_values[9] < 0.0f ? -1 : 1 - }; - - std::vector new_values = { - std::max(1, static_cast(std::abs(temporary_values[0]))) * signs[0], - std::max(1, static_cast(std::abs(temporary_values[1]))) * signs[1], - std::max(1, static_cast(std::abs(temporary_values[2]))) * signs[2], - std::max(1, static_cast(std::abs(temporary_values[3]))) * signs[3], - std::max(1, static_cast(std::abs(temporary_values[4]))) * signs[4], - std::max(1, static_cast(std::abs(temporary_values[5]))) * signs[5], - std::max(1, static_cast(std::abs(temporary_values[6]))) * signs[6], - std::max(1, static_cast(std::abs(temporary_values[7]))) * signs[7], - std::max(1, static_cast(std::abs(temporary_values[8]))) * signs[8], - std::max(1, static_cast(std::abs(temporary_values[9]))) * signs[9] - }; - - for (uint16 slot_id = 0; slot_id < faction_ids.size(); slot_id++) { - if (faction_ids[slot_id] > 0) { - SetFactionLevel2( - CharacterID(), - faction_ids[slot_id], - GetClass(), - GetBaseRace(), - GetDeity(), - new_values[slot_id], - false - ); - } - } +std::vector Client::GetMemmedSpells() { + std::vector memmed_spells; + for (int index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { + if (IsValidSpell(m_pp.mem_spells[index])) { + memmed_spells.push_back(m_pp.mem_spells[index]); + } + } + return memmed_spells; } -void Client::SendHPUpdateMarquee(){ - if (!IsClient() || !current_hp || !max_hp) { - return; - } +std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { + std::vector scribeable_spells; + std::unordered_map> spell_group_cache = LoadSpellGroupCache(min_level, max_level); - /* Health Update Marquee Display: Custom*/ - const auto health_percentage = static_cast(current_hp * 100 / max_hp); - if (health_percentage >= 100) { - return; - } + for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { + bool scribeable = true; + if (!IsValidSpell(spell_id)) { + continue; + } - const auto health_update_notification = fmt::format("Health: {}%%", health_percentage); - SendMarqueeMessage(Chat::Yellow, 510, 0, 3000, 3000, health_update_notification); -} + if (IsDiscipline(spell_id)) { + continue; + } -uint32 Client::GetMoney(uint8 type, uint8 subtype) { - uint32 value = 0; - - switch (type) { - case MoneyTypes::Copper: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.copper); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.copper_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.copper_cursor); - break; - default: - break; - } - break; - } - case MoneyTypes::Silver: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.silver); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.silver_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.silver_cursor); - break; - default: - break; - } - break; - } - case MoneyTypes::Gold: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.gold); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.gold_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.gold_cursor); - break; - default: - break; - } - break; - } - case MoneyTypes::Platinum: { - switch (subtype) { - case MoneySubtypes::Personal: - value = static_cast(m_pp.platinum); - break; - case MoneySubtypes::Bank: - value = static_cast(m_pp.platinum_bank); - break; - case MoneySubtypes::Cursor: - value = static_cast(m_pp.platinum_cursor); - break; - case MoneySubtypes::SharedBank: - value = static_cast(m_pp.platinum_shared); - break; - default: - break; - } - break; - } - default: - break; - } - - return value; -} + if (spells[spell_id].classes[Class::Warrior] == 0) { + continue; + } -int Client::GetAccountAge() { - return (time(nullptr) - GetAccountCreation()); -} + if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { + continue; + } -void Client::CheckRegionTypeChanges() -{ - if (!zone->HasWaterMap()) { - return; - } + if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { + continue; + } - auto new_region = zone->watermap->ReturnRegionType(glm::vec3(m_Position)); + if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { + continue; + } - // still same region, do nothing - if (last_region_type == new_region) { - return; - } + if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { + continue; + } - // If we got out of water clear any water aggro for water only npcs - if (last_region_type == RegionTypeWater) { - entity_list.ClearWaterAggro(this); - } + if (HasSpellScribed(spell_id)) { + continue; + } - // region type changed - last_region_type = new_region; + if ( + RuleB(Spells, EnableSpellGlobals) && + !SpellGlobalCheck(spell_id, CharacterID()) + ) { + scribeable = false; + } else if ( + RuleB(Spells, EnableSpellBuckets) && + !SpellBucketCheck(spell_id, CharacterID()) + ) { + scribeable = false; + } - // PVP is the only state we need to keep track of, so we can just return now for PVP servers - if (RuleI(World, PVPSettings) > 0) { - return; - } + if (spells[spell_id].spell_group) { + const auto& g = spell_group_cache.find(spells[spell_id].spell_group); + if (g != spell_group_cache.end()) { + for (const auto& s : g->second) { + if ( + EQ::ValueWithin(spells[s].classes[m_pp.class_ - 1], min_level, max_level) && + s == spell_id && + scribeable + ) { + scribeable_spells.push_back(spell_id); + } + continue; + } + } + } else if (scribeable) { + scribeable_spells.push_back(spell_id); + } + } + return scribeable_spells; +} - if (last_region_type == RegionTypePVP && RuleB(World, EnablePVPRegions)) { - temp_pvp = true; - } else if (temp_pvp) { - temp_pvp = false; - } +std::vector Client::GetScribedSpells() { + std::vector scribed_spells; + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + if (IsValidSpell(m_pp.spell_book[index])) { + scribed_spells.push_back(m_pp.spell_book[index]); + } + } + return scribed_spells; } -void Client::ProcessAggroMeter() -{ - if (!AggroMeterAvailable()) { - aggro_meter_timer.Disable(); - return; - } - - // we need to decide if we need to send OP_AggroMeterTargetInfo now - // This packet sends the current lock target ID and the current target ID - // target ID will be either our target or our target of target when we're targeting a PC - bool send_targetinfo = false; - auto cur_tar = GetTarget(); - - // probably should have PVP rules ... - if (cur_tar && cur_tar != this) { - if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerClient() && cur_tar->GetID() != m_aggrometer.get_target_id()) { - m_aggrometer.set_target_id(cur_tar->GetID()); - send_targetinfo = true; - } else if ((cur_tar->IsPetOwnerClient() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { - m_aggrometer.set_target_id(cur_tar->GetTarget()->GetID()); - send_targetinfo = true; - } - } else if (m_aggrometer.get_target_id()) { - m_aggrometer.set_target_id(0); - send_targetinfo = true; - } - - if (m_aggrometer.update_lock()) - send_targetinfo = true; - - if (send_targetinfo) { - auto app = new EQApplicationPacket(OP_AggroMeterTargetInfo, sizeof(uint32) * 2); - app->WriteUInt32(m_aggrometer.get_lock_id()); - app->WriteUInt32(m_aggrometer.get_target_id()); - FastQueuePacket(&app); - } - - // we could just calculate how big the packet would need to be ... but it's easier this way :P should be 87 bytes - auto app = new EQApplicationPacket(OP_AggroMeterUpdate, m_aggrometer.max_packet_size()); - - cur_tar = entity_list.GetMob(m_aggrometer.get_target_id()); - - // first we must check the secondary - // TODO: lock target should affect secondary as well - bool send = false; - Mob *secondary = nullptr; - bool has_aggro = false; - if (cur_tar) { - if (cur_tar->GetTarget() == this) {// we got aggro - secondary = cur_tar->GetSecondaryHate(this); - has_aggro = true; - } else { - secondary = cur_tar->CheckAggro(cur_tar->GetTarget()) ? cur_tar->GetTarget() : nullptr; // make sure they are targeting for aggro reasons - } - } - - if (secondary && secondary->GetID() != m_aggrometer.get_secondary_id()) { - m_aggrometer.set_secondary_id(secondary->GetID()); - app->WriteUInt8(1); - app->WriteUInt32(m_aggrometer.get_secondary_id()); - send = true; - } else if (!secondary && m_aggrometer.get_secondary_id()) { - m_aggrometer.set_secondary_id(0); - app->WriteUInt8(1); - app->WriteUInt32(0); - send = true; - } else { // might not need to send in this case - app->WriteUInt8(0); - } - - auto count_offset = app->GetWritePosition(); - app->WriteUInt8(0); - - int count = 0; - auto add_entry = [&app, &count, this](AggroMeter::AggroTypes i) { - count++; - app->WriteUInt8(i); - app->WriteUInt16(m_aggrometer.get_pct(i)); - }; - // TODO: Player entry should either be lock or yourself, ignoring lock for now - // player, secondary, and group depend on your target/lock - if (cur_tar) { - if (m_aggrometer.set_pct(AggroMeter::AT_Player, cur_tar->GetHateRatio(cur_tar->GetTarget(), this))) - add_entry(AggroMeter::AT_Player); - - if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, has_aggro ? cur_tar->GetHateRatio(this, secondary) : secondary ? 100 : 0)) - add_entry(AggroMeter::AT_Secondary); - - if (IsRaidGrouped()) { - auto raid = GetRaid(); - if (raid) { - auto gid = raid->GetGroup(this); - if (gid < MAX_RAID_GROUPS) { - int at_id = AggroMeter::AT_Group1; - for (const auto& m : raid->members) { - if (m.member && m.member != this && m.group_number == gid) { - if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), m.member))) - add_entry(static_cast(at_id)); - at_id++; - if (at_id > AggroMeter::AT_Group5) - break; - } - } - } - } - } else if (IsGrouped()) { - auto group = GetGroup(); - if (group) { - int at_id = AggroMeter::AT_Group1; - for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { - if (group->members[i] && group->members[i] != this) { - if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), group->members[i]))) - add_entry(static_cast(at_id)); - at_id++; - } - } - } - } - } else { // we might need to clear out some data now - if (m_aggrometer.set_pct(AggroMeter::AT_Player, 0)) - add_entry(AggroMeter::AT_Player); - if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, 0)) - add_entry(AggroMeter::AT_Secondary); - if (m_aggrometer.set_pct(AggroMeter::AT_Group1, 0)) - add_entry(AggroMeter::AT_Group1); - if (m_aggrometer.set_pct(AggroMeter::AT_Group2, 0)) - add_entry(AggroMeter::AT_Group2); - if (m_aggrometer.set_pct(AggroMeter::AT_Group3, 0)) - add_entry(AggroMeter::AT_Group3); - if (m_aggrometer.set_pct(AggroMeter::AT_Group4, 0)) - add_entry(AggroMeter::AT_Group4); - if (m_aggrometer.set_pct(AggroMeter::AT_Group5, 0)) - add_entry(AggroMeter::AT_Group5); - } - - // now to go over our xtargets - // if the entry is an NPC it's our hate relative to the NPCs current tank - // if it's a PC, it's their hate relative to our current target - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].ID) { - auto mob = entity_list.GetMob(XTargets[i].ID); - if (mob) { - int ratio = 0; - if (mob->IsNPC()) - ratio = mob->GetHateRatio(mob->GetTarget(), this); - else if (cur_tar) - ratio = cur_tar->GetHateRatio(cur_tar->GetTarget(), mob); - if (m_aggrometer.set_pct(static_cast(AggroMeter::AT_XTarget1 + i), ratio)) - add_entry(static_cast(AggroMeter::AT_XTarget1 + i)); - } - } - } - - if (send || count) { - app->size = app->GetWritePosition(); // this should be safe, although not recommended - // but this way we can have a smaller buffer created for the packet dispatched to the client w/o resizing this one - app->SetWritePosition(count_offset); - app->WriteUInt8(count); - FastQueuePacket(&app); - } else { - safe_delete(app); - } +void Client::SetAnon(uint8 anon_flag) { + m_pp.anon = anon_flag; + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; + spawn_appearance->spawn_id = GetID(); + spawn_appearance->type = AppearanceType::Anonymous; + spawn_appearance->parameter = anon_flag; + entity_list.QueueClients(this, outapp); + Save(); + UpdateWho(); + safe_delete(outapp); } -void Client::SetPetCommandState(int button, int state) -{ - auto app = new EQApplicationPacket(OP_PetCommandState, sizeof(PetCommandState_Struct)); - auto pcs = (PetCommandState_Struct *)app->pBuffer; - pcs->button_id = button; - pcs->state = state; - FastQueuePacket(&app); +void Client::SetAFK(uint8 afk_flag) { + AFK = afk_flag; + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; + spawn_appearance->spawn_id = GetID(); + spawn_appearance->type = AppearanceType::AFK; + spawn_appearance->parameter = afk_flag; + entity_list.QueueClients(this, outapp); + safe_delete(outapp); } -bool Client::CanMedOnHorse() -{ - // no horse is false - if (GetHorseId() == 0) - return false; +void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { + uint32 zone_id = ZoneID(zone_short_name); + std::string current_instance_type = Strings::ToLower(instance_type); + std::string instance_type_name = "public"; + if (current_instance_type.find("solo") != std::string::npos) { + instance_type_name = GetCleanName(); + } else if (current_instance_type.find("group") != std::string::npos) { + uint32 group_id = (GetGroup() ? GetGroup()->GetID() : 0); + instance_type_name = itoa(group_id); + } else if (current_instance_type.find("raid") != std::string::npos) { + uint32 raid_id = (GetRaid() ? GetRaid()->GetID() : 0); + instance_type_name = itoa(raid_id); + } else if (current_instance_type.find("guild") != std::string::npos) { + uint32 guild_id = (GuildID() > 0 ? GuildID() : 0); + instance_type_name = itoa(guild_id); + } - // can't med while attacking - if (auto_attack) - return false; + std::string full_bucket_name = fmt::format( + "{}_{}_{}_{}", + current_instance_type, + instance_type_name, + instance_identifier, + zone_short_name + ); + std::string current_bucket_value = DataBucket::GetData(full_bucket_name); + uint16 instance_id = 0; + + if (current_bucket_value.length() > 0) { + instance_id = Strings::ToInt(current_bucket_value); + } else { + if(!database.GetUnusedInstanceID(instance_id)) { + Message(Chat::White, "Server was unable to find a free instance id."); + return; + } - return animation == 0 && m_Delta.x == 0.0f && m_Delta.y == 0.0f; // TODO: animation is SpeedRun -} + if(!database.CreateInstance(instance_id, zone_id, instance_version, duration)) { + Message(Chat::White, "Server was unable to create a new instance."); + return; + } -void Client::EnableAreaHPRegen(int value) -{ - AreaHPRegen = value * 0.001f; - SendAppearancePacket(AppearanceType::AreaHealthRegen, value, false); -} + DataBucket::SetData(full_bucket_name, itoa(instance_id), itoa(duration)); + } -void Client::DisableAreaHPRegen() -{ - AreaHPRegen = 1.0f; - SendAppearancePacket(AppearanceType::AreaHealthRegen, 1000, false); + AssignToInstance(instance_id); + MovePC(zone_id, instance_id, x, y, z, heading); } -void Client::EnableAreaManaRegen(int value) +uint32 Client::CountItem(uint32 item_id) { - AreaManaRegen = value * 0.001f; - SendAppearancePacket(AppearanceType::AreaManaRegen, value, false); -} + uint32 quantity = 0; + EQ::ItemInstance *item = nullptr; -void Client::DisableAreaManaRegen() -{ - AreaManaRegen = 1.0f; - SendAppearancePacket(AppearanceType::AreaManaRegen, 1000, false); -} + for (const int16& slot_id : GetInventorySlots()) { + item = GetInv().GetItem(slot_id); + if (item && item->GetID() == item_id) { + quantity += (item->IsStackable() ? item->GetCharges() : 1); + } + } -void Client::EnableAreaEndRegen(int value) -{ - AreaEndRegen = value * 0.001f; - SendAppearancePacket(AppearanceType::AreaEnduranceRegen, value, false); + return quantity; } -void Client::DisableAreaEndRegen() +void Client::ResetItemCooldown(uint32 item_id) { - AreaEndRegen = 1.0f; - SendAppearancePacket(AppearanceType::AreaEnduranceRegen, 1000, false); -} + EQ::ItemInstance *item = nullptr; + const EQ::ItemData* item_d = database.GetItem(item_id); + if (!item_d) { + return; + } + int recast_type = item_d->RecastType; + bool found_item = false; + + for (const int16& slot_id : GetInventorySlots()) { + item = GetInv().GetItem(slot_id); + if (item) { + item_d = item->GetItem(); + if ( + item_d && + item->GetID() == item_id || + ( + item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && + item_d->RecastType == recast_type + ) + ) { + item->SetRecastTimestamp(0); + DeleteItemRecastTimer(item_d->ID); + SendItemPacket(slot_id, item, ItemPacketCharmUpdate); + found_item = true; + } + } + } -void Client::EnableAreaRegens(int value) -{ - EnableAreaHPRegen(value); - EnableAreaManaRegen(value); - EnableAreaEndRegen(value); + if (!found_item) { + DeleteItemRecastTimer(item_id); //We didn't find the item but we still want to remove the timer + } } -void Client::DisableAreaRegens() +void Client::SetItemCooldown(uint32 item_id, bool use_saved_timer, uint32 in_seconds) { - DisableAreaHPRegen(); - DisableAreaManaRegen(); - DisableAreaEndRegen(); -} + EQ::ItemInstance *item = nullptr; + const EQ::ItemData* item_d = database.GetItem(item_id); + if (!item_d) { + return; + } + int recast_type = item_d->RecastType; + auto timestamps = database.GetItemRecastTimestamps(CharacterID()); + uint32 total_time = 0; + uint32 current_time = static_cast(std::time(nullptr)); + uint32 final_time = 0; + const auto timer_type = item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM ? item_d->RecastType : item_id; + const int timer_id = recast_type != RECAST_TYPE_UNLINKED_ITEM ? (pTimerItemStart + recast_type) : (pTimerNegativeItemReuse * item_id); + + if (use_saved_timer) { + if (item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM) { + total_time = timestamps.count(item_d->RecastType) ? timestamps.at(item_d->RecastType) : 0; + } else { + total_time = timestamps.count(item_id) ? timestamps.at(item_id) : 0; + } + } else { + total_time = current_time + in_seconds; + } -void Client::InitInnates() -{ - // this function on the client also inits the level one innate skills (like swimming, hide, etc) - // we won't do that here, lets just do the InnateSkills for now. Basically translation of what the client is doing - // A lot of these we could probably have ignored because they have no known use or are 100% client side - // but I figured just in case we'll do them all out - // - // The client calls this in a few places. When you remove a vision buff and in SetHeights, which is called in - // illusions, mounts, and a bunch of other cases. All of the calls to InitInnates are wrapped in restoring regen - // besides the call initializing the first time - auto race = GetRace(); - auto class_ = GetClass(); - - for (int i = 0; i < InnateSkillMax; ++i) { - m_pp.InnateSkills[i] = InnateDisabled; - } - - m_pp.InnateSkills[InnateInspect] = InnateEnabled; - m_pp.InnateSkills[InnateOpen] = InnateEnabled; - - if (race >= Race::Froglok2) { - if (race == Race::Skeleton2 || race == Race::Froglok2) { - m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; - } else { - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - } - } - - switch (race) { - case Race::Barbarian: - case Race::HalasCitizen: - m_pp.InnateSkills[InnateSlam] = InnateEnabled; - break; - case Race::Erudite: - case Race::EruditeCitizen: - m_pp.InnateSkills[InnateLore] = InnateEnabled; - break; - case Race::WoodElf: - case Race::Fayguard: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Gnome: - case Race::HighElf: - case Race::Felguard: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - m_pp.InnateSkills[InnateLore] = InnateEnabled; - break; - case Race::Troll: - case Race::GrobbCitizen: - m_pp.InnateSkills[InnateRegen] = InnateEnabled; - m_pp.InnateSkills[InnateSlam] = InnateEnabled; - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Dwarf: - case Race::KaladimCitizen: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Ogre: - case Race::OggokCitizen: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - m_pp.InnateSkills[InnateSlam] = InnateEnabled; - m_pp.InnateSkills[InnateNoBash] = InnateEnabled; - m_pp.InnateSkills[InnateBashDoor] = InnateEnabled; - break; - case Race::Halfling: - case Race::RivervaleCitizen: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::Iksar: - m_pp.InnateSkills[InnateRegen] = InnateEnabled; - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::VahShir: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - case Race::DarkElf: - case Race::NeriakCitizen: - case Race::ElfVampire: - case Race::FroglokGhoul: - case Race::Ghost: - case Race::Ghoul: - case Race::Skeleton: - case Race::Vampire: - case Race::Wisp: - case Race::Zombie: - case Race::Spectre: - case Race::DwarfGhost: - case Race::EruditeGhost: - case Race::DragonSkeleton: - case Race::Innoruuk: - m_pp.InnateSkills[InnateUltraVision] = InnateEnabled; - break; - case Race::Human: - case Race::FreeportGuard: - case Race::HumanBeggar: - case Race::HighpassCitizen: - case Race::QeynosCitizen: - case Race::Froglok2: // client does froglok weird, but this should work out fine - break; - default: - m_pp.InnateSkills[InnateInfravision] = InnateEnabled; - break; - } - - switch (class_) { - case Class::Druid: - m_pp.InnateSkills[InnateHarmony] = InnateEnabled; - break; - case Class::Bard: - m_pp.InnateSkills[InnateReveal] = InnateEnabled; - break; - case Class::Rogue: - m_pp.InnateSkills[InnateSurprise] = InnateEnabled; - m_pp.InnateSkills[InnateReveal] = InnateEnabled; - break; - case Class::Ranger: - m_pp.InnateSkills[InnateAwareness] = InnateEnabled; - break; - case Class::Monk: - m_pp.InnateSkills[InnateSurprise] = InnateEnabled; - m_pp.InnateSkills[InnateAwareness] = InnateEnabled; - default: - break; - } -} + if (total_time > current_time) { + final_time = total_time - current_time; + } -bool Client::GetDisplayMobInfoWindow() const -{ - return display_mob_info_window; -} + for (const int16& slot_id : GetInventorySlots()) { + item = GetInv().GetItem(slot_id); + if (item) { + item_d = item->GetItem(); + if ( + item_d && + item->GetID() == item_id || + ( + item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && + item_d->RecastType == recast_type + ) + ) { + item->SetRecastTimestamp(total_time); + SendItemPacket(slot_id, item, ItemPacketCharmUpdate); + } + } + } -void Client::SetDisplayMobInfoWindow(bool display_mob_info_window) -{ - Client::display_mob_info_window = display_mob_info_window; + //Start timers and update in database only when timer is changed + if (!use_saved_timer) { + GetPTimers().Clear(&database, timer_id); + GetPTimers().Start((timer_id), in_seconds); + database.UpdateItemRecast( + CharacterID(), + timer_type, + GetPTimers().Get(timer_id)->GetReadyTimestamp() + ); + } + SendItemRecastTimer(recast_type, final_time, true); } -bool Client::IsDevToolsEnabled() const +uint32 Client::GetItemCooldown(uint32 item_id) { - return dev_tools_enabled && GetGM() && RuleB(World, EnableDevTools); + const EQ::ItemData* item_d = database.GetItem(item_id); + if (!item_d) { + return 0; + } + + int recast_type = item_d->RecastType; + auto timestamps = database.GetItemRecastTimestamps(CharacterID()); + const auto timer_type = recast_type != RECAST_TYPE_UNLINKED_ITEM ? recast_type : item_id; + uint32 total_time = 0; + uint32 current_time = static_cast(std::time(nullptr)); + uint32 final_time = 0; + + total_time = timestamps.count(timer_type) ? timestamps.at(timer_type) : 0; + + if (total_time > current_time) { + final_time = total_time - current_time; + } + + return final_time; } -void Client::SetDevToolsEnabled(bool in_dev_tools_enabled) +void Client::RemoveItem(uint32 item_id, uint32 quantity) { - const auto dev_tools_key = fmt::format("{}-dev-tools-disabled", AccountID()); + uint32 removed_count = 0; + EQ::ItemInstance *item = nullptr; - if (in_dev_tools_enabled) { - DataBucket::DeleteData(dev_tools_key); - } else { - DataBucket::SetData(dev_tools_key, "true"); - } + for (const int16& slot_id : GetInventorySlots()) { + if (removed_count == quantity) { + break; + } - Client::dev_tools_enabled = in_dev_tools_enabled; + item = GetInv().GetItem(slot_id); + if (item && item->GetID() == item_id) { + uint32 charges = item->IsStackable() ? item->GetCharges() : 0; + uint32 stack_size = std::max(charges, static_cast(1)); + if ((removed_count + stack_size) <= quantity) { + removed_count += stack_size; + DeleteItemInInventory(slot_id, charges, true); + } else { + uint32 amount_left = (quantity - removed_count); + if (amount_left > 0 && stack_size >= amount_left) { + removed_count += amount_left; + DeleteItemInInventory(slot_id, amount_left, true); + } + } + } + } } -bool Client::IsEXPEnabled() const { - return m_exp_enabled; +void Client::SetGMStatus(int new_status) { + if (Admin() != new_status) { + database.UpdateGMStatus(AccountID(), new_status); + UpdateAdmin(); + } } -void Client::SetEXPEnabled(bool is_exp_enabled) +void Client::ApplyWeaponsStance() { - auto c = CharacterDataRepository::FindOne(database, CharacterID()); + /* - c.exp_enabled = is_exp_enabled; + If you have a weapons stance bonus from at least one bonus type, each time you change weapons this function will ensure the correct + associated buffs are applied, and previous buff is removed. If your weapon stance bonus is completely removed it will, ensure buff is + also removed (ie, removing an item that has worn effect with weapon stance, or clicking off a buff). If client no longer has/never had + any spells/item/aa bonuses with weapon stance effect this function will only do a simple bool check. - auto updated = CharacterDataRepository::UpdateOne(database, c); + Note: Live like behavior is once you have the triggered buff you can manually click it off to remove it. Swaping any items in inventory will + reapply it automatically. - if (!updated) { - return; - } - - m_exp_enabled = is_exp_enabled; -} + Only buff spells should be used as triggered spell effect. IsBuffSpell function also checks spell id validity. + WeaponStance bonus arrary: 0=2H Weapon 1=Shield 2=Dualweild -void Client::SetPrimaryWeaponOrnamentation(uint32 model_id) -{ - auto primary_item = m_inv.GetItem(EQ::invslot::slotPrimary); - if (primary_item) { - auto l = InventoryRepository::GetWhere( - database, - fmt::format( - "`charid` = {} AND `slotid` = {}", - character_id, - EQ::invslot::slotPrimary - ) - ); + Toggling ON or OFF + - From spells, just remove the Primary buff that contains the WeaponStance effect in it. + - For items with worn effect, unequip the item. + - For AA abilities, a hotkey is used to Enable and Disable the effect. See. Client::TogglePassiveAlternativeAdvancement in aa.cpp for extensive details. - if (l.empty()) { - return; - } + Rank + - Most important for AA, but if you have more than one of WeaponStance effect for a given type, the spell trigger buff will apply whatever has the highest + 'rank' value from the spells table. AA's on live for this effect naturally do this. Be awere of this if making custom spells/worn effects/AA. - auto e = l.front(); + When creating weapon stance effects, you do not need to use all three types. For example, can make an effect where you only get a buff from equiping shield. - e.ornamentidfile = model_id; + */ - const int updated = InventoryRepository::UpdateOne(database, e); + if (!IsWeaponStanceEnabled()) { + return; + } - if (updated) { - primary_item->SetOrnamentationIDFile(model_id); - SendItemPacket(EQ::invslot::slotPrimary, primary_item, ItemPacketTrade); - WearChange(EQ::textures::weaponPrimary, model_id, 0); + bool enabled = false; + bool item_bonus_exists = false; + bool aa_bonus_exists = false; - Message(Chat::Yellow, "Your primary weapon appearance has been modified."); - } - } -} + if (weaponstance.spellbonus_enabled) { -void Client::SetSecondaryWeaponOrnamentation(uint32 model_id) -{ - auto secondary_item = m_inv.GetItem(EQ::invslot::slotSecondary); - if (secondary_item) { - auto l = InventoryRepository::GetWhere( - database, - fmt::format( - "`charid` = {} AND `slotid` = {}", - character_id, - EQ::invslot::slotSecondary - ) - ); + if (spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { - if (l.empty()) { - return; - } + enabled = true; - auto e = l.front(); + // Check if no longer has correct combination of weapon type and buff, if so remove buff. + if (!HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && + FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); + } + else if (!HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && + FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); + } + else if (!HasDualWeaponsEquipped() && + IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && + FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); + } + // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. + if (HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); + } + weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; + } + else if (HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - e.ornamentidfile = model_id; + if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); + } + weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; + } + else if (HasDualWeaponsEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - const int updated = InventoryRepository::UpdateOne(database, e); + if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); + } + weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; + } + } + } - if (updated) { - secondary_item->SetOrnamentationIDFile(model_id); - SendItemPacket(EQ::invslot::slotSecondary, secondary_item, ItemPacketTrade); - WearChange(EQ::textures::weaponSecondary, model_id, 0); + // Spellbonus effect removal is checked in BuffFadeBySlot(int slot, bool iRecalcBonuses) in spell_effects.cpp when the buff is clicked off or fades. - Message(Chat::Yellow, "Your secondary weapon appearance has been modified."); - } - } -} + if (weaponstance.itembonus_enabled) { -/** - * Used in #goto - * - * @param player_name - */ -bool Client::GotoPlayer(const std::string& player_name) -{ - const auto& l = CharacterDataRepository::GetWhere( - database, - fmt::format( - "name = '{}' AND last_login > (UNIX_TIMESTAMP() - 600) LIMIT 1", - Strings::Escape(player_name) - ) - ); + if (itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { - if (l.empty()) { - return false; - } + enabled = true; + item_bonus_exists = true; - const auto& e = l.front(); - if (e.zone_instance > 0 && !database.CheckInstanceExists(e.zone_instance)) { - Message(Chat::Yellow, "Instance no longer exists..."); - return false; - } + // Edge case check if have multiple items with WeaponStance worn effect. Make sure correct buffs are applied if items are removed but others left on. + if (weaponstance.itembonus_buff_spell_id) { - if (e.zone_instance > 0) { - database.AddClientToInstance(e.zone_instance, CharacterID()); - } + bool buff_desync = true; + if (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || + weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + buff_desync = false; + } - MovePC(e.zone_id, e.zone_instance, e.x, e.y, e.z, e.heading); + if (buff_desync) { + int fade_spell = weaponstance.itembonus_buff_spell_id; + weaponstance.itembonus_buff_spell_id = 0; //Need to zero this before we fade to prevent any recursive loops. + BuffFadeBySpellID(fade_spell); + } + } - return true; -} + // Check if no longer has correct combination of weapon type and buff, if so remove buff. + if (!HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && + FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); + } + else if (!HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && + FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); + } + else if (!HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && + FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); + } -bool Client::GotoPlayerGroup(const std::string& player_name) -{ - if (!GetGroup()) { - return GotoPlayer(player_name); - } + // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. + if (HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - for (auto &m: GetGroup()->members) { - if (m && m->IsClient()) { - auto c = m->CastToClient(); - if (!c->GotoPlayer(player_name)) { - return false; - } - } - } + if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); + } + weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; + } + else if (HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - return true; -} + if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); + } + weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; + } + else if (HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); + } + weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; + } + } + } -bool Client::GotoPlayerRaid(const std::string& player_name) -{ - if (!GetRaid()) { - return GotoPlayer(player_name); - } + // Itembonus effect removal when item is removed + if (!item_bonus_exists && weaponstance.itembonus_enabled) { + weaponstance.itembonus_enabled = false; - for (auto &m: GetRaid()->members) { - if (m.member && m.member->IsClient()) { - auto c = m.member->CastToClient(); - if (!c->GotoPlayer(player_name)) { - return false; - } - } - } + if (weaponstance.itembonus_buff_spell_id) { + BuffFadeBySpellID(weaponstance.itembonus_buff_spell_id); + weaponstance.itembonus_buff_spell_id = 0; + } + } - return true; -} + if (weaponstance.aabonus_enabled) { -void Client::SendToGuildHall() -{ - std::string zone_short_name = "guildhall"; - uint32 zone_id = ZoneID(zone_short_name.c_str()); - if (zone_id == 0) { - return; - } - - uint32 expiration_time = (RuleI(Instances, GuildHallExpirationDays) * 86400); - uint16 instance_id = 0; - std::string guild_hall_instance_key = fmt::format("guild-hall-instance-{}", GuildID()); - std::string instance_data = DataBucket::GetData(guild_hall_instance_key); - if (!instance_data.empty() && Strings::ToInt(instance_data) > 0) { - instance_id = Strings::ToInt(instance_data); - } - - if (instance_id <= 0) { - if (!database.GetUnusedInstanceID(instance_id)) { - Message(Chat::Red, "Server was unable to find a free instance id."); - return; - } - - if (!database.CreateInstance(instance_id, zone_id, 1, expiration_time)) { - Message(Chat::Red, "Server was unable to create a new instance."); - return; - } - - DataBucket::SetData( - guild_hall_instance_key, - std::to_string(instance_id), - std::to_string(expiration_time) - ); - } - - AssignToInstance(instance_id); - MovePC(345, instance_id, -1.00, -1.00, 3.34, 0, 1); -} + if (aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || + aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { -void Client::CheckVirtualZoneLines() -{ - for (auto &virtual_zone_point : zone->virtual_zone_point_list) { - float half_width = ((float) virtual_zone_point.width / 2); - - if ( - GetX() > (virtual_zone_point.x - half_width) && - GetX() < (virtual_zone_point.x + half_width) && - GetY() > (virtual_zone_point.y - half_width) && - GetY() < (virtual_zone_point.y + half_width) && - GetZ() >= (virtual_zone_point.z - 10) && - GetZ() < (virtual_zone_point.z + (float) virtual_zone_point.height) - ) { - - MovePC( - virtual_zone_point.target_zone_id, - virtual_zone_point.target_instance, - virtual_zone_point.target_x, - virtual_zone_point.target_y, - virtual_zone_point.target_z, - virtual_zone_point.target_heading - ); - - LogZonePoints( - "Virtual Zone Box Sending player [{}] to [{}]", - GetCleanName(), - ZoneLongName(virtual_zone_point.target_zone_id) - ); - } - } -} + enabled = true; + aa_bonus_exists = true; -void Client::ShowDevToolsMenu() -{ - std::string menu_search; - std::string menu_show; - std::string menu_reload_one; - std::string menu_reload_two; - std::string menu_reload_three; - std::string menu_reload_four; - std::string menu_reload_five; - std::string menu_reload_six; - std::string menu_reload_seven; - std::string menu_reload_eight; - std::string menu_reload_nine; - std::string menu_toggle; - std::string window_toggle; - - /** - * Search entity commands - */ - menu_search += Saylink::Silent("#list corpses", "Corpses"); - menu_search += " | " + Saylink::Silent("#list doors", "Doors"); - menu_search += " | " + Saylink::Silent("#finditem", "Items"); - menu_search += " | " + Saylink::Silent("#list npcs", "NPC"); - menu_search += " | " + Saylink::Silent("#list objects", "Objects"); - menu_search += " | " + Saylink::Silent("#list players", "Players"); - menu_search += " | " + Saylink::Silent("#findzone", "Zones"); - - /** - * Show - */ - menu_show += Saylink::Silent("#showzonepoints", "Zone Points"); - menu_show += " | " + Saylink::Silent("#showzonegloballoot", "Zone Global Loot"); - menu_show += " | " + Saylink::Silent("#show content_flags", "Content Flags"); - - /** - * Reload - */ - menu_reload_one += Saylink::Silent("#reload aa", "AAs"); - menu_reload_one += " | " + Saylink::Silent("#reload alternate_currencies", "Alternate Currencies"); - menu_reload_one += " | " + Saylink::Silent("#reload base_data", "Base Data"); - menu_reload_one += " | " + Saylink::Silent("#reload blocked_spells", "Blocked Spells"); - - menu_reload_two += Saylink::Silent("#reload commands", "Commands"); - menu_reload_two += " | " + Saylink::Silent("#reload content_flags", "Content Flags"); - - menu_reload_three += Saylink::Silent("#reload data_buckets_cache", "Databuckets"); - menu_reload_three += " | " + Saylink::Silent("#reload doors", "Doors"); - menu_reload_three += " | " + Saylink::Silent("#reload factions", "Factions"); - menu_reload_three += " | " + Saylink::Silent("#reload ground_spawns", "Ground Spawns"); - - menu_reload_four += Saylink::Silent("#reload logs", "Level Based Experience Modifiers"); - menu_reload_four += " | " + Saylink::Silent("#reload logs", "Log Settings"); - menu_reload_four += " | " + Saylink::Silent("#reload Loot", "Loot"); - - menu_reload_five += Saylink::Silent("#reload merchants", "Merchants"); - menu_reload_five += " | " + Saylink::Silent("#reload npc_emotes", "NPC Emotes"); - menu_reload_five += " | " + Saylink::Silent("#reload npc_spells", "NPC Spells"); - menu_reload_five += " | " + Saylink::Silent("#reload objects", "Objects"); - menu_reload_five += " | " + Saylink::Silent("#reload opcodes", "Opcodes"); - - menu_reload_six += Saylink::Silent("#reload perl_export", "Perl Event Export Settings"); - menu_reload_six += " | " + Saylink::Silent("#reload quest", "Quests"); - - menu_reload_seven += Saylink::Silent("#reload rules", "Rules"); - menu_reload_seven += " | " + Saylink::Silent("#reload skill_caps", "Skill Caps"); - menu_reload_seven += " | " + Saylink::Silent("#reload static", "Static Zone Data"); - menu_reload_seven += " | " + Saylink::Silent("#reload tasks", "Tasks"); - - menu_reload_eight += Saylink::Silent("#reload titles", "Titles"); - menu_reload_eight += " | " + Saylink::Silent("#reload traps 1", "Traps"); - menu_reload_eight += " | " + Saylink::Silent("#reload variables", "Variables"); - menu_reload_eight += " | " + Saylink::Silent("#reload veteran_rewards", "Veteran Rewards"); - - menu_reload_nine += Saylink::Silent("#reload world", "World"); - menu_reload_nine += " | " + Saylink::Silent("#reload zone", "Zone"); - menu_reload_nine += " | " + Saylink::Silent("#reload zone_points", "Zone Points"); - - /** - * Show window status - */ - menu_toggle = Saylink::Silent("#devtools menu enable", "Enable"); - if (IsDevToolsEnabled()) { - menu_toggle = Saylink::Silent("#devtools menu disable", "Disable"); - } - - window_toggle = Saylink::Silent("#devtools window enable", "Enable"); - if (GetDisplayMobInfoWindow()) { - window_toggle = Saylink::Silent("#devtools window disable", "Disable"); - } - - /** - * Print menu - */ - SendChatLineBreak(); - - Message(Chat::White, "Developer Tools Menu"); - - Message( - Chat::White, - fmt::format( - "Show Menu | {}", - Saylink::Silent("#dev") - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Toggle Menu | {}", - menu_toggle - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Toggle Window | {}", - window_toggle - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Search | {}", - menu_search - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Show | {}", - menu_show - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_one - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_two - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_three - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_four - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_five - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_six - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_seven - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_eight - ).c_str() - ); - - Message( - Chat::White, - fmt::format( - "Reload | {}", - menu_reload_nine - ).c_str() - ); - - auto help_link = Saylink::Silent("#help"); - - Message( - Chat::White, - fmt::format( - "Note: You can search for commands with {} [Search String]", - help_link - ).c_str() - ); - - SendChatLineBreak(); - - Message( - Chat::White, - fmt::format( - "Current Expansion | {} ({})", - content_service.GetCurrentExpansionName(), - content_service.GetCurrentExpansion() - ).c_str() - ); - - - auto z = GetZoneVersionWithFallback(zone->GetZoneID(), zone->GetInstanceVersion()); - - if (z) { - Message( - Chat::White, - fmt::format( - "Current Zone | [{}] ({}) version [{}] instance_id [{}] min/max expansion ({}/{}) content_flags [{}]", - z->short_name, - z->long_name, - z->version, - zone->GetInstanceID(), - z->min_expansion, - z->max_expansion, - z->content_flags - ).c_str() - ); - } - - SendChatLineBreak(); -} + //Check if no longer has correct combination of weapon type and buff, if so remove buff. + if (!HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && + FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); + } -void Client::SendChatLineBreak(uint16 color) { - Message(color, "------------------------------------------------"); -} + else if (!HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && + FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); + } -void Client::SendCrossZoneMessage( - Client* client, const std::string& character_name, uint16_t chat_type, const std::string& message) -{ - // if client is null, falls back to sending a cross zone message by name - if (!client && !character_name.empty()) - { - client = entity_list.GetClientByName(character_name.c_str()); - } - - if (client) - { - client->Message(chat_type, message.c_str()); - } - else if (!character_name.empty() && !message.empty()) - { - uint32_t pack_size = sizeof(CZMessage_Struct); - auto pack = std::make_unique(ServerOP_CZMessage, pack_size); - auto buf = reinterpret_cast(pack->pBuffer); - uint8 update_type = CZUpdateType_Character; - int update_identifier = 0; - buf->update_type = update_type; - buf->update_identifier = update_identifier; - buf->type = chat_type; - strn0cpy(buf->message, message.c_str(), sizeof(buf->message)); - strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); - - worldserver.SendPacket(pack.get()); - } -} + else if (!HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && + FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); + } -void Client::SendCrossZoneMessageString( - Client* client, const std::string& character_name, uint16_t chat_type, - uint32_t string_id, const std::initializer_list& arguments) -{ - // if client is null, falls back to sending a cross zone message by name - if (!client && !character_name.empty()) // double check client isn't in this zone - { - client = entity_list.GetClientByName(character_name.c_str()); - } - - if (!client && character_name.empty()) - { - return; - } - - SerializeBuffer argument_buffer; - for (const auto& argument : arguments) - { - argument_buffer.WriteString(argument); - } - - uint32_t args_size = static_cast(argument_buffer.size()); - uint32_t pack_size = sizeof(CZClientMessageString_Struct) + args_size; - auto pack = std::make_unique(ServerOP_CZClientMessageString, pack_size); - auto buf = reinterpret_cast(pack->pBuffer); - buf->string_id = string_id; - buf->chat_type = chat_type; - strn0cpy(buf->client_name, character_name.c_str(), sizeof(buf->client_name)); - buf->args_size = args_size; - memcpy(buf->args, argument_buffer.buffer(), argument_buffer.size()); - - if (client) - { - client->MessageString(buf); - } - else - { - worldserver.SendPacket(pack.get()); - } -} + //If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. + if (HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { + SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); + } + weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; + } -void Client::SendDynamicZoneUpdates() -{ - // bit inefficient since each do lookups but it avoids duplicating code here - SendDzCompassUpdate(); - SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Online); + else if (HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { + SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); + } + weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; + } - m_expedition_lockouts = ExpeditionDatabase::LoadCharacterLockouts(CharacterID()); + else if (HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - // expeditions are the only dz type that keep the window updated - auto expedition = GetExpedition(); - if (expedition) - { - expedition->GetDynamicZone()->SendClientWindowUpdate(this); + if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { + SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); + } + weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; + } + } + } - // live synchronizes lockouts obtained during the active expedition to - // members once they zone into the expedition's dynamic zone instance - if (expedition->GetDynamicZone()->IsCurrentZoneDzInstance()) - { - expedition->SyncCharacterLockouts(CharacterID(), m_expedition_lockouts); - } - } + // AA bonus removal is checked in TogglePassiveAA in aa.cpp. when the hot key is toggled. - SendExpeditionLockoutTimers(); + // If no bonuses remain present, prevent additional future checks until new bonus is applied. + if (!enabled) { + SetWeaponStanceEnabled(false); + weaponstance.aabonus_enabled = false; + weaponstance.itembonus_enabled = false; + weaponstance.spellbonus_enabled = false; + } +} - // ask world for any pending invite we saved from a previous zone - RequestPendingExpeditionInvite(); +uint16 Client::GetDoorToolEntityId() const +{ + return m_door_tool_entity_id; } -Expedition* Client::CreateExpedition(DynamicZone& dz, bool disable_messages) +void Client::SetDoorToolEntityId(uint16 door_tool_entity_id) { - return Expedition::TryCreate(this, dz, disable_messages); + Client::m_door_tool_entity_id = door_tool_entity_id; } -Expedition* Client::CreateExpedition( - const std::string& zone_name, uint32 version, uint32 duration, const std::string& expedition_name, - uint32 min_players, uint32 max_players, bool disable_messages) +uint16 Client::GetObjectToolEntityId() const { - DynamicZone dz{ ZoneID(zone_name), version, duration, DynamicZoneType::Expedition }; - dz.SetName(expedition_name); - dz.SetMinPlayers(min_players); - dz.SetMaxPlayers(max_players); + return m_object_tool_entity_id; +} - return Expedition::TryCreate(this, dz, disable_messages); +void Client::SetObjectToolEntityId(uint16 object_tool_entity_id) +{ + Client::m_object_tool_entity_id = object_tool_entity_id; } -Expedition* Client::CreateExpeditionFromTemplate(uint32_t dz_template_id) +int Client::GetIPExemption() { - Expedition* expedition = nullptr; - auto it = zone->dz_template_cache.find(dz_template_id); - if (it != zone->dz_template_cache.end()) - { - DynamicZone dz(DynamicZoneType::Expedition); - dz.LoadTemplate(it->second); - expedition = Expedition::TryCreate(this, dz, false); - } - return expedition; + return database.GetIPExemption(GetIPString()); } -void Client::CreateTaskDynamicZone(int task_id, DynamicZone& dz_request) +std::string Client::GetIPString() { - if (task_state) - { - task_state->CreateTaskDynamicZone(this, task_id, dz_request); - } + in_addr client_ip{}; + client_ip.s_addr = GetIP(); + return inet_ntoa(client_ip); } -Expedition* Client::GetExpedition() const +void Client::SetIPExemption(int exemption_amount) { - if (zone && m_expedition_id) - { - auto expedition_cache_iter = zone->expedition_cache.find(m_expedition_id); - if (expedition_cache_iter != zone->expedition_cache.end()) - { - return expedition_cache_iter->second.get(); - } - } - return nullptr; + database.SetIPExemption(GetIPString(), exemption_amount); } -void Client::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db) +void Client::ReadBookByName(std::string book_name, uint8 book_type) { - // todo: support for account based lockouts like live AoC expeditions + auto b = content_db.GetBook(book_name); - // if client already has this lockout, we're replacing it with the new one - m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& existing_lockout) { - return existing_lockout.IsSameLockout(lockout); - } - ), m_expedition_lockouts.end()); + if (!b.text.empty()) { + LogDebug("Book Name: [{}] Text: [{}]", book_name, b.text); - m_expedition_lockouts.emplace_back(lockout); + auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); - if (update_db) // for quest api - { - ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { lockout }); - } + auto o = (BookText_Struct *) outapp->pBuffer; - SendExpeditionLockoutTimers(); -} + o->window = std::numeric_limits::max(); + o->type = book_type; + o->invslot = 0; -void Client::AddNewExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, uint32_t seconds, std::string uuid) -{ - auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); - AddExpeditionLockout(lockout, true); + memcpy(o->booktext, b.text.c_str(), b.text.size()); + + if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { + if (m_pp.languages[b.language] < Language::MaxValue) { + GarbleMessage(o->booktext, (Language::MaxValue - m_pp.languages[b.language])); + } + } + + QueuePacket(outapp); + safe_delete(outapp); + } } -void Client::AddExpeditionLockoutDuration( - const std::string& expedition_name, const std::string& event_name, int seconds, - const std::string& uuid, bool update_db) +// this will fetch raid clients if exists +// fallback to group if raid doesn't exist +// fallback to self if group doesn't exist +std::vector Client::GetPartyMembers() { - auto it = std::find_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& lockout) { - return lockout.IsSameLockout(expedition_name, event_name); - }); + // get clients to update + std::vector clients_to_update = {}; - if (it != m_expedition_lockouts.end()) - { - it->AddLockoutTime(seconds); + // raid + if (const auto raid = entity_list.GetRaidByClient(this)) { + for (auto &m : raid->members) { + if (m.is_bot) { + continue; + } + + if (m.member && m.member->IsClient()) { + clients_to_update.push_back(m.member->CastToClient()); + } + } + } - if (!uuid.empty()) - { - it->SetUUID(uuid); - } + // group + if (clients_to_update.empty()) { + Group *group = entity_list.GetGroupByClient(this); + if (group) { + for (auto &m : group->members) { + if (m && m->IsClient()) { + clients_to_update.push_back(m->CastToClient()); + } + } + } + } - if (update_db) - { - ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { *it }); - } + // solo + if (clients_to_update.empty()) { + clients_to_update.push_back(this); + } - SendExpeditionLockoutTimers(); - } - else if (seconds > 0) // missing lockouts inserted for reductions would be instantly expired - { - auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); - AddExpeditionLockout(lockout, update_db); - } + return clients_to_update; } -void Client::RemoveExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, bool update_db) +void Client::SummonBaggedItems(uint32 bag_item_id, const std::vector& bag_items) { - m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& lockout) { - return lockout.IsSameLockout(expedition_name, event_name); - } - ), m_expedition_lockouts.end()); + if (bag_items.empty()) + { + return; + } - if (update_db) // for quest api - { - ExpeditionDatabase::DeleteCharacterLockout(CharacterID(), expedition_name, event_name); - } + // todo: maybe some common functions for SE_SummonItem and SE_SummonItemIntoBag - SendExpeditionLockoutTimers(); -} + const EQ::ItemData* bag_item = database.GetItem(bag_item_id); + if (!bag_item) + { + Message(Chat::Red, fmt::format("Unable to summon item [{}]. Item not found.", bag_item_id).c_str()); + return; + } -void Client::RemoveAllExpeditionLockouts(const std::string& expedition_name, bool update_db) -{ - if (expedition_name.empty()) - { - if (update_db) - { - ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID()); - } - m_expedition_lockouts.clear(); - } - else - { - if (update_db) - { - ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID(), expedition_name); - } - - m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), - [&](const ExpeditionLockoutTimer& lockout) { - return lockout.GetExpeditionName() == expedition_name; - } - ), m_expedition_lockouts.end()); - } - - SendExpeditionLockoutTimers(); -} + if (CheckLoreConflict(bag_item)) + { + DuplicateLoreMessage(bag_item_id); + return; + } -const ExpeditionLockoutTimer* Client::GetExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, bool include_expired) const -{ - for (const auto& expedition_lockout : m_expedition_lockouts) - { - if ((include_expired || !expedition_lockout.IsExpired()) && - expedition_lockout.IsSameLockout(expedition_name, event_name)) - { - return &expedition_lockout; - } - } - return nullptr; -} + int bag_item_charges = 1; // just summoning a single bag + EQ::ItemInstance* summoned_bag = database.CreateItem(bag_item_id, bag_item_charges); + if (!summoned_bag || !summoned_bag->IsClassBag()) + { + Message(Chat::Red, fmt::format("Failed to summon bag item [{}]", bag_item_id).c_str()); + safe_delete(summoned_bag); + return; + } -std::vector Client::GetExpeditionLockouts( - const std::string& expedition_name, bool include_expired) -{ - std::vector lockouts; - for (const auto& lockout : m_expedition_lockouts) - { - if ((include_expired || !lockout.IsExpired()) && - lockout.GetExpeditionName() == expedition_name) - { - lockouts.emplace_back(lockout); - } - } - return lockouts; -} + for (const auto& item : bag_items) + { + uint8 open_slot = summoned_bag->FirstOpenSlot(); + if (open_slot == 0xff) + { + Message(Chat::Red, "Attempting to summon item in to bag, but there is no room in the summoned bag!"); + break; + } -bool Client::HasExpeditionLockout( - const std::string& expedition_name, const std::string& event_name, bool include_expired) -{ - return (GetExpeditionLockout(expedition_name, event_name, include_expired) != nullptr); -} + const EQ::ItemData* current_item = database.GetItem(item.item_id); -void Client::SendExpeditionLockoutTimers() -{ - std::vector lockout_entries; - - // client displays lockouts rounded down to nearest minute, send lockouts - // with 60s offset added to compensate (live does this too) - constexpr uint32_t rounding_seconds = 60; - - // erases expired lockouts while building lockout timer list - for (auto it = m_expedition_lockouts.begin(); it != m_expedition_lockouts.end();) - { - uint32_t seconds_remaining = it->GetSecondsRemaining(); - if (seconds_remaining == 0) - { - it = m_expedition_lockouts.erase(it); - } - else - { - ExpeditionLockoutTimerEntry_Struct lockout; - strn0cpy(lockout.expedition_name, it->GetExpeditionName().c_str(), sizeof(lockout.expedition_name)); - lockout.seconds_remaining = seconds_remaining + rounding_seconds; - lockout.event_type = it->IsReplayTimer() ? Expedition::REPLAY_TIMER_ID : Expedition::EVENT_TIMER_ID; - strn0cpy(lockout.event_name, it->GetEventName().c_str(), sizeof(lockout.event_name)); - - lockout_entries.emplace_back(lockout); - ++it; - } - } - - uint32_t lockout_count = static_cast(lockout_entries.size()); - uint32_t lockout_entries_size = sizeof(ExpeditionLockoutTimerEntry_Struct) * lockout_count; - uint32_t outsize = sizeof(ExpeditionLockoutTimers_Struct) + lockout_entries_size; - auto outapp = std::make_unique(OP_DzExpeditionLockoutTimers, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->count = lockout_count; - if (!lockout_entries.empty()) - { - memcpy(outbuf->timers, lockout_entries.data(), lockout_entries_size); - } - QueuePacket(outapp.get()); -} + if (CheckLoreConflict(current_item)) + { + DuplicateLoreMessage(item.item_id); + } + else + { + EQ::ItemInstance* summoned_bag_item = database.CreateItem( + item.item_id, + item.charges, + item.aug_1, + item.aug_2, + item.aug_3, + item.aug_4, + item.aug_5, + item.aug_6, + item.attuned, + item.custom_data, + item.ornamenticon, + item.ornamentidfile, + item.ornament_hero_model + ); + if (summoned_bag_item) + { + summoned_bag->PutItem(open_slot, *summoned_bag_item); + safe_delete(summoned_bag_item); + } + } + } -void Client::RequestPendingExpeditionInvite() -{ - uint32_t packsize = sizeof(ServerExpeditionCharacterID_Struct); - auto pack = std::make_unique(ServerOP_ExpeditionRequestInvite, packsize); - auto packbuf = reinterpret_cast(pack->pBuffer); - packbuf->character_id = CharacterID(); - worldserver.SendPacket(pack.get()); + PushItemOnCursor(*summoned_bag); + SendItemPacket(EQ::invslot::slotCursor, summoned_bag, ItemPacketLimbo); + safe_delete(summoned_bag); } -void Client::DzListTimers() +void Client::SaveSpells() { - // only lists player's current replay timer lockouts, not all event lockouts - bool found = false; - for (const auto& lockout : m_expedition_lockouts) - { - if (lockout.IsReplayTimer()) - { - found = true; - auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); - MessageString( - Chat::Yellow, DZLIST_REPLAY_TIMER, - time_remaining.days.c_str(), - time_remaining.hours.c_str(), - time_remaining.mins.c_str(), - lockout.GetExpeditionName().c_str() - ); - } - } - - if (!found) - { - MessageString(Chat::Yellow, EXPEDITION_NO_TIMERS); - } -} + std::vector character_spells = {}; -void Client::SetDzRemovalTimer(bool enable_timer) -{ - uint32_t timer_ms = RuleI(DynamicZone, ClientRemovalDelayMS); + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + if (IsValidSpell(m_pp.spell_book[index])) { + auto spell = CharacterSpellsRepository::NewEntity(); + spell.id = CharacterID(); + spell.slot_id = index; + spell.spell_id = m_pp.spell_book[index]; + character_spells.emplace_back(spell); + } + } - LogDynamicZones( - "Character [{}] instance [{}] removal timer enabled: [{}] delay (ms): [{}]", - CharacterID(), zone ? zone->GetInstanceID() : 0, enable_timer, timer_ms - ); + CharacterSpellsRepository::DeleteWhere(database, fmt::format("id = {}", CharacterID())); - if (enable_timer) - { - dynamiczone_removal_timer.Start(timer_ms); - } - else - { - dynamiczone_removal_timer.Disable(); - } + if (!character_spells.empty()) { + CharacterSpellsRepository::InsertMany(database, character_spells); + } } -void Client::SendDzCompassUpdate() +void Client::SaveDisciplines() { - // client may be associated with multiple dynamic zone compasses in this zone - std::vector compass_entries; - - // need to sort by local doorid in case multiple have same dz switch id (live only sends first) - // todo: just store zone's door list ordered and ditch this - std::vector switches; - switches.reserve(entity_list.GetDoorsList().size()); - for (const auto& door_pair : entity_list.GetDoorsList()) - { - switches.push_back(door_pair.second); - } - std::sort(switches.begin(), switches.end(), - [](Doors* lhs, Doors* rhs) { return lhs->GetDoorID() < rhs->GetDoorID(); }); - - for (const auto& client_dz : GetDynamicZones()) - { - auto& compass = client_dz->GetCompassLocation(); - if (zone && zone->IsZone(compass.zone_id, 0)) - { - DynamicZoneCompassEntry_Struct entry{}; - entry.dz_zone_id = client_dz->GetZoneID(); - entry.dz_instance_id = client_dz->GetInstanceID(); - entry.dz_type = static_cast(client_dz->GetType()); - entry.x = compass.x; - entry.y = compass.y; - entry.z = compass.z; - - compass_entries.emplace_back(entry); - } - - // if client has a dz with a switch id add compass to any switch locs that share it - if (client_dz->GetSwitchID() != 0) - { - // live only sends one if multiple in zone have the same switch id - auto it = std::find_if(switches.begin(), switches.end(), - [&](const auto& eqswitch) { - return eqswitch->GetDzSwitchID() == client_dz->GetSwitchID(); - }); - - if (it != switches.end()) - { - DynamicZoneCompassEntry_Struct entry{}; - entry.dz_zone_id = client_dz->GetZoneID(); - entry.dz_instance_id = client_dz->GetInstanceID(); - entry.dz_type = static_cast(client_dz->GetType()); - entry.dz_switch_id = client_dz->GetSwitchID(); - entry.x = (*it)->GetX(); - entry.y = (*it)->GetY(); - entry.z = (*it)->GetZ(); - - compass_entries.emplace_back(entry); - } - } - } - - // compass set via MarkSingleCompassLocation() - if (m_has_quest_compass) - { - DynamicZoneCompassEntry_Struct entry{}; - entry.dz_zone_id = 0; - entry.dz_instance_id = 0; - entry.dz_type = 0; - entry.x = m_quest_compass.x; - entry.y = m_quest_compass.y; - entry.z = m_quest_compass.z; - - compass_entries.emplace_back(entry); - } - - QueuePacket(CreateCompassPacket(compass_entries).get()); -} + std::vector v; -std::unique_ptr Client::CreateCompassPacket( - const std::vector& compass_entries) -{ - uint32 count = static_cast(compass_entries.size()); - uint32 entries_size = sizeof(DynamicZoneCompassEntry_Struct) * count; - uint32 outsize = sizeof(DynamicZoneCompass_Struct) + entries_size; - auto outapp = std::make_unique(OP_DzCompass, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->count = count; - memcpy(outbuf->entries, compass_entries.data(), entries_size); + std::vector delete_slots; - return outapp; -} + for (uint16 slot_id = 0; slot_id < MAX_PP_DISCIPLINES; slot_id++) { + if (IsValidSpell(m_pp.disciplines.values[slot_id])) { + auto e = CharacterDisciplinesRepository::NewEntity(); -void Client::GoToDzSafeReturnOrBind(const DynamicZone* dynamic_zone) -{ - if (dynamic_zone) - { - auto safereturn = dynamic_zone->GetSafeReturnLocation(); - if (safereturn.zone_id != 0) - { - LogDynamicZonesDetail("Sending [{}] to safereturn zone [{}]", CharacterID(), safereturn.zone_id); - MovePC(safereturn.zone_id, 0, safereturn.x, safereturn.y, safereturn.z, safereturn.heading); - return; - } - } + e.id = CharacterID(); + e.slot_id = slot_id; + e.disc_id = m_pp.disciplines.values[slot_id]; - GoToBind(); -} + v.emplace_back(e); + } else { + delete_slots.emplace_back(std::to_string(slot_id)); + } + } -void Client::AddDynamicZoneID(uint32_t dz_id) -{ - auto it = std::find_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), - [&](uint32_t current_dz_id) { return current_dz_id == dz_id; }); + if (!delete_slots.empty()) { + CharacterDisciplinesRepository::DeleteWhere( + database, + fmt::format( + "`id` = {} AND `slot_id` IN ({})", + CharacterID(), + Strings::Join(delete_slots, ", ") + ) + ); + } - if (it == m_dynamic_zone_ids.end()) - { - LogDynamicZonesDetail("Adding dz [{}] to client [{}]", dz_id, GetName()); - m_dynamic_zone_ids.push_back(dz_id); - } + if (!v.empty()) { + CharacterDisciplinesRepository::ReplaceMany(database, v); + } } -void Client::RemoveDynamicZoneID(uint32_t dz_id) +uint16 Client::ScribeSpells(uint8 min_level, uint8 max_level) { - LogDynamicZonesDetail("Removing dz [{}] from client [{}]", dz_id, GetName()); - m_dynamic_zone_ids.erase(std::remove_if(m_dynamic_zone_ids.begin(), m_dynamic_zone_ids.end(), - [&](uint32_t current_dz_id) { return current_dz_id == dz_id; } - ), m_dynamic_zone_ids.end()); + auto available_book_slot = GetNextAvailableSpellBookSlot(); + std::vector spell_ids = GetScribeableSpells(min_level, max_level); + uint16 scribed_spells = 0; + + if (!spell_ids.empty()) { + for (const auto& spell_id : spell_ids) { + if (available_book_slot == -1) { + Message( + Chat::Red, + fmt::format( + "Unable to scribe {} ({}) to Spell Book because your Spell Book is full.", + spells[spell_id].name, + spell_id + ).c_str() + ); + break; + } + + if (HasSpellScribed(spell_id)) { + continue; + } + + // defer saving per spell and bulk save at the end + ScribeSpell(spell_id, available_book_slot, true, true); + available_book_slot = GetNextAvailableSpellBookSlot(available_book_slot); + scribed_spells++; + } + } + + if (scribed_spells > 0) { + std::string spell_message = ( + scribed_spells == 1 ? + "a new spell" : + fmt::format("{} new spells", scribed_spells) + ); + Message(Chat::White, fmt::format("You have learned {}!", spell_message).c_str()); + + // bulk insert spells + SaveSpells(); + } + return scribed_spells; } -std::vector Client::GetDynamicZones(uint32_t zone_id, int zone_version) +uint16 Client::LearnDisciplines(uint8 min_level, uint8 max_level) { - std::vector client_dzs; + auto available_discipline_slot = GetNextAvailableDisciplineSlot(); + auto character_id = CharacterID(); + std::vector spell_ids = GetLearnableDisciplines(min_level, max_level); + uint16 learned_disciplines = 0; + + if (!spell_ids.empty()) { + for (const auto& spell_id : spell_ids) { + if (available_discipline_slot == -1) { + Message( + Chat::Red, + fmt::format( + "Unable to learn {} ({}) because your Discipline slots are full.", + spells[spell_id].name, + spell_id + ).c_str() + ); + break; + } + + if (HasDisciplineLearned(spell_id)) { + continue; + } - for (uint32_t dz_id : m_dynamic_zone_ids) - { - auto dz = DynamicZone::FindDynamicZoneByID(dz_id); - if (dz && - (zone_id == 0 || dz->GetZoneID() == zone_id) && - (zone_version < 0 || dz->GetZoneVersion() == zone_version)) - { - client_dzs.emplace_back(dz); - } - } + GetPP().disciplines.values[available_discipline_slot] = spell_id; + available_discipline_slot = GetNextAvailableDisciplineSlot(available_discipline_slot); + learned_disciplines++; + } + } + + if (learned_disciplines > 0) { + std::string discipline_message = ( + learned_disciplines == 1 ? + "a new discipline" : + fmt::format("{} new disciplines", learned_disciplines) + ); + Message(Chat::White, fmt::format("You have learned {}!", discipline_message).c_str()); + SendDisciplineUpdate(); + SaveDisciplines(); + } - return client_dzs; + return learned_disciplines; } -void Client::SetDynamicZoneMemberStatus(DynamicZoneMemberStatus status) -{ - // sets status on all associated dzs client may have. if client is online - // inside a dz, only that dz has the "In Dynamic Zone" status set - for (auto& dz : GetDynamicZones()) - { - // the rule to disable this status is handled internally by the dz - if (status == DynamicZoneMemberStatus::Online && dz->IsCurrentZoneDzInstance()) - { - status = DynamicZoneMemberStatus::InDynamicZone; - } - dz->SetMemberStatus(CharacterID(), status); - } +uint16 Client::GetClassTrackingDistanceMultiplier(uint16 class_) { + switch (class_) { + case Class::Warrior: + return RuleI(Character, WarriorTrackingDistanceMultiplier); + case Class::Cleric: + return RuleI(Character, ClericTrackingDistanceMultiplier); + case Class::Paladin: + return RuleI(Character, PaladinTrackingDistanceMultiplier); + case Class::Ranger: + return RuleI(Character, RangerTrackingDistanceMultiplier); + case Class::ShadowKnight: + return RuleI(Character, ShadowKnightTrackingDistanceMultiplier); + case Class::Druid: + return RuleI(Character, DruidTrackingDistanceMultiplier); + case Class::Monk: + return RuleI(Character, MonkTrackingDistanceMultiplier); + case Class::Bard: + return RuleI(Character, BardTrackingDistanceMultiplier); + case Class::Rogue: + return RuleI(Character, RogueTrackingDistanceMultiplier); + case Class::Shaman: + return RuleI(Character, ShamanTrackingDistanceMultiplier); + case Class::Necromancer: + return RuleI(Character, NecromancerTrackingDistanceMultiplier); + case Class::Wizard: + return RuleI(Character, WizardTrackingDistanceMultiplier); + case Class::Magician: + return RuleI(Character, MagicianTrackingDistanceMultiplier); + case Class::Enchanter: + return RuleI(Character, EnchanterTrackingDistanceMultiplier); + case Class::Beastlord: + return RuleI(Character, BeastlordTrackingDistanceMultiplier); + case Class::Berserker: + return RuleI(Character, BerserkerTrackingDistanceMultiplier); + default: + return 0; + } } -void Client::MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid) -{ - if (zone_id == 0) - { - return; - } - - auto client_dzs = GetDynamicZones(zone_id, zone_version); - - if (client_dzs.empty()) - { - if (msg_if_invalid) - { - MessageString(Chat::Red, DYNAMICZONE_WAY_IS_BLOCKED); // unconfirmed message - } - } - else if (client_dzs.size() == 1) - { - auto dz = client_dzs.front(); - DynamicZoneLocation zonein = dz->GetZoneInLocation(); - ZoneMode zone_mode = dz->HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords; - MovePC(zone_id, dz->GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode); - } - else - { - LogDynamicZonesDetail("Sending DzSwitchListWnd to [{}] for zone [{}] with [{}] dynamic zone(s)", - CharacterID(), zone_id, client_dzs.size()); - - // client has more than one dz for this zone, send out the switchlist window - QueuePacket(CreateDzSwitchListPacket(client_dzs).get()); - } +bool Client::CanThisClassTrack() { + return (GetClassTrackingDistanceMultiplier(GetClass()) > 0) ? true : false; } -bool Client::TryMovePCDynamicZoneSwitch(int dz_switch_id) +void Client::ReconnectUCS() { - auto client_dzs = GetDynamicZones(); + EQApplicationPacket *outapp = nullptr; + std::string buffer; + std::string mail_key = m_mail_key; + EQ::versions::UCSVersion connection_type = EQ::versions::ucsUnknown; + + // chat server packet + switch (ClientVersion()) { + case EQ::versions::ClientVersion::Titanium: + connection_type = EQ::versions::ucsTitaniumChat; + break; + case EQ::versions::ClientVersion::SoF: + connection_type = EQ::versions::ucsSoFCombined; + break; + case EQ::versions::ClientVersion::SoD: + connection_type = EQ::versions::ucsSoDCombined; + break; + case EQ::versions::ClientVersion::UF: + connection_type = EQ::versions::ucsUFCombined; + break; + case EQ::versions::ClientVersion::RoF: + connection_type = EQ::versions::ucsRoFCombined; + break; + case EQ::versions::ClientVersion::RoF2: + connection_type = EQ::versions::ucsRoF2Combined; + break; + default: + connection_type = EQ::versions::ucsUnknown; + break; + } - std::vector switch_dzs; - auto it = std::copy_if(client_dzs.begin(), client_dzs.end(), std::back_inserter(switch_dzs), - [&](const DynamicZone* dz) { return dz->GetSwitchID() == dz_switch_id; }); + buffer = StringFormat( + "%s,%i,%s.%s,%c%s", + Config->GetUCSHost().c_str(), + Config->GetUCSPort(), + Config->ShortName.c_str(), + GetName(), + connection_type, + mail_key.c_str() + ); - if (switch_dzs.size() == 1) - { - LogDynamicZonesDetail("Moving client [{}] to dz with switch id [{}]", GetName(), dz_switch_id); - switch_dzs.front()->MovePCInto(this, true); - } - else if (switch_dzs.size() > 1) - { - QueuePacket(CreateDzSwitchListPacket(switch_dzs).get()); - } + outapp = new EQApplicationPacket(OP_SetChatServer, (buffer.length() + 1)); + memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); + outapp->pBuffer[buffer.length()] = '\0'; + + QueuePacket(outapp); + safe_delete(outapp); + + // mail server packet + switch (ClientVersion()) { + case EQ::versions::ClientVersion::Titanium: + connection_type = EQ::versions::ucsTitaniumMail; + break; + default: + // retain value from previous switch + break; + } - return !switch_dzs.empty(); -} + buffer = StringFormat( + "%s,%i,%s.%s,%c%s", + Config->GetUCSHost().c_str(), + Config->GetUCSPort(), + Config->ShortName.c_str(), + GetName(), + connection_type, + mail_key.c_str() + ); -std::unique_ptr Client::CreateDzSwitchListPacket( - const std::vector& client_dzs) -{ - uint32 count = static_cast(client_dzs.size()); - uint32 entries_size = sizeof(DynamicZoneChooseZoneEntry_Struct) * count; - uint32 outsize = sizeof(DynamicZoneChooseZone_Struct) + entries_size; - auto outapp = std::make_unique(OP_DzChooseZone, outsize); - auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->count = count; - for (int i = 0; i < client_dzs.size(); ++i) - { - outbuf->choices[i].dz_zone_id = client_dzs[i]->GetZoneID(); - outbuf->choices[i].dz_instance_id = client_dzs[i]->GetInstanceID(); - outbuf->choices[i].dz_type = static_cast(client_dzs[i]->GetType()); - strn0cpy(outbuf->choices[i].description, client_dzs[i]->GetName().c_str(), sizeof(outbuf->choices[i].description)); - strn0cpy(outbuf->choices[i].leader_name, client_dzs[i]->GetLeaderName().c_str(), sizeof(outbuf->choices[i].leader_name)); - } - return outapp; -} + outapp = new EQApplicationPacket(OP_SetChatServer2, (buffer.length() + 1)); + memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); + outapp->pBuffer[buffer.length()] = '\0'; -void Client::MovePCDynamicZone(const std::string& zone_name, int zone_version, bool msg_if_invalid) -{ - auto zone_id = ZoneID(zone_name.c_str()); - MovePCDynamicZone(zone_id, zone_version, msg_if_invalid); + QueuePacket(outapp); + safe_delete(outapp); } -void Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clip_through_walls, bool calculate_speed) { - BuffFadeByEffect(SE_Levitate); - if (CheckLosFN(target_x, target_y, target_z, 6.0f) || ignore_los) { - auto p = new EQApplicationPacket(OP_Fling, sizeof(fling_struct)); - auto* f = (fling_struct*) p->pBuffer; - - if (!calculate_speed) { - f->speed_z = value; - } else { - auto speed = 1.0f; - const auto distance = CalculateDistance(target_x, target_y, target_z); +void Client::SendReloadCommandMessages() { + SendChatLineBreak(); - auto z_diff = target_z - GetZ(); - if (z_diff != 0.0f) { - speed += std::abs(z_diff) / 12.0f; - } + auto aa_link = Saylink::Silent("#reload aa"); - speed += distance / 200.0f; + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Alternate Advancement Data globally", + aa_link + ).c_str() + ); - speed++; + auto alternate_currencies_link = Saylink::Silent("#reload alternate_currencies"); - speed = std::abs(speed); + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Alternate Currencies globally", + alternate_currencies_link + ).c_str() + ); - f->speed_z = speed; - } + auto base_data_link = Saylink::Silent("#reload base_data"); - f->collision = clip_through_walls ? 0 : -1; - f->travel_time = -1; - f->unk3 = 1; - f->disable_fall_damage = 1; - f->new_y = target_y; - f->new_x = target_x; - f->new_z = target_z; - p->priority = 6; - FastQueuePacket(&p); - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Base Data globally", + base_data_link + ).c_str() + ); -std::vector Client::GetLearnableDisciplines(uint8 min_level, uint8 max_level) { - std::vector learnable_disciplines; - for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { - bool learnable = true; - if (!IsValidSpell(spell_id)) { - continue; - } - - if (!IsDiscipline(spell_id)) { - continue; - } - - if (spells[spell_id].classes[Class::Warrior] == 0) { - continue; - } - - if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { - continue; - } - - if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { - continue; - } - - if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { - continue; - } - - if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { - continue; - } - - if (HasDisciplineLearned(spell_id)) { - continue; - } - - if (RuleB(Spells, EnableSpellGlobals) && !SpellGlobalCheck(spell_id, CharacterID())) { - learnable = false; - } else if (RuleB(Spells, EnableSpellBuckets) && !SpellBucketCheck(spell_id, CharacterID())) { - learnable = false; - } - - if (learnable) { - learnable_disciplines.push_back(spell_id); - } - } - return learnable_disciplines; -} + auto blocked_spells_link = Saylink::Silent("#reload blocked_spells"); -std::vector Client::GetLearnedDisciplines() { - std::vector learned_disciplines; - for (int index = 0; index < MAX_PP_DISCIPLINES; index++) { - if (IsValidSpell(m_pp.disciplines.values[index])) { - learned_disciplines.push_back(m_pp.disciplines.values[index]); - } - } - return learned_disciplines; -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Blocked Spells globally", + blocked_spells_link + ).c_str() + ); -std::vector Client::GetMemmedSpells() { - std::vector memmed_spells; - for (int index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { - if (IsValidSpell(m_pp.mem_spells[index])) { - memmed_spells.push_back(m_pp.mem_spells[index]); - } - } - return memmed_spells; -} + auto commands_link = Saylink::Silent("#reload commands"); -std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { - std::vector scribeable_spells; - std::unordered_map> spell_group_cache = LoadSpellGroupCache(min_level, max_level); - - for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { - bool scribeable = true; - if (!IsValidSpell(spell_id)) { - continue; - } - - if (IsDiscipline(spell_id)) { - continue; - } - - if (spells[spell_id].classes[Class::Warrior] == 0) { - continue; - } - - if (max_level && spells[spell_id].classes[m_pp.class_ - 1] > max_level) { - continue; - } - - if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) { - continue; - } - - if (spells[spell_id].skill == EQ::skills::SkillTigerClaw) { - continue; - } - - if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effect_id[EFFECT_COUNT - 1] == SE_CHA) { - continue; - } - - if (HasSpellScribed(spell_id)) { - continue; - } - - if ( - RuleB(Spells, EnableSpellGlobals) && - !SpellGlobalCheck(spell_id, CharacterID()) - ) { - scribeable = false; - } else if ( - RuleB(Spells, EnableSpellBuckets) && - !SpellBucketCheck(spell_id, CharacterID()) - ) { - scribeable = false; - } - - if (spells[spell_id].spell_group) { - const auto& g = spell_group_cache.find(spells[spell_id].spell_group); - if (g != spell_group_cache.end()) { - for (const auto& s : g->second) { - if ( - EQ::ValueWithin(spells[s].classes[m_pp.class_ - 1], min_level, max_level) && - s == spell_id && - scribeable - ) { - scribeable_spells.push_back(spell_id); - } - continue; - } - } - } else if (scribeable) { - scribeable_spells.push_back(spell_id); - } - } - return scribeable_spells; -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Commands globally", + commands_link + ).c_str() + ); -std::vector Client::GetScribedSpells() { - std::vector scribed_spells; - for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { - if (IsValidSpell(m_pp.spell_book[index])) { - scribed_spells.push_back(m_pp.spell_book[index]); - } - } - return scribed_spells; -} + auto content_flags_link = Saylink::Silent("#reload content_flags"); -void Client::SetAnon(uint8 anon_flag) { - m_pp.anon = anon_flag; - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; - spawn_appearance->spawn_id = GetID(); - spawn_appearance->type = AppearanceType::Anonymous; - spawn_appearance->parameter = anon_flag; - entity_list.QueueClients(this, outapp); - Save(); - UpdateWho(); - safe_delete(outapp); -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Content Flags globally", + content_flags_link + ).c_str() + ); -void Client::SetAFK(uint8 afk_flag) { - AFK = afk_flag; - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; - spawn_appearance->spawn_id = GetID(); - spawn_appearance->type = AppearanceType::AFK; - spawn_appearance->parameter = afk_flag; - entity_list.QueueClients(this, outapp); - safe_delete(outapp); -} + auto doors_link = Saylink::Silent("#reload doors"); -void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { - uint32 zone_id = ZoneID(zone_short_name); - std::string current_instance_type = Strings::ToLower(instance_type); - std::string instance_type_name = "public"; - if (current_instance_type.find("solo") != std::string::npos) { - instance_type_name = GetCleanName(); - } else if (current_instance_type.find("group") != std::string::npos) { - uint32 group_id = (GetGroup() ? GetGroup()->GetID() : 0); - instance_type_name = itoa(group_id); - } else if (current_instance_type.find("raid") != std::string::npos) { - uint32 raid_id = (GetRaid() ? GetRaid()->GetID() : 0); - instance_type_name = itoa(raid_id); - } else if (current_instance_type.find("guild") != std::string::npos) { - uint32 guild_id = (GuildID() > 0 ? GuildID() : 0); - instance_type_name = itoa(guild_id); - } - - std::string full_bucket_name = fmt::format( - "{}_{}_{}_{}", - current_instance_type, - instance_type_name, - instance_identifier, - zone_short_name - ); - std::string current_bucket_value = DataBucket::GetData(full_bucket_name); - uint16 instance_id = 0; - - if (current_bucket_value.length() > 0) { - instance_id = Strings::ToInt(current_bucket_value); - } else { - if(!database.GetUnusedInstanceID(instance_id)) { - Message(Chat::White, "Server was unable to find a free instance id."); - return; - } - - if(!database.CreateInstance(instance_id, zone_id, instance_version, duration)) { - Message(Chat::White, "Server was unable to create a new instance."); - return; - } - - DataBucket::SetData(full_bucket_name, itoa(instance_id), itoa(duration)); - } - - AssignToInstance(instance_id); - MovePC(zone_id, instance_id, x, y, z, heading); -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Doors globally", + doors_link + ).c_str() + ); -uint32 Client::CountItem(uint32 item_id) -{ - uint32 quantity = 0; - EQ::ItemInstance *item = nullptr; + auto data_buckets_link = Saylink::Silent("#reload data_buckets_cache"); - for (const int16& slot_id : GetInventorySlots()) { - item = GetInv().GetItem(slot_id); - if (item && item->GetID() == item_id) { - quantity += (item->IsStackable() ? item->GetCharges() : 1); - } - } + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads data buckets cache globally", + data_buckets_link + ).c_str() + ); - return quantity; -} + auto dztemplates_link = Saylink::Silent("#reload dztemplates"); -void Client::ResetItemCooldown(uint32 item_id) -{ - EQ::ItemInstance *item = nullptr; - const EQ::ItemData* item_d = database.GetItem(item_id); - if (!item_d) { - return; - } - int recast_type = item_d->RecastType; - bool found_item = false; - - for (const int16& slot_id : GetInventorySlots()) { - item = GetInv().GetItem(slot_id); - if (item) { - item_d = item->GetItem(); - if ( - item_d && - item->GetID() == item_id || - ( - item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && - item_d->RecastType == recast_type - ) - ) { - item->SetRecastTimestamp(0); - DeleteItemRecastTimer(item_d->ID); - SendItemPacket(slot_id, item, ItemPacketCharmUpdate); - found_item = true; - } - } - } - - if (!found_item) { - DeleteItemRecastTimer(item_id); //We didn't find the item but we still want to remove the timer - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Dynamic Zone Templates globally", + dztemplates_link + ).c_str() + ); -void Client::SetItemCooldown(uint32 item_id, bool use_saved_timer, uint32 in_seconds) -{ - EQ::ItemInstance *item = nullptr; - const EQ::ItemData* item_d = database.GetItem(item_id); - if (!item_d) { - return; - } - int recast_type = item_d->RecastType; - auto timestamps = database.GetItemRecastTimestamps(CharacterID()); - uint32 total_time = 0; - uint32 current_time = static_cast(std::time(nullptr)); - uint32 final_time = 0; - const auto timer_type = item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM ? item_d->RecastType : item_id; - const int timer_id = recast_type != RECAST_TYPE_UNLINKED_ITEM ? (pTimerItemStart + recast_type) : (pTimerNegativeItemReuse * item_id); - - if (use_saved_timer) { - if (item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM) { - total_time = timestamps.count(item_d->RecastType) ? timestamps.at(item_d->RecastType) : 0; - } else { - total_time = timestamps.count(item_id) ? timestamps.at(item_id) : 0; - } - } else { - total_time = current_time + in_seconds; - } - - if (total_time > current_time) { - final_time = total_time - current_time; - } - - for (const int16& slot_id : GetInventorySlots()) { - item = GetInv().GetItem(slot_id); - if (item) { - item_d = item->GetItem(); - if ( - item_d && - item->GetID() == item_id || - ( - item_d->RecastType != RECAST_TYPE_UNLINKED_ITEM && - item_d->RecastType == recast_type - ) - ) { - item->SetRecastTimestamp(total_time); - SendItemPacket(slot_id, item, ItemPacketCharmUpdate); - } - } - } - - //Start timers and update in database only when timer is changed - if (!use_saved_timer) { - GetPTimers().Clear(&database, timer_id); - GetPTimers().Start((timer_id), in_seconds); - database.UpdateItemRecast( - CharacterID(), - timer_type, - GetPTimers().Get(timer_id)->GetReadyTimestamp() - ); - } - SendItemRecastTimer(recast_type, final_time, true); -} + auto factions_link = Saylink::Silent("#reload factions"); -uint32 Client::GetItemCooldown(uint32 item_id) -{ - const EQ::ItemData* item_d = database.GetItem(item_id); - if (!item_d) { - return 0; - } + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Factions globally", + factions_link + ).c_str() + ); - int recast_type = item_d->RecastType; - auto timestamps = database.GetItemRecastTimestamps(CharacterID()); - const auto timer_type = recast_type != RECAST_TYPE_UNLINKED_ITEM ? recast_type : item_id; - uint32 total_time = 0; - uint32 current_time = static_cast(std::time(nullptr)); - uint32 final_time = 0; + auto ground_spawns_link = Saylink::Silent("#reload ground_spawns"); - total_time = timestamps.count(timer_type) ? timestamps.at(timer_type) : 0; + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Ground Spawns globally", + ground_spawns_link + ).c_str() + ); - if (total_time > current_time) { - final_time = total_time - current_time; - } + auto level_mods_link = Saylink::Silent("#reload level_mods"); - return final_time; -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Level Based Experience Modifiers globally", + level_mods_link + ).c_str() + ); -void Client::RemoveItem(uint32 item_id, uint32 quantity) -{ - uint32 removed_count = 0; - EQ::ItemInstance *item = nullptr; - - for (const int16& slot_id : GetInventorySlots()) { - if (removed_count == quantity) { - break; - } - - item = GetInv().GetItem(slot_id); - if (item && item->GetID() == item_id) { - uint32 charges = item->IsStackable() ? item->GetCharges() : 0; - uint32 stack_size = std::max(charges, static_cast(1)); - if ((removed_count + stack_size) <= quantity) { - removed_count += stack_size; - DeleteItemInInventory(slot_id, charges, true); - } else { - uint32 amount_left = (quantity - removed_count); - if (amount_left > 0 && stack_size >= amount_left) { - removed_count += amount_left; - DeleteItemInInventory(slot_id, amount_left, true); - } - } - } - } -} + auto logs_link = Saylink::Silent("#reload logs"); -void Client::SetGMStatus(int new_status) { - if (Admin() != new_status) { - database.UpdateGMStatus(AccountID(), new_status); - UpdateAdmin(); - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Log Settings globally", + logs_link + ).c_str() + ); -void Client::ApplyWeaponsStance() -{ - /* - - If you have a weapons stance bonus from at least one bonus type, each time you change weapons this function will ensure the correct - associated buffs are applied, and previous buff is removed. If your weapon stance bonus is completely removed it will, ensure buff is - also removed (ie, removing an item that has worn effect with weapon stance, or clicking off a buff). If client no longer has/never had - any spells/item/aa bonuses with weapon stance effect this function will only do a simple bool check. - - Note: Live like behavior is once you have the triggered buff you can manually click it off to remove it. Swaping any items in inventory will - reapply it automatically. - - Only buff spells should be used as triggered spell effect. IsBuffSpell function also checks spell id validity. - WeaponStance bonus arrary: 0=2H Weapon 1=Shield 2=Dualweild - - Toggling ON or OFF - - From spells, just remove the Primary buff that contains the WeaponStance effect in it. - - For items with worn effect, unequip the item. - - For AA abilities, a hotkey is used to Enable and Disable the effect. See. Client::TogglePassiveAlternativeAdvancement in aa.cpp for extensive details. - - Rank - - Most important for AA, but if you have more than one of WeaponStance effect for a given type, the spell trigger buff will apply whatever has the highest - 'rank' value from the spells table. AA's on live for this effect naturally do this. Be awere of this if making custom spells/worn effects/AA. - - When creating weapon stance effects, you do not need to use all three types. For example, can make an effect where you only get a buff from equiping shield. - - */ - - if (!IsWeaponStanceEnabled()) { - return; - } - - bool enabled = false; - bool item_bonus_exists = false; - bool aa_bonus_exists = false; - - if (weaponstance.spellbonus_enabled) { - - if (spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { - - enabled = true; - - // Check if no longer has correct combination of weapon type and buff, if so remove buff. - if (!HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && - FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); - } - else if (!HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && - FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); - } - else if (!HasDualWeaponsEquipped() && - IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && - FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - BuffFadeBySpellID(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); - } - // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. - if (HasTwoHanderEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); - } - weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; - } - else if (HasShieldEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - - if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); - } - weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; - } - else if (HasDualWeaponsEquipped() && IsBuffSpell(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - - if (!FindBuff(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - SpellOnTarget(spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); - } - weaponstance.spellbonus_buff_spell_id = spellbonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; - } - } - } - - // Spellbonus effect removal is checked in BuffFadeBySlot(int slot, bool iRecalcBonuses) in spell_effects.cpp when the buff is clicked off or fades. - - if (weaponstance.itembonus_enabled) { - - if (itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { - - enabled = true; - item_bonus_exists = true; - - - // Edge case check if have multiple items with WeaponStance worn effect. Make sure correct buffs are applied if items are removed but others left on. - if (weaponstance.itembonus_buff_spell_id) { - - bool buff_desync = true; - if (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || - weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - (weaponstance.itembonus_buff_spell_id == itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - buff_desync = false; - } - - if (buff_desync) { - int fade_spell = weaponstance.itembonus_buff_spell_id; - weaponstance.itembonus_buff_spell_id = 0; //Need to zero this before we fade to prevent any recursive loops. - BuffFadeBySpellID(fade_spell); - } - } - - // Check if no longer has correct combination of weapon type and buff, if so remove buff. - if (!HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && - FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); - } - else if (!HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && - FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); - } - else if (!HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && - FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - BuffFadeBySpellID(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); - } - - // If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. - if (HasTwoHanderEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - - if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); - } - weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; - } - else if (HasShieldEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - - if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); - } - weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; - } - else if (HasDualWeaponsEquipped() && IsBuffSpell(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - if (!FindBuff(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - SpellOnTarget(itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); - } - weaponstance.itembonus_buff_spell_id = itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; - } - } - } - - // Itembonus effect removal when item is removed - if (!item_bonus_exists && weaponstance.itembonus_enabled) { - weaponstance.itembonus_enabled = false; - - if (weaponstance.itembonus_buff_spell_id) { - BuffFadeBySpellID(weaponstance.itembonus_buff_spell_id); - weaponstance.itembonus_buff_spell_id = 0; - } - } - - if (weaponstance.aabonus_enabled) { - - if (aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || - aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { - - enabled = true; - aa_bonus_exists = true; - - //Check if no longer has correct combination of weapon type and buff, if so remove buff. - if (!HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]) && - FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]); - } - - else if (!HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]) && - FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]); - } - - else if (!HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) && - FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - BuffFadeBySpellID(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]); - } - - //If you have correct combination of weapon type and bonus, and do not already have buff, then apply buff. - if (HasTwoHanderEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H])) { - SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H], this); - } - weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_2H]; - } - - else if (HasShieldEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD])) { - SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD], this); - } - weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD]; - } - - else if (HasDualWeaponsEquipped() && IsBuffSpell(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - - if (!FindBuff(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD])) { - SpellOnTarget(aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD], this); - } - weaponstance.aabonus_buff_spell_id = aabonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]; - } - } - } - - // AA bonus removal is checked in TogglePassiveAA in aa.cpp. when the hot key is toggled. - - // If no bonuses remain present, prevent additional future checks until new bonus is applied. - if (!enabled) { - SetWeaponStanceEnabled(false); - weaponstance.aabonus_enabled = false; - weaponstance.itembonus_enabled = false; - weaponstance.spellbonus_enabled = false; - } -} + auto loot_link = Saylink::Silent("#reload loot"); -uint16 Client::GetDoorToolEntityId() const -{ - return m_door_tool_entity_id; -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Loot globally", + loot_link + ).c_str() + ); -void Client::SetDoorToolEntityId(uint16 door_tool_entity_id) -{ - Client::m_door_tool_entity_id = door_tool_entity_id; -} + auto merchants_link = Saylink::Silent("#reload merchants"); -uint16 Client::GetObjectToolEntityId() const -{ - return m_object_tool_entity_id; -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Merchants globally", + merchants_link + ).c_str() + ); -void Client::SetObjectToolEntityId(uint16 object_tool_entity_id) -{ - Client::m_object_tool_entity_id = object_tool_entity_id; -} + auto npc_emotes_link = Saylink::Silent("#reload npc_emotes"); -int Client::GetIPExemption() -{ - return database.GetIPExemption(GetIPString()); -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads NPC Emotes globally", + npc_emotes_link + ).c_str() + ); -std::string Client::GetIPString() -{ - in_addr client_ip{}; - client_ip.s_addr = GetIP(); - return inet_ntoa(client_ip); -} + auto npc_spells_link = Saylink::Silent("#reload npc_spells"); + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads NPC Spells globally", + npc_spells_link + ).c_str() + ); -void Client::SetIPExemption(int exemption_amount) -{ - database.SetIPExemption(GetIPString(), exemption_amount); -} + auto objects_link = Saylink::Silent("#reload objects"); -void Client::ReadBookByName(std::string book_name, uint8 book_type) -{ - auto b = content_db.GetBook(book_name); + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Objects globally", + objects_link + ).c_str() + ); - if (!b.text.empty()) { - LogDebug("Book Name: [{}] Text: [{}]", book_name, b.text); + auto opcodes_link = Saylink::Silent("#reload opcodes"); - auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct)); + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Opcodes globally", + opcodes_link + ).c_str() + ); - auto o = (BookText_Struct *) outapp->pBuffer; + auto perl_export_link = Saylink::Silent("#reload perl_export"); - o->window = std::numeric_limits::max(); - o->type = book_type; - o->invslot = 0; + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Perl Event Export Settings globally", + perl_export_link + ).c_str() + ); - memcpy(o->booktext, b.text.c_str(), b.text.size()); + auto quest_link_one = Saylink::Silent("#reload quest"); + auto quest_link_two = Saylink::Silent("#reload quest", "0"); + auto quest_link_three = Saylink::Silent("#reload quest 1", "1"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [{}|{}] - Reloads Quests and Timers in your current zone if specified (0 = Do Not Reload Timers, 1 = Reload Timers)", + quest_link_one, + quest_link_two, + quest_link_three + ).c_str() + ); - if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) { - if (m_pp.languages[b.language] < Language::MaxValue) { - GarbleMessage(o->booktext, (Language::MaxValue - m_pp.languages[b.language])); - } - } + auto rules_link = Saylink::Silent("#reload rules"); - QueuePacket(outapp); - safe_delete(outapp); - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Rules globally", + rules_link + ).c_str() + ); -// this will fetch raid clients if exists -// fallback to group if raid doesn't exist -// fallback to self if group doesn't exist -std::vector Client::GetPartyMembers() -{ - // get clients to update - std::vector clients_to_update = {}; - - // raid - if (const auto raid = entity_list.GetRaidByClient(this)) { - for (auto &m : raid->members) { - if (m.is_bot) { - continue; - } - - if (m.member && m.member->IsClient()) { - clients_to_update.push_back(m.member->CastToClient()); - } - } - } - - // group - if (clients_to_update.empty()) { - Group *group = entity_list.GetGroupByClient(this); - if (group) { - for (auto &m : group->members) { - if (m && m->IsClient()) { - clients_to_update.push_back(m->CastToClient()); - } - } - } - } - - // solo - if (clients_to_update.empty()) { - clients_to_update.push_back(this); - } - - return clients_to_update; -} + auto skill_caps_link = Saylink::Silent("#reload skill_caps"); -void Client::SummonBaggedItems(uint32 bag_item_id, const std::vector& bag_items) -{ - if (bag_items.empty()) - { - return; - } - - // todo: maybe some common functions for SE_SummonItem and SE_SummonItemIntoBag - - const EQ::ItemData* bag_item = database.GetItem(bag_item_id); - if (!bag_item) - { - Message(Chat::Red, fmt::format("Unable to summon item [{}]. Item not found.", bag_item_id).c_str()); - return; - } - - if (CheckLoreConflict(bag_item)) - { - DuplicateLoreMessage(bag_item_id); - return; - } - - int bag_item_charges = 1; // just summoning a single bag - EQ::ItemInstance* summoned_bag = database.CreateItem(bag_item_id, bag_item_charges); - if (!summoned_bag || !summoned_bag->IsClassBag()) - { - Message(Chat::Red, fmt::format("Failed to summon bag item [{}]", bag_item_id).c_str()); - safe_delete(summoned_bag); - return; - } - - for (const auto& item : bag_items) - { - uint8 open_slot = summoned_bag->FirstOpenSlot(); - if (open_slot == 0xff) - { - Message(Chat::Red, "Attempting to summon item in to bag, but there is no room in the summoned bag!"); - break; - } - - const EQ::ItemData* current_item = database.GetItem(item.item_id); - - if (CheckLoreConflict(current_item)) - { - DuplicateLoreMessage(item.item_id); - } - else - { - EQ::ItemInstance* summoned_bag_item = database.CreateItem( - item.item_id, - item.charges, - item.aug_1, - item.aug_2, - item.aug_3, - item.aug_4, - item.aug_5, - item.aug_6, - item.attuned, - item.custom_data, - item.ornamenticon, - item.ornamentidfile, - item.ornament_hero_model - ); - if (summoned_bag_item) - { - summoned_bag->PutItem(open_slot, *summoned_bag_item); - safe_delete(summoned_bag_item); - } - } - } - - PushItemOnCursor(*summoned_bag); - SendItemPacket(EQ::invslot::slotCursor, summoned_bag, ItemPacketLimbo); - safe_delete(summoned_bag); -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Skill Caps globally", + skill_caps_link + ).c_str() + ); -void Client::SaveSpells() -{ - std::vector character_spells = {}; + auto static_link = Saylink::Silent("#reload static"); - for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { - if (IsValidSpell(m_pp.spell_book[index])) { - auto spell = CharacterSpellsRepository::NewEntity(); - spell.id = CharacterID(); - spell.slot_id = index; - spell.spell_id = m_pp.spell_book[index]; - character_spells.emplace_back(spell); - } - } + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Static Zone Data globally", + static_link + ).c_str() + ); - CharacterSpellsRepository::DeleteWhere(database, fmt::format("id = {}", CharacterID())); + auto tasks_link = Saylink::Silent("#reload tasks"); - if (!character_spells.empty()) { - CharacterSpellsRepository::InsertMany(database, character_spells); - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} [Task ID] - Reloads Tasks globally or by ID if specified", + tasks_link + ).c_str() + ); -void Client::SaveDisciplines() -{ - std::vector v; + auto titles_link = Saylink::Silent("#reload titles"); - std::vector delete_slots; + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Titles globally", + titles_link + ).c_str() + ); - for (uint16 slot_id = 0; slot_id < MAX_PP_DISCIPLINES; slot_id++) { - if (IsValidSpell(m_pp.disciplines.values[slot_id])) { - auto e = CharacterDisciplinesRepository::NewEntity(); + auto traps_link_one = Saylink::Silent("#reload traps"); + auto traps_link_two = Saylink::Silent("#reload traps", "0"); + auto traps_link_three = Saylink::Silent("#reload traps 1", "1"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [{}|{}] - Reloads Traps in your current zone or globally if specified", + traps_link_one, + traps_link_two, + traps_link_three + ).c_str() + ); - e.id = CharacterID(); - e.slot_id = slot_id; - e.disc_id = m_pp.disciplines.values[slot_id]; + auto variables_link = Saylink::Silent("#reload variables"); - v.emplace_back(e); - } else { - delete_slots.emplace_back(std::to_string(slot_id)); - } - } + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Variables globally", + variables_link + ).c_str() + ); - if (!delete_slots.empty()) { - CharacterDisciplinesRepository::DeleteWhere( - database, - fmt::format( - "`id` = {} AND `slot_id` IN ({})", - CharacterID(), - Strings::Join(delete_slots, ", ") - ) - ); - } + auto veteran_rewards_link = Saylink::Silent("#reload veteran_rewards"); - if (!v.empty()) { - CharacterDisciplinesRepository::ReplaceMany(database, v); - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Veteran Rewards globally", + veteran_rewards_link + ).c_str() + ); -uint16 Client::ScribeSpells(uint8 min_level, uint8 max_level) -{ - auto available_book_slot = GetNextAvailableSpellBookSlot(); - std::vector spell_ids = GetScribeableSpells(min_level, max_level); - uint16 scribed_spells = 0; - - if (!spell_ids.empty()) { - for (const auto& spell_id : spell_ids) { - if (available_book_slot == -1) { - Message( - Chat::Red, - fmt::format( - "Unable to scribe {} ({}) to Spell Book because your Spell Book is full.", - spells[spell_id].name, - spell_id - ).c_str() - ); - break; - } - - if (HasSpellScribed(spell_id)) { - continue; - } - - // defer saving per spell and bulk save at the end - ScribeSpell(spell_id, available_book_slot, true, true); - available_book_slot = GetNextAvailableSpellBookSlot(available_book_slot); - scribed_spells++; - } - } - - if (scribed_spells > 0) { - std::string spell_message = ( - scribed_spells == 1 ? - "a new spell" : - fmt::format("{} new spells", scribed_spells) - ); - Message(Chat::White, fmt::format("You have learned {}!", spell_message).c_str()); - - // bulk insert spells - SaveSpells(); - } - return scribed_spells; -} + auto world_link_one = Saylink::Silent("#reload world"); + auto world_link_two = Saylink::Silent("#reload world", "0"); + auto world_link_three = Saylink::Silent("#reload world 1", "1"); + auto world_link_four = Saylink::Silent("#reload world 2", "2"); + + Message( + Chat::White, + fmt::format( + "Usage: {} [{}|{}|{}] - Reloads Quests and repops globally if specified (0 = No Repop, 1 = Repop, 2 = Force Repop)", + world_link_one, + world_link_two, + world_link_three, + world_link_four + ).c_str() + ); -uint16 Client::LearnDisciplines(uint8 min_level, uint8 max_level) -{ - auto available_discipline_slot = GetNextAvailableDisciplineSlot(); - auto character_id = CharacterID(); - std::vector spell_ids = GetLearnableDisciplines(min_level, max_level); - uint16 learned_disciplines = 0; - - if (!spell_ids.empty()) { - for (const auto& spell_id : spell_ids) { - if (available_discipline_slot == -1) { - Message( - Chat::Red, - fmt::format( - "Unable to learn {} ({}) because your Discipline slots are full.", - spells[spell_id].name, - spell_id - ).c_str() - ); - break; - } - - if (HasDisciplineLearned(spell_id)) { - continue; - } - - GetPP().disciplines.values[available_discipline_slot] = spell_id; - available_discipline_slot = GetNextAvailableDisciplineSlot(available_discipline_slot); - learned_disciplines++; - } - } - - if (learned_disciplines > 0) { - std::string discipline_message = ( - learned_disciplines == 1 ? - "a new discipline" : - fmt::format("{} new disciplines", learned_disciplines) - ); - Message(Chat::White, fmt::format("You have learned {}!", discipline_message).c_str()); - SendDisciplineUpdate(); - SaveDisciplines(); - } - - return learned_disciplines; -} + auto zone_link = Saylink::Silent("#reload zone"); -uint16 Client::GetClassTrackingDistanceMultiplier(uint16 class_) { - switch (class_) { - case Class::Warrior: - return RuleI(Character, WarriorTrackingDistanceMultiplier); - case Class::Cleric: - return RuleI(Character, ClericTrackingDistanceMultiplier); - case Class::Paladin: - return RuleI(Character, PaladinTrackingDistanceMultiplier); - case Class::Ranger: - return RuleI(Character, RangerTrackingDistanceMultiplier); - case Class::ShadowKnight: - return RuleI(Character, ShadowKnightTrackingDistanceMultiplier); - case Class::Druid: - return RuleI(Character, DruidTrackingDistanceMultiplier); - case Class::Monk: - return RuleI(Character, MonkTrackingDistanceMultiplier); - case Class::Bard: - return RuleI(Character, BardTrackingDistanceMultiplier); - case Class::Rogue: - return RuleI(Character, RogueTrackingDistanceMultiplier); - case Class::Shaman: - return RuleI(Character, ShamanTrackingDistanceMultiplier); - case Class::Necromancer: - return RuleI(Character, NecromancerTrackingDistanceMultiplier); - case Class::Wizard: - return RuleI(Character, WizardTrackingDistanceMultiplier); - case Class::Magician: - return RuleI(Character, MagicianTrackingDistanceMultiplier); - case Class::Enchanter: - return RuleI(Character, EnchanterTrackingDistanceMultiplier); - case Class::Beastlord: - return RuleI(Character, BeastlordTrackingDistanceMultiplier); - case Class::Berserker: - return RuleI(Character, BerserkerTrackingDistanceMultiplier); - default: - return 0; - } -} + Message( + Chat::White, + fmt::format( + "Usage: {} [Zone ID] [Version] - Reloads Zone configuration for your current zone, can load another Zone's configuration if specified", + zone_link + ).c_str() + ); -bool Client::CanThisClassTrack() { - return (GetClassTrackingDistanceMultiplier(GetClass()) > 0) ? true : false; -} + auto zone_points_link = Saylink::Silent("#reload zone_points"); -void Client::ReconnectUCS() -{ - EQApplicationPacket *outapp = nullptr; - std::string buffer; - std::string mail_key = m_mail_key; - EQ::versions::UCSVersion connection_type = EQ::versions::ucsUnknown; - - // chat server packet - switch (ClientVersion()) { - case EQ::versions::ClientVersion::Titanium: - connection_type = EQ::versions::ucsTitaniumChat; - break; - case EQ::versions::ClientVersion::SoF: - connection_type = EQ::versions::ucsSoFCombined; - break; - case EQ::versions::ClientVersion::SoD: - connection_type = EQ::versions::ucsSoDCombined; - break; - case EQ::versions::ClientVersion::UF: - connection_type = EQ::versions::ucsUFCombined; - break; - case EQ::versions::ClientVersion::RoF: - connection_type = EQ::versions::ucsRoFCombined; - break; - case EQ::versions::ClientVersion::RoF2: - connection_type = EQ::versions::ucsRoF2Combined; - break; - default: - connection_type = EQ::versions::ucsUnknown; - break; - } - - buffer = StringFormat( - "%s,%i,%s.%s,%c%s", - Config->GetUCSHost().c_str(), - Config->GetUCSPort(), - Config->ShortName.c_str(), - GetName(), - connection_type, - mail_key.c_str() - ); - - outapp = new EQApplicationPacket(OP_SetChatServer, (buffer.length() + 1)); - memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); - outapp->pBuffer[buffer.length()] = '\0'; - - QueuePacket(outapp); - safe_delete(outapp); - - // mail server packet - switch (ClientVersion()) { - case EQ::versions::ClientVersion::Titanium: - connection_type = EQ::versions::ucsTitaniumMail; - break; - default: - // retain value from previous switch - break; - } - - buffer = StringFormat( - "%s,%i,%s.%s,%c%s", - Config->GetUCSHost().c_str(), - Config->GetUCSPort(), - Config->ShortName.c_str(), - GetName(), - connection_type, - mail_key.c_str() - ); - - outapp = new EQApplicationPacket(OP_SetChatServer2, (buffer.length() + 1)); - memcpy(outapp->pBuffer, buffer.c_str(), buffer.length()); - outapp->pBuffer[buffer.length()] = '\0'; - - QueuePacket(outapp); - safe_delete(outapp); -} + Message( + Chat::White, + fmt::format( + "Usage: {} - Reloads Zone Points globally", + zone_points_link + ).c_str() + ); -void Client::SendReloadCommandMessages() { - SendChatLineBreak(); - - auto aa_link = Saylink::Silent("#reload aa"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Alternate Advancement Data globally", - aa_link - ).c_str() - ); - - auto alternate_currencies_link = Saylink::Silent("#reload alternate_currencies"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Alternate Currencies globally", - alternate_currencies_link - ).c_str() - ); - - auto base_data_link = Saylink::Silent("#reload base_data"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Base Data globally", - base_data_link - ).c_str() - ); - - auto blocked_spells_link = Saylink::Silent("#reload blocked_spells"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Blocked Spells globally", - blocked_spells_link - ).c_str() - ); - - auto commands_link = Saylink::Silent("#reload commands"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Commands globally", - commands_link - ).c_str() - ); - - auto content_flags_link = Saylink::Silent("#reload content_flags"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Content Flags globally", - content_flags_link - ).c_str() - ); - - auto doors_link = Saylink::Silent("#reload doors"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Doors globally", - doors_link - ).c_str() - ); - - auto data_buckets_link = Saylink::Silent("#reload data_buckets_cache"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads data buckets cache globally", - data_buckets_link - ).c_str() - ); - - auto dztemplates_link = Saylink::Silent("#reload dztemplates"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Dynamic Zone Templates globally", - dztemplates_link - ).c_str() - ); - - auto factions_link = Saylink::Silent("#reload factions"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Factions globally", - factions_link - ).c_str() - ); - - auto ground_spawns_link = Saylink::Silent("#reload ground_spawns"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Ground Spawns globally", - ground_spawns_link - ).c_str() - ); - - auto level_mods_link = Saylink::Silent("#reload level_mods"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Level Based Experience Modifiers globally", - level_mods_link - ).c_str() - ); - - auto logs_link = Saylink::Silent("#reload logs"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Log Settings globally", - logs_link - ).c_str() - ); - - auto loot_link = Saylink::Silent("#reload loot"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Loot globally", - loot_link - ).c_str() - ); - - auto merchants_link = Saylink::Silent("#reload merchants"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Merchants globally", - merchants_link - ).c_str() - ); - - auto npc_emotes_link = Saylink::Silent("#reload npc_emotes"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads NPC Emotes globally", - npc_emotes_link - ).c_str() - ); - - auto npc_spells_link = Saylink::Silent("#reload npc_spells"); - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads NPC Spells globally", - npc_spells_link - ).c_str() - ); - - auto objects_link = Saylink::Silent("#reload objects"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Objects globally", - objects_link - ).c_str() - ); - - auto opcodes_link = Saylink::Silent("#reload opcodes"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Opcodes globally", - opcodes_link - ).c_str() - ); - - auto perl_export_link = Saylink::Silent("#reload perl_export"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Perl Event Export Settings globally", - perl_export_link - ).c_str() - ); - - auto quest_link_one = Saylink::Silent("#reload quest"); - auto quest_link_two = Saylink::Silent("#reload quest", "0"); - auto quest_link_three = Saylink::Silent("#reload quest 1", "1"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [{}|{}] - Reloads Quests and Timers in your current zone if specified (0 = Do Not Reload Timers, 1 = Reload Timers)", - quest_link_one, - quest_link_two, - quest_link_three - ).c_str() - ); - - auto rules_link = Saylink::Silent("#reload rules"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Rules globally", - rules_link - ).c_str() - ); - - auto skill_caps_link = Saylink::Silent("#reload skill_caps"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Skill Caps globally", - skill_caps_link - ).c_str() - ); - - auto static_link = Saylink::Silent("#reload static"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Static Zone Data globally", - static_link - ).c_str() - ); - - auto tasks_link = Saylink::Silent("#reload tasks"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [Task ID] - Reloads Tasks globally or by ID if specified", - tasks_link - ).c_str() - ); - - auto titles_link = Saylink::Silent("#reload titles"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Titles globally", - titles_link - ).c_str() - ); - - auto traps_link_one = Saylink::Silent("#reload traps"); - auto traps_link_two = Saylink::Silent("#reload traps", "0"); - auto traps_link_three = Saylink::Silent("#reload traps 1", "1"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [{}|{}] - Reloads Traps in your current zone or globally if specified", - traps_link_one, - traps_link_two, - traps_link_three - ).c_str() - ); - - auto variables_link = Saylink::Silent("#reload variables"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Variables globally", - variables_link - ).c_str() - ); - - auto veteran_rewards_link = Saylink::Silent("#reload veteran_rewards"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Veteran Rewards globally", - veteran_rewards_link - ).c_str() - ); - - auto world_link_one = Saylink::Silent("#reload world"); - auto world_link_two = Saylink::Silent("#reload world", "0"); - auto world_link_three = Saylink::Silent("#reload world 1", "1"); - auto world_link_four = Saylink::Silent("#reload world 2", "2"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [{}|{}|{}] - Reloads Quests and repops globally if specified (0 = No Repop, 1 = Repop, 2 = Force Repop)", - world_link_one, - world_link_two, - world_link_three, - world_link_four - ).c_str() - ); - - auto zone_link = Saylink::Silent("#reload zone"); - - Message( - Chat::White, - fmt::format( - "Usage: {} [Zone ID] [Version] - Reloads Zone configuration for your current zone, can load another Zone's configuration if specified", - zone_link - ).c_str() - ); - - auto zone_points_link = Saylink::Silent("#reload zone_points"); - - Message( - Chat::White, - fmt::format( - "Usage: {} - Reloads Zone Points globally", - zone_points_link - ).c_str() - ); - - SendChatLineBreak(); + SendChatLineBreak(); } void Client::Undye() { - for (uint8 slot = EQ::textures::textureBegin; slot <= EQ::textures::LastTexture; slot++) { - auto inventory_slot = SlotConvert(slot); - auto inst = m_inv.GetItem(inventory_slot); + for (uint8 slot = EQ::textures::textureBegin; slot <= EQ::textures::LastTexture; slot++) { + auto inventory_slot = SlotConvert(slot); + auto inst = m_inv.GetItem(inventory_slot); - if (inst) { - inst->SetColor(inst->GetItem()->Color); - database.SaveInventory(CharacterID(), inst, inventory_slot); - } + if (inst) { + inst->SetColor(inst->GetItem()->Color); + database.SaveInventory(CharacterID(), inst, inventory_slot); + } - m_pp.item_tint.Slot[slot].Color = 0; - SendWearChange(slot); - } + m_pp.item_tint.Slot[slot].Color = 0; + SendWearChange(slot); + } - database.DeleteCharacterMaterialColor(CharacterID()); + database.DeleteCharacterMaterialColor(CharacterID()); } void Client::SetTrackingID(uint32 entity_id) { - if (!entity_id) { - TrackingID = 0; - return; - } + if (!entity_id) { + TrackingID = 0; + return; + } - auto *m = entity_list.GetMob(entity_id); - if (!m) { - TrackingID = 0; - return; - } + auto *m = entity_list.GetMob(entity_id); + if (!m) { + TrackingID = 0; + return; + } - TrackingID = entity_id; + TrackingID = entity_id; } int Client::GetRecipeMadeCount(uint32 recipe_id) { - auto r = CharRecipeListRepository::GetWhere( - database, - fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) - ); + auto r = CharRecipeListRepository::GetWhere( + database, + fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) + ); - if (!r.empty() && r[0].recipe_id) { - return r[0].madecount; - } + if (!r.empty() && r[0].recipe_id) { + return r[0].madecount; + } - return 0; + return 0; } bool Client::HasRecipeLearned(uint32 recipe_id) { - auto r = CharRecipeListRepository::GetWhere( - database, - fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) - ); + auto r = CharRecipeListRepository::GetWhere( + database, + fmt::format("char_id = {} AND recipe_id = {}", CharacterID(), recipe_id) + ); - if (!r.empty() && r[0].recipe_id) { - return true; - } + if (!r.empty() && r[0].recipe_id) { + return true; + } - return false; + return false; } bool Client::IsLockSavePosition() const { - return m_lock_save_position; + return m_lock_save_position; } void Client::SetLockSavePosition(bool lock_save_position) { - Client::m_lock_save_position = lock_save_position; + Client::m_lock_save_position = lock_save_position; } void Client::SetAAPoints(uint32 points) { - const uint32 current_points = m_pp.aapoints; + const uint32 current_points = m_pp.aapoints; - m_pp.aapoints = points; + m_pp.aapoints = points; - QuestEventID event_id = points > current_points ? EVENT_AA_GAIN : EVENT_AA_LOSS; - const uint32 change = event_id == EVENT_AA_GAIN ? points - current_points : current_points - points; + QuestEventID event_id = points > current_points ? EVENT_AA_GAIN : EVENT_AA_LOSS; + const uint32 change = event_id == EVENT_AA_GAIN ? points - current_points : current_points - points; - if (parse->PlayerHasQuestSub(event_id)) { - parse->EventPlayer(event_id, this, std::to_string(change), 0); - } + if (parse->PlayerHasQuestSub(event_id)) { + parse->EventPlayer(event_id, this, std::to_string(change), 0); + } - SendAlternateAdvancementStats(); + SendAlternateAdvancementStats(); } bool Client::RemoveAAPoints(uint32 points) { - if (m_pp.aapoints < points) { - return false; - } + if (m_pp.aapoints < points) { + return false; + } - m_pp.aapoints -= points; + m_pp.aapoints -= points; - if (parse->PlayerHasQuestSub(EVENT_AA_LOSS)) { - parse->EventPlayer(EVENT_AA_LOSS, this, std::to_string(points), 0); - } + if (parse->PlayerHasQuestSub(EVENT_AA_LOSS)) { + parse->EventPlayer(EVENT_AA_LOSS, this, std::to_string(points), 0); + } - SendAlternateAdvancementStats(); + SendAlternateAdvancementStats(); - return true; + return true; } void Client::AddAAPoints(uint32 points) { - m_pp.aapoints += points; + m_pp.aapoints += points; - if (parse->PlayerHasQuestSub(EVENT_AA_GAIN)) { - parse->EventPlayer(EVENT_AA_GAIN, this, std::to_string(points), 0); - } + if (parse->PlayerHasQuestSub(EVENT_AA_GAIN)) { + parse->EventPlayer(EVENT_AA_GAIN, this, std::to_string(points), 0); + } - if (points == 1 && m_pp.aapoints == 1) { - MessageString(Chat::Yellow, GAIN_SINGLE_AA_SINGLE_AA, fmt::format_int(m_pp.aapoints).c_str()); - } else if (points == 1 && m_pp.aapoints > 1) { - MessageString(Chat::Yellow, GAIN_SINGLE_AA_MULTI_AA, fmt::format_int(m_pp.aapoints).c_str()); - } else { - MessageString(Chat::Yellow, GAIN_MULTI_AA_MULTI_AA, fmt::format_int(points).c_str(), fmt::format_int(m_pp.aapoints).c_str()); - } + if (points == 1 && m_pp.aapoints == 1) { + MessageString(Chat::Yellow, GAIN_SINGLE_AA_SINGLE_AA, fmt::format_int(m_pp.aapoints).c_str()); + } else if (points == 1 && m_pp.aapoints > 1) { + MessageString(Chat::Yellow, GAIN_SINGLE_AA_MULTI_AA, fmt::format_int(m_pp.aapoints).c_str()); + } else { + MessageString(Chat::Yellow, GAIN_MULTI_AA_MULTI_AA, fmt::format_int(points).c_str(), fmt::format_int(m_pp.aapoints).c_str()); + } - SendAlternateAdvancementStats(); + SendAlternateAdvancementStats(); } bool Client::SendGMCommand(std::string message, bool ignore_status) { - return command_dispatch(this, message, ignore_status) >= 0 ? true : false; + return command_dispatch(this, message, ignore_status) >= 0 ? true : false; } void Client::RegisterBug(BugReport_Struct* r) { - if (!r) { - return; - } - - auto b = BugReportsRepository::NewEntity(); - - b.zone = zone->GetShortName(); - b.client_version_id = static_cast(ClientVersion()); - b.client_version_name = EQ::versions::ClientVersionName(ClientVersion()); - b.account_id = AccountID(); - b.character_id = CharacterID(); - b.character_name = GetName(); - b.reporter_spoof = (strcmp(GetCleanName(), r->reporter_name) != 0 ? 1 : 0); - b.category_id = r->category_id; - b.category_name = r->category_name; - b.reporter_name = r->reporter_name; - b.ui_path = r->ui_path; - b.pos_x = r->pos_x; - b.pos_y = r->pos_y; - b.pos_z = r->pos_z; - b.heading = r->heading; - b.time_played = r->time_played; - b.target_id = r->target_id; - b.target_name = r->target_name; - b.optional_info_mask = r->optional_info_mask; - b._can_duplicate = ((r->optional_info_mask & Bug::InformationFlag::Repeatable) != 0 ? 1 : 0); - b._crash_bug = ((r->optional_info_mask & Bug::InformationFlag::Crash) != 0 ? 1 : 0); - b._target_info = ((r->optional_info_mask & Bug::InformationFlag::TargetInfo) != 0 ? 1 : 0); - b._character_flags = ((r->optional_info_mask & Bug::InformationFlag::CharacterFlags) != 0 ? 1 : 0); - b._unknown_value = ((r->optional_info_mask & Bug::InformationFlag::Unknown) != 0 ? 1 : 0); - b.bug_report = r->bug_report; - b.system_info = r->system_info; + if (!r) { + return; + } + + auto b = BugReportsRepository::NewEntity(); + + b.zone = zone->GetShortName(); + b.client_version_id = static_cast(ClientVersion()); + b.client_version_name = EQ::versions::ClientVersionName(ClientVersion()); + b.account_id = AccountID(); + b.character_id = CharacterID(); + b.character_name = GetName(); + b.reporter_spoof = (strcmp(GetCleanName(), r->reporter_name) != 0 ? 1 : 0); + b.category_id = r->category_id; + b.category_name = r->category_name; + b.reporter_name = r->reporter_name; + b.ui_path = r->ui_path; + b.pos_x = r->pos_x; + b.pos_y = r->pos_y; + b.pos_z = r->pos_z; + b.heading = r->heading; + b.time_played = r->time_played; + b.target_id = r->target_id; + b.target_name = r->target_name; + b.optional_info_mask = r->optional_info_mask; + b._can_duplicate = ((r->optional_info_mask & Bug::InformationFlag::Repeatable) != 0 ? 1 : 0); + b._crash_bug = ((r->optional_info_mask & Bug::InformationFlag::Crash) != 0 ? 1 : 0); + b._target_info = ((r->optional_info_mask & Bug::InformationFlag::TargetInfo) != 0 ? 1 : 0); + b._character_flags = ((r->optional_info_mask & Bug::InformationFlag::CharacterFlags) != 0 ? 1 : 0); + b._unknown_value = ((r->optional_info_mask & Bug::InformationFlag::Unknown) != 0 ? 1 : 0); + b.bug_report = r->bug_report; + b.system_info = r->system_info; #ifdef LUA_EQEMU - bool ignore_default = false; - LuaParser::Instance()->RegisterBug(this, b, ignore_default); - if (ignore_default) { - return; - } + bool ignore_default = false; + LuaParser::Instance()->RegisterBug(this, b, ignore_default); + if (ignore_default) { + return; + } #endif - auto n = BugReportsRepository::InsertOne(database, b); - if (!n.id) { - Message(Chat::White, "Failed to created your bug report."); // Client sends success message - return; - } - - LogBugs("id [{}] report [{}] account [{}] name [{}] charid [{}] zone [{}]", n.id, r->bug_report, AccountID(), GetCleanName(), CharacterID(), zone->GetShortName()); - - worldserver.SendEmoteMessage( - 0, - 0, - AccountStatus::QuestTroupe, - Chat::Yellow, - fmt::format( - "{} has created a new bug report, would you like to {} it?", - GetCleanName(), - Saylink::Silent( - fmt::format( - "#bugs view {}", - n.id - ), - "view" - ) - ).c_str() - ); + auto n = BugReportsRepository::InsertOne(database, b); + if (!n.id) { + Message(Chat::White, "Failed to created your bug report."); // Client sends success message + return; + } + + LogBugs("id [{}] report [{}] account [{}] name [{}] charid [{}] zone [{}]", n.id, r->bug_report, AccountID(), GetCleanName(), CharacterID(), zone->GetShortName()); + + worldserver.SendEmoteMessage( + 0, + 0, + AccountStatus::QuestTroupe, + Chat::Yellow, + fmt::format( + "{} has created a new bug report, would you like to {} it?", + GetCleanName(), + Saylink::Silent( + fmt::format( + "#bugs view {}", + n.id + ), + "view" + ) + ).c_str() + ); } std::vector Client::GetApplySpellList( - ApplySpellType apply_type, - bool allow_pets, - bool is_raid_group_only, - bool allow_bots + ApplySpellType apply_type, + bool allow_pets, + bool is_raid_group_only, + bool allow_bots ) { - std::vector l; - - if (apply_type == ApplySpellType::Raid && IsRaidGrouped()) { - auto* r = GetRaid(); - if (r) { - auto group_id = r->GetGroup(this); - if (EQ::ValueWithin(group_id, 0, (MAX_RAID_GROUPS - 1))) { - for (const auto& m : r->members) { - if (m.member && m.member->IsClient() && (!is_raid_group_only || r->GetGroup(m.member) == group_id)) { - l.push_back(m.member); - - if (allow_pets && m.member->HasPet()) { - l.push_back(m.member->GetPet()); - } - - if (allow_bots) { - const auto& sbl = entity_list.GetBotListByCharacterID(m.member->CharacterID()); - for (const auto& b : sbl) { - l.push_back(b); - } - } - } - } - } - } - } else if (apply_type == ApplySpellType::Group && IsGrouped()) { - auto* g = GetGroup(); - if (g) { - for (auto i = 0; i < MAX_GROUP_MEMBERS; i++) { - auto* m = g->members[i]; - if (m && m->IsClient()) { - l.push_back(m->CastToClient()); - - if (allow_pets && m->HasPet()) { - l.push_back(m->GetPet()); - } - - if (allow_bots) { - const auto& sbl = entity_list.GetBotListByCharacterID(m->CastToClient()->CharacterID()); - for (const auto& b : sbl) { - l.push_back(b); - } - } - } - } - } - } else { - l.push_back(this); - - if (allow_pets && HasPet()) { - l.push_back(GetPet()); - } - - if (allow_bots) { - const auto& sbl = entity_list.GetBotListByCharacterID(CharacterID()); - for (const auto& b : sbl) { - l.push_back(b); - } - } - } - - return l; + std::vector l; + + if (apply_type == ApplySpellType::Raid && IsRaidGrouped()) { + auto* r = GetRaid(); + if (r) { + auto group_id = r->GetGroup(this); + if (EQ::ValueWithin(group_id, 0, (MAX_RAID_GROUPS - 1))) { + for (const auto& m : r->members) { + if (m.member && m.member->IsClient() && (!is_raid_group_only || r->GetGroup(m.member) == group_id)) { + l.push_back(m.member); + + if (allow_pets && m.member->HasPet()) { + l.push_back(m.member->GetPet()); + } + + if (allow_bots) { + const auto& sbl = entity_list.GetBotListByCharacterID(m.member->CharacterID()); + for (const auto& b : sbl) { + l.push_back(b); + } + } + } + } + } + } + } else if (apply_type == ApplySpellType::Group && IsGrouped()) { + auto* g = GetGroup(); + if (g) { + for (auto i = 0; i < MAX_GROUP_MEMBERS; i++) { + auto* m = g->members[i]; + if (m && m->IsClient()) { + l.push_back(m->CastToClient()); + + if (allow_pets && m->HasPet()) { + l.push_back(m->GetPet()); + } + + if (allow_bots) { + const auto& sbl = entity_list.GetBotListByCharacterID(m->CastToClient()->CharacterID()); + for (const auto& b : sbl) { + l.push_back(b); + } + } + } + } + } + } else { + l.push_back(this); + + if (allow_pets && HasPet()) { + l.push_back(GetPet()); + } + + if (allow_bots) { + const auto& sbl = entity_list.GetBotListByCharacterID(CharacterID()); + for (const auto& b : sbl) { + l.push_back(b); + } + } + } + + return l; } void Client::ApplySpell( - int spell_id, - int duration, - int level, - ApplySpellType apply_type, - bool allow_pets, - bool is_raid_group_only, - bool allow_bots + int spell_id, + int duration, + int level, + ApplySpellType apply_type, + bool allow_pets, + bool is_raid_group_only, + bool allow_bots ) { - const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); + const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); - for (const auto& m : l) { - m->ApplySpellBuff(spell_id, duration, level); - } + for (const auto& m : l) { + m->ApplySpellBuff(spell_id, duration, level); + } } void Client::SetSpellDuration( - int spell_id, - int duration, - int level, - ApplySpellType apply_type, - bool allow_pets, - bool is_raid_group_only, - bool allow_bots + int spell_id, + int duration, + int level, + ApplySpellType apply_type, + bool allow_pets, + bool is_raid_group_only, + bool allow_bots ) { - const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); + const auto& l = GetApplySpellList(apply_type, allow_pets, is_raid_group_only, allow_bots); - for (const auto& m : l) { - m->SetBuffDuration(spell_id, duration, level); - } + for (const auto& m : l) { + m->SetBuffDuration(spell_id, duration, level); + } } std::string Client::GetGuildPublicNote() { - if (!IsInAGuild()) { - return std::string(); - } + if (!IsInAGuild()) { + return std::string(); + } - CharGuildInfo gci; - if (!guild_mgr.GetCharInfo(character_id, gci)) { - return std::string(); - } + CharGuildInfo gci; + if (!guild_mgr.GetCharInfo(character_id, gci)) { + return std::string(); + } - return gci.public_note; + return gci.public_note; } void Client::MaxSkills() { - for (const auto &s : EQ::skills::GetSkillTypeMap()) { - auto current_skill_value = ( - EQ::skills::IsSpecializedSkill(s.first) ? - MAX_SPECIALIZED_SKILL : - skill_caps.GetSkillCap(GetClass(), s.first, GetLevel()).cap - ); + for (const auto &s : EQ::skills::GetSkillTypeMap()) { + auto current_skill_value = ( + EQ::skills::IsSpecializedSkill(s.first) ? + MAX_SPECIALIZED_SKILL : + skill_caps.GetSkillCap(GetClass(), s.first, GetLevel()).cap + ); - if (GetSkill(s.first) < current_skill_value) { - SetSkill(s.first, current_skill_value); - } - } + if (GetSkill(s.first) < current_skill_value) { + SetSkill(s.first, current_skill_value); + } + } } void Client::SendPath(Mob* target) { - if (!target) { - EQApplicationPacket outapp(OP_FindPersonReply, 0); - QueuePacket(&outapp); - return; - } - - - if ( - !RuleB(Pathing, Find) && - RuleB(Bazaar, EnableWarpToTrader) && - target->IsClient() && - ( - target->CastToClient()->IsTrader() || - target->CastToClient()->IsBuyer() - ) - ) { - Message( - Chat::Yellow, - fmt::format( - "Moving you to Trader {}.", - target->GetName() - ).c_str() - ); - MovePC( - zone->GetZoneID(), - zone->GetInstanceID(), - target->GetX(), - target->GetY(), - target->GetZ(), - 0.0f - ); - return; - } - - std::vector points; - - if (!RuleB(Pathing, Find) || !zone->pathing) { - points.clear(); - FindPerson_Point a; - FindPerson_Point b; - - a.x = GetX(); - a.y = GetY(); - a.z = GetZ(); - b.x = target->GetX(); - b.y = target->GetY(); - b.z = target->GetZ(); - - points.push_back(a); - points.push_back(b); - } - else { - glm::vec3 path_start( - GetX(), - GetY(), - GetZ() + (GetSize() < 6.0 ? 6 : GetSize()) * HEAD_POSITION - ); - - glm::vec3 path_end( - target->GetX(), - target->GetY(), - target->GetZ() + (target->GetSize() < 6.0 ? 6 : target->GetSize()) * HEAD_POSITION - ); - - bool partial = false; - bool stuck = false; - auto path_list = zone->pathing->FindRoute(path_start, path_end, partial, stuck); - - if (path_list.empty() || partial) { - EQApplicationPacket outapp(OP_FindPersonReply, 0); - QueuePacket(&outapp); - return; - } - - // Live appears to send the points in this order: - // Final destination. - // Current Position. - // rest of the points. - FindPerson_Point p; - - int point_number = 0; - - bool leads_to_teleporter = false; - - auto v = path_list.back(); - - p.x = v.pos.x; - p.y = v.pos.y; - p.z = v.pos.z; - points.push_back(p); - - p.x = GetX(); - p.y = GetY(); - p.z = GetZ(); - points.push_back(p); - - for (const auto &n: path_list) { - if (n.teleport) { - leads_to_teleporter = true; - break; - } - - glm::vec3 v = n.pos; - p.x = v.x; - p.y = v.y; - p.z = v.z; - - points.push_back(p); - - ++point_number; - } - - if (!leads_to_teleporter) { - p.x = target->GetX(); - p.y = target->GetY(); - p.z = target->GetZ(); - - points.push_back(p); - } - } - - SendPathPacket(points); + if (!target) { + EQApplicationPacket outapp(OP_FindPersonReply, 0); + QueuePacket(&outapp); + return; + } + + + if ( + !RuleB(Pathing, Find) && + RuleB(Bazaar, EnableWarpToTrader) && + target->IsClient() && + ( + target->CastToClient()->IsTrader() || + target->CastToClient()->IsBuyer() + ) + ) { + Message( + Chat::Yellow, + fmt::format( + "Moving you to Trader {}.", + target->GetName() + ).c_str() + ); + MovePC( + zone->GetZoneID(), + zone->GetInstanceID(), + target->GetX(), + target->GetY(), + target->GetZ(), + 0.0f + ); + return; + } + + std::vector points; + + if (!RuleB(Pathing, Find) || !zone->pathing) { + points.clear(); + FindPerson_Point a; + FindPerson_Point b; + + a.x = GetX(); + a.y = GetY(); + a.z = GetZ(); + b.x = target->GetX(); + b.y = target->GetY(); + b.z = target->GetZ(); + + points.push_back(a); + points.push_back(b); + } + else { + glm::vec3 path_start( + GetX(), + GetY(), + GetZ() + (GetSize() < 6.0 ? 6 : GetSize()) * HEAD_POSITION + ); + + glm::vec3 path_end( + target->GetX(), + target->GetY(), + target->GetZ() + (target->GetSize() < 6.0 ? 6 : target->GetSize()) * HEAD_POSITION + ); + + bool partial = false; + bool stuck = false; + auto path_list = zone->pathing->FindRoute(path_start, path_end, partial, stuck); + + if (path_list.empty() || partial) { + EQApplicationPacket outapp(OP_FindPersonReply, 0); + QueuePacket(&outapp); + return; + } + + // Live appears to send the points in this order: + // Final destination. + // Current Position. + // rest of the points. + FindPerson_Point p; + + int point_number = 0; + + bool leads_to_teleporter = false; + + auto v = path_list.back(); + + p.x = v.pos.x; + p.y = v.pos.y; + p.z = v.pos.z; + points.push_back(p); + + p.x = GetX(); + p.y = GetY(); + p.z = GetZ(); + points.push_back(p); + + for (const auto &n: path_list) { + if (n.teleport) { + leads_to_teleporter = true; + break; + } + + glm::vec3 v = n.pos; + p.x = v.x; + p.y = v.y; + p.z = v.z; + + points.push_back(p); + + ++point_number; + } + + if (!leads_to_teleporter) { + p.x = target->GetX(); + p.y = target->GetY(); + p.z = target->GetZ(); + + points.push_back(p); + } + } + + SendPathPacket(points); } 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; - Object::HandleAugmentation(this, in_augment, nullptr); - safe_delete_array(in_augment); + auto in_augment = new AugmentItem_Struct[sizeof(AugmentItem_Struct)]; + in_augment->container_slot = container_slot; + 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; + 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); + 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); } void Client::NPCHandinEventLog(Trade* t, NPC* n) { - Client* c = t->GetOwner()->CastToClient(); - - std::vector hi = {}; - std::vector ri = {}; - PlayerEvent::HandinMoney hm{}; - PlayerEvent::HandinMoney rm{}; - - if ( - c->EntityVariableExists("HANDIN_ITEMS") && - c->EntityVariableExists("HANDIN_MONEY") && - c->EntityVariableExists("RETURN_ITEMS") && - c->EntityVariableExists("RETURN_MONEY") - ) { - const std::string& handin_items = c->GetEntityVariable("HANDIN_ITEMS"); - const std::string& return_items = c->GetEntityVariable("RETURN_ITEMS"); - const std::string& handin_money = c->GetEntityVariable("HANDIN_MONEY"); - const std::string& return_money = c->GetEntityVariable("RETURN_MONEY"); - - // 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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - if (item_id != 0) { - const auto* item = database.GetItem(item_id); - - if (item) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - - // Handin Money - if (!handin_money.empty()) { - const auto hms = Strings::Split(handin_money, "|"); - - hm.copper = Strings::ToUnsignedInt(hms[0]); - hm.silver = Strings::ToUnsignedInt(hms[1]); - hm.gold = Strings::ToUnsignedInt(hms[2]); - hm.platinum = Strings::ToUnsignedInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - ri.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - ri.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - - // Return Money - if (!return_money.empty()) { - const auto rms = Strings::Split(return_money, "|"); - rm.copper = static_cast(Strings::ToUnsignedInt(rms[0])); - rm.silver = static_cast(Strings::ToUnsignedInt(rms[1])); - rm.gold = static_cast(Strings::ToUnsignedInt(rms[2])); - rm.platinum = static_cast(Strings::ToUnsignedInt(rms[3])); - } - - c->DeleteEntityVariable("HANDIN_ITEMS"); - c->DeleteEntityVariable("HANDIN_MONEY"); - c->DeleteEntityVariable("RETURN_ITEMS"); - c->DeleteEntityVariable("RETURN_MONEY"); - - const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; - - const bool event_has_data_to_record = ( - !hi.empty() || handed_in_money - ); - - if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { - auto e = PlayerEvent::HandinEvent{ - .npc_id = n->GetNPCTypeID(), - .npc_name = n->GetCleanName(), - .handin_items = hi, - .handin_money = hm, - .return_items = ri, - .return_money = rm, - .is_quest_handin = true - }; - - RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); - } - - return; - } - - uint8 item_count = 0; - - hm.platinum = t->pp; - hm.gold = t->gp; - hm.silver = t->sp; - hm.copper = t->cp; - - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { - if (c->GetInv().GetItem(i)) { - item_count++; - } - } - - hi.reserve(item_count); - - if (item_count > 0) { - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { - const EQ::ItemInstance* inst = c->GetInv().GetItem(i); - if (inst) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = inst->GetItem()->ID, - .item_name = inst->GetItem()->Name, - .charges = static_cast(inst->GetCharges()), - .attuned = inst->IsAttuned() - } - ); - - if (inst->IsClassBag()) { - for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { - inst = c->GetInv().GetItem(i, j); - if (inst) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = inst->GetItem()->ID, - .item_name = inst->GetItem()->Name, - .charges = static_cast(inst->GetCharges()), - .attuned = inst->IsAttuned() - } - ); - } - } - } - } - } - } - - const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; - - ri = hi; - rm = hm; - - const bool event_has_data_to_record = !hi.empty() || handed_in_money; - - if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { - auto e = PlayerEvent::HandinEvent{ - .npc_id = n->GetNPCTypeID(), - .npc_name = n->GetCleanName(), - .handin_items = hi, - .handin_money = hm, - .return_items = ri, - .return_money = rm, - .is_quest_handin = false - }; - - RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); - } + Client* c = t->GetOwner()->CastToClient(); + + std::vector hi = {}; + std::vector ri = {}; + PlayerEvent::HandinMoney hm{}; + PlayerEvent::HandinMoney rm{}; + + if ( + c->EntityVariableExists("HANDIN_ITEMS") && + c->EntityVariableExists("HANDIN_MONEY") && + c->EntityVariableExists("RETURN_ITEMS") && + c->EntityVariableExists("RETURN_MONEY") + ) { + const std::string& handin_items = c->GetEntityVariable("HANDIN_ITEMS"); + const std::string& return_items = c->GetEntityVariable("RETURN_ITEMS"); + const std::string& handin_money = c->GetEntityVariable("HANDIN_MONEY"); + const std::string& return_money = c->GetEntityVariable("RETURN_MONEY"); + + // 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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + if (item_id != 0) { + const auto* item = database.GetItem(item_id); + + if (item) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + const auto* item = database.GetItem(item_id); + + if (item) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(item_data[2]) ? true : false + } + ); + } + } + } + } + + // Handin Money + if (!handin_money.empty()) { + const auto hms = Strings::Split(handin_money, "|"); + + hm.copper = Strings::ToUnsignedInt(hms[0]); + hm.silver = Strings::ToUnsignedInt(hms[1]); + hm.gold = Strings::ToUnsignedInt(hms[2]); + hm.platinum = Strings::ToUnsignedInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + const auto* item = database.GetItem(item_id); + + if (item) { + ri.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(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 uint32 item_id = Strings::ToUnsignedInt(item_data[0]); + const auto* item = database.GetItem(item_id); + + if (item) { + ri.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = item_id, + .item_name = item->Name, + .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), + .attuned = Strings::ToInt(item_data[2]) ? true : false + } + ); + } + } + } + } + + // Return Money + if (!return_money.empty()) { + const auto rms = Strings::Split(return_money, "|"); + rm.copper = static_cast(Strings::ToUnsignedInt(rms[0])); + rm.silver = static_cast(Strings::ToUnsignedInt(rms[1])); + rm.gold = static_cast(Strings::ToUnsignedInt(rms[2])); + rm.platinum = static_cast(Strings::ToUnsignedInt(rms[3])); + } + + c->DeleteEntityVariable("HANDIN_ITEMS"); + c->DeleteEntityVariable("HANDIN_MONEY"); + c->DeleteEntityVariable("RETURN_ITEMS"); + c->DeleteEntityVariable("RETURN_MONEY"); + + const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; + + const bool event_has_data_to_record = ( + !hi.empty() || handed_in_money + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { + auto e = PlayerEvent::HandinEvent{ + .npc_id = n->GetNPCTypeID(), + .npc_name = n->GetCleanName(), + .handin_items = hi, + .handin_money = hm, + .return_items = ri, + .return_money = rm, + .is_quest_handin = true + }; + + RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); + } + + return; + } + + uint8 item_count = 0; + + hm.platinum = t->pp; + hm.gold = t->gp; + hm.silver = t->sp; + hm.copper = t->cp; + + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { + if (c->GetInv().GetItem(i)) { + item_count++; + } + } + + hi.reserve(item_count); + + if (item_count > 0) { + for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { + const EQ::ItemInstance* inst = c->GetInv().GetItem(i); + if (inst) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .attuned = inst->IsAttuned() + } + ); + + if (inst->IsClassBag()) { + for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { + inst = c->GetInv().GetItem(i, j); + if (inst) { + hi.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = inst->GetItem()->ID, + .item_name = inst->GetItem()->Name, + .charges = static_cast(inst->GetCharges()), + .attuned = inst->IsAttuned() + } + ); + } + } + } + } + } + } + + const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; + + ri = hi; + rm = hm; + + const bool event_has_data_to_record = !hi.empty() || handed_in_money; + + if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { + auto e = PlayerEvent::HandinEvent{ + .npc_id = n->GetNPCTypeID(), + .npc_name = n->GetCleanName(), + .handin_items = hi, + .handin_money = hm, + .return_items = ri, + .return_money = rm, + .is_quest_handin = false + }; + + RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); + } } void Client::ShowSpells(Client* c, ShowSpellType show_spell_type) { - std::string spell_string; - - switch (show_spell_type) { - case ShowSpellType::Disciplines: - spell_string = "Discipline"; - break; - case ShowSpellType::Spells: - spell_string = "Spell"; - break; - default: - return; - } - - std::string spell_table; - - // Headers - spell_table += DialogueWindow::TableRow( - fmt::format( - "{}{}{}", - DialogueWindow::TableCell("Slot"), - DialogueWindow::TableCell(spell_string), - DialogueWindow::TableCell("Spell ID") - ) - ); - - std::map m; - auto spell_count = 0; - - if (show_spell_type == ShowSpellType::Disciplines) { - for (auto index = 0; index < MAX_PP_DISCIPLINES; index++) { - if (IsValidSpell(m_pp.disciplines.values[index])) { - m[index] = static_cast(m_pp.disciplines.values[index]); - } - } - } else if (show_spell_type == ShowSpellType::Spells) { - for (auto index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { - if (IsValidSpell(m_pp.mem_spells[index])) { - m[index] = static_cast(m_pp.mem_spells[index]); - } - } - } - - for (const auto& s : m) { - spell_table += DialogueWindow::TableRow( - fmt::format( - "{}{}{}", - DialogueWindow::TableCell(std::to_string(s.first)), - DialogueWindow::TableCell(GetSpellName(s.second)), - DialogueWindow::TableCell(Strings::Commify(s.second)) - ) - ); - - spell_count++; - } - - if (!spell_count) { - c->Message( - Chat::White, - fmt::format( - "{} {} not have any {}s {}.", - c->GetTargetDescription(this, TargetDescriptionType::UCYou), - c == this ? "do" : "does", - Strings::ToLower(spell_string), - show_spell_type == ShowSpellType::Disciplines ? "learned" : "memorized" - ).c_str() - ); - return; - } - - if (spell_table.size() >= 4096) { - for (const auto& [index, spell_id] : m) { - c->Message( - Chat::White, - fmt::format( - "{}. {} ({})", - index, - GetSpellName(spell_id), - spell_id - ).c_str() - ); - } - return; - } - - spell_table = DialogueWindow::Table(spell_table); - - c->SendPopupToClient( - fmt::format( - "{}s for {}", - spell_string, - c->GetTargetDescription(this, TargetDescriptionType::UCSelf) - ).c_str(), - spell_table.c_str() - ); + std::string spell_string; + + switch (show_spell_type) { + case ShowSpellType::Disciplines: + spell_string = "Discipline"; + break; + case ShowSpellType::Spells: + spell_string = "Spell"; + break; + default: + return; + } + + std::string spell_table; + + // Headers + spell_table += DialogueWindow::TableRow( + fmt::format( + "{}{}{}", + DialogueWindow::TableCell("Slot"), + DialogueWindow::TableCell(spell_string), + DialogueWindow::TableCell("Spell ID") + ) + ); + + std::map m; + auto spell_count = 0; + + if (show_spell_type == ShowSpellType::Disciplines) { + for (auto index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (IsValidSpell(m_pp.disciplines.values[index])) { + m[index] = static_cast(m_pp.disciplines.values[index]); + } + } + } else if (show_spell_type == ShowSpellType::Spells) { + for (auto index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { + if (IsValidSpell(m_pp.mem_spells[index])) { + m[index] = static_cast(m_pp.mem_spells[index]); + } + } + } + + for (const auto& s : m) { + spell_table += DialogueWindow::TableRow( + fmt::format( + "{}{}{}", + DialogueWindow::TableCell(std::to_string(s.first)), + DialogueWindow::TableCell(GetSpellName(s.second)), + DialogueWindow::TableCell(Strings::Commify(s.second)) + ) + ); + + spell_count++; + } + + if (!spell_count) { + c->Message( + Chat::White, + fmt::format( + "{} {} not have any {}s {}.", + c->GetTargetDescription(this, TargetDescriptionType::UCYou), + c == this ? "do" : "does", + Strings::ToLower(spell_string), + show_spell_type == ShowSpellType::Disciplines ? "learned" : "memorized" + ).c_str() + ); + return; + } + + if (spell_table.size() >= 4096) { + for (const auto& [index, spell_id] : m) { + c->Message( + Chat::White, + fmt::format( + "{}. {} ({})", + index, + GetSpellName(spell_id), + spell_id + ).c_str() + ); + } + return; + } + + spell_table = DialogueWindow::Table(spell_table); + + c->SendPopupToClient( + fmt::format( + "{}s for {}", + spell_string, + c->GetTargetDescription(this, TargetDescriptionType::UCSelf) + ).c_str(), + spell_table.c_str() + ); } std::string GetZoneModeString(ZoneMode mode) { - switch (mode) { - case ZoneToSafeCoords: - return "ZoneToSafeCoords"; - case GMSummon: - return "GMSummon"; - case GMHiddenSummon: - return "GMHiddenSummon"; - case ZoneToBindPoint: - return "ZoneToBindPoint"; - case ZoneSolicited: - return "ZoneSolicited"; - case ZoneUnsolicited: - return "ZoneUnsolicited"; - case GateToBindPoint: - return "GateToBindPoint"; - case SummonPC: - return "SummonPC"; - case Rewind: - return "Rewind"; - case EvacToSafeCoords: - return "EvacToSafeCoords"; - default: - return "Unknown"; - } + switch (mode) { + case ZoneToSafeCoords: + return "ZoneToSafeCoords"; + case GMSummon: + return "GMSummon"; + case GMHiddenSummon: + return "GMHiddenSummon"; + case ZoneToBindPoint: + return "ZoneToBindPoint"; + case ZoneSolicited: + return "ZoneSolicited"; + case ZoneUnsolicited: + return "ZoneUnsolicited"; + case GateToBindPoint: + return "GateToBindPoint"; + case SummonPC: + return "SummonPC"; + case Rewind: + return "Rewind"; + case EvacToSafeCoords: + return "EvacToSafeCoords"; + default: + return "Unknown"; + } } void Client::ClearXTargets() { - if (!XTargettingAvailable()) { - return; - } + if (!XTargettingAvailable()) { + return; + } - for (int i = 0; i < GetMaxXTargets(); ++i) { - if (XTargets[i].ID) { - Mob* m = entity_list.GetMob(XTargets[i].ID); + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].ID) { + Mob* m = entity_list.GetMob(XTargets[i].ID); - if (m) { - RemoveXTarget(m, false); - } + if (m) { + RemoveXTarget(m, false); + } - XTargets[i].ID = 0; - XTargets[i].Name[0] = 0; - XTargets[i].dirty = false; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + XTargets[i].dirty = false; - SendXTargetPacket(i, nullptr); - } - } + SendXTargetPacket(i, nullptr); + } + } } float Client::GetAAEXPModifier(uint32 zone_id, int16 instance_version) { - return database.GetAAEXPModifierByCharID( - CharacterID(), - zone_id, - instance_version - ); + return database.GetAAEXPModifierByCharID( + CharacterID(), + zone_id, + instance_version + ); } float Client::GetEXPModifier(uint32 zone_id, int16 instance_version) { - return database.GetEXPModifierByCharID( - CharacterID(), - zone_id, - instance_version - ); + return database.GetEXPModifierByCharID( + CharacterID(), + zone_id, + instance_version + ); } void Client::SetAAEXPModifier(uint32 zone_id, float aa_modifier, int16 instance_version) { - database.SetAAEXPModifierByCharID( - CharacterID(), - zone_id, - aa_modifier, - instance_version - ); + database.SetAAEXPModifierByCharID( + CharacterID(), + zone_id, + aa_modifier, + instance_version + ); - database.LoadCharacterEXPModifier(this); + database.LoadCharacterEXPModifier(this); } void Client::SetEXPModifier(uint32 zone_id, float exp_modifier, int16 instance_version) { - database.SetEXPModifierByCharID( - CharacterID(), - zone_id, - exp_modifier, - instance_version - ); + database.SetEXPModifierByCharID( + CharacterID(), + zone_id, + exp_modifier, + instance_version + ); - database.LoadCharacterEXPModifier(this); + database.LoadCharacterEXPModifier(this); } int Client::GetAAEXPPercentage() { - int scaled = static_cast(330.0f * static_cast(GetAAXP()) / GetRequiredAAExperience()); + int scaled = static_cast(330.0f * static_cast(GetAAXP()) / GetRequiredAAExperience()); - return static_cast(std::round(scaled * 100.0 / 330.0)); + return static_cast(std::round(scaled * 100.0 / 330.0)); } int Client::GetEXPPercentage() { - float norm = 0.0f; - uint32_t min = GetEXPForLevel(GetLevel()); - uint32_t max = GetEXPForLevel(GetLevel() + 1); + float norm = 0.0f; + uint32_t min = GetEXPForLevel(GetLevel()); + uint32_t max = GetEXPForLevel(GetLevel() + 1); - if (min != max) { - norm = static_cast(GetEXP() - min) / (max - min); - } + if (min != max) { + norm = static_cast(GetEXP() - min) / (max - min); + } - int scaled = static_cast(330.0f * norm); // scale and truncate + int scaled = static_cast(330.0f * norm); // scale and truncate - return static_cast(std::round(scaled * 100.0 / 330.0)); // unscaled pct + return static_cast(std::round(scaled * 100.0 / 330.0)); // unscaled pct } std::vector Client::GetRaidOrGroupOrSelf(bool clients_only) { - std::vector v; + std::vector v; - if (IsRaidGrouped()) { - Raid* r = GetRaid(); + if (IsRaidGrouped()) { + Raid* r = GetRaid(); - if (r) { - for (const auto& m : r->members) { - if (m.member && (!m.is_bot || !clients_only)) { - v.emplace_back(m.member); - } - } - } - } else if (IsGrouped()) { - Group* g = GetGroup(); + if (r) { + for (const auto& m : r->members) { + if (m.member && (!m.is_bot || !clients_only)) { + v.emplace_back(m.member); + } + } + } + } else if (IsGrouped()) { + Group* g = GetGroup(); - if (g) { - for (const auto& m : g->members) { - if (m && (m->IsClient() || !clients_only)) { - v.emplace_back(m); - } - } - } - } else { - v.emplace_back(this); - } + if (g) { + for (const auto& m : g->members) { + if (m && (m->IsClient() || !clients_only)) { + v.emplace_back(m); + } + } + } + } else { + v.emplace_back(this); + } - return v; + return v; } uint16 Client::GetSkill(EQ::skills::SkillType skill_id) const { - if (skill_id <= EQ::skills::HIGHEST_SKILL) { - return (itembonuses.skillmod[skill_id] > 0 ? (itembonuses.skillmodmax[skill_id] > 0 ? std::min( - m_pp.skills[skill_id] + itembonuses.skillmodmax[skill_id], - m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100 - ) : m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100) : m_pp.skills[skill_id]); - } - return 0; + if (skill_id <= EQ::skills::HIGHEST_SKILL) { + return (itembonuses.skillmod[skill_id] > 0 ? (itembonuses.skillmodmax[skill_id] > 0 ? std::min( + m_pp.skills[skill_id] + itembonuses.skillmodmax[skill_id], + m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100 + ) : m_pp.skills[skill_id] * (100 + itembonuses.skillmod[skill_id]) / 100) : m_pp.skills[skill_id]); + } + return 0; } void Client::RemoveItemBySerialNumber(uint32 serial_number, uint32 quantity) { - EQ::ItemInstance *item = nullptr; + EQ::ItemInstance *item = nullptr; - uint32 removed_count = 0; + uint32 removed_count = 0; - const auto& slot_ids = GetInventorySlots(); + const auto& slot_ids = GetInventorySlots(); - for (const int16& slot_id : slot_ids) { - if (removed_count == quantity) { - break; - } + for (const int16& slot_id : slot_ids) { + if (removed_count == quantity) { + break; + } - item = GetInv().GetItem(slot_id); - if (item && item->GetSerialNumber() == serial_number) { - uint32 charges = item->IsStackable() ? item->GetCharges() : 0; - uint32 stack_size = std::max(charges, static_cast(1)); - if ((removed_count + stack_size) <= quantity) { - removed_count += stack_size; - DeleteItemInInventory(slot_id, charges, true); - } else { - uint32 amount_left = (quantity - removed_count); - if (amount_left > 0 && stack_size >= amount_left) { - removed_count += amount_left; - DeleteItemInInventory(slot_id, amount_left, true); - } - } - } - } + item = GetInv().GetItem(slot_id); + if (item && item->GetSerialNumber() == serial_number) { + uint32 charges = item->IsStackable() ? item->GetCharges() : 0; + uint32 stack_size = std::max(charges, static_cast(1)); + if ((removed_count + stack_size) <= quantity) { + removed_count += stack_size; + DeleteItemInInventory(slot_id, charges, true); + } else { + uint32 amount_left = (quantity - removed_count); + if (amount_left > 0 && stack_size >= amount_left) { + removed_count += amount_left; + DeleteItemInInventory(slot_id, amount_left, true); + } + } + } + } } void Client::AddMoneyToPPWithOverflow(uint64 copper, bool update_client) { - //I noticed in the ROF2 client that the client auto updates the currency values using overflow - //Therefore, I created this method to ensure that the db matches and clients don't see 10 pp 5 gp - //becoming 9pp 15 gold with the current AddMoneyToPP method. + //I noticed in the ROF2 client that the client auto updates the currency values using overflow + //Therefore, I created this method to ensure that the db matches and clients don't see 10 pp 5 gp + //becoming 9pp 15 gold with the current AddMoneyToPP method. - auto add_pp = copper / 1000; - auto add_gp = (copper - add_pp * 1000) / 100; - auto add_sp = (copper - add_pp * 1000 - add_gp * 100) / 10; - auto add_cp = copper - add_pp * 1000 - add_gp * 100 - add_sp * 10; + auto add_pp = copper / 1000; + auto add_gp = (copper - add_pp * 1000) / 100; + auto add_sp = (copper - add_pp * 1000 - add_gp * 100) / 10; + auto add_cp = copper - add_pp * 1000 - add_gp * 100 - add_sp * 10; - m_pp.copper += add_cp; - if (m_pp.copper >= 10) { - m_pp.silver += m_pp.copper / 10; - m_pp.copper = m_pp.copper % 10; - } + m_pp.copper += add_cp; + if (m_pp.copper >= 10) { + m_pp.silver += m_pp.copper / 10; + m_pp.copper = m_pp.copper % 10; + } - m_pp.silver += add_sp; - if (m_pp.silver >= 10) { - m_pp.gold += m_pp.silver / 10; - m_pp.silver = m_pp.silver % 10; - } + m_pp.silver += add_sp; + if (m_pp.silver >= 10) { + m_pp.gold += m_pp.silver / 10; + m_pp.silver = m_pp.silver % 10; + } - m_pp.gold += add_gp; - if (m_pp.gold >= 10) { - m_pp.platinum += m_pp.gold / 10; - m_pp.gold = m_pp.gold % 10; - } + m_pp.gold += add_gp; + if (m_pp.gold >= 10) { + m_pp.platinum += m_pp.gold / 10; + m_pp.gold = m_pp.gold % 10; + } - m_pp.platinum += add_pp; + m_pp.platinum += add_pp; - if (update_client) { - SendMoneyUpdate(); - } + if (update_client) { + SendMoneyUpdate(); + } - RecalcWeight(); - SaveCurrency(); + RecalcWeight(); + SaveCurrency(); - LogDebug("Client::AddMoneyToPPWithOverflow() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", - GetName(), - m_pp.platinum, - m_pp.gold, - m_pp.silver, - m_pp.copper - ); + LogDebug("Client::AddMoneyToPPWithOverflow() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", + GetName(), + m_pp.platinum, + m_pp.gold, + m_pp.silver, + m_pp.copper + ); } bool Client::TakeMoneyFromPPWithOverFlow(uint64 copper, bool update_client) { - int32 remove_pp = copper / 1000; - int32 remove_gp = (copper - remove_pp * 1000) / 100; - int32 remove_sp = (copper - remove_pp * 1000 - remove_gp * 100) / 10; - int32 remove_cp = copper - remove_pp * 1000 - remove_gp * 100 - remove_sp * 10; - - uint64 current_money = GetCarriedMoney(); - - if (copper > current_money) { - return false; //client does not have enough money on them - } - - m_pp.copper -= remove_cp; - if (m_pp.copper < 0) { - m_pp.silver -= 1; - m_pp.copper = m_pp.copper + 10; - if (m_pp.copper >= 10) { - m_pp.silver += m_pp.copper / 10; - m_pp.copper = m_pp.copper % 10; - } - } - - m_pp.silver -= remove_sp; - if (m_pp.silver < 0) { - m_pp.gold -= 1; - m_pp.silver = m_pp.silver + 10; - if (m_pp.silver >= 10) { - m_pp.gold += m_pp.silver / 10; - m_pp.silver = m_pp.silver % 10; - } - } - - m_pp.gold -= remove_gp; - if (m_pp.gold < 0) { - m_pp.platinum -= 1; - m_pp.gold = m_pp.gold + 10; - if (m_pp.gold >= 10) { - m_pp.platinum += m_pp.gold / 10; - m_pp.gold = m_pp.gold % 10; - } - } - - m_pp.platinum -= remove_pp; - - if (update_client) { - SendMoneyUpdate(); - } - - SaveCurrency(); - RecalcWeight(); - return true; + int32 remove_pp = copper / 1000; + int32 remove_gp = (copper - remove_pp * 1000) / 100; + int32 remove_sp = (copper - remove_pp * 1000 - remove_gp * 100) / 10; + int32 remove_cp = copper - remove_pp * 1000 - remove_gp * 100 - remove_sp * 10; + + uint64 current_money = GetCarriedMoney(); + + if (copper > current_money) { + return false; //client does not have enough money on them + } + + m_pp.copper -= remove_cp; + if (m_pp.copper < 0) { + m_pp.silver -= 1; + m_pp.copper = m_pp.copper + 10; + if (m_pp.copper >= 10) { + m_pp.silver += m_pp.copper / 10; + m_pp.copper = m_pp.copper % 10; + } + } + + m_pp.silver -= remove_sp; + if (m_pp.silver < 0) { + m_pp.gold -= 1; + m_pp.silver = m_pp.silver + 10; + if (m_pp.silver >= 10) { + m_pp.gold += m_pp.silver / 10; + m_pp.silver = m_pp.silver % 10; + } + } + + m_pp.gold -= remove_gp; + if (m_pp.gold < 0) { + m_pp.platinum -= 1; + m_pp.gold = m_pp.gold + 10; + if (m_pp.gold >= 10) { + m_pp.platinum += m_pp.gold / 10; + m_pp.gold = m_pp.gold % 10; + } + } + + m_pp.platinum -= remove_pp; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + RecalcWeight(); + return true; } void Client::SendTopLevelInventory() { - EQ::ItemInstance* inst = nullptr; + EQ::ItemInstance* inst = nullptr; - static const int16 slots[][2] = { - { EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END }, - { EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END }, - { EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END } - }; + static const int16 slots[][2] = { + { EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END }, + { EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END }, + { EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END } + }; - const auto& inv = GetInv(); + const auto& inv = GetInv(); - const size_t slot_index_count = sizeof(slots) / sizeof(slots[0]); - for (int slot_index = 0; slot_index < slot_index_count; ++slot_index) { - for (int slot_id = slots[slot_index][0]; slot_id <= slots[slot_index][1]; ++slot_id) { - inst = inv.GetItem(slot_id); - if (inst) { - SendItemPacket(slot_id, inst, ItemPacketType::ItemPacketTrade); - } - } - } + const size_t slot_index_count = sizeof(slots) / sizeof(slots[0]); + for (int slot_index = 0; slot_index < slot_index_count; ++slot_index) { + for (int slot_id = slots[slot_index][0]; slot_id <= slots[slot_index][1]; ++slot_id) { + inst = inv.GetItem(slot_id); + if (inst) { + SendItemPacket(slot_id, inst, ItemPacketType::ItemPacketTrade); + } + } + } } void Client::CheckSendBulkNpcPositions() @@ -13083,8 +13082,8 @@ void Client::CheckSendBulkNpcPositions() update_range ); - m_last_position_before_bulk_update = GetPosition(); - } + m_last_position_before_bulk_update = GetPosition(); + } } @@ -13093,123 +13092,123 @@ const uint16 scan_npc_aggro_timer_moving = RuleI(Aggro, ClientAggroCheckMovingIn void Client::CheckClientToNpcAggroTimer() { - LogAggroDetail( - "ClientUpdate [{}] {}moving, scan timer [{}]", - GetCleanName(), - IsMoving() ? "" : "NOT ", - m_client_npc_aggro_scan_timer.GetRemainingTime() - ); - - if (IsMoving()) { - if (m_client_npc_aggro_scan_timer.GetRemainingTime() > scan_npc_aggro_timer_moving) { - LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName()); - m_client_npc_aggro_scan_timer.Disable(); - m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_moving); - m_client_npc_aggro_scan_timer.Trigger(); - } - } - else if (m_client_npc_aggro_scan_timer.GetDuration() == scan_npc_aggro_timer_moving) { - LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName()); - m_client_npc_aggro_scan_timer.Disable(); - m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_idle); - } + LogAggroDetail( + "ClientUpdate [{}] {}moving, scan timer [{}]", + GetCleanName(), + IsMoving() ? "" : "NOT ", + m_client_npc_aggro_scan_timer.GetRemainingTime() + ); + + if (IsMoving()) { + if (m_client_npc_aggro_scan_timer.GetRemainingTime() > scan_npc_aggro_timer_moving) { + LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName()); + m_client_npc_aggro_scan_timer.Disable(); + m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_moving); + m_client_npc_aggro_scan_timer.Trigger(); + } + } + else if (m_client_npc_aggro_scan_timer.GetDuration() == scan_npc_aggro_timer_moving) { + LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName()); + m_client_npc_aggro_scan_timer.Disable(); + m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_idle); + } } void Client::ClientToNpcAggroProcess() { - if (zone->CanDoCombat() && !GetFeigned() && m_client_npc_aggro_scan_timer.Check()) { - int npc_scan_count = 0; - for (auto &close_mob: GetCloseMobList()) { - Mob *mob = close_mob.second; - if (!mob) { - continue; - } + if (zone->CanDoCombat() && !GetFeigned() && m_client_npc_aggro_scan_timer.Check()) { + int npc_scan_count = 0; + for (auto &close_mob: GetCloseMobList()) { + Mob *mob = close_mob.second; + if (!mob) { + continue; + } - if (mob->IsClient()) { - continue; - } + if (mob->IsClient()) { + continue; + } - if (mob->CheckWillAggro(this) && !mob->CheckAggro(this)) { - mob->AddToHateList(this, 25); - } + if (mob->CheckWillAggro(this) && !mob->CheckAggro(this)) { + mob->AddToHateList(this, 25); + } - npc_scan_count++; - } - LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count); - } + npc_scan_count++; + } + LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count); + } } const std::vector& Client::GetInventorySlots() { - static const std::vector> slots = { - {EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END}, - {EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END}, - {EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END}, - {EQ::invslot::BANK_BEGIN, EQ::invslot::BANK_END}, - {EQ::invbag::BANK_BAGS_BEGIN, EQ::invbag::BANK_BAGS_END}, - {EQ::invslot::SHARED_BANK_BEGIN, EQ::invslot::SHARED_BANK_END}, - {EQ::invbag::SHARED_BANK_BAGS_BEGIN, EQ::invbag::SHARED_BANK_BAGS_END}, - }; - - static std::vector slot_ids; - - if (slot_ids.empty()) { - for (const auto &[begin, end]: slots) { - for (int16 slot_id = begin; slot_id <= end; ++slot_id) { - slot_ids.emplace_back(slot_id); - } - } - } + static const std::vector> slots = { + {EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END}, + {EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END}, + {EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END}, + {EQ::invslot::BANK_BEGIN, EQ::invslot::BANK_END}, + {EQ::invbag::BANK_BAGS_BEGIN, EQ::invbag::BANK_BAGS_END}, + {EQ::invslot::SHARED_BANK_BEGIN, EQ::invslot::SHARED_BANK_END}, + {EQ::invbag::SHARED_BANK_BAGS_BEGIN, EQ::invbag::SHARED_BANK_BAGS_END}, + }; + + static std::vector slot_ids; + + if (slot_ids.empty()) { + for (const auto &[begin, end]: slots) { + for (int16 slot_id = begin; slot_id <= end; ++slot_id) { + slot_ids.emplace_back(slot_id); + } + } + } - return slot_ids; + return slot_ids; } void Client::ShowZoneShardMenu() { - auto z = GetZone(GetZoneID()); - if (z && !z->shard_at_player_count) { - return; - } - - auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, GetZoneID()); - LogZoning("Zone sharding results count [{}]", results.size()); - - if (results.empty()) { - Message(Chat::White, "No zone shards found."); - return; - } - - if (!results.empty()) { - Message(Chat::White, "Available Zone Shards:"); - } - - int number = 1; - for (auto &e: results) { - std::string teleport_link = Saylink::Silent( - fmt::format("#zoneshard {} {}", e.zone_id, (e.instance_id == 0 ? -1 : e.instance_id)), - "Teleport" - ); - - std::string yours; - if (e.zone_id == GetZoneID() && e.instance_id == GetInstanceID()) { - teleport_link = "Teleport"; - yours = " (Yours)"; - } - - Message( - Chat::White, fmt::format( - " --> [{}] #{} {} ({}) [{}/{}] players {}", - teleport_link, - number, - z->long_name, - e.instance_id, - e.player_count, - z->shard_at_player_count, - yours - ).c_str() - ); - number++; - } + auto z = GetZone(GetZoneID()); + if (z && !z->shard_at_player_count) { + return; + } + + auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, GetZoneID()); + LogZoning("Zone sharding results count [{}]", results.size()); + + if (results.empty()) { + Message(Chat::White, "No zone shards found."); + return; + } + + if (!results.empty()) { + Message(Chat::White, "Available Zone Shards:"); + } + + int number = 1; + for (auto &e: results) { + std::string teleport_link = Saylink::Silent( + fmt::format("#zoneshard {} {}", e.zone_id, (e.instance_id == 0 ? -1 : e.instance_id)), + "Teleport" + ); + + std::string yours; + if (e.zone_id == GetZoneID() && e.instance_id == GetInstanceID()) { + teleport_link = "Teleport"; + yours = " (Yours)"; + } + + Message( + Chat::White, fmt::format( + " --> [{}] #{} {} ({}) [{}/{}] players {}", + teleport_link, + number, + z->long_name, + e.instance_id, + e.player_count, + z->shard_at_player_count, + yours + ).c_str() + ); + number++; + } } void Client::SetAAEXPPercentage(uint8 percentage) From 9e5a45654ea2877597f5d20863fd984d8fe4d38c Mon Sep 17 00:00:00 2001 From: Akkadius Date: Wed, 22 Jan 2025 03:13:32 -0600 Subject: [PATCH 6/7] Repository fixes --- .../base/base_character_pet_name_repository.h | 52 +++++++------------ zone/client.cpp | 12 +++-- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/common/repositories/base/base_character_pet_name_repository.h b/common/repositories/base/base_character_pet_name_repository.h index 5c4307666d..59a8a572f2 100644 --- a/common/repositories/base/base_character_pet_name_repository.h +++ b/common/repositories/base/base_character_pet_name_repository.h @@ -19,31 +19,28 @@ class BaseCharacterPetNameRepository { public: struct CharacterPetName { - int32_t char_id; + int32_t character_id; std::string name; - int8_t class_id; }; static std::string PrimaryKey() { - return std::string("char_id"); + return std::string("character_id"); } static std::vector Columns() { return { - "char_id", + "character_id", "name", - "class_id", }; } static std::vector SelectColumns() { return { - "char_id", + "character_id", "name", - "class_id", }; } @@ -84,9 +81,8 @@ class BaseCharacterPetNameRepository { { CharacterPetName e{}; - e.char_id = 0; - e.name = ""; - e.class_id = -1; + e.character_id = 0; + e.name = ""; return e; } @@ -97,7 +93,7 @@ class BaseCharacterPetNameRepository { ) { for (auto &character_pet_name : character_pet_names) { - if (character_pet_name.char_id == character_pet_name_id) { + if (character_pet_name.character_id == character_pet_name_id) { return character_pet_name; } } @@ -123,9 +119,8 @@ class BaseCharacterPetNameRepository { if (results.RowCount() == 1) { CharacterPetName e{}; - e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; - e.name = row[1] ? row[1] : ""; - e.class_id = row[2] ? static_cast(atoi(row[2])) : -1; + e.character_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.name = row[1] ? row[1] : ""; return e; } @@ -159,9 +154,8 @@ class BaseCharacterPetNameRepository { auto columns = Columns(); - v.push_back(columns[0] + " = " + std::to_string(e.char_id)); + v.push_back(columns[0] + " = " + std::to_string(e.character_id)); v.push_back(columns[1] + " = '" + Strings::Escape(e.name) + "'"); - v.push_back(columns[2] + " = " + std::to_string(e.class_id)); auto results = db.QueryDatabase( fmt::format( @@ -169,7 +163,7 @@ class BaseCharacterPetNameRepository { TableName(), Strings::Implode(", ", v), PrimaryKey(), - e.char_id + e.character_id ) ); @@ -183,9 +177,8 @@ class BaseCharacterPetNameRepository { { std::vector v; - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back("'" + Strings::Escape(e.name) + "'"); - v.push_back(std::to_string(e.class_id)); auto results = db.QueryDatabase( fmt::format( @@ -196,7 +189,7 @@ class BaseCharacterPetNameRepository { ); if (results.Success()) { - e.char_id = results.LastInsertedID(); + e.character_id = results.LastInsertedID(); return e; } @@ -215,9 +208,8 @@ class BaseCharacterPetNameRepository { for (auto &e: entries) { std::vector v; - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back("'" + Strings::Escape(e.name) + "'"); - v.push_back(std::to_string(e.class_id)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -251,9 +243,8 @@ class BaseCharacterPetNameRepository { for (auto row = results.begin(); row != results.end(); ++row) { CharacterPetName e{}; - e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; - e.name = row[1] ? row[1] : ""; - e.class_id = row[2] ? static_cast(atoi(row[2])) : -1; + e.character_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.name = row[1] ? row[1] : ""; all_entries.push_back(e); } @@ -278,9 +269,8 @@ class BaseCharacterPetNameRepository { for (auto row = results.begin(); row != results.end(); ++row) { CharacterPetName e{}; - e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; - e.name = row[1] ? row[1] : ""; - e.class_id = row[2] ? static_cast(atoi(row[2])) : -1; + e.character_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.name = row[1] ? row[1] : ""; all_entries.push_back(e); } @@ -355,9 +345,8 @@ class BaseCharacterPetNameRepository { { std::vector v; - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back("'" + Strings::Escape(e.name) + "'"); - v.push_back(std::to_string(e.class_id)); auto results = db.QueryDatabase( fmt::format( @@ -380,9 +369,8 @@ class BaseCharacterPetNameRepository { for (auto &e: entries) { std::vector v; - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back("'" + Strings::Escape(e.name) + "'"); - v.push_back(std::to_string(e.class_id)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/zone/client.cpp b/zone/client.cpp index 4ce51a77ca..9f8d0a899a 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -4454,11 +4454,13 @@ bool Client::ChangePetName(std::string new_name) { return false; } - CharacterPetNameRepository::ReplaceOne(database, { - .char_id = static_cast(CharacterID()), - .name = new_name - }); - + CharacterPetNameRepository::ReplaceOne( + database, + CharacterPetNameRepository::CharacterPetName{ + .character_id = static_cast(CharacterID()), + .name = new_name + } + ); if (GetPet()) { GetPet()->TempName(new_name.c_str()); From a25b76e13d68975d7776d395fcff2348a7e6ed75 Mon Sep 17 00:00:00 2001 From: Akkadius Date: Wed, 22 Jan 2025 03:21:45 -0600 Subject: [PATCH 7/7] Cleanup --- common/eq_packet_structs.h | 12 +++--- .../character_pet_name_repository.h | 37 ------------------- zone/client.cpp | 18 ++++----- zone/client_packet.cpp | 12 +++--- zone/pets.cpp | 1 - 5 files changed, 20 insertions(+), 60 deletions(-) diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 922ff040e6..f6e2b9ed1b 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -5826,12 +5826,12 @@ struct ChangePetName_Struct { }; enum ChangePetNameResponse : int { - Denied = 0, // 5167 You have requested an invalid name or a Customer Service Representative has denied your name request. Please try another name. - Accepted = 1, // 5976 Your request for a name change was successful. - Timeout = -3, // 5979 You must wait longer before submitting another name request. Please try again in a few minutes. - NotEligible = -4, // 5980 Your character is not eligible for a name change. - Pending = -5, // 5193 You already have a name change pending. Please wait until it is fully processed before attempting another name change. - Unhandled = -1 + Denied = 0, // 5167 You have requested an invalid name or a Customer Service Representative has denied your name request. Please try another name. + Accepted = 1, // 5976 Your request for a name change was successful. + Timeout = -3, // 5979 You must wait longer before submitting another name request. Please try again in a few minutes. + NotEligible = -4, // 5980 Your character is not eligible for a name change. + Pending = -5, // 5193 You already have a name change pending. Please wait until it is fully processed before attempting another name change. + Unhandled = -1 }; // New OpCode/Struct for SoD+ diff --git a/common/repositories/character_pet_name_repository.h b/common/repositories/character_pet_name_repository.h index 47c6279a9d..8fbb1e94bf 100644 --- a/common/repositories/character_pet_name_repository.h +++ b/common/repositories/character_pet_name_repository.h @@ -7,44 +7,7 @@ class CharacterPetNameRepository: public BaseCharacterPetNameRepository { public: - - /** - * This file was auto generated and can be modified and extended upon - * - * Base repository methods are automatically - * generated in the "base" version of this repository. The base repository - * is immutable and to be left untouched, while methods in this class - * are used as extension methods for more specific persistence-layer - * accessors or mutators. - * - * Base Methods (Subject to be expanded upon in time) - * - * Note: Not all tables are designed appropriately to fit functionality with all base methods - * - * InsertOne - * UpdateOne - * DeleteOne - * FindOne - * GetWhere(std::string where_filter) - * DeleteWhere(std::string where_filter) - * InsertMany - * All - * - * Example custom methods in a repository - * - * CharacterPetNameRepository::GetByZoneAndVersion(int zone_id, int zone_version) - * CharacterPetNameRepository::GetWhereNeverExpires() - * CharacterPetNameRepository::GetWhereXAndY() - * CharacterPetNameRepository::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 - * method that can be re-used easily elsewhere especially if it can use a base repository - * method and encapsulate filters there - */ - // Custom extended repository methods here - }; #endif //EQEMU_CHARACTER_PET_NAME_REPOSITORY_H diff --git a/zone/client.cpp b/zone/client.cpp index 9f8d0a899a..1c577c08d2 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -4433,7 +4433,8 @@ void Client::ClearPetNameChange() { DataBucket::DeleteData(k); } -bool Client::ChangePetName(std::string new_name) { +bool Client::ChangePetName(std::string new_name) +{ if (new_name.empty()) { return false; } @@ -4442,12 +4443,13 @@ bool Client::ChangePetName(std::string new_name) { return false; } - if (GetPet()) { - std::string cur_name = GetPet()->GetName(); + auto pet = GetPet(); + if (!pet) { + return false; + } - if (cur_name == new_name) { - return false; - } + if (pet->GetName() == new_name) { + return false; } if (!database.CheckNameFilter(new_name) || database.IsNameUsed(new_name)) { @@ -4462,9 +4464,7 @@ bool Client::ChangePetName(std::string new_name) { } ); - if (GetPet()) { - GetPet()->TempName(new_name.c_str()); - } + pet->TempName(new_name.c_str()); ClearPetNameChange(); return true; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 77fcf8e602..95a9a970ec 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -4580,18 +4580,16 @@ void Client::Handle_OP_ChangePetName(const EQApplicationPacket *app) { return; } - auto payload = (ChangePetName_Struct*)app->pBuffer; - + auto p = (ChangePetName_Struct *) app->pBuffer; if (!IsPetNameChangeAllowed()) { - payload->response_code = ChangePetNameResponse::NotEligible; + p->response_code = ChangePetNameResponse::NotEligible; QueuePacket(app); return; } - if (ChangePetName(payload->new_pet_name)) { - payload->response_code = ChangePetNameResponse::Accepted; - } else { - payload->response_code = ChangePetNameResponse::Denied; // not actually needed but included here for completeness + p->response_code = ChangePetNameResponse::Denied; + if (ChangePetName(p->new_pet_name)) { + p->response_code = ChangePetNameResponse::Accepted; } QueuePacket(app); diff --git a/zone/pets.cpp b/zone/pets.cpp index 84f3baee8e..da84ca4e14 100644 --- a/zone/pets.cpp +++ b/zone/pets.cpp @@ -167,7 +167,6 @@ void Mob::MakePoweredPet(uint16 spell_id, const char* pettype, int16 petpower, if (IsClient() && !petname) { const auto vanity_name = CharacterPetNameRepository::FindOne(database, CastToClient()->CharacterID()); - if (!vanity_name.name.empty()) { petname = vanity_name.name.c_str(); }