From a4f2ad415643386d1db4acc43d1a18ad1b366c52 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Wed, 30 Jul 2025 16:34:08 -0500 Subject: [PATCH] convert more internals --- CLAUDE.md | 39 + internal/GroundSpawn.cpp | 582 --- internal/GroundSpawn.h | 86 - internal/Player.cpp | 7945 ++++++++++++++++++++++++++++++ internal/Player.h | 1276 +++++ internal/languages/constants.go | 39 + internal/languages/interfaces.go | 396 ++ internal/languages/manager.go | 525 ++ internal/languages/types.go | 460 ++ internal/npc/ai/brain.go | 635 +++ internal/npc/ai/constants.go | 90 + internal/npc/ai/interfaces.go | 483 ++ internal/npc/ai/types.go | 504 ++ internal/npc/ai/variants.go | 324 ++ internal/npc/constants.go | 93 + internal/npc/interfaces.go | 570 +++ internal/npc/manager.go | 762 +++ internal/npc/npc.go | 806 +++ internal/npc/types.go | 440 ++ internal/quests/actions.go | 426 ++ internal/quests/constants.go | 77 + internal/quests/interfaces.go | 476 ++ internal/quests/manager.go | 450 ++ internal/quests/prerequisites.go | 378 ++ internal/quests/quest.go | 737 +++ internal/quests/rewards.go | 427 ++ internal/quests/types.go | 602 +++ 27 files changed, 18960 insertions(+), 668 deletions(-) delete mode 100644 internal/GroundSpawn.cpp delete mode 100644 internal/GroundSpawn.h create mode 100644 internal/Player.cpp create mode 100644 internal/Player.h create mode 100644 internal/languages/constants.go create mode 100644 internal/languages/interfaces.go create mode 100644 internal/languages/manager.go create mode 100644 internal/languages/types.go create mode 100644 internal/npc/ai/brain.go create mode 100644 internal/npc/ai/constants.go create mode 100644 internal/npc/ai/interfaces.go create mode 100644 internal/npc/ai/types.go create mode 100644 internal/npc/ai/variants.go create mode 100644 internal/npc/constants.go create mode 100644 internal/npc/interfaces.go create mode 100644 internal/npc/manager.go create mode 100644 internal/npc/npc.go create mode 100644 internal/npc/types.go create mode 100644 internal/quests/actions.go create mode 100644 internal/quests/constants.go create mode 100644 internal/quests/interfaces.go create mode 100644 internal/quests/manager.go create mode 100644 internal/quests/prerequisites.go create mode 100644 internal/quests/quest.go create mode 100644 internal/quests/rewards.go create mode 100644 internal/quests/types.go diff --git a/CLAUDE.md b/CLAUDE.md index bd8bf0e..79a69c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,6 +91,7 @@ go run golang.org/x/vuln/cmd/govulncheck@latest ./... - `appearances/` - Appearance management system with client version compatibility and efficient ID-based lookups - `factions/` - Faction reputation system with player standings, consideration levels, and inter-faction relationships - `ground_spawn/` - Harvestable resource node system with skill-based harvesting, rare item generation, and multi-attempt mechanics +- `languages/` - Multilingual character communication system with master language registry and player-specific language knowledge ### Network Protocol EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation, connection management, packet combining. @@ -227,6 +228,36 @@ XML-driven packet definitions with version-specific formats, conditional fields, - `internal/ground_spawn/interfaces.go`: Integration interfaces with database, players, items, skills, and event handling systems - `internal/ground_spawn/manager.go`: High-level ground spawn management with respawn scheduling, statistics, and command processing +**Languages System:** +- `internal/languages/constants.go`: Language ID constants, validation limits, and system configuration values +- `internal/languages/types.go`: Core Language struct, master language list, player language list, and statistics tracking +- `internal/languages/manager.go`: High-level language management with database integration, statistics, and command processing +- `internal/languages/interfaces.go`: Integration interfaces with database, players, chat processing, and event handling systems + +**Quests System:** +- `internal/quests/constants.go`: Quest step types, display status flags, sharing constants, and validation limits +- `internal/quests/types.go`: Core Quest and QuestStep structures with complete quest data management and thread-safe operations +- `internal/quests/quest.go`: Core quest functionality including step management, progress tracking, update checking, and validation +- `internal/quests/prerequisites.go`: Quest prerequisite management with class, race, level, faction, and quest requirements +- `internal/quests/rewards.go`: Quest reward system with coins, experience, status, faction rewards, and level-based calculations +- `internal/quests/actions.go`: Quest action system for Lua script execution on completion, progress, and failure events +- `internal/quests/manager.go`: MasterQuestList and QuestManager for system-wide quest management and player quest tracking +- `internal/quests/interfaces.go`: Integration interfaces with player, client, spawn, item systems and QuestSystemAdapter for complete quest lifecycle management + +**NPC System:** +- `internal/npc/constants.go`: AI strategy constants, randomization flags, pet types, cast types, and system limits +- `internal/npc/types.go`: Core NPC struct extending Entity, NPCSpell configurations, skill bonuses, movement locations, brain system +- `internal/npc/npc.go`: Complete NPC functionality with AI, combat, spell casting, movement, appearance randomization, and validation +- `internal/npc/manager.go`: High-level NPC management with zone indexing, appearance tracking, combat processing, and statistics +- `internal/npc/interfaces.go`: Integration interfaces with database, spell/skill/appearance systems, combat, movement, and entity adapters + +**NPC AI System:** +- `internal/npc/ai/constants.go`: AI timing constants, combat ranges, hate limits, brain types, and decision parameters +- `internal/npc/ai/types.go`: HateList and EncounterList management, BrainState tracking, performance statistics, and thread-safe operations +- `internal/npc/ai/brain.go`: BaseBrain with complete AI logic including hate management, encounter tracking, spell/melee processing, and combat decisions +- `internal/npc/ai/variants.go`: Specialized brain types (CombatPet, NonCombatPet, Blank, Lua, DumbFire) with unique behaviors and factory functions +- `internal/npc/ai/interfaces.go`: Integration interfaces with NPC/Entity systems, AIManager for brain lifecycle, adapters, and debugging utilities + **Packet Definitions:** - `internal/packets/xml/`: XML packet structure definitions - `internal/packets/PARSER.md`: Packet definition language documentation @@ -307,6 +338,14 @@ Command-line flags override JSON configuration. **Ground Spawn System**: Harvestable resource node system extending spawn functionality for gathering, mining, fishing, trapping, and foresting. Features skill-based table selection, probabilistic harvest outcomes (1/3/5/10 items + rare/imbue types), multi-attempt harvesting sessions, skill progression mechanics, and automatic respawn scheduling. Implements complex C++ harvest logic with table filtering by skill/level requirements, random item selection from filtered pools, grid-based location restrictions, and comprehensive item reward processing. Supports collection vs. harvesting skill differentiation, harvest message generation, spell integration for casting animations, and statistics tracking. Includes PlayerGroundSpawnAdapter and HarvestEventAdapter for seamless integration with player and event systems. Thread-safe operations with separate mutexes for harvest processing and usage handling. +**Languages System**: Multilingual character communication system managing language learning and chat processing. Features master language registry with ID-based and name-based lookups, individual player language collections with thread-safe operations, language validation and persistence, and comprehensive multilingual chat processing. Supports all EverQuest II racial languages (Common, Elvish, Dwarven, Halfling, Gnomish, Iksar, Trollish, Ogrish, Fae, Arasai, Sarnak, Froglok), language learning/forgetting mechanics, primary language selection, and message scrambling for unknown languages. Includes PlayerLanguageAdapter for seamless player integration, ChatLanguageProcessor for multilingual communication, and statistics tracking for language usage patterns. Thread-safe operations with efficient hash-based lookups and comprehensive validation systems. + +**Quests System**: Complete quest management with quest definitions, step system, and real-time progress tracking. Features quest system with multiple step types (kill, chat, obtain item, location, spell, normal, craft, harvest, kill race requirements), comprehensive prerequisite system (level, class, race, faction, quest dependencies), flexible reward system (coins, experience, status points, faction reputation, items), step-based progress tracking with percentage-based success chances, task group organization for complex quests, Lua action system for completion/progress/failure events, quest sharing system with configurable sharing rules, repeatable quest support, player quest management with active quest tracking, master quest list with categorization and search, validation system for quest integrity, and thread-safe operations with proper mutex usage. Includes QuestSystemAdapter for complete quest lifecycle management and integration with player, client, spawn, and item systems. + +**NPC System**: Non-player character system extending Entity with complete AI, combat, and spell casting capabilities. Features NPC struct with brain system, spell management (cast-on-spawn/aggro triggers), skill bonuses, movement with runback mechanics, appearance randomization (33+ flags for race/gender/colors/features), AI strategies (balanced/offensive/defensive), and combat state management. Includes NPCSpell configurations with HP ratio requirements, skill bonus system with spell-based modifications, movement locations with navigation pathfinding, timer system for pause/movement control, and comprehensive appearance randomization covering all EQ2 races and visual elements. Manager provides zone-based indexing, appearance tracking, combat processing, AI processing, statistics collection, and command interface. Integration interfaces support database persistence, spell/skill/appearance systems, combat management, movement control, and entity adapters for seamless system integration. Thread-safe operations with proper mutex usage and atomic flags for state management. + +**NPC AI System**: Comprehensive artificial intelligence system for NPCs with hate management, encounter tracking, and specialized brain types. Features BaseBrain with complete AI logic including target selection, spell/melee processing, combat decisions, movement control, and runback mechanics. HateList provides thread-safe hate value tracking with percentage calculations and most-hated selection. EncounterList manages player/group participation for loot rights and rewards with character ID mapping. Specialized brain variants include CombatPetBrain (follows owner, assists in combat), NonCombatPetBrain (cosmetic pet following), BlankBrain (minimal processing), LuaBrain (script-controlled AI), and DumbFirePetBrain (temporary combat pets with expiration). BrainState tracks timing, spell recovery, active status, and debug levels. AIManager provides centralized brain lifecycle management with type-based creation, active brain processing, and performance statistics. Integration interfaces support NPC/Entity systems, Lua scripting, zone operations, and debugging utilities. Thread-safe operations with proper mutex usage and performance tracking for all AI operations. + All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.). **Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility. diff --git a/internal/GroundSpawn.cpp b/internal/GroundSpawn.cpp deleted file mode 100644 index 86d243a..0000000 --- a/internal/GroundSpawn.cpp +++ /dev/null @@ -1,582 +0,0 @@ -/* - EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) - - This file is part of EQ2Emulator. - - EQ2Emulator 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, either version 3 of the License, or - (at your option) any later version. - - EQ2Emulator is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; 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 EQ2Emulator. If not, see . -*/ -#include "GroundSpawn.h" -#include "World.h" -#include "Spells.h" -#include "Rules/Rules.h" -#include "../common/MiscFunctions.h" -#include "../common/Log.h" - -extern ConfigReader configReader; -extern MasterSpellList master_spell_list; -extern World world; -extern RuleManager rule_manager; - -GroundSpawn::GroundSpawn(){ - packet_num = 0; - appearance.difficulty = 0; - spawn_type = 2; - appearance.pos.state = 129; - number_harvests = 0; - num_attempts_per_harvest = 0; - groundspawn_id = 0; - MHarvest.SetName("GroundSpawn::MHarvest"); - MHarvestUse.SetName("GroundSpawn::MHarvestUse"); - randomize_heading = true; // we by default randomize heading of groundspawns DB overrides -} - -GroundSpawn::~GroundSpawn(){ - -} - -EQ2Packet* GroundSpawn::serialize(Player* player, int16 version){ - return spawn_serialize(player, version); -} - -int8 GroundSpawn::GetNumberHarvests(){ - return number_harvests; -} - -void GroundSpawn::SetNumberHarvests(int8 val){ - number_harvests = val; -} - -int8 GroundSpawn::GetAttemptsPerHarvest(){ - return num_attempts_per_harvest; -} - -void GroundSpawn::SetAttemptsPerHarvest(int8 val){ - num_attempts_per_harvest = val; -} - -int32 GroundSpawn::GetGroundSpawnEntryID(){ - return groundspawn_id; -} - -void GroundSpawn::SetGroundSpawnEntryID(int32 val){ - groundspawn_id = val; -} - -void GroundSpawn::SetCollectionSkill(const char* val){ - if(val) - collection_skill = string(val); -} - -const char* GroundSpawn::GetCollectionSkill(){ - return collection_skill.c_str(); -} - -void GroundSpawn::ProcessHarvest(Client* client) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Process harvesting for player '%s' (%u)", client->GetPlayer()->GetName(), client->GetPlayer()->GetID()); - - MHarvest.lock(); - - vector* groundspawn_entries = GetZone()->GetGroundSpawnEntries(groundspawn_id); - vector* groundspawn_items = GetZone()->GetGroundSpawnEntryItems(groundspawn_id); - - Item* master_item = 0; - Item* master_rare = 0; - Item* item = 0; - Item* item_rare = 0; - - int16 lowest_skill_level = 0; - int16 table_choice = 0; - int32 item_choice = 0; - int32 rare_choice = 0; - int8 harvest_type = 0; - int32 item_harvested = 0; - int8 reward_total = 1; - int32 rare_harvested = 0; - int8 rare_item = 0; - bool is_collection = false; - - if (!groundspawn_entries || !groundspawn_items) { - LogWrite(GROUNDSPAWN__ERROR, 3, "GSpawn", "No groundspawn entries or items assigned to groundspawn id: %u", groundspawn_id); - client->Message(CHANNEL_COLOR_RED, "Error: There are no groundspawn entries or items assigned to groundspawn id: %u", groundspawn_id); - MHarvest.unlock(); - return; - } - - if (number_harvests == 0) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Total harvests depleated for groundspawn id: %u", groundspawn_id); - client->SimpleMessage(CHANNEL_COLOR_RED, "Error: This spawn has nothing more to harvest!"); - MHarvest.unlock(); - return; - } - - Skill* skill = 0; - if (collection_skill == "Collecting") { - skill = client->GetPlayer()->GetSkillByName("Gathering"); - is_collection = true; - } - else - skill = client->GetPlayer()->GetSkillByName(collection_skill.c_str()); // Fix: #576 - don't skill up yet with GetSkillByName(skill, true), we might be trying to harvest low level - - if (!skill) { - LogWrite(GROUNDSPAWN__WARNING, 3, "GSpawn", "Player '%s' lacks the skill: '%s'", client->GetPlayer()->GetName(), collection_skill.c_str()); - client->Message(CHANNEL_COLOR_RED, "Error: You do not have the '%s' skill!", collection_skill.c_str()); - MHarvest.unlock(); - return; - } - - int16 totalSkill = skill->current_val; - int32 skillID = master_item_list.GetItemStatIDByName(collection_skill); - int16 max_skill_req_groundspawn = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, MinSkillMultiplierValue)->GetInt16(); - if(max_skill_req_groundspawn < 1) // can't be 0 - max_skill_req_groundspawn = 1; - - if(skillID != 0xFFFFFFFF) - { - ((Entity*)client->GetPlayer())->MStats.lock(); - totalSkill += ((Entity*)client->GetPlayer())->stats[skillID]; - ((Entity*)client->GetPlayer())->MStats.unlock(); - } - - for (int8 i = 0; i < num_attempts_per_harvest; i++) { - vector mod_groundspawn_entries; - - if (groundspawn_entries) { - vector highest_match; - vector::iterator itr; - - GroundSpawnEntry* entry = 0; // current data - GroundSpawnEntry* selected_table = 0; // selected table data - - // first, iterate through groundspawn_entries, discard tables player cannot use - for (itr = groundspawn_entries->begin(); itr != groundspawn_entries->end(); itr++) { - entry = *itr; - - if(entry->min_skill_level > max_skill_req_groundspawn) - max_skill_req_groundspawn = entry->min_skill_level; - - // if player lacks skill, skip table - if (entry->min_skill_level > totalSkill) - continue; - // if bonus, but player lacks level, skip table - if (entry->bonus_table && (client->GetPlayer()->GetLevel() < entry->min_adventure_level)) - continue; - - // build modified entries table - mod_groundspawn_entries.push_back(entry); - LogWrite(GROUNDSPAWN__DEBUG, 5, "GSpawn", "Keeping groundspawn_entry: %i", entry->min_skill_level); - } - - // if anything remains, find lowest min_skill_level in remaining set(s) - if (mod_groundspawn_entries.size() > 0) { - vector::iterator itr; - GroundSpawnEntry* entry = 0; - - for (itr = mod_groundspawn_entries.begin(); itr != mod_groundspawn_entries.end(); itr++) { - entry = *itr; - - // find the low range of available tables for random roll - if (lowest_skill_level > entry->min_skill_level || lowest_skill_level == 0) - lowest_skill_level = entry->min_skill_level; - } - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Lowest Skill Level: %i", lowest_skill_level); - } - else { - // if no tables chosen, you must lack the skills - // TODO: move this check to LUA when harvest command is first selected - client->Message(CHANNEL_COLOR_RED, "You lack the skills to harvest this node!"); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "All groundspawn_entry tables tossed! No Skills? Something broke?"); - MHarvest.unlock(); - return; - } - - // now roll to see which table to use - table_choice = MakeRandomInt(lowest_skill_level, totalSkill); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random INT for Table by skill level: %i", table_choice); - - int16 highest_score = 0; - for (itr = mod_groundspawn_entries.begin(); itr != mod_groundspawn_entries.end(); itr++) { - entry = *itr; - - // determines the highest min_skill_level in the current set of tables (if multiple tables) - if (table_choice >= entry->min_skill_level && (highest_score == 0 || highest_score < table_choice)) { - // removes old highest for the new one - highest_match.clear(); - highest_score = entry->min_skill_level; - } - // if the score = level, push into highest_match set - if (highest_score == entry->min_skill_level) - highest_match.push_back(entry); - } - - // if there is STILL more than 1 table player qualifies for, rand() and pick one - if (highest_match.size() > 1) { - int16 rand_index = rand() % highest_match.size(); - selected_table = highest_match.at(rand_index); - } - else if (highest_match.size() > 0) - selected_table = highest_match.at(0); - - // by this point, we should have 1 table who's min skill matches the score (selected_table) - if (selected_table) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Using Table: %i, %i, %i, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %i", - selected_table->min_skill_level, - selected_table->min_adventure_level, - selected_table->bonus_table, - selected_table->harvest1, - selected_table->harvest3, - selected_table->harvest5, - selected_table->harvest_imbue, - selected_table->harvest_rare, - selected_table->harvest10, - selected_table->harvest_coin); - - - // roll 1-100 for chance-to-harvest percentage - float chance = MakeRandomFloat(0, 100); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random FLOAT for harvest percentages: %.2f", chance); - - // starting with the lowest %, select a harvest type + reward qty - if (chance <= selected_table->harvest10 && is_collection == false) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 10 items + Rare Item from table : %i", selected_table->min_skill_level); - harvest_type = 6; - reward_total = 10; - } - else if (chance <= selected_table->harvest_rare && is_collection == false) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest Rare Item from table : %i", selected_table->min_skill_level); - harvest_type = 5; - } - else if (chance <= selected_table->harvest_imbue && is_collection == false) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest Imbue Item from table : %i", selected_table->min_skill_level); - harvest_type = 4; - } - else if (chance <= selected_table->harvest5 && is_collection == false) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 5 Items from table : %i", selected_table->min_skill_level); - harvest_type = 3; - reward_total = 5; - } - else if (chance <= selected_table->harvest3 && is_collection == false) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 3 Items from table : %i", selected_table->min_skill_level); - harvest_type = 2; - reward_total = 3; - } - else if (chance <= selected_table->harvest1 || totalSkill >= skill->max_val || is_collection) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 1 Item from table : %i", selected_table->min_skill_level); - harvest_type = 1; - } - else - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest nothing..."); - - float node_maxskill_multiplier = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, HarvestSkillUpMultiplier)->GetFloat(); - if(node_maxskill_multiplier <= 0.0f) { - node_maxskill_multiplier = 1.0f; - } - int16 skillup_max_skill_allowed = (int16)((float)max_skill_req_groundspawn*node_maxskill_multiplier); - if (!is_collection && skill && skill->current_val < skillup_max_skill_allowed) { - skill = client->GetPlayer()->GetSkillByName(collection_skill.c_str(), true); // Fix: #576 - skill up after min skill and adv level checks - } - } - - // once you know how many and what type of item to harvest, pick an item from the list - if (harvest_type) { - vector mod_groundspawn_items; - vector mod_groundspawn_rares; - vector mod_groundspawn_imbue; - - vector::iterator itr; - GroundSpawnEntryItem* entry = 0; - - // iterate through groundspawn_items, discard items player cannot roll for - for (itr = groundspawn_items->begin(); itr != groundspawn_items->end(); itr++) { - entry = *itr; - - // if this is a Rare, or an Imbue, but is_rare flag is 0, skip item - if ((harvest_type == 5 || harvest_type == 4) && entry->is_rare == 0) - continue; - // if it is a 1, 3, or 5 and is_rare = 1, skip - else if (harvest_type < 4 && entry->is_rare == 1) - continue; - - // if the grid_id on the item matches player grid, or is 0, keep the item - if (!entry->grid_id || (entry->grid_id == client->GetPlayer()->GetLocation())) { - // build modified entries table - if ((entry->is_rare == 1 && harvest_type == 5) || (entry->is_rare == 1 && harvest_type == 6)) { - // if the matching item is rare, or harvest10 push to mod rares - mod_groundspawn_rares.push_back(entry); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_rare_item: %u", entry->item_id); - } - if (entry->is_rare == 0 && harvest_type != 4 && harvest_type != 5) { - // if the matching item is normal,or harvest 10 push to mod items - mod_groundspawn_items.push_back(entry); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_common_item: %u", entry->item_id); - } - if (entry->is_rare == 2 && harvest_type == 4) { - // if the matching item is imbue item, push to mod imbue - mod_groundspawn_imbue.push_back(entry); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_imbue_item: %u", entry->item_id); - } - } - } - - // if any items remain in the list, random to see which one gets awarded - if (mod_groundspawn_items.size() > 0) { - // roll to see which item index to use - item_choice = rand() % mod_groundspawn_items.size(); - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random INT for which item to award: %i", item_choice); - - // set item_id to be awarded - item_harvested = mod_groundspawn_items[item_choice]->item_id; - - // if reward is rare, set flag - rare_item = mod_groundspawn_items[item_choice]->is_rare; - - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID to award: %u, Rare = %i", item_harvested, item_rare); - - // if 10+rare, handle additional "rare" reward - if (harvest_type == 6) { - // make sure there is a rare table to choose from! - if (mod_groundspawn_rares.size() > 0) { - // roll to see which rare index to use - rare_choice = rand() % mod_groundspawn_rares.size(); - - // set (rare) item_id to be awarded - rare_harvested = mod_groundspawn_rares[rare_choice]->item_id; - - // we're picking a rare here, so obviously this is true ;) - rare_item = 1; - - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "RARE Item ID to award: %u", rare_harvested); - } - else { - // all rare entries were eliminated above, or none are assigned. Either way, shouldn't be here! - LogWrite(GROUNDSPAWN__ERROR, 3, "GSpawn", "Groundspawn Entry for '%s' (%i) has no RARE items!", GetName(), GetID()); - } - } - } - else if (mod_groundspawn_rares.size() > 0) { - // roll to see which rare index to use - item_choice = rand() % mod_groundspawn_rares.size(); - - // set (rare) item_id to be awarded - item_harvested = mod_groundspawn_rares[item_choice]->item_id; - - // we're picking a rare here, so obviously this is true ;) - rare_item = 1; - - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "RARE Item ID to award: %u", rare_harvested); - } - else if (mod_groundspawn_imbue.size() > 0) { - // roll to see which rare index to use - item_choice = rand() % mod_groundspawn_imbue.size(); - - // set (rare) item_id to be awarded - item_harvested = mod_groundspawn_imbue[item_choice]->item_id; - - // we're picking a rare here, so obviously this is true ;) - rare_item = 0; - - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "imbue Item ID to award: %u", rare_harvested); - } - - - - - else { - // all item entries were eliminated above, or none are assigned. Either way, shouldn't be here! - LogWrite(GROUNDSPAWN__ERROR, 0, "GSpawn", "Groundspawn Entry for '%s' (%i) has no items!", GetName(), GetID()); - } - - // if an item was harvested, send updates to client, add item to inventory - if (item_harvested) { - char tmp[200] = { 0 }; - - // set Normal item harvested - master_item = master_item_list.GetItem(item_harvested); - if (master_item) { - // set details of Normal item - item = new Item(master_item); - // set how many of this item the player receives - item->details.count = reward_total; - - // chat box update for normal item (todo: verify output text) - client->Message(CHANNEL_HARVESTING, "You %s %i %s from the %s.", GetHarvestMessageName(true).c_str(), item->details.count, item->CreateItemLink(client->GetVersion(), true).c_str(), GetName()); - // add Normal item to player inventory - bool itemDeleted = false; - client->AddItem(item, &itemDeleted); - - if(!itemDeleted) { - //Check if the player has a harvesting quest for this - client->GetPlayer()->CheckQuestsHarvestUpdate(item, reward_total); - - // if this is a 10+rare, handle sepErately - if (harvest_type == 6 && rare_item == 1) { - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is Normal. Qty %i", item_harvested, item->details.count); - - // send Normal harvest message to client - sprintf(tmp, "\\#64FFFFYou have %s:\12\\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str()); - client->SendPopupMessage(10, tmp, "ui_harvested_normal", 2.25, 0xFF, 0xFF, 0xFF); - client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_ITEMS_HARVESTED, item->details.count); - - // set Rare item harvested - master_rare = master_item_list.GetItem(rare_harvested); - if (master_rare) { - // set details of Rare item - item_rare = new Item(master_rare); - // count of Rare is always 1 - item_rare->details.count = 1; - - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is RARE!", rare_harvested); - - // send Rare harvest message to client - sprintf(tmp, "\\#FFFF6ERare item found!\12%s: \\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item_rare->details.count, item_rare->name.c_str()); - client->Message(CHANNEL_HARVESTING, "You have found a rare item!"); - client->SendPopupMessage(11, tmp, "ui_harvested_rare", 2.25, 0xFF, 0xFF, 0xFF); - client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_RARES_HARVESTED, item_rare->details.count); - - // chat box update for rare item (todo: verify output text) - client->Message(CHANNEL_HARVESTING, "You %s %i %s from the %s.", GetHarvestMessageName(true).c_str(), item_rare->details.count, item->CreateItemLink(client->GetVersion(), true).c_str(), GetName()); - // add Rare item to player inventory - client->AddItem(item_rare); - //Check if the player has a harvesting quest for this - client->GetPlayer()->CheckQuestsHarvestUpdate(item_rare, 1); - } - } - else if (rare_item == 1) { - // if harvest signaled rare or imbue type - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is RARE! Qty: %i", item_harvested, item->details.count); - - // send Rare harvest message to client - sprintf(tmp, "\\#FFFF6ERare item found!\12%s: \\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str()); - client->Message(CHANNEL_HARVESTING, "You have found a rare item!"); - client->SendPopupMessage(11, tmp, "ui_harvested_rare", 2.25, 0xFF, 0xFF, 0xFF); - client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_RARES_HARVESTED, item->details.count); - } - else { - // send Normal harvest message to client - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is Normal. Qty %i", item_harvested, item->details.count); - sprintf(tmp, "\\#64FFFFYou have %s:\12\\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str()); - client->SendPopupMessage(10, tmp, "ui_harvested_normal", 2.25, 0xFF, 0xFF, 0xFF); - client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_ITEMS_HARVESTED, item->details.count); - } - - } - } - else { - // error! - LogWrite(GROUNDSPAWN__ERROR, 0, "GSpawn", "Error: Item ID Not Found - %u", item_harvested); - client->Message(CHANNEL_COLOR_RED, "Error: Unable to find item id %u", item_harvested); - } - // decrement # of pulls on this node before it despawns - number_harvests--; - } - else { - // if no item harvested - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "No item_harvested"); - client->Message(CHANNEL_HARVESTING, "You failed to %s anything from %s.", GetHarvestMessageName(true, true).c_str(), GetName()); - } - } - else { - // if no harvest type - LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "No harvest_type"); - client->Message(CHANNEL_HARVESTING, "You failed to %s anything from %s.", GetHarvestMessageName(true, true).c_str(), GetName()); - } - } - } // cycle through num_attempts_per_harvest - MHarvest.unlock(); - - LogWrite(GROUNDSPAWN__DEBUG, 0, "GSpawn", "Process harvest complete for player '%s' (%u)", client->GetPlayer()->GetName(), client->GetPlayer()->GetID()); -} - -string GroundSpawn::GetHarvestMessageName(bool present_tense, bool failure){ - string ret = ""; - if((collection_skill == "Gathering" ||collection_skill == "Collecting") && !present_tense) - ret = "gathered"; - else if(collection_skill == "Gathering" || collection_skill == "Collecting") - ret = "gather"; - else if(collection_skill == "Mining" && !present_tense) - ret = "mined"; - else if(collection_skill == "Mining") - ret = "mine"; - else if (collection_skill == "Fishing" && !present_tense) - ret = "fished"; - else if(collection_skill == "Fishing") - ret = "fish"; - else if(collection_skill == "Trapping" && !present_tense && !failure) - ret = "acquired"; - else if(collection_skill == "Trapping" && failure) - ret = "trap"; - else if(collection_skill == "Trapping") - ret = "acquire"; - else if(collection_skill == "Foresting" && !present_tense) - ret = "forested"; - else if(collection_skill == "Foresting") - ret = "forest"; - else if (collection_skill == "Collecting") - ret = "collect"; - return ret; -} - -string GroundSpawn::GetHarvestSpellType(){ - string ret = ""; - if(collection_skill == "Gathering" || collection_skill == "Collecting") - ret = "gather"; - else if(collection_skill == "Mining") - ret = "mine"; - else if(collection_skill == "Trapping") - ret = "trap"; - else if(collection_skill == "Foresting") - ret = "chop"; - else if(collection_skill == "Fishing") - ret = "fish"; - return ret; -} - -string GroundSpawn::GetHarvestSpellName() { - string ret = ""; - if (collection_skill == "Collecting") - ret = "Gathering"; - else - ret = collection_skill; - return ret; -} - -void GroundSpawn::HandleUse(Client* client, string type){ - if(!client || (client->GetVersion() > 561 && type.length() == 0)) // older clients do not send the type - return; - //The following check disables the use of the groundspawn if spawn access is not granted - if (client) { - bool meets_quest_reqs = MeetsSpawnAccessRequirements(client->GetPlayer()); - if (!meets_quest_reqs && (GetQuestsRequiredOverride() & 2) == 0) - return; - else if (meets_quest_reqs && appearance.show_command_icon != 1) - return; - } - - MHarvestUse.lock(); - std::string typeLwr = ToLower(type); - if(client->GetVersion() <= 561 && (typeLwr == "" || typeLwr == "collect" || typeLwr == "gather" || typeLwr == "chop" || typeLwr == "mine")) - type = GetHarvestSpellType(); - - if (type == GetHarvestSpellType() && MeetsSpawnAccessRequirements(client->GetPlayer())) { - Spell* spell = master_spell_list.GetSpellByName(GetHarvestSpellName().c_str()); - if (spell) - client->GetCurrentZone()->ProcessSpell(spell, client->GetPlayer(), client->GetPlayer()->GetTarget(), true, true); - } - else if (appearance.show_command_icon == 1 && MeetsSpawnAccessRequirements(client->GetPlayer())) { - EntityCommand* entity_command = FindEntityCommand(type); - if (entity_command) - client->GetCurrentZone()->ProcessEntityCommand(entity_command, client->GetPlayer(), client->GetPlayer()->GetTarget()); - } - MHarvestUse.unlock(); -} diff --git a/internal/GroundSpawn.h b/internal/GroundSpawn.h deleted file mode 100644 index f830686..0000000 --- a/internal/GroundSpawn.h +++ /dev/null @@ -1,86 +0,0 @@ -/* - EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) - - This file is part of EQ2Emulator. - - EQ2Emulator 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, either version 3 of the License, or - (at your option) any later version. - - EQ2Emulator is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; 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 EQ2Emulator. If not, see . -*/ -#ifndef __EQ2_GroundSpawn__ -#define __EQ2_GroundSpawn__ - -#include "Spawn.h" -#include "client.h" -#include "../common/Mutex.h" - -class GroundSpawn : public Spawn { -public: - GroundSpawn(); - virtual ~GroundSpawn(); - GroundSpawn* Copy(){ - GroundSpawn* new_spawn = new GroundSpawn(); - new_spawn->size = size; - new_spawn->SetPrimaryCommands(&primary_command_list); - new_spawn->SetSecondaryCommands(&secondary_command_list); - new_spawn->database_id = database_id; - new_spawn->primary_command_list_id = primary_command_list_id; - new_spawn->secondary_command_list_id = secondary_command_list_id; - memcpy(&new_spawn->appearance, &appearance, sizeof(AppearanceData)); - new_spawn->faction_id = faction_id; - new_spawn->target = 0; - new_spawn->SetTotalHP(GetTotalHP()); - new_spawn->SetTotalPower(GetTotalPower()); - new_spawn->SetHP(GetHP()); - new_spawn->SetPower(GetPower()); - new_spawn->SetNumberHarvests(number_harvests); - new_spawn->SetAttemptsPerHarvest(num_attempts_per_harvest); - new_spawn->SetGroundSpawnEntryID(groundspawn_id); - new_spawn->SetCollectionSkill(collection_skill.c_str()); - SetQuestsRequired(new_spawn); - new_spawn->forceMapCheck = forceMapCheck; - new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag()); - new_spawn->SetLootTier(GetLootTier()); - new_spawn->SetLootDropType(GetLootDropType()); - new_spawn->SetRandomizeHeading(GetRandomizeHeading()); - return new_spawn; - } - bool IsGroundSpawn(){ return true; } - EQ2Packet* serialize(Player* player, int16 version); - int8 GetNumberHarvests(); - void SetNumberHarvests(int8 val); - int8 GetAttemptsPerHarvest(); - void SetAttemptsPerHarvest(int8 val); - int32 GetGroundSpawnEntryID(); - void SetGroundSpawnEntryID(int32 val); - void ProcessHarvest(Client* client); - void SetCollectionSkill(const char* val); - const char* GetCollectionSkill(); - string GetHarvestMessageName(bool present_tense = false, bool failure = false); - string GetHarvestSpellType(); - string GetHarvestSpellName(); - void HandleUse(Client* client, string type); - - void SetRandomizeHeading(bool val) { randomize_heading = val; } - bool GetRandomizeHeading() { return randomize_heading; } -private: - int8 number_harvests; - int8 num_attempts_per_harvest; - int32 groundspawn_id; - string collection_skill; - Mutex MHarvest; - Mutex MHarvestUse; - bool randomize_heading; -}; -#endif - diff --git a/internal/Player.cpp b/internal/Player.cpp new file mode 100644 index 0000000..2476f17 --- /dev/null +++ b/internal/Player.cpp @@ -0,0 +1,7945 @@ +/* + EQ2Emulator: Everquest II Server Emulator + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + + This file is part of EQ2Emulator. + + EQ2Emulator 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, either version 3 of the License, or + (at your option) any later version. + + EQ2Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; 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 EQ2Emulator. If not, see . +*/ + +#include "Player.h" +#include "../common/MiscFunctions.h" +#include "World.h" +#include "WorldDatabase.h" +#include +#include "classes.h" +#include "LuaInterface.h" +#include "../common/Log.h" +#include "Rules/Rules.h" +#include "Titles.h" +#include "Languages.h" +#include "SpellProcess.h" +#include +#include +#include "ClientPacketFunctions.h" + +extern Classes classes; +extern WorldDatabase database; +extern World world; +extern ConfigReader configReader; +extern MasterSkillList master_skill_list; +extern MasterSpellList master_spell_list; +extern MasterQuestList master_quest_list; +extern Variables variables; +extern LuaInterface* lua_interface; +extern MasterItemList master_item_list; +extern RuleManager rule_manager; +extern MasterTitlesList master_titles_list; +extern MasterLanguagesList master_languages_list; +std::map Player::m_levelXPReq; + +Player::Player(){ + tutorial_step = 0; + char_id = 0; + group = 0; + appearance.pos.grid_id = 0; + spawn_index = 1; + info = 0; + movement_packet = 0; + last_movement_activity = 0; + //speed = 0; + packet_num = 0; + range_attack = false; + old_movement_packet = 0; + charsheet_changed = false; + quickbar_updated = false; + custNPC = false; + spawn_tmp_vis_xor_packet = 0; + spawn_tmp_pos_xor_packet = 0; + spawn_tmp_info_xor_packet = 0; + pending_collection_reward = 0; + pos_packet_speed = 0; + + appearance.display_name = 1; + appearance.show_command_icon = 1; + appearance.player_flag = 1; + appearance.targetable = 1; + appearance.show_level = 1; + spell_count = 0; + spell_orig_packet = 0; + spell_xor_packet = 0; + raid_orig_packet = nullptr; + raid_xor_packet = nullptr; + resurrecting = false; + spawn_id = 1; + spawn_type = 4; + player_spawn_id_map[1] = this; + player_spawn_reverse_id_map[this] = 1; + MPlayerQuests.SetName("Player::MPlayerQuests"); + test_time = 0; + returning_from_ld = false; + away_message = "Sorry, I am A.F.K. (Away From Keyboard)"; + AddSecondaryEntityCommand("Inspect", 10000, "inspect_player", "", 0, 0); + AddSecondaryEntityCommand("Who", 10000, "who", "", 0, 0); + // commented out commands a player canNOT use on themselves... move these to Client::HandleVerbRequest()? + //AddSecondaryEntityCommand("Assist", 10, "assist", "", 0, 0); + //AddSecondaryEntityCommand("Duel", 10, "duel", "", 0, 0); + //AddSecondaryEntityCommand("Duel Bet", 10, "duelbet", "", 0, 0); + //AddSecondaryEntityCommand("Trade", 10, "trade", "", 0, 0); + is_tracking = false; + guild = 0; + following = false; + combat_target = 0; + //InitXPTable(); + pending_deletion = false; + spawn_vis_struct = 0; + spawn_pos_struct = 0; + spawn_info_struct = 0; + spawn_header_struct = 0; + spawn_footer_struct = 0; + widget_footer_struct = 0; + sign_footer_struct = 0; + pos_xor_size = 0; + info_xor_size = 0; + vis_xor_size = 0; + pos_mutex.SetName("Player::pos_mutex"); + vis_mutex.SetName("Player::vis_mutex"); + info_mutex.SetName("Player::info_mutex"); + index_mutex.SetName("Player::index_mutex"); + spawn_mutex.SetName("Player::spawn_mutex"); + m_playerSpawnQuestsRequired.SetName("Player::player_spawn_quests_required"); + m_playerSpawnHistoryRequired.SetName("Player::player_spawn_history_required"); + gm_vision = false; + SetSaveSpellEffects(true); + reset_mentorship = false; + all_spells_locked = false; + current_language_id = 0; + active_reward = false; + + SortedTraitList = new map > >; + ClassTraining = new map >; + RaceTraits = new map >; + InnateRaceTraits = new map >; + FocusEffects = new map >; + need_trait_update = true; + active_food_unique_id = 0; + active_drink_unique_id = 0; + raidsheet_changed = false; + hassent_raid = false; + house_vault_slots = 0; +} +Player::~Player(){ + SetSaveSpellEffects(true); + for(int32 i=0;i*>::iterator itr; + for (itr = player_spawn_quests_required.begin(); itr != player_spawn_quests_required.end(); itr++){ + safe_delete(itr->second); + } + player_spawn_quests_required.clear(); + + for (itr = player_spawn_history_required.begin(); itr != player_spawn_history_required.end(); itr++){ + safe_delete(itr->second); + } + player_spawn_history_required.clear(); + + map > >::iterator itr1; + map >::iterator itr2; + vector::iterator itr3; + // Type + for (itr1 = m_characterHistory.begin(); itr1 != m_characterHistory.end(); itr1++) { + // Sub type + for (itr2 = itr1->second.begin(); itr2 != itr1->second.end(); itr2++) { + // vector of data + for (itr3 = itr2->second.begin(); itr3 != itr2->second.end(); itr3++) { + safe_delete(*itr3); + } + } + } + m_characterHistory.clear(); + + mLUAHistory.writelock(); + map::iterator itr4; + for (itr4 = m_charLuaHistory.begin(); itr4 != m_charLuaHistory.end(); itr4++) { + safe_delete(itr4->second); + } + m_charLuaHistory.clear(); + mLUAHistory.releasewritelock(); + + safe_delete_array(movement_packet); + safe_delete_array(old_movement_packet); + safe_delete_array(spawn_tmp_info_xor_packet); + safe_delete_array(spawn_tmp_vis_xor_packet); + safe_delete_array(spawn_tmp_pos_xor_packet); + safe_delete_array(spell_xor_packet); + safe_delete_array(spell_orig_packet); + safe_delete_array(raid_orig_packet); + safe_delete_array(raid_xor_packet); + DestroyQuests(); + WritePlayerStatistics(); + RemovePlayerStatistics(); + DeleteMail(); + world.RemoveLottoPlayer(GetCharacterID()); + safe_delete(info); + index_mutex.writelock(__FUNCTION__, __LINE__); + player_spawn_reverse_id_map.clear(); + player_spawn_id_map.clear(); + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + + info_mutex.writelock(__FUNCTION__, __LINE__); + spawn_info_packet_list.clear(); + info_mutex.releasewritelock(__FUNCTION__, __LINE__); + vis_mutex.writelock(__FUNCTION__, __LINE__); + spawn_vis_packet_list.clear(); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); + pos_mutex.writelock(__FUNCTION__, __LINE__); + spawn_pos_packet_list.clear(); + pos_mutex.releasewritelock(__FUNCTION__, __LINE__); + + safe_delete(spawn_header_struct); + safe_delete(spawn_footer_struct); + safe_delete(sign_footer_struct); + safe_delete(widget_footer_struct); + safe_delete(spawn_info_struct); + safe_delete(spawn_vis_struct); + safe_delete(spawn_pos_struct); + ClearPendingSelectableItemRewards(0, true); + ClearPendingItemRewards(); + ClearEverything(); + + safe_delete(SortedTraitList); + safe_delete(ClassTraining); + safe_delete(RaceTraits); + safe_delete(InnateRaceTraits); + safe_delete(FocusEffects); + // leak fix on Language* pointer from Player::AddLanguage + player_languages_list.Clear(); +} + +EQ2Packet* Player::serialize(Player* player, int16 version){ + return spawn_serialize(player, version); +} + +EQ2Packet* Player::Move(float x, float y, float z, int16 version, float heading){ + PacketStruct* packet = configReader.getStruct("WS_MoveClient", version); + if(packet){ + packet->setDataByName("x", x); + packet->setDataByName("y", y); + packet->setDataByName("z", z); + packet->setDataByName("unknown", 1); // 1 seems to force the client to re-render the zone at the new location + packet->setDataByName("location", 0xFFFFFFFF); //added in 869 + if (heading != -1.0f) + packet->setDataByName("heading", heading); + EQ2Packet* outapp = packet->serialize(); + safe_delete(packet); + return outapp; + } + return 0; +} + +void Player::DestroyQuests(){ + MPlayerQuests.writelock(__FUNCTION__, __LINE__); + map::iterator itr; + for(itr = completed_quests.begin(); itr != completed_quests.end(); itr++){ + if(itr->second) { + safe_delete(itr->second); + } + } + completed_quests.clear(); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second) { + safe_delete(itr->second); + } + } + player_quests.clear(); + for(itr = pending_quests.begin(); itr != pending_quests.end(); itr++){ + if(itr->second) { + safe_delete(itr->second); + } + } + pending_quests.clear(); + MPlayerQuests.releasewritelock(__FUNCTION__, __LINE__); +} + +PlayerInfo* Player::GetPlayerInfo(){ + if(info == 0) + info = new PlayerInfo(this); + return info; +} + +void PlayerInfo::CalculateXPPercentages(){ + int32 xp_needed = info_struct->get_xp_needed(); + if(xp_needed > 0){ + double div_percent = ((double)info_struct->get_xp() / xp_needed) * 100.0; + int16 percentage = (int16)(div_percent) * 10; + double whole, fractional = 0.0; + fractional = std::modf(div_percent, &whole); + info_struct->set_xp_yellow(percentage); + info_struct->set_xp_blue((int16)(fractional * 1000)); + + // vitality bars probably need a revisit + info_struct->set_xp_blue_vitality_bar(0); + info_struct->set_xp_yellow_vitality_bar(0); + if(player->GetXPVitality() > 0){ + float vitality_total = player->GetXPVitality()*10 + percentage; + vitality_total -= ((int)(percentage/100)*100); + if(vitality_total < 100){ //10% + info_struct->set_xp_blue_vitality_bar(info_struct->get_xp_blue() + (int16)(player->GetXPVitality() *10)); + } + else + info_struct->set_xp_yellow_vitality_bar(info_struct->get_xp_yellow() + (int16)(player->GetXPVitality() *10)); + } + } +} + +void PlayerInfo::CalculateTSXPPercentages(){ + int32 ts_xp_needed = info_struct->get_ts_xp_needed(); + if(ts_xp_needed > 0){ + float percentage = ((double)info_struct->get_ts_xp() / ts_xp_needed) * 1000; + info_struct->set_tradeskill_exp_yellow((int16)percentage); + info_struct->set_tradeskill_exp_blue((int16)((percentage - info_struct->get_tradeskill_exp_yellow()) * 1000)); + /*info_struct->xp_blue_vitality_bar = 0; + info_struct->xp_yellow_vitality_bar = 0; + if(player->GetXPVitality() > 0){ + float vitality_total = player->GetXPVitality()*10 + percentage; + vitality_total -= ((int)(percentage/100)*100); + if(vitality_total < 100){ //10% + info_struct->xp_blue_vitality_bar = info_struct->xp_blue + (int16)(player->GetXPVitality() *10); + } + else + info_struct->xp_yellow_vitality_bar = info_struct->xp_yellow + (int16)(player->GetXPVitality() *10); + }*/ + } +} + +void PlayerInfo::SetHouseZone(int32 id){ + house_zone_id = id; +} + +void PlayerInfo::SetBindZone(int32 id){ + bind_zone_id = id; +} + +void PlayerInfo::SetBindX(float x){ + bind_x = x; +} + +void PlayerInfo::SetBindY(float y){ + bind_y = y; +} + +void PlayerInfo::SetBindZ(float z){ + bind_z = z; +} + +void PlayerInfo::SetBindHeading(float heading){ + bind_heading = heading; +} + +int32 PlayerInfo::GetHouseZoneID(){ + return house_zone_id; +} + +int32 PlayerInfo::GetBindZoneID(){ + return bind_zone_id; +} + +float PlayerInfo::GetBindZoneX(){ + return bind_x; +} + +float PlayerInfo::GetBindZoneY(){ + return bind_y; +} + +float PlayerInfo::GetBindZoneZ(){ + return bind_z; +} + +float PlayerInfo::GetBindZoneHeading(){ + return bind_heading; +} + +PacketStruct* PlayerInfo::serialize2(int16 version){ + PacketStruct* packet = configReader.getStruct("WS_CharacterSheet", version); + if(packet){ + //TODO: 2021 FIX THIS CASTING + char deity[32]; + strncpy(deity, info_struct->get_deity().c_str(), 32); + packet->setDataByName("deity", deity); + + char name[40]; + strncpy(name, info_struct->get_name().c_str(), 40); + packet->setDataByName("character_name", name); + packet->setDataByName("race", info_struct->get_race()); + packet->setDataByName("gender", info_struct->get_gender()); + packet->setDataByName("class1", info_struct->get_class1()); + packet->setDataByName("class2", info_struct->get_class2()); + packet->setDataByName("class3", info_struct->get_class3()); + packet->setDataByName("tradeskill_class1", info_struct->get_tradeskill_class1()); + packet->setDataByName("tradeskill_class2", info_struct->get_tradeskill_class2()); + packet->setDataByName("tradeskill_class3", info_struct->get_tradeskill_class3()); + packet->setDataByName("level", info_struct->get_level()); + packet->setDataByName("effective_level", info_struct->get_effective_level() != 0 ? info_struct->get_effective_level() : info_struct->get_level()); + packet->setDataByName("tradeskill_level", info_struct->get_tradeskill_level()); + packet->setDataByName("account_age_base", info_struct->get_account_age_base()); + +// for(int8 i=0;i<19;i++) +// { +// packet->setDataByName("account_age_bonus", info_struct->get_account_age_bonus(i)); +// } + + // + packet->setDataByName("current_hp", player->GetHP()); + packet->setDataByName("max_hp",player-> GetTotalHP()); + packet->setDataByName("base_hp", player->GetTotalHPBase()); + float bonus_health = floor( (float)(info_struct->get_sta() * player->CalculateBonusMod())); + packet->setDataByName("bonus_health", bonus_health); + packet->setDataByName("stat_bonus_health", player->CalculateBonusMod()); + packet->setDataByName("current_power", player->GetPower()); + packet->setDataByName("max_power", player->GetTotalPower()); + packet->setDataByName("base_power", player->GetTotalPowerBase()); + packet->setDataByName("bonus_power", floor( (float)(player->GetPrimaryStat() * player->CalculateBonusMod()))); + packet->setDataByName("stat_bonus_power", player->CalculateBonusMod()); + packet->setDataByName("conc_used", info_struct->get_cur_concentration()); + packet->setDataByName("conc_max", info_struct->get_max_concentration()); + packet->setDataByName("attack", info_struct->get_cur_attack()); + packet->setDataByName("attack_base", info_struct->get_attack_base()); + packet->setDataByName("absorb", info_struct->get_absorb()); + packet->setDataByName("mitigation_skill1", info_struct->get_mitigation_skill1()); + packet->setDataByName("mitigation_skill2", info_struct->get_mitigation_skill2()); + packet->setDataByName("mitigation_skill3", info_struct->get_mitigation_skill3()); + CalculateXPPercentages(); + packet->setDataByName("exp_yellow", info_struct->get_xp_yellow()); + packet->setDataByName("exp_blue", info_struct->get_xp_blue()); + packet->setDataByName("tradeskill_exp_yellow", info_struct->get_tradeskill_exp_yellow()); + packet->setDataByName("tradeskill_exp_blue", info_struct->get_tradeskill_exp_blue()); + packet->setDataByName("flags", info_struct->get_flags()); + packet->setDataByName("flags2", info_struct->get_flags2()); + + packet->setDataByName("avoidance_pct", (int16)info_struct->get_avoidance_display()*10.0f);//avoidance_pct 192 = 19.2% // confirmed DoV + packet->setDataByName("avoidance_base", (int16)info_struct->get_avoidance_base()*10.0f); // confirmed DoV + packet->setDataByName("avoidance", info_struct->get_cur_avoidance()); + packet->setDataByName("base_avoidance_pct", info_struct->get_base_avoidance_pct());// confirmed DoV + float parry_pct = info_struct->get_parry(); // client works off of int16, but we use floats to track the actual x/100% + packet->setDataByName("parry",(int16)(parry_pct*10.0f));// confirmed DoV + + float block_pct = info_struct->get_block()*10.0f; + + packet->setDataByName("block", (int16)block_pct);// confirmed DoV + packet->setDataByName("uncontested_block", info_struct->get_uncontested_block());// confirmed DoV + + packet->setDataByName("str", info_struct->get_str()); + packet->setDataByName("sta", info_struct->get_sta()); + packet->setDataByName("agi", info_struct->get_agi()); + packet->setDataByName("wis", info_struct->get_wis()); + packet->setDataByName("int", info_struct->get_intel()); + packet->setDataByName("str_base", info_struct->get_str_base()); + packet->setDataByName("sta_base", info_struct->get_sta_base()); + packet->setDataByName("agi_base", info_struct->get_agi_base()); + packet->setDataByName("wis_base", info_struct->get_wis_base()); + packet->setDataByName("int_base", info_struct->get_intel_base()); + packet->setDataByName("mitigation_cur", info_struct->get_cur_mitigation()); + packet->setDataByName("mitigation_max", info_struct->get_max_mitigation()); + packet->setDataByName("mitigation_base", info_struct->get_mitigation_base()); + packet->setDataByName("heat", info_struct->get_heat()); + packet->setDataByName("cold", info_struct->get_cold()); + packet->setDataByName("magic", info_struct->get_magic()); + packet->setDataByName("mental", info_struct->get_mental()); + packet->setDataByName("divine", info_struct->get_divine()); + packet->setDataByName("disease", info_struct->get_disease()); + packet->setDataByName("poison", info_struct->get_poison()); + packet->setDataByName("heat_base", info_struct->get_heat_base()); + packet->setDataByName("cold_base", info_struct->get_cold_base()); + packet->setDataByName("magic_base", info_struct->get_magic_base()); + packet->setDataByName("mental_base", info_struct->get_mental_base()); + packet->setDataByName("divine_base", info_struct->get_divine_base()); + packet->setDataByName("disease_base", info_struct->get_disease_base()); + packet->setDataByName("poison_base", info_struct->get_poison_base()); + packet->setDataByName("mitigation_cur2", info_struct->get_cur_mitigation()); + packet->setDataByName("mitigation_max2", info_struct->get_max_mitigation()); + packet->setDataByName("mitigation_base2", info_struct->get_mitigation_base()); + packet->setDataByName("coins_copper", info_struct->get_coin_copper()); + packet->setDataByName("coins_silver", info_struct->get_coin_silver()); + packet->setDataByName("coins_gold", info_struct->get_coin_gold()); + packet->setDataByName("coins_plat", info_struct->get_coin_plat()); + packet->setDataByName("weight", info_struct->get_weight()); + packet->setDataByName("max_weight", info_struct->get_max_weight()); + + if(info_struct->get_pet_id() != 0xFFFFFFFF) { + char pet_name[32]; + strncpy(pet_name, info_struct->get_pet_name().c_str(), version <= 373 ? 16 : 32); + packet->setDataByName("pet_name", pet_name); + } + else { + packet->setDataByName("pet_name", "No Pet"); + } + + packet->setDataByName("pet_health_pct", info_struct->get_pet_health_pct()); + packet->setDataByName("pet_power_pct", info_struct->get_pet_power_pct()); + + packet->setDataByName("pet_movement", info_struct->get_pet_movement()); + packet->setDataByName("pet_behavior", info_struct->get_pet_behavior()); + + packet->setDataByName("status_points", info_struct->get_status_points()); + if(bind_zone_id > 0){ + string bind_name = database.GetZoneName(bind_zone_id); + if (bind_name.length() > 0) + packet->setDataByName("bind_zone", bind_name.c_str()); + } + else + packet->setDataByName("bind_zone", "None"); + if(house_zone_id > 0){ + string house_name = database.GetZoneName(house_zone_id); + if (house_name.length() > 0) + packet->setDataByName("house_zone", house_name.c_str()); + } + else + packet->setDataByName("house_zone", "None"); + //packet->setDataByName("account_age_base", 14); + packet->setDataByName("hp_regen", info_struct->get_hp_regen()); + packet->setDataByName("power_regen", info_struct->get_power_regen()); + /*packet->setDataByName("unknown11", -1, 0); + packet->setDataByName("unknown11", -1, 1); + packet->setDataByName("unknown13", 201, 0); + packet->setDataByName("unknown13", 201, 1); + packet->setDataByName("unknown13", 234, 2); + packet->setDataByName("unknown13", 201, 3); + packet->setDataByName("unknown13", 214, 4); + packet->setDataByName("unknown13", 234, 5); + packet->setDataByName("unknown13", 234, 6); + + packet->setDataByName("unknown14", 78); + */ + packet->setDataByName("adventure_exp_vitality", (int16)(player->GetXPVitality() *10)); + //packet->setDataByName("unknown15b", 9911); + packet->setDataByName("unknown15a", 78); + packet->setDataByName("xp_yellow_vitality_bar", info_struct->get_xp_yellow_vitality_bar()); + packet->setDataByName("xp_blue_vitality_bar", info_struct->get_xp_blue_vitality_bar()); + packet->setDataByName("tradeskill_exp_vitality", 100); + packet->setDataByName("unknown15c", 200); + + //packet->setDataByName("unknown15", 100, 10); + packet->setDataByName("unknown18", 16880, 1); + /*packet->setDataByName("unknown19", 1); + packet->setDataByName("unknown19", 3, 1); + packet->setDataByName("unknown19", 1074301064, 2); + packet->setDataByName("unknown19", 1, 3); + packet->setDataByName("unknown19", 3, 4); + packet->setDataByName("unknown19", 1074301064, 5); + packet->setDataByName("unknown19", 6, 6); + packet->setDataByName("unknown19", 14, 7); + packet->setDataByName("unknown19", 1083179008, 8);*/ + player->SetGroupInformation(packet); + packet->setDataByName("unknown20", 1, 107); + packet->setDataByName("unknown20", 1, 108); + packet->setDataByName("unknown20", 1, 109); + packet->setDataByName("unknown20", 1, 110); + packet->setDataByName("unknown20", 1, 111); + //packet->setDataByName("unknown20b", 255); + //packet->setDataByName("unknown20b", 255, 1); + //packet->setDataByName("unknown20b", 255, 2); + packet->setDataByName("unknown11", 123); + packet->setDataByName("unknown11", 234, 1); + + //packet->setDataByName("in_combat", 32768); + //make name flash red + /*packet->setDataByName("unknown20", 8); + packet->setDataByName("unknown20", 38, 70); + packet->setDataByName("unknown20", 17, 77); + packet->setDataByName("unknown20", 1, 112); //melee stats and such + packet->setDataByName("unknown20", 1, 113); + packet->setDataByName("unknown20", 1, 114); + packet->setDataByName("unknown20", 1, 115); + + packet->setDataByName("unknown20", 4294967295, 309); + packet->setDataByName("unknown22", 2, 4); + packet->setDataByName("unknown23", 2, 29); + */ + //packet->setDataByName("unknown20b", 1, i); // pet bar in here + // for(int i=0;i<19;i++) + // packet->setDataByName("unknown7", 257, i); + //packet->setDataByName("unknown21", info_struct->rain, 2); + packet->setDataByName("rain", info_struct->get_rain()); + packet->setDataByName("rain2", info_struct->get_wind()); //-102.24); + /*packet->setDataByName("unknown22", 3, 4); + packet->setDataByName("unknown23", 3, 161); + packet->setDataByName("unknown20", 103); + packet->setDataByName("unknown20", 1280, 70); + packet->setDataByName("unknown20", 9, 71); + packet->setDataByName("unknown20", 5, 72); + packet->setDataByName("unknown20", 4294967271, 73); + packet->setDataByName("unknown20", 5, 75); + packet->setDataByName("unknown20", 1051, 77); + packet->setDataByName("unknown20", 3, 78); + packet->setDataByName("unknown20", 6, 104); + packet->setDataByName("unknown20", 1, 105); + packet->setDataByName("unknown20", 20, 106); + packet->setDataByName("unknown20", 3, 107); + packet->setDataByName("unknown20", 1, 108); + packet->setDataByName("unknown20", 1, 109); + packet->setDataByName("unknown20", 4278190080, 494); + packet->setDataByName("unknown20b", 255); + packet->setDataByName("unknown20b", 255, 1); + packet->setDataByName("unknown20b", 255, 2); + packet->setDataByName("unknown20", 50, 75); + */ + //packet->setDataByName("rain2", -102.24); + player->GetSpellEffectMutex()->readlock(__FUNCTION__, __LINE__); + for(int i=0;i<45;i++){ + if(i < 30){ + packet->setSubstructDataByName("maintained_effects", "name", info_struct->maintained_effects[i].name, i, 0); + packet->setSubstructDataByName("maintained_effects", "target", info_struct->maintained_effects[i].target, i, 0); + packet->setSubstructDataByName("maintained_effects", "spell_id", info_struct->maintained_effects[i].spell_id, i, 0); + packet->setSubstructDataByName("maintained_effects", "slot_pos", info_struct->maintained_effects[i].slot_pos, i, 0); + packet->setSubstructDataByName("maintained_effects", "icon", info_struct->maintained_effects[i].icon, i, 0); + packet->setSubstructDataByName("maintained_effects", "icon_type", info_struct->maintained_effects[i].icon_backdrop, i, 0); + packet->setSubstructDataByName("maintained_effects", "conc_used", info_struct->maintained_effects[i].conc_used, i, 0); + packet->setSubstructDataByName("maintained_effects", "unknown3", 1, i, 0); + packet->setSubstructDataByName("maintained_effects", "total_time", info_struct->maintained_effects[i].total_time, i, 0); + packet->setSubstructDataByName("maintained_effects", "expire_timestamp", info_struct->maintained_effects[i].expire_timestamp, i, 0); + } + else if(version < 942)//version 942 added 15 additional spell effect slots + break; + packet->setSubstructDataByName("spell_effects", "spell_id", info_struct->spell_effects[i].spell_id, i, 0); + if(info_struct->spell_effects[i].spell_id > 0 && info_struct->spell_effects[i].spell_id < 0xFFFFFFFF) + packet->setSubstructDataByName("spell_effects", "unknown2", 514, i, 0); + packet->setSubstructDataByName("spell_effects", "total_time", info_struct->spell_effects[i].total_time, i, 0); + packet->setSubstructDataByName("spell_effects", "expire_timestamp", info_struct->spell_effects[i].expire_timestamp, i, 0); + packet->setSubstructDataByName("spell_effects", "icon", info_struct->spell_effects[i].icon, i, 0); + packet->setSubstructDataByName("spell_effects", "icon_type", info_struct->spell_effects[i].icon_backdrop, i, 0); + } + player->GetSpellEffectMutex()->releasereadlock(__FUNCTION__, __LINE__); + return packet; + } + return 0; +} + +EQ2Packet* PlayerInfo::serialize3(PacketStruct* packet, int16 version){ + if(packet){ + string* data = packet->serializeString(); + int32 size = data->length(); + //DumpPacket((uchar*)data->c_str(), size); + uchar* tmp = new uchar[size]; + if(!changes){ + orig_packet = new uchar[size]; + changes = new uchar[size]; + memcpy(orig_packet, (uchar*)data->c_str(), size); + size = Pack(tmp, (uchar*)data->c_str(), size, size, version); + } + else{ + memcpy(changes, (uchar*)data->c_str(), size); + Encode(changes, orig_packet, size); + size = Pack(tmp, changes, size, size, version); + //cout << "INFO HERE:\n"; + //DumpPacket(tmp, size); + } + EQ2Packet* ret_packet = new EQ2Packet(OP_UpdateCharacterSheetMsg, tmp, size+4); + safe_delete_array(tmp); + safe_delete(packet); + return ret_packet; + } + return 0; +} + +void PlayerInfo::SetAccountAge(int32 age){ + info_struct->set_account_age_base(age); +} + +EQ2Packet* PlayerInfo::serialize(int16 version, int16 modifyPos, int32 modifyValue) { + PacketStruct* packet = configReader.getStruct("WS_CharacterSheet", version); + //0-69, locked screen movement + //30-69 normal movement + //10-30 normal movement + + if (packet) { + char name[40]; + strncpy(name,info_struct->get_name().c_str(),40); + packet->setDataByName("character_name", name); + packet->setDataByName("race", info_struct->get_race()); + packet->setDataByName("gender", info_struct->get_gender()); + packet->setDataByName("exiled", 0); // need exiled data + packet->setDataByName("class1", info_struct->get_class1()); + packet->setDataByName("class2", info_struct->get_class2()); + packet->setDataByName("class3", info_struct->get_class3()); + packet->setDataByName("tradeskill_class1", info_struct->get_tradeskill_class1()); + packet->setDataByName("tradeskill_class2", info_struct->get_tradeskill_class2()); + packet->setDataByName("tradeskill_class3", info_struct->get_tradeskill_class3()); + packet->setDataByName("level", info_struct->get_level()); + packet->setDataByName("effective_level", info_struct->get_effective_level() != 0 ? info_struct->get_effective_level() : info_struct->get_level()); + packet->setDataByName("tradeskill_level", info_struct->get_tradeskill_level()); + packet->setDataByName("account_age_base", info_struct->get_account_age_base()); + + //TODO: 2021 FIX THIS CASTING + for (int8 i = 0; i < 19; i++) + packet->setDataByName("account_age_bonus", 0); + //TODO: 2021 FIX THIS CASTING + char deity[32]; + strncpy(deity, info_struct->get_deity().c_str(), 32); + packet->setDataByName("deity", deity); + + packet->setDataByName("last_name", player->GetLastName()); + packet->setDataByName("current_hp", player->GetHP()); + packet->setDataByName("max_hp", player->GetTotalHP()); + packet->setDataByName("base_hp", player->GetTotalHPBase()); + + packet->setDataByName("current_power", player->GetPower()); + packet->setDataByName("max_power", player->GetTotalPower()); + packet->setDataByName("base_power", player->GetTotalPowerBase()); + packet->setDataByName("conc_used", info_struct->get_cur_concentration()); + packet->setDataByName("conc_max", info_struct->get_max_concentration()); + packet->setDataByName("hp_regen", player->GetInfoStruct()->get_hp_regen()); + packet->setDataByName("power_regen", player->GetInfoStruct()->get_power_regen()); + + packet->setDataByName("stat_bonus_health", player->CalculateBonusMod());//bonus health and bonus power getting same value? + packet->setDataByName("stat_bonus_power", player->CalculateBonusMod());//bonus health and bonus power getting same value? + float bonus_health = floor((float)(info_struct->get_sta() * player->CalculateBonusMod())); + packet->setDataByName("bonus_health", bonus_health); + packet->setDataByName("bonus_power", floor((float)(player->GetPrimaryStat() * player->CalculateBonusMod()))); + packet->setDataByName("stat_bonus_damage", 95); //stat_bonus_damage + packet->setDataByName("mitigation_cur", info_struct->get_cur_mitigation());// confirmed DoV + packet->setDataByName("mitigation_base", info_struct->get_mitigation_base());// confirmed DoV + + packet->setDataByName("mitigation_pct_pve", info_struct->get_mitigation_pve()); // % calculation Mitigation % vs PvE 392 = 39.2%// confirmed DoV + packet->setDataByName("mitigation_pct_pvp", info_struct->get_mitigation_pvp()); // % calculation Mitigation % vs PvP 559 = 55.9%// confirmed DoV + packet->setDataByName("toughness", 0);//toughness// confirmed DoV + packet->setDataByName("toughness_resist_dmg_pvp", 0);//toughness_resist_dmg_pvp 73 = 7300% // confirmed DoV + packet->setDataByName("avoidance_pct", (int16)info_struct->get_avoidance_display()*10.0f);//avoidance_pct 192 = 19.2% // confirmed DoV + packet->setDataByName("avoidance_base", (int16)info_struct->get_avoidance_base()*10.0f); // confirmed DoV + packet->setDataByName("avoidance", info_struct->get_cur_avoidance()); + packet->setDataByName("base_avoidance_pct", info_struct->get_base_avoidance_pct());// confirmed DoV + float parry_pct = info_struct->get_parry(); // client works off of int16, but we use floats to track the actual x/100% + packet->setDataByName("parry",(int16)(parry_pct*10.0f));// confirmed DoV + + float block_pct = info_struct->get_block()*10.0f; + + packet->setDataByName("block", (int16)block_pct);// confirmed DoV + packet->setDataByName("uncontested_block", info_struct->get_uncontested_block());// confirmed DoV + packet->setDataByName("str", info_struct->get_str());// confirmed DoV + packet->setDataByName("sta", info_struct->get_sta());// confirmed DoV + packet->setDataByName("agi", info_struct->get_agi());// confirmed DoV + packet->setDataByName("wis", info_struct->get_wis());// confirmed DoV + packet->setDataByName("int", info_struct->get_intel());// confirmed DoV + packet->setDataByName("str_base", info_struct->get_str_base()); // confirmed DoV + packet->setDataByName("sta_base", info_struct->get_sta_base());// confirmed DoV + packet->setDataByName("agi_base", info_struct->get_agi_base());// confirmed DoV + packet->setDataByName("wis_base", info_struct->get_wis_base());// confirmed DoV + packet->setDataByName("int_base", info_struct->get_intel_base());// confirmed DoV + if (version <= 996) { + packet->setDataByName("heat", info_struct->get_heat()); + packet->setDataByName("cold", info_struct->get_cold()); + packet->setDataByName("magic", info_struct->get_magic()); + packet->setDataByName("mental", info_struct->get_mental()); + packet->setDataByName("divine", info_struct->get_divine()); + packet->setDataByName("disease", info_struct->get_disease()); + packet->setDataByName("poison", info_struct->get_poison()); + packet->setDataByName("heat_base", info_struct->get_heat_base()); + packet->setDataByName("cold_base", info_struct->get_cold_base()); + packet->setDataByName("magic_base", info_struct->get_magic_base()); + packet->setDataByName("mental_base", info_struct->get_mental_base()); + packet->setDataByName("divine_base", info_struct->get_divine_base()); + packet->setDataByName("disease_base", info_struct->get_disease_base()); + packet->setDataByName("poison_base", info_struct->get_poison_base()); + } + else { + packet->setDataByName("elemental", info_struct->get_heat());// confirmed DoV + packet->setDataByName("noxious", info_struct->get_poison());// confirmed DoV + packet->setDataByName("arcane", info_struct->get_magic());// confirmed DoV + packet->setDataByName("elemental_base", info_struct->get_elemental_base());// confirmed DoV + packet->setDataByName("noxious_base", info_struct->get_noxious_base());// confirmed DoV + packet->setDataByName("arcane_base", info_struct->get_arcane_base());// confirmed DoV + } + packet->setDataByName("elemental_absorb_pve", 0); //210 = 21.0% confirmed DoV + packet->setDataByName("noxious_absorb_pve", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("arcane_absorb_pve", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("elemental_absorb_pvp", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("noxious_absorb_pvp", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("arcane_absorb_pvp", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("elemental_dmg_reduction", 0);// confirmed DoV + packet->setDataByName("noxious_dmg_reduction", 0);// confirmed DoV + packet->setDataByName("arcane_dmg_reduction", 0);// confirmed DoV + packet->setDataByName("elemental_dmg_reduction_pct", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("noxious_dmg_reduction_pct", 0);//210 = 21.0% confirmed DoV + packet->setDataByName("arcane_dmg_reduction_pct", 0);//210 = 21.0% confirmed DoV + CalculateXPPercentages(); + packet->setDataByName("current_adv_xp", info_struct->get_xp()); // confirmed DoV + packet->setDataByName("needed_adv_xp", info_struct->get_xp_needed());// confirmed DoV + + if(version >= 60114) + { + // AoM ends up the debt_adv_xp field is the percentage of xp to the next level needed to advance out of debt (WHYY CANT THIS JUST BE A PERCENTAGE LIKE DOV!) + float currentPctOfLevel = (float)info_struct->get_xp() / (float)info_struct->get_xp_needed(); + float neededPctAdvanceOutOfDebt = currentPctOfLevel + (info_struct->get_xp_debt() / 100.0f); + packet->setDataByName("debt_adv_xp", neededPctAdvanceOutOfDebt); + } + else + { + double currentPctOfLevel = (double)info_struct->get_xp() / (double)info_struct->get_xp_needed(); + double neededPctAdvanceOutOfDebt = (currentPctOfLevel + ((double)info_struct->get_xp_debt() / 100.0)) * 1000.0; + packet->setDataByName("exp_debt", (int16)(neededPctAdvanceOutOfDebt));//95= 9500% //confirmed DoV + } + + packet->setDataByName("current_trade_xp", info_struct->get_ts_xp());// confirmed DoV + packet->setDataByName("needed_trade_xp", info_struct->get_ts_xp_needed());// confirmed DoV + + packet->setDataByName("debt_trade_xp", 0);//95= 9500% //confirmed DoV + packet->setDataByName("server_bonus", 0);//confirmed DoV + packet->setDataByName("adventure_vet_bonus", 145);//confirmed DoV + packet->setDataByName("tradeskill_vet_bonus", 123);//confirmed DoV + packet->setDataByName("recruit_friend", 110);// 110 = 11000% //confirmed DoV + packet->setDataByName("recruit_friend_bonus", 0);//confirmed DoV + + packet->setDataByName("adventure_vitality", (int16)(player->GetXPVitality() * 10)); // a %% + packet->setDataByName("adventure_vitality_yellow_arrow", info_struct->get_xp_yellow_vitality_bar()); //change info_struct to match struct + packet->setDataByName("adventure_vitality_blue_arrow", info_struct->get_xp_blue_vitality_bar()); //change info_struct to match struct + + packet->setDataByName("tradeskill_vitality", 300); //300 = 30% + + packet->setDataByName("tradeskill_vitality_purple_arrow", 0);// dov confirmed + packet->setDataByName("tradeskill_vitality_blue_arrow", 0);// dov confirmed + packet->setDataByName("mentor_bonus", 50);//mentor_bonus //this converts wrong says mentor bonus enabled but earning 0 + + packet->setDataByName("assigned_aa", player->GetAssignedAA()); + packet->setDataByName("max_aa", rule_manager.GetGlobalRule(R_Player, MaxAA)->GetInt16()); + packet->setDataByName("unassigned_aa", player->GetUnassignedAA()); // dov confirmed + packet->setDataByName("aa_green_bar", 0);// dov confirmed + packet->setDataByName("adv_xp_to_aa_xp_slider", 0); // aa slider max // dov confirmed + packet->setDataByName("adv_xp_to_aa_xp_max", 100); // aa slider position // dov confirmed + packet->setDataByName("aa_blue_bar", 0);// dov confirmed + packet->setDataByName("bonus_achievement_xp", 0); // dov confirmed + + packet->setDataByName("level_events", 32);// dov confirmed + packet->setDataByName("items_found", 62);// dov confirmed + packet->setDataByName("named_npcs_killed", 192);// dov confirmed + packet->setDataByName("quests_completed", 670);// dov confirmed + packet->setDataByName("exploration_events", 435);// dov confirmed + packet->setDataByName("completed_collections", 144);// dov confirmed + packet->setDataByName("unknown_1096_13_MJ", 80);//unknown_1096_13_MJ + packet->setDataByName("unknown_1096_14_MJ", 50);//unknown_1096_14_MJ + packet->setDataByName("coins_copper", info_struct->get_coin_copper());// dov confirmed + packet->setDataByName("coins_silver", info_struct->get_coin_silver());// dov confirmed + packet->setDataByName("coins_gold", info_struct->get_coin_gold());// dov confirmed + packet->setDataByName("coins_plat", info_struct->get_coin_plat());// dov confirmed + + Skill* skill = player->GetSkillByName("Swimming", false); + float breath_modifier = rule_manager.GetZoneRule(player->GetZoneID(), R_Player, SwimmingSkillMinBreathLength)->GetFloat(); + if(skill) { + int32 max_val = 450; + if(skill->max_val > 0) + max_val = skill->max_val; + float diff = (float)(skill->current_val + player->GetStat(ITEM_STAT_SWIMMING)) / (float)max_val; + float max_breath_mod = rule_manager.GetZoneRule(player->GetZoneID(), R_Player, SwimmingSkillMaxBreathLength)->GetFloat(); + float diff_mod = max_breath_mod * diff; + if(diff_mod > max_breath_mod) + breath_modifier = max_breath_mod; + else if(diff_mod > breath_modifier) + breath_modifier = diff_mod; + } + packet->setDataByName("breath", breath_modifier); + + packet->setDataByName("melee_pri_dmg_min", player->GetPrimaryWeaponMinDamage());// dov confirmed + packet->setDataByName("melee_pri_dmg_max", player->GetPrimaryWeaponMaxDamage());// dov confirmed + packet->setDataByName("melee_sec_dmg_min", player->GetSecondaryWeaponMinDamage());// dov confirmed + packet->setDataByName("melee_sec_dmg_max", player->GetSecondaryWeaponMaxDamage());// dov confirmed // this is off when using 2 handed weapon + packet->setDataByName("ranged_dmg_min", player->GetRangedWeaponMinDamage());// dov confirmed + packet->setDataByName("ranged_dmg_max", player->GetRangedWeaponMaxDamage());// dov confirmed + if (info_struct->get_attackspeed() > 0) { + packet->setDataByName("melee_pri_delay", (((float)player->GetPrimaryWeaponDelay() * 1.33) / player->CalculateAttackSpeedMod()) * .001);// dov confirmed + packet->setDataByName("melee_sec_delay", (((float)player->GetSecondaryWeaponDelay() * 1.33) / player->CalculateAttackSpeedMod()) * .001);// dov confirmed + packet->setDataByName("ranged_delay", (((float)player->GetRangeWeaponDelay() * 1.33) / player->CalculateAttackSpeedMod()) * .001);// dov confirmed + } + else { + packet->setDataByName("melee_pri_delay", (float)player->GetPrimaryWeaponDelay() * .001);// dov confirmed + packet->setDataByName("melee_sec_delay", (float)player->GetSecondaryWeaponDelay() * .001);// dov confirmed + packet->setDataByName("ranged_delay", (float)player->GetRangeWeaponDelay() * .001);// dov confirmed + } + + packet->setDataByName("ability_mod_pve", info_struct->get_ability_modifier());// dov confirmed + packet->setDataByName("base_melee_crit", 85);//85 = 8500% dov confirmed + packet->setDataByName("base_spell_crit", 84);// dov confirmed + packet->setDataByName("base_taunt_crit", 83);// dov confirmed + packet->setDataByName("base_heal_crit", 82);// dov confirmed + packet->setDataByName("flags", info_struct->get_flags()); + packet->setDataByName("flags2", info_struct->get_flags2()); + if (version == 546) { + if (player->get_character_flag(CF_ANONYMOUS)) + packet->setDataByName("flags_anonymous", 1); + if (player->get_character_flag(CF_ROLEPLAYING)) + packet->setDataByName("flags_roleplaying", 1); + if (player->get_character_flag(CF_AFK)) + packet->setDataByName("flags_afk", 1); + if (player->get_character_flag(CF_LFG)) + packet->setDataByName("flags_lfg", 1); + if (player->get_character_flag(CF_LFW)) + packet->setDataByName("flags_lfw", 1); + if (!player->get_character_flag(CF_HIDE_HOOD) && !player->get_character_flag(CF_HIDE_HELM)) + packet->setDataByName("flags_show_hood", 1); + if (player->get_character_flag(CF_SHOW_ILLUSION)) + packet->setDataByName("flags_show_illusion_form", 1); + if (player->get_character_flag(CF_ALLOW_DUEL_INVITES)) + packet->setDataByName("flags_show_duel_invites", 1); + if (player->get_character_flag(CF_ALLOW_TRADE_INVITES)) + packet->setDataByName("flags_show_trade_invites", 1); + if (player->get_character_flag(CF_ALLOW_GROUP_INVITES)) + packet->setDataByName("flags_show_group_invites", 1); + if (player->get_character_flag(CF_ALLOW_RAID_INVITES)) + packet->setDataByName("flags_show_raid_invites", 1); + if (player->get_character_flag(CF_ALLOW_GUILD_INVITES)) + packet->setDataByName("flags_show_guild_invites", 1); + } + + packet->setDataByName("haste", info_struct->get_haste());// dov confirmed + packet->setDataByName("drunk", info_struct->get_drunk());// dov confirmed + + packet->setDataByName("hate_mod", info_struct->get_hate_mod());// dov confirmed + packet->setDataByName("adventure_effects_bonus", 55);// NEED an adventure_effects_bonus// dov confirmed + packet->setDataByName("tradeskill_effects_bonus", 56);// NEED an tradeskill_effects_bonus// dov confirmed + packet->setDataByName("dps", info_struct->get_dps());// dov confirmed + packet->setDataByName("melee_ae", info_struct->get_melee_ae());// dov confirmed + packet->setDataByName("multi_attack", info_struct->get_multi_attack());// dov confirmed + packet->setDataByName("spell_multi_attack", info_struct->get_spell_multi_attack());// dov confirmed + packet->setDataByName("block_chance", info_struct->get_block_chance());// dov confirmed + packet->setDataByName("crit_chance", info_struct->get_crit_chance());// dov confirmed + packet->setDataByName("crit_bonus", info_struct->get_crit_bonus());// dov confirmed + + packet->setDataByName("potency", info_struct->get_potency());//info_struct->get_potency);// dov confirmed + + packet->setDataByName("reuse_speed", info_struct->get_reuse_speed());// dov confirmed + packet->setDataByName("recovery_speed", info_struct->get_recovery_speed());// dov confirmed + packet->setDataByName("casting_speed", info_struct->get_casting_speed());// dov confirmed + packet->setDataByName("spell_reuse_speed", info_struct->get_spell_reuse_speed());// dov confirmed + packet->setDataByName("strikethrough", info_struct->get_strikethrough());//dov confirmed + packet->setDataByName("accuracy", info_struct->get_accuracy());//dov confirmed + packet->setDataByName("critical_mit", info_struct->get_critical_mitigation());//dov /confirmed + + ((Entity*)player)->MStats.lock(); + packet->setDataByName("durability_mod", player->stats[ITEM_STAT_DURABILITY_MOD]);// dov confirmed + packet->setDataByName("durability_add", player->stats[ITEM_STAT_DURABILITY_ADD]);// dov confirmed + packet->setDataByName("progress_mod", player->stats[ITEM_STAT_PROGRESS_MOD]);// dov confirmed + packet->setDataByName("progress_add", player->stats[ITEM_STAT_PROGRESS_ADD]);// dov confirmed + packet->setDataByName("success_mod", player->stats[ITEM_STAT_SUCCESS_MOD]);// dov confirmed + packet->setDataByName("crit_success_mod", player->stats[ITEM_STAT_CRIT_SUCCESS_MOD]);// dov confirmed + ((Entity*)player)->MStats.unlock(); + + if (version <= 373 && info_struct->get_pet_id() == 0xFFFFFFFF) + packet->setDataByName("pet_id", 0); + else { + packet->setDataByName("pet_id", info_struct->get_pet_id()); + char pet_name[32]; + strncpy(pet_name, info_struct->get_pet_name().c_str(), version <= 373 ? 16 : 32); + packet->setDataByName("pet_name", pet_name); + } + + packet->setDataByName("pet_health_pct", info_struct->get_pet_health_pct()); + packet->setDataByName("pet_power_pct", info_struct->get_pet_power_pct()); + + packet->setDataByName("pet_movement", info_struct->get_pet_movement()); + packet->setDataByName("pet_behavior", info_struct->get_pet_behavior()); + packet->setDataByName("rain", info_struct->get_rain()); + packet->setDataByName("rain2", info_struct->get_wind()); //-102.24); + packet->setDataByName("status_points", info_struct->get_status_points()); + packet->setDataByName("guild_status", 888888); + packet->setDataByName("vault_slots", player->GetHouseVaultSlots()); + if (house_zone_id > 0){ + string house_name = database.GetZoneName(house_zone_id); + if(house_name.length() > 0) + packet->setDataByName("house_zone", house_name.c_str()); + } + else + packet->setDataByName("house_zone", "None"); + + if (bind_zone_id > 0){ + string bind_name = database.GetZoneName(bind_zone_id); + if(bind_name.length() > 0) + packet->setDataByName("bind_zone", bind_name.c_str()); + } + else + packet->setDataByName("bind_zone", "None"); + + + ((Entity*)player)->MStats.lock(); + packet->setDataByName("rare_harvest_chance", player->stats[ITEM_STAT_RARE_HARVEST_CHANCE]); + packet->setDataByName("max_crafting", player->stats[ITEM_STAT_MAX_CRAFTING]); + packet->setDataByName("component_refund", player->stats[ITEM_STAT_COMPONENT_REFUND]); + packet->setDataByName("ex_durability_mod", player->stats[ITEM_STAT_EX_DURABILITY_MOD]); + packet->setDataByName("ex_durability_add", player->stats[ITEM_STAT_EX_DURABILITY_ADD]); + packet->setDataByName("ex_crit_success_mod", player->stats[ITEM_STAT_EX_CRIT_SUCCESS_MOD]); + packet->setDataByName("ex_crit_failure_mod", player->stats[ITEM_STAT_EX_CRIT_FAILURE_MOD]); + packet->setDataByName("ex_progress_mod", player->stats[ITEM_STAT_EX_PROGRESS_MOD]); + packet->setDataByName("ex_progress_add", player->stats[ITEM_STAT_EX_PROGRESS_ADD]); + packet->setDataByName("ex_success_mod", player->stats[ITEM_STAT_EX_SUCCESS_MOD]); + ((Entity*)player)->MStats.unlock(); + + packet->setDataByName("flurry", info_struct->get_flurry()); + packet->setDataByName("unknown153", 153); + packet->setDataByName("bountiful_harvest", 0); // need bountiful harvest + + packet->setDataByName("unknown156", 156); + packet->setDataByName("unknown157", 157); + + packet->setDataByName("unknown159", 159); + packet->setDataByName("unknown160", 160); + + + packet->setDataByName("unknown163", 163); + + + packet->setDataByName("unknown168", 168); + packet->setDataByName("decrease_falling_dmg", 169); + + if (version <= 561) { + packet->setDataByName("exp_yellow", info_struct->get_xp_yellow() / 10); + packet->setDataByName("exp_blue", ((int16)info_struct->get_xp_yellow() % 100) + (info_struct->get_xp_blue() / 100)); + } + else { + packet->setDataByName("exp_yellow", info_struct->get_xp_yellow()); + packet->setDataByName("exp_blue", info_struct->get_xp_blue()); + } + + if (version <= 561) { + packet->setDataByName("tradeskill_exp_yellow", info_struct->get_tradeskill_exp_yellow() / 10); + packet->setDataByName("tradeskill_exp_blue", info_struct->get_tradeskill_exp_blue() / 10); + } + else { + packet->setDataByName("tradeskill_exp_yellow", info_struct->get_tradeskill_exp_yellow()); + packet->setDataByName("tradeskill_exp_blue", info_struct->get_tradeskill_exp_blue()); + } + + packet->setDataByName("attack", info_struct->get_cur_attack()); + packet->setDataByName("attack_base", info_struct->get_attack_base()); + packet->setDataByName("absorb", info_struct->get_absorb()); + packet->setDataByName("mitigation_skill1", info_struct->get_mitigation_skill1()); + packet->setDataByName("mitigation_skill2", info_struct->get_mitigation_skill2()); + packet->setDataByName("mitigation_skill3", info_struct->get_mitigation_skill3()); + + packet->setDataByName("mitigation_max", info_struct->get_max_mitigation()); + + packet->setDataByName("savagery", 250); + packet->setDataByName("max_savagery", 500); + packet->setDataByName("savagery_level", 1); + packet->setDataByName("max_savagery_level", 5); + packet->setDataByName("dissonance", 5000); + packet->setDataByName("max_dissonance", 10000); + + packet->setDataByName("mitigation_cur2", info_struct->get_cur_mitigation()); + packet->setDataByName("mitigation_max2", info_struct->get_max_mitigation()); + packet->setDataByName("mitigation_base2", info_struct->get_mitigation_base()); + + packet->setDataByName("weight", info_struct->get_weight()); + packet->setDataByName("max_weight", info_struct->get_max_weight()); + packet->setDataByName("unknownint32a", 777777); + packet->setDataByName("unknownint32b", 666666); + packet->setDataByName("mitigation2_cur", 2367); + packet->setDataByName("uncontested_riposte", info_struct->get_uncontested_riposte()); + packet->setDataByName("uncontested_dodge", info_struct->get_uncontested_dodge()); + packet->setDataByName("uncontested_parry", info_struct->get_uncontested_parry()); //???? + packet->setDataByName("uncontested_riposte_pve", 0); //???? + packet->setDataByName("uncontested_parry_pve", 0); //???? + packet->setDataByName("total_prestige_points", player->GetPrestigeAA()); + packet->setDataByName("unassigned_prestige_points", player->GetUnassignedPretigeAA()); + packet->setDataByName("total_tradeskill_points", player->GetTradeskillAA()); + packet->setDataByName("unassigned_tradeskill_points", player->GetUnassignedTradeskillAA()); + packet->setDataByName("total_tradeskill_prestige_points", player->GetTradeskillPrestigeAA()); + packet->setDataByName("unassigned_tradeskill_prestige_points", player->GetUnassignedTradeskillPrestigeAA()); + + // unknown14c = percent aa exp to next level + packet->setDataByName("unknown14d", 100, 0); + packet->setDataByName("unknown20", 1084227584, 72); + packet->setDataByName("unknown15c", 200); + + player->SetGroupInformation(packet); + + packet->setDataByName("in_combat_movement_speed", 125); + + packet->setDataByName("increase_max_power", 127); + packet->setDataByName("increase_max_power2", 128); + + packet->setDataByName("vision", info_struct->get_vision()); + packet->setDataByName("breathe_underwater", info_struct->get_breathe_underwater()); + + int32 expireTimestamp = 0; + Spawn* maintained_target = 0; + player->GetSpellEffectMutex()->readlock(__FUNCTION__, __LINE__); + player->GetMaintainedMutex()->readlock(__FUNCTION__, __LINE__); + for (int i = 0; i < 45; i++) { + if (i < 30) { + maintained_target = player->GetZone() ? player->GetZone()->GetSpawnByID(info_struct->maintained_effects[i].target) : nullptr; + packet->setSubstructDataByName("maintained_effects", "name", info_struct->maintained_effects[i].name, i, 0); + if (maintained_target) + packet->setSubstructDataByName("maintained_effects", "target", player->GetIDWithPlayerSpawn(maintained_target), i, 0); + packet->setSubstructDataByName("maintained_effects", "target_type", info_struct->maintained_effects[i].target_type, i, 0); + packet->setSubstructDataByName("maintained_effects", "spell_id", info_struct->maintained_effects[i].spell_id, i, 0); + packet->setSubstructDataByName("maintained_effects", "slot_pos", info_struct->maintained_effects[i].slot_pos, i, 0); + packet->setSubstructDataByName("maintained_effects", "icon", info_struct->maintained_effects[i].icon, i, 0); + packet->setSubstructDataByName("maintained_effects", "icon_type", info_struct->maintained_effects[i].icon_backdrop, i, 0); + packet->setSubstructDataByName("maintained_effects", "conc_used", info_struct->maintained_effects[i].conc_used, i, 0); + packet->setSubstructDataByName("maintained_effects", "unknown3", 1, i, 0); + packet->setSubstructDataByName("maintained_effects", "total_time", info_struct->maintained_effects[i].total_time, i, 0); + expireTimestamp = info_struct->maintained_effects[i].expire_timestamp; + if (expireTimestamp == 0xFFFFFFFF) + expireTimestamp = 0; + packet->setSubstructDataByName("maintained_effects", "expire_timestamp", expireTimestamp, i, 0); + } + else if (version < 942)//version 942 added 15 additional spell effect slots + break; + packet->setSubstructDataByName("spell_effects", "spell_id", info_struct->spell_effects[i].spell_id, i, 0); + packet->setSubstructDataByName("spell_effects", "total_time", info_struct->spell_effects[i].total_time, i, 0); + expireTimestamp = info_struct->spell_effects[i].expire_timestamp; + if (expireTimestamp == 0xFFFFFFFF) + expireTimestamp = 0; + packet->setSubstructDataByName("spell_effects", "expire_timestamp", expireTimestamp, i, 0); + packet->setSubstructDataByName("spell_effects", "icon", info_struct->spell_effects[i].icon, i, 0); + packet->setSubstructDataByName("spell_effects", "icon_type", info_struct->spell_effects[i].icon_backdrop, i, 0); + if(info_struct->spell_effects[i].spell && info_struct->spell_effects[i].spell->spell && info_struct->spell_effects[i].spell->spell->GetSpellData()->friendly_spell == 1) + packet->setSubstructDataByName("spell_effects", "cancellable", 1, i); + } + player->GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); + player->GetSpellEffectMutex()->releasereadlock(__FUNCTION__, __LINE__); + + int8 det_count = 0; + //Send detriment counts as 255 if all dets of that type are incurable + det_count = player->GetTraumaCount(); + if (det_count > 0) { + if (!player->HasCurableDetrimentType(DET_TYPE_TRAUMA)) + det_count = 255; + } + packet->setDataByName("trauma_count", det_count); + + det_count = player->GetArcaneCount(); + if (det_count > 0) { + if (!player->HasCurableDetrimentType(DET_TYPE_ARCANE)) + det_count = 255; + } + packet->setDataByName("arcane_count", det_count); + + det_count = player->GetNoxiousCount(); + if (det_count > 0) { + if (!player->HasCurableDetrimentType(DET_TYPE_NOXIOUS)) + det_count = 255; + } + packet->setDataByName("noxious_count", det_count); + + det_count = player->GetElementalCount(); + if (det_count > 0) { + if (!player->HasCurableDetrimentType(DET_TYPE_ELEMENTAL)) + det_count = 255; + } + packet->setDataByName("elemental_count", det_count); + + det_count = player->GetCurseCount(); + if (det_count > 0) { + if (!player->HasCurableDetrimentType(DET_TYPE_CURSE)) + det_count = 255; + } + packet->setDataByName("curse_count", det_count); + + player->GetDetrimentMutex()->readlock(__FUNCTION__, __LINE__); + vector* det_list = player->GetDetrimentalSpellEffects(); + DetrimentalEffects det; + int32 i = 0; + for (i = 0; i < det_list->size(); i++) { + det = det_list->at(i); + packet->setSubstructDataByName("detrimental_spell_effects", "spell_id", det.spell_id, i); + packet->setSubstructDataByName("detrimental_spell_effects", "total_time", det.total_time, i); + packet->setSubstructDataByName("detrimental_spell_effects", "icon", det.icon, i); + packet->setSubstructDataByName("detrimental_spell_effects", "icon_type", det.icon_backdrop, i); + expireTimestamp = det.expire_timestamp; + if (expireTimestamp == 0xFFFFFFFF) + expireTimestamp = 0; + packet->setSubstructDataByName("detrimental_spell_effects", "expire_timestamp", expireTimestamp, i); + packet->setSubstructDataByName("detrimental_spell_effects", "unknown2", 2, i); + if (i == 30) { + if (version < 942) + break; + } + else if (i == 45) + break; + } + if (version < 942) { + while (i < 30) { + packet->setSubstructDataByName("detrimental_spell_effects", "spell_id", 0xFFFFFFFF, i); + i++; + } + } + else { + while (i < 45) { + packet->setSubstructDataByName("detrimental_spell_effects", "spell_id", 0xFFFFFFFF, i); + i++; + } + } + player->GetDetrimentMutex()->releasereadlock(__FUNCTION__, __LINE__); + + // disabling as not in use right now + //packet->setDataByName("spirit_rank", 2); + //packet->setDataByName("spirit", 1); + //packet->setDataByName("spirit_progress", .67); + + packet->setDataByName("combat_exp_enabled", 1); + + string* data = packet->serializeString(); + int32 size = data->length(); + + //printf("CharSheet size: %u for version %u\n", size, version); + //DumpPacket((uchar*)data->c_str(), data->size()); + //packet->PrintPacket(); + uchar* tmp = new uchar[size]; + bool reverse = version > 373; + if (!changes) { + orig_packet = new uchar[size]; + changes = new uchar[size]; + memcpy(orig_packet, (uchar*)data->c_str(), size); + size = Pack(tmp, orig_packet, size, size, version, reverse); + } + else { + memcpy(changes, (uchar*)data->c_str(), size); + if (modifyPos > 0) { + uchar* ptr2 = (uchar*)changes; + ptr2 += modifyPos - 1; + if (modifyValue > 0xFFFF) { + memcpy(ptr2, (uchar*)&modifyValue, 4); + } + else if (modifyValue > 0xFF) { + memcpy(ptr2, (uchar*)&modifyValue, 2); + } + else + memcpy(ptr2, (uchar*)&modifyValue, 1); + } + Encode(changes, orig_packet, size); + if (modifyPos > 0) { + uchar* ptr2 = (uchar*)orig_packet; + if (modifyPos > 64) + ptr2 += modifyPos - 64; + int16 tmpsize = modifyPos + 128; + if (tmpsize > size) + tmpsize = size; + } + size = Pack(tmp, changes, size, size, version, reverse); + } + + if (version >= 546 && player->GetClient()) { + player->GetClient()->SendControlGhost(); + } + + EQ2Packet* ret_packet = new EQ2Packet(OP_UpdateCharacterSheetMsg, tmp, size); + safe_delete(packet); + safe_delete_array(tmp); + return ret_packet; + } + return 0; +} + +EQ2Packet* PlayerInfo::serializePet(int16 version) { + PacketStruct* packet = configReader.getStruct("WS_CharacterPet", version); + if(packet) { + Spawn* pet = 0; + pet = player->GetPet(); + if (!pet) + pet = player->GetCharmedPet(); + + if (pet) { + packet->setDataByName("current_hp", pet->GetHP()); + packet->setDataByName("max_hp", pet->GetTotalHP()); + packet->setDataByName("base_hp", pet->GetTotalHPBase()); + + packet->setDataByName("current_power", pet->GetPower()); + packet->setDataByName("max_power", pet->GetTotalPower()); + packet->setDataByName("base_power", pet->GetTotalPowerBase()); + + packet->setDataByName("spawn_id", info_struct->get_pet_id()); + packet->setDataByName("spawn_id2", info_struct->get_pet_id()); + + if(info_struct->get_pet_id() != 0xFFFFFFFF) { + packet->setDataByName("pet_id", info_struct->get_pet_id()); + char pet_name[32]; + strncpy(pet_name, info_struct->get_pet_name().c_str(), 32); + packet->setDataByName("name", pet_name); + } + else { + packet->setDataByName("name", "No Pet"); + packet->setDataByName("no_pet", "No Pet"); + } + + if (version >= 57000) { + packet->setDataByName("current_power3", pet->GetPower()); + packet->setDataByName("max_power3", pet->GetTotalPower()); + packet->setDataByName("health_pct_tooltip", (double)info_struct->get_pet_health_pct()); + packet->setDataByName("health_pct_bar", (double)info_struct->get_pet_health_pct()); + } + else { + packet->setDataByName("health_pct_tooltip", info_struct->get_pet_health_pct()); + packet->setDataByName("health_pct_bar", info_struct->get_pet_health_pct()); + } + packet->setDataByName("power_pct_tooltip", info_struct->get_pet_power_pct()); + packet->setDataByName("power_pct_bar", info_struct->get_pet_power_pct()); + packet->setDataByName("unknown5", 255); // Hate % maybe + packet->setDataByName("movement", info_struct->get_pet_movement()); + packet->setDataByName("behavior", info_struct->get_pet_behavior()); + } + else { + packet->setDataByName("current_hp", 0); + packet->setDataByName("max_hp", 0); + packet->setDataByName("base_hp", 0); + packet->setDataByName("current_power", 0); + packet->setDataByName("max_power", 0); + packet->setDataByName("base_power", 0); + + packet->setDataByName("spawn_id", 0); + packet->setDataByName("spawn_id2", 0xFFFFFFFF); + packet->setDataByName("name", ""); + packet->setDataByName("no_pet", "No Pet"); + packet->setDataByName("health_pct_tooltip", 0); + packet->setDataByName("health_pct_bar", 0); + packet->setDataByName("power_pct_tooltip", 0); + packet->setDataByName("power_pct_bar", 0); + packet->setDataByName("unknown5", 0); + packet->setDataByName("movement", 0); + packet->setDataByName("behavior", 0); + } + + + string* data = packet->serializeString(); + int32 size = data->length(); + uchar* tmp = new uchar[size]; + // if this is the first time sending this packet create the buffers + if(!pet_changes){ + pet_orig_packet = new uchar[size]; + pet_changes = new uchar[size]; + // copy the packet into the pet_orig_packet so we can xor against it in the future + memcpy(pet_orig_packet, (uchar*)data->c_str(), size); + // pack the packet, result ends up in tmp + size = Pack(tmp, (uchar*)data->c_str(), size, size, version); + } + else{ + // copy the packet into pet_changes + memcpy(pet_changes, (uchar*)data->c_str(), size); + // XOR's the packet to the original, stores the new packet in the orig packet (will xor against that for the next update) + // puts the xor packet into pet_changes. + Encode(pet_changes, pet_orig_packet, size); + // Pack the pet_changes packet, will put the packed size at the start, result ends up in tmp + size = Pack(tmp, pet_changes, size, size, version); + } + + // Create the packet that we will send + EQ2Packet* ret_packet = new EQ2Packet(OP_CharacterPet, tmp, size+4); + // Clean up + safe_delete_array(tmp); + safe_delete(packet); + // Return the packet that will be sent to the client + return ret_packet; + } + return 0; +} + +bool Player::DamageEquippedItems(int8 amount, Client* client) { + bool ret = false; + int8 item_type; + Item* item = 0; + equipment_list.MEquipmentItems.readlock(__FUNCTION__, __LINE__); + for(int8 i=0;igeneric_info.item_type; + if (item->details.item_id > 0 && item_type != ITEM_TYPE_FOOD && item_type != ITEM_TYPE_BAUBLE && item_type != ITEM_TYPE_THROWN && + !item->CheckFlag2(INDESTRUCTABLE)){ + ret = true; + if((item->generic_info.condition - amount) > 0) + item->generic_info.condition -= amount; + else + item->generic_info.condition = 0; + item->save_needed = true; + if (client) + client->QueuePacket(item->serialize(client->GetVersion(), false, this)); + } + } + } + equipment_list.MEquipmentItems.releasereadlock(__FUNCTION__, __LINE__); + + return ret; +} + +int16 Player::ConvertSlotToClient(int8 slot, int16 version) { + if (version <= 373) { + if (slot == EQ2_FOOD_SLOT) + slot = EQ2_ORIG_FOOD_SLOT; + else if (slot == EQ2_DRINK_SLOT) + slot = EQ2_ORIG_DRINK_SLOT; + else if (slot > EQ2_EARS_SLOT_1 && slot <= EQ2_WAIST_SLOT) + slot -= 1; + } + else if (version <= 561) { + if (slot == EQ2_FOOD_SLOT) + slot = EQ2_DOF_FOOD_SLOT; + else if (slot == EQ2_DRINK_SLOT) + slot = EQ2_DOF_DRINK_SLOT; + else if (slot == EQ2_CHARM_SLOT_1) + slot = EQ2_DOF_CHARM_SLOT_1; + else if (slot == EQ2_CHARM_SLOT_2) + slot = EQ2_DOF_CHARM_SLOT_2; + else if (slot > EQ2_EARS_SLOT_1 && slot <= EQ2_WAIST_SLOT) + slot -= 1; + } + return slot; +} + +int16 Player::ConvertSlotFromClient(int8 slot, int16 version) { + if (version <= 373) { + if (slot == EQ2_ORIG_FOOD_SLOT) + slot = EQ2_FOOD_SLOT; + else if (slot == EQ2_ORIG_DRINK_SLOT) + slot = EQ2_DRINK_SLOT; + else if (slot > EQ2_EARS_SLOT_1 && slot <= EQ2_WAIST_SLOT) + slot += 1; + } + else if (version <= 561) { + if (slot == EQ2_DOF_FOOD_SLOT) + slot = EQ2_FOOD_SLOT; + else if (slot == EQ2_DOF_DRINK_SLOT) + slot = EQ2_DRINK_SLOT; + else if (slot == EQ2_DOF_CHARM_SLOT_1) + slot = EQ2_CHARM_SLOT_1; + else if (slot == EQ2_DOF_CHARM_SLOT_2) + slot = EQ2_CHARM_SLOT_2; + else if (slot > EQ2_EARS_SLOT_1 && slot <= EQ2_WAIST_SLOT) + slot += 1; + } + return slot; +} + +int16 Player::GetNumSlotsEquip(int16 version) { + if(version <= 561) { + return CLASSIC_NUM_SLOTS; + } + + return NUM_SLOTS; +} + +int8 Player::GetMaxBagSlots(int16 version) { + if(version <= 373) { + return CLASSIC_EQ_MAX_BAG_SLOTS; + } + else if(version <= 561) { + return DOF_EQ_MAX_BAG_SLOTS; + } + + return 255; +} + +vector Player::UnequipItem(int16 index, sint32 bag_id, int8 slot, int16 version, int8 appearance_type, bool send_item_updates) { + vector packets; + EquipmentItemList* equipList = &equipment_list; + + if(appearance_type) + equipList = &appearance_equipment_list; + + if(index >= NUM_SLOTS) { + LogWrite(PLAYER__ERROR, 0, "Player", "%u index is out of range for equip items, bag_id: %i, slot: %u, version: %u, appearance: %u", index, bag_id, slot, version, appearance_type); + return packets; + } + equipList->MEquipmentItems.readlock(__FUNCTION__, __LINE__); + Item* item = equipList->items[index]; + + if(item && !IsAllowedCombatEquip(item->details.slot_id, true)) { + LogWrite(PLAYER__ERROR, 0, "Player", "Attempt to unequip item %s (%u) FAILED in combat!", item->name.c_str(), item->details.item_id); + equipList->MEquipmentItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + equipList->MEquipmentItems.releasereadlock(__FUNCTION__, __LINE__); + + if (item && bag_id == -999) { + int8 old_slot = item->details.slot_id; + if(item->details.equip_slot_id) { + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "unequipped", item, this); + const char* zone_script = world.GetZoneScript(GetZone()->GetZoneID()); + if (zone_script && lua_interface) + lua_interface->RunZoneScript(zone_script, "item_unequipped", GetZone(), this, item->details.item_id, item->name.c_str(), 0, item->details.unique_id); + item->save_needed = true; + EQ2Packet* outapp = item_list.serialize(this, version); + if (outapp) { + packets.push_back(outapp); + packets.push_back(item->serialize(version, false)); + EQ2Packet* bag_packet = SendBagUpdate(item->details.inv_slot_id, version); + if (bag_packet) + packets.push_back(bag_packet); + } + sint16 equip_slot_id = item->details.equip_slot_id; + item->details.equip_slot_id = 0; + equipList->RemoveItem(index); + SetEquippedItemAppearances(); + packets.push_back(equipList->serialize(version, this)); + SetCharSheetChanged(true); + SetEquipment(0, equip_slot_id ? equip_slot_id : old_slot); + } + else if (item_list.AssignItemToFreeSlot(item, true)) { + if(appearance_type) + database.DeleteItem(GetCharacterID(), item, "APPEARANCE"); + else + database.DeleteItem(GetCharacterID(), item, "EQUIPPED"); + + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "unequipped", item, this); + const char* zone_script = world.GetZoneScript(GetZone()->GetZoneID()); + if (zone_script && lua_interface) + lua_interface->RunZoneScript(zone_script, "item_unequipped", GetZone(), this, item->details.item_id, item->name.c_str(), 0, item->details.unique_id); + item->save_needed = true; + EQ2Packet* outapp = item_list.serialize(this, version); + if (outapp) { + packets.push_back(outapp); + packets.push_back(item->serialize(version, false)); + EQ2Packet* bag_packet = SendBagUpdate(item->details.inv_slot_id, version); + if (bag_packet) + packets.push_back(bag_packet); + } + equipList->RemoveItem(index); + SetEquippedItemAppearances(); + packets.push_back(equipList->serialize(version, this)); + SetCharSheetChanged(true); + SetEquipment(0, old_slot); + } + else { + PacketStruct* packet = configReader.getStruct("WS_DisplayText", version); + if (packet) { + packet->setDataByName("color", CHANNEL_COLOR_YELLOW); + packet->setMediumStringByName("text", "Unable to unequip item: no free inventory locations."); + packet->setDataByName("unknown02", 0x00ff); + packets.push_back(packet->serialize()); + safe_delete(packet); + } + } + } + else if (item) { + Item* to_item = 0; + if(appearance_type && slot == 255) + { + sint16 tmpSlot = 0; + item_list.GetFirstFreeSlot(&bag_id, &tmpSlot); + if(tmpSlot >= 0 && tmpSlot < 255) + slot = tmpSlot; + else + bag_id = 0; + } + + item_list.MPlayerItems.readlock(__FUNCTION__, __LINE__); + if (item_list.items.count(bag_id) > 0 && item_list.items[bag_id][BASE_EQUIPMENT].count(slot) > 0) + to_item = item_list.items[bag_id][BASE_EQUIPMENT][slot]; + + bool canEquipToSlot = false; + if (to_item && equipList->CanItemBeEquippedInSlot(to_item, item->details.slot_id)) { + canEquipToSlot = true; + } + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + + if (canEquipToSlot) { + equipList->RemoveItem(index); + if(item->details.appearance_type) + database.DeleteItem(GetCharacterID(), item, "APPEARANCE"); + else + database.DeleteItem(GetCharacterID(), item, "EQUIPPED"); + + database.DeleteItem(GetCharacterID(), to_item, "NOT-EQUIPPED"); + + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "unequipped", item, this); + + if (to_item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(to_item->GetItemScript(), "equipped", to_item, this); + + if(item->IsBag() && ( item->details.inv_slot_id != bag_id || item->details.slot_id != slot)) { + item_list.EraseItem(item); + } + item_list.RemoveItem(to_item); + equipList->SetItem(item->details.slot_id, to_item); + to_item->save_needed = true; + packets.push_back(to_item->serialize(version, false)); + SetEquipment(to_item); + item->details.inv_slot_id = bag_id; + item->details.slot_id = slot; + item->details.appearance_type = 0; + item->details.equip_slot_id = 0; + + if(!item->IsBag() && item_list.AddItem(item)) { // bags are omitted because they are equipped while remaining in inventory + item->save_needed = true; + SetEquippedItemAppearances(); + // SerializeItemPackets serves item and equipList in opposite order is why we don't use that function here.. + packets.push_back(item->serialize(version, false)); + packets.push_back(equipList->serialize(version, this)); + packets.push_back(item_list.serialize(this, version)); + } + else if(item->IsBag()) { + // already in inventory + } + else { + LogWrite(PLAYER__ERROR, 0, "Player", "failed to add item to item_list during UnequipItem, index %u, bag id %i, slot %u, version %u, appearance type %u", index, bag_id, slot, version, appearance_type); + } + } + else if (to_item && to_item->IsBag() && to_item->details.num_slots > 0) { + bool free_slot = false; + for (int8 i = 0; i < to_item->details.num_slots; i++) { + item_list.MPlayerItems.readlock(__FUNCTION__, __LINE__); + int32 count = item_list.items[to_item->details.bag_id][appearance_type].count(i); + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + if (count == 0) { + SetEquipment(0, item->details.equip_slot_id ? item->details.equip_slot_id : item->details.slot_id); + + if(item->details.appearance_type) + database.DeleteItem(GetCharacterID(), item, "APPEARANCE"); + else + database.DeleteItem(GetCharacterID(), item, "EQUIPPED"); + + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "unequipped", item, this); + + if(item->IsBag() && item != to_item) { + item_list.EraseItem(item); + } + + equipList->RemoveItem(index); + if(!item->IsBag()) { + item->details.inv_slot_id = to_item->details.bag_id; + item->details.slot_id = i; + item->details.appearance_type = to_item->details.appearance_type; + } + else { + item->details.appearance_type = 0; + } + item->details.equip_slot_id = 0; + + SerializeItemPackets(equipList, &packets, item, version, to_item); + free_slot = true; + break; + } + } + if (!free_slot) { + PacketStruct* packet = configReader.getStruct("WS_DisplayText", version); + if (packet) { + packet->setDataByName("color", CHANNEL_COLOR_YELLOW); + packet->setMediumStringByName("text", "Unable to unequip item: no free space in the bag."); + packet->setDataByName("unknown02", 0x00ff); + packets.push_back(packet->serialize()); + safe_delete(packet); + } + } + } + else if (to_item) { + PacketStruct* packet = configReader.getStruct("WS_DisplayText", version); + if (packet) { + packet->setDataByName("color", CHANNEL_COLOR_YELLOW); + packet->setMediumStringByName("text", "Unable to swap items: that item cannot be equipped there."); + packet->setDataByName("unknown02", 0x00ff); + packets.push_back(packet->serialize()); + safe_delete(packet); + } + } + else { + if ((bag_id == 0 && slot < NUM_INV_SLOTS) || (bag_id == InventorySlotType::BANK && slot < NUM_BANK_SLOTS) || (bag_id == InventorySlotType::SHARED_BANK && slot < NUM_SHARED_BANK_SLOTS) || (bag_id == InventorySlotType::HOUSE_VAULT && slot < GetHouseVaultSlots())) { + if ((bag_id == InventorySlotType::SHARED_BANK || bag_id == InventorySlotType::HOUSE_VAULT) && !item_list.SharedBankAddAllowed(item)) { + PacketStruct* packet = configReader.getStruct("WS_DisplayText", version); + if (packet) { + packet->setDataByName("color", CHANNEL_COLOR_YELLOW); + packet->setMediumStringByName("text", "Unable to unequip item: that item cannot be traded."); + packet->setDataByName("unknown02", 0x00ff); + packets.push_back(packet->serialize()); + safe_delete(packet); + } + } + else { + // need to check if appearance slot vs equipped + SetEquipment(0, item->details.equip_slot_id ? item->details.equip_slot_id : item->details.slot_id); + if(item->details.appearance_type) + database.DeleteItem(GetCharacterID(), item, "APPEARANCE"); + else + database.DeleteItem(GetCharacterID(), item, "EQUIPPED"); + + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "unequipped", item, this); + + if(item->IsBag() && (item->details.inv_slot_id != bag_id || item->details.slot_id != slot)) { + item_list.EraseItem(item); + } + equipList->RemoveItem(index); + item->details.inv_slot_id = bag_id; + item->details.slot_id = slot; + item->details.appearance_type = 0; + item->details.equip_slot_id = 0; + SerializeItemPackets(equipList, &packets, item, version); + } + } + else { + Item* bag = item_list.GetItemFromUniqueID(bag_id, true); + if (bag && bag->IsBag() && slot < bag->details.num_slots) { + SetEquipment(0, item->details.equip_slot_id ? item->details.equip_slot_id : item->details.slot_id); + if(item->details.appearance_type) + database.DeleteItem(GetCharacterID(), item, "APPEARANCE"); + else + database.DeleteItem(GetCharacterID(), item, "EQUIPPED"); + + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "unequipped", item, this); + + if(item->IsBag() && ( item->details.inv_slot_id != bag_id || item->details.slot_id != slot)) { + item_list.EraseItem(item); + } + equipList->RemoveItem(index); + item->details.inv_slot_id = bag_id; + item->details.slot_id = slot; + item->details.appearance_type = 0; + item->details.equip_slot_id = 0; + SerializeItemPackets(equipList, &packets, item, version); + } + } + } + Item* bag = item_list.GetItemFromUniqueID(bag_id, true); + if (bag && bag->IsBag()) + packets.push_back(bag->serialize(version, false, this)); + } + + if(send_item_updates && GetClient()) + { + GetClient()->UpdateSentSpellList(); + GetClient()->ClearSentSpellList(); + } + + return packets; +} + +map* Player::GetItemList(){ + return item_list.GetAllItems(); +} + +vector* Player::GetEquippedItemList(){ + return equipment_list.GetAllEquippedItems(); +} + +vector* Player::GetAppearanceEquippedItemList(){ + return appearance_equipment_list.GetAllEquippedItems(); +} + +EQ2Packet* Player::SendBagUpdate(int32 bag_unique_id, int16 version){ + Item* bag = 0; + if(bag_unique_id > 0) + bag = item_list.GetItemFromUniqueID(bag_unique_id, true); + + if(bag && bag->IsBag()) + return bag->serialize(version, false, this); + return 0; +} + +void Player::SetEquippedItemAppearances(){ + vector* items = GetEquipmentList()->GetAllEquippedItems(); + vector* appearance_items = GetAppearanceEquipmentList()->GetAllEquippedItems(); + if(items){ + for(int32 i=0;isize();i++) + SetEquipment(items->at(i)); + + // just have appearance items brute force replace the slots after the fact + for(int32 i=0;isize();i++) + SetEquipment(appearance_items->at(i)); + } + safe_delete(items); + safe_delete(appearance_items); + info_changed = true; + GetZone()->SendSpawnChanges(this); +} + +EQ2Packet* Player::SwapEquippedItems(int8 slot1, int8 slot2, int16 version, int16 equip_type){ + EquipmentItemList* equipList = &equipment_list; + + // right now client seems to pass 3 for this? Not sure why when other fields has appearance equipment as type 1 + if(equip_type == 3) + equipList = &appearance_equipment_list; + + equipList->MEquipmentItems.readlock(__FUNCTION__, __LINE__); + Item* item_from = equipList->items[slot1]; + Item* item_to = equipList->items[slot2]; + equipList->MEquipmentItems.releasereadlock(__FUNCTION__, __LINE__); + + if(item_from && equipList->CanItemBeEquippedInSlot(item_from, slot2)){ + if(item_to){ + if(!equipList->CanItemBeEquippedInSlot(item_to, slot1)) + return 0; + } + equipList->MEquipmentItems.writelock(__FUNCTION__, __LINE__); + equipList->items[slot1] = nullptr; + equipList->MEquipmentItems.releasewritelock(__FUNCTION__, __LINE__); + equipList->SetItem(slot2, item_from); + if(item_to) + { + equipList->SetItem(slot1, item_to); + item_to->save_needed = true; + } + item_from->save_needed = true; + + if (GetClient()) + { + //EquipmentItemList* equipList = &equipment_list; + + //if(appearance_type) + // equipList = &appearance_equipment_list; + + if(item_to) + GetClient()->QueuePacket(item_to->serialize(version, false, this)); + GetClient()->QueuePacket(item_from->serialize(version, false, this)); + GetClient()->QueuePacket(item_list.serialize(this, version)); + } + return equipList->serialize(version, this); + } + return 0; +} +bool Player::CanEquipItem(Item* item, int8 slot) { + if(client && client->GetVersion() <= 561 && slot == EQ2_EARS_SLOT_2) + return false; + + if (item) { + Client* client = GetClient(); + if (client) { + if (item->IsWeapon() && slot == 1) { + bool dwable = item->IsDualWieldAble(client, item, slot); + + if (dwable == 0) { + return false; + } + } + + if (item->CheckFlag(EVIL_ONLY) && GetAlignment() != ALIGNMENT_EVIL) { + client->Message(0, "%s requires an evil race.", item->name.c_str()); + } + else if (item->CheckFlag(GOOD_ONLY) && GetAlignment() != ALIGNMENT_GOOD) { + client->Message(0, "%s requires a good race.", item->name.c_str()); + } + else if (item->IsArmor() || item->IsWeapon() || item->IsFood() || item->IsRanged() || item->IsShield() || item->IsBauble() || item->IsAmmo() || item->IsThrown()) { + if (((item->generic_info.skill_req1 == 0 || item->generic_info.skill_req1 == 0xFFFFFFFF || skill_list.HasSkill(item->generic_info.skill_req1)) && (item->generic_info.skill_req2 == 0 || item->generic_info.skill_req2 == 0xFFFFFFFF || skill_list.HasSkill(item->generic_info.skill_req2)))) { + int16 override_level = item->GetOverrideLevel(GetAdventureClass(), GetTradeskillClass()); + if (override_level > 0 && override_level <= GetLevel()) + return true; + if (item->CheckClass(GetAdventureClass(), GetTradeskillClass())) + if (item->CheckLevel(GetAdventureClass(), GetTradeskillClass(), GetLevel())) + return true; + else + client->Message(CHANNEL_COLOR_RED, "You must be at least level %u to equip %s.", item->generic_info.adventure_default_level, item->CreateItemLink(client->GetVersion()).c_str()); + else + client->Message(CHANNEL_COLOR_RED, "Your class may not equip %s.", item->CreateItemLink(client->GetVersion()).c_str()); + } + else { + Skill* firstSkill = master_skill_list.GetSkill(item->generic_info.skill_req1); + Skill* secondSkill = master_skill_list.GetSkill(item->generic_info.skill_req2); + std::string msg(""); + if(GetClient()->GetAdminStatus() >= 200) { + if(firstSkill && !skill_list.HasSkill(item->generic_info.skill_req1)) { + msg += "(" + std::string(firstSkill->name.data.c_str()); + } + + if(secondSkill && !skill_list.HasSkill(item->generic_info.skill_req2)) { + if(msg.length() > 0) { + msg += ", "; + } + else { + msg = "("; + } + msg += std::string(secondSkill->name.data.c_str()); + } + + if(msg.length() > 0) { + msg += ") "; + } + } + client->Message(0, "You lack the skill %srequired to equip this item.",msg.c_str()); + } + } + else + client->Message(0, "Item %s isn't equipable.", item->name.c_str()); + } + } + return false; +} + +vector Player::EquipItem(int16 index, int16 version, int8 appearance_type, int8 slot_id) { + + EquipmentItemList* equipList = &equipment_list; + if(appearance_type) + equipList = &appearance_equipment_list; + + vector packets; + item_list.MPlayerItems.readlock(__FUNCTION__, __LINE__); + if (item_list.indexed_items.count(index) == 0) { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + Item* item = item_list.indexed_items[index]; + int8 orig_slot_id = slot_id; + int8 slot = 255; + if (item) { + if(orig_slot_id == 255 && item->CheckFlag2(APPEARANCE_ONLY)) { + appearance_type = 1; + equipList = &appearance_equipment_list; + } + if (slot_id != 255 && !item->HasSlot(slot_id)) { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + slot = equipList->GetFreeSlot(item, slot_id, version); + + bool canEquip = CanEquipItem(item,slot); + int32 conflictSlot = 0; + + if(canEquip && !appearance_type && item->CheckFlag2(APPEARANCE_ONLY)) + { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + if(GetClient()) { + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "This item is for appearance slots only."); + } + return packets; + } + else if(canEquip && (conflictSlot = equipList->CheckSlotConflict(item)) > 0) { + bool abort = true; + switch(conflictSlot) { + case LORE: + if(GetClient()) + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "Lore conflict, cannot equip this item."); + break; + case LORE_EQUIP: + if(GetClient()) + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "You already have this item equipped, you cannot equip another."); + break; + case STACK_LORE: + if(GetClient()) + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "Cannot equip as it exceeds lore stack."); + break; + default: + abort = false; + break; + } + if(abort) { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + } + else if (canEquip && item->CheckFlag(ATTUNEABLE)) { + PacketStruct* packet = configReader.getStruct("WS_ChoiceWindow", version); + char text[255]; + sprintf(text, "%s must be attuned before it can be equipped. Would you like to attune it now?", item->name.c_str()); + char accept_command[25]; + sprintf(accept_command, "attune_inv %i 1 0 -1", index); + packet->setDataByName("text", text); + packet->setDataByName("accept_text", "Attune"); + packet->setDataByName("accept_command", accept_command); + packet->setDataByName("cancel_text", "Cancel"); + // No clue if we even need the following 2 unknowns, just added them so the packet matches what live sends + packet->setDataByName("max_length", 50); + packet->setDataByName("unknown4", 1); + packets.push_back(packet->serialize()); + safe_delete(packet); + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + if (canEquip && slot == 255) + { + if (slot_id == 255) { + if(item->slot_data.size() > 0) { + slot = item->slot_data.at(0); + if(!IsAllowedCombatEquip(slot, true)) { + LogWrite(PLAYER__ERROR, 0, "Player", "Attempt to equip item %s (%u) with FAILED in combat!", item->name.c_str(), item->details.item_id); + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + } + else { + LogWrite(PLAYER__ERROR, 0, "Player", "Attempt to equip item %s (%u) with auto equip FAILED, no slot_data exists! Check items table, 'slots' column value should not be 0.", item->name.c_str(), item->details.item_id); + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + } + else + slot = slot_id; + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + packets = UnequipItem(slot, item->details.inv_slot_id, item->details.slot_id, version, appearance_type, false); + // grab player items lock again and assure item still present + item_list.MPlayerItems.readlock(__FUNCTION__, __LINE__); + if (item_list.indexed_items.count(index) == 0) { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + // If item is a 2handed weapon and something is in the secondary, unequip the secondary + if (item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && equipList->GetItem(EQ2_SECONDARY_SLOT) != 0) { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + vector tmp_packets = UnequipItem(EQ2_SECONDARY_SLOT, -999, 0, version, appearance_type, false); + //packets.reserve(packets.size() + tmp_packets.size()); + packets.insert(packets.end(), tmp_packets.begin(), tmp_packets.end()); + } + else { + // release for delete item / scripting etc + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + } + else if (canEquip && slot < 255) { + + if(!IsAllowedCombatEquip(slot, true)) { + LogWrite(PLAYER__ERROR, 0, "Player", "Attempt to equip item %s (%u) with auto equip FAILED in combat!", item->name.c_str(), item->details.item_id); + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return packets; + } + // If item is a 2handed weapon and something is in the secondary, unequip the secondary + if (item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && equipList->GetItem(EQ2_SECONDARY_SLOT) != 0) { + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + vector tmp_packets = UnequipItem(EQ2_SECONDARY_SLOT, -999, 0, version, appearance_type, false); + //packets.reserve(packets.size() + tmp_packets.size()); + packets.insert(packets.end(), tmp_packets.begin(), tmp_packets.end()); + } + else { + // release for delete item / scripting etc + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + + database.DeleteItem(GetCharacterID(), item, "NOT-EQUIPPED"); + + if (item->GetItemScript() && lua_interface) + lua_interface->RunItemScript(item->GetItemScript(), "equipped", item, this); + + if(!item->IsBag()) { + item_list.RemoveItem(item); + } + equipList->SetItem(slot, item); + item->save_needed = true; + packets.push_back(item->serialize(version, false)); + SetEquipment(item); + const char* zone_script = world.GetZoneScript(GetZone()->GetZoneID()); + if (zone_script && lua_interface) + lua_interface->RunZoneScript(zone_script, "item_equipped", GetZone(), this, item->details.item_id, item->name.c_str(), 0, item->details.unique_id); + sint32 bag_id = item->details.inv_slot_id; + if (item->generic_info.condition == 0) { + Client* client = GetClient(); + if (client) { + string popup_text = "Your "; + string popup_item = item->CreateItemLink(client->GetVersion(), true).c_str(); + string popup_textcont = " is worn out and will not be effective until repaired."; + popup_text.append(popup_item); + popup_text.append(popup_textcont); + //devn00b: decided to use "crimson" for the color. (220,20,60 rgb) + client->SendPopupMessage(10, popup_text.c_str(), "", 5, 0xDC, 0x14, 0x3C); + client->Message(CHANNEL_COLOR_RED, "Your %s is worn out and will not be effective until repaired.", item->CreateItemLink(client->GetVersion(), true).c_str()); + } + } + SetEquippedItemAppearances(); + packets.push_back(equipList->serialize(version, this)); + EQ2Packet* outapp = item_list.serialize(this, version); + if (outapp) { + packets.push_back(outapp); + EQ2Packet* bag_packet = SendBagUpdate(bag_id, version); + if (bag_packet) + packets.push_back(bag_packet); + } + SetCharSheetChanged(true); + } + else { + // clear items lock + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + } + else { + // clear items lock + item_list.MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + + if(slot < 255) { + if (slot == EQ2_FOOD_SLOT && item->IsFoodFood() && get_character_flag(CF_FOOD_AUTO_CONSUME)) { + Item* item = GetEquipmentList()->GetItem(EQ2_FOOD_SLOT); + if(item && GetClient() && GetClient()->CheckConsumptionAllowed(slot, false)) + GetClient()->ConsumeFoodDrink(item, EQ2_FOOD_SLOT); + + if(item) + SetActiveFoodUniqueID(item->details.unique_id); + } + else if (slot == EQ2_DRINK_SLOT && item->IsFoodDrink() && get_character_flag(CF_DRINK_AUTO_CONSUME)) { + Item* item = GetEquipmentList()->GetItem(EQ2_DRINK_SLOT); + if(item && GetClient() && GetClient()->CheckConsumptionAllowed(slot, false)) + GetClient()->ConsumeFoodDrink(item, EQ2_DRINK_SLOT); + + if(item) + SetActiveDrinkUniqueID(item->details.unique_id); + } + } + + client->UpdateSentSpellList(); + client->ClearSentSpellList(); + + return packets; +} +bool Player::AddItem(Item* item, AddItemType type) { + int32 conflictItemList = 0, conflictequipmentList = 0, conflictAppearanceEquipmentList = 0; + int16 lore_stack_count = 0; + if (item && item->details.item_id > 0) { + if( ((conflictItemList = item_list.CheckSlotConflict(item, true, true, &lore_stack_count)) == LORE || + (conflictequipmentList = equipment_list.CheckSlotConflict(item, true, &lore_stack_count)) == LORE || + (conflictAppearanceEquipmentList = appearance_equipment_list.CheckSlotConflict(item, true, &lore_stack_count)) == LORE) && !item->CheckFlag(STACK_LORE)) { + + switch(type) + { + case AddItemType::BUY_FROM_BROKER: + client->Message(CHANNEL_COLOR_CHAT_RELATIONSHIP, "You already own this item and cannot have another."); + break; + default: + client->Message(CHANNEL_COLOR_CHAT_RELATIONSHIP, "You cannot obtain %s due to lore conflict.", item->name.c_str()); + break; + } + safe_delete(item); + return false; + } + else if(conflictItemList == STACK_LORE || conflictequipmentList == STACK_LORE || + conflictAppearanceEquipmentList == STACK_LORE) { + switch(type) + { + default: + client->Message(CHANNEL_COLOR_CHAT_RELATIONSHIP, "You already have one stack of the LORE item: %s.", item->name.c_str()); + break; + } + safe_delete(item); + return false; + } + else if (item_list.AssignItemToFreeSlot(item, true)) { + item->save_needed = true; + CalculateApplyWeight(); + return true; + } + else if (item_list.AddOverflowItem(item)) { + CalculateApplyWeight(); + return true; + } + } + return false; +} +bool Player::AddItemToBank(Item* item) { + + if (item && item->details.item_id > 0) { + + sint32 bag = -3; + sint16 slot = -1; + if (item_list.GetFirstFreeBankSlot(&bag, &slot)) { + item->details.inv_slot_id = bag; + item->details.slot_id = slot; + item->save_needed = true; + + return item_list.AddItem(item); + } + else if (item_list.AddOverflowItem(item)) + return true; + } + return false; +} +EQ2Packet* Player::SendInventoryUpdate(int16 version) { + // assure any inventory updates are reflected in sell window + if(GetClient() && GetClient()->GetMerchantTransactionID()) + GetClient()->SendSellMerchantList(); + + return item_list.serialize(this, version); +} + +void Player::UpdateInventory(int32 bag_id) { + + EQ2Packet* outapp = client->GetPlayer()->SendInventoryUpdate(client->GetVersion()); + client->QueuePacket(outapp); + + outapp = client->GetPlayer()->SendBagUpdate(bag_id, client->GetVersion()); + + if (outapp) + client->QueuePacket(outapp); + +} +EQ2Packet* Player::MoveInventoryItem(sint32 to_bag_id, int16 from_index, int8 new_slot, int8 charges, int8 appearance_type, bool* item_deleted, int16 version) { + + Item* item = item_list.GetItemFromIndex(from_index); + bool isOverflow = ((item != nullptr) && (item->details.inv_slot_id == InventorySlotType::OVERFLOW)); + int8 result = item_list.MoveItem(to_bag_id, from_index, new_slot, appearance_type, charges); + if (result == 1) { + if(isOverflow && item->details.inv_slot_id != -2) { + item_list.RemoveOverflowItem(item); + } + if (item) { + if (!item->needs_deletion) + item->save_needed = true; + else if (item->needs_deletion) { + database.DeleteItem(GetCharacterID(), item, 0); + client->GetPlayer()->item_list.DestroyItem(from_index); + client->GetPlayer()->UpdateInventory(to_bag_id); + if(item_deleted) + *item_deleted = true; + } + } + return item_list.serialize(this, version); + } + else { + PacketStruct* packet = configReader.getStruct("WS_DisplayText", version); + if (packet) { + packet->setDataByName("color", CHANNEL_COLOR_YELLOW); + packet->setMediumStringByName("text", "Could not move item to that location."); + packet->setDataByName("unknown02", 0x00ff); + EQ2Packet* outapp = packet->serialize(); + safe_delete(packet); + return outapp; + } + } + return 0; +} + +int32 Player::GetCoinsCopper(){ + return GetInfoStruct()->get_coin_copper(); +} + +int32 Player::GetCoinsSilver(){ + return GetInfoStruct()->get_coin_silver(); +} + +int32 Player::GetCoinsGold(){ + return GetInfoStruct()->get_coin_gold(); +} + +int32 Player::GetCoinsPlat(){ + return GetInfoStruct()->get_coin_plat(); +} + +int32 Player::GetBankCoinsCopper(){ + return GetInfoStruct()->get_bank_coin_copper(); +} + +int32 Player::GetBankCoinsSilver(){ + return GetInfoStruct()->get_bank_coin_silver(); +} + +int32 Player::GetBankCoinsGold(){ + return GetInfoStruct()->get_bank_coin_gold(); +} + +int32 Player::GetBankCoinsPlat(){ + return GetInfoStruct()->get_bank_coin_plat(); +} + +int32 Player::GetStatusPoints(){ + return GetInfoStruct()->get_status_points(); +} + +vector* Player::GetQuickbar(){ + return &quickbar_items; +} + +bool Player::UpdateQuickbarNeeded(){ + return quickbar_updated; +} + +void Player::ResetQuickbarNeeded(){ + quickbar_updated = false; +} + +void Player::AddQuickbarItem(int32 bar, int32 slot, int32 type, int16 icon, int16 icon_type, int32 id, int8 tier, int32 unique_id, const char* text, bool update){ + RemoveQuickbarItem(bar, slot, false); + QuickBarItem* ability = new QuickBarItem; + ability->deleted = false; + ability->hotbar = bar; + ability->slot = slot; + ability->type = type; + ability->icon = icon; + ability->tier = tier; + ability->icon_type = icon_type; + ability->id = id; + if(unique_id == 0) + unique_id = database.NextUniqueHotbarID(); + ability->unique_id = unique_id; + if(type == QUICKBAR_TEXT_CMD && text){ + ability->text.data = string(text); + ability->text.size = ability->text.data.length(); + } + else + ability->text.size = 0; + quickbar_items.push_back(ability); + if(update) + quickbar_updated = true; +} + +void Player::RemoveQuickbarItem(int32 bar, int32 slot, bool update){ + vector::iterator itr; + QuickBarItem* qbi = 0; + for(itr=quickbar_items.begin();itr!=quickbar_items.end();itr++){ + qbi = *itr; + if(qbi && qbi->deleted == false && qbi->hotbar == bar && qbi->slot == slot){ + qbi->deleted = true; + break; + } + } + if(update) + quickbar_updated = true; +} + +void Player::ClearQuickbarItems(){ + quickbar_items.clear(); +} + +EQ2Packet* Player::GetQuickbarPacket(int16 version){ + PacketStruct* packet = configReader.getStruct("WS_QuickBarInit", version); + if(packet){ + vector::iterator itr; + packet->setArrayLengthByName("num_abilities", quickbar_items.size()); + int16 i=0; + for(itr=quickbar_items.begin();itr != quickbar_items.end(); itr++){ + QuickBarItem* ability = *itr; + if(!ability || ability->deleted) + continue; + packet->setArrayDataByName("hotbar", ability->hotbar, i); + packet->setArrayDataByName("slot", ability->slot, i); + packet->setArrayDataByName("type", ability->type, i); + packet->setArrayDataByName("icon", ability->icon, i); + packet->setArrayDataByName("icon_type", ability->icon_type, i); + packet->setArrayDataByName("id", ability->id, i); + packet->setArrayDataByName("unique_id", ability->tier, i); + packet->setArrayDataByName("text", &ability->text, i); + i++; + } + EQ2Packet* app = packet->serialize(); + safe_delete(packet); + return app; + } + return 0; +} + +void Player::AddSpellBookEntry(int32 spell_id, int8 tier, sint32 slot, int32 type, int32 timer, bool save_needed){ + SpellBookEntry* spell = new SpellBookEntry; + spell->status = 169; + spell->slot = slot; + spell->spell_id = spell_id; + spell->type = type; + spell->tier = tier; + spell->timer = timer; + spell->save_needed = save_needed; + spell->recast = 0; + spell->recast_available = 0; + spell->player = this; + spell->visible = true; + spell->in_use = false; + spell->in_remiss = false; + MSpellsBook.lock(); + spells.push_back(spell); + MSpellsBook.unlock(); + + if (type == SPELL_BOOK_TYPE_NOT_SHOWN) + AddPassiveSpell(spell_id, tier); +} + +void Player::DeleteSpellBook(int8 type_selection){ + MSpellsBook.lock(); + vector::iterator itr; + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end();){ + spell = *itr; + if((type_selection & DELETE_TRADESKILLS) == 0 && spell->type == SPELL_BOOK_TYPE_TRADESKILL) { + itr++; + continue; + } + else if((type_selection & DELETE_SPELLS) == 0 && spell->type == SPELL_BOOK_TYPE_SPELL) { + itr++; + continue; + } + else if((type_selection & DELETE_COMBAT_ART) == 0 && spell->type == SPELL_BOOK_TYPE_COMBAT_ART) { + itr++; + continue; + } + else if((type_selection & DELETE_ABILITY) == 0 && spell->type == SPELL_BOOK_TYPE_ABILITY) { + itr++; + continue; + } + else if((type_selection & DELETE_NOT_SHOWN) == 0 && spell->type == SPELL_BOOK_TYPE_NOT_SHOWN) { + itr++; + continue; + } + database.DeleteCharacterSpell(GetCharacterID(), spell->spell_id); + if (spell->type == SPELL_BOOK_TYPE_NOT_SHOWN) + RemovePassive(spell->spell_id, spell->tier, true); + itr = spells.erase(itr); + } + MSpellsBook.unlock(); +} + +void Player::RemoveSpellBookEntry(int32 spell_id, bool remove_passives_from_list){ + MSpellsBook.lock(); + vector::iterator itr; + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = *itr; + if(spell->spell_id == spell_id){ + if (spell->type == SPELL_BOOK_TYPE_NOT_SHOWN) + RemovePassive(spell->spell_id, spell->tier, remove_passives_from_list); + spells.erase(itr); + break; + } + } + MSpellsBook.unlock(); +} + +void Player::ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 maxlvl_only, int32 book_type) +{ + //sort_by : 0 - alpha, 1 - level, 2 - category + //order : 0 - ascending, 1 - descending + //pattern : 0 - zigzag, 1 - down, 2 - across + MSpellsBook.lock(); + + std::vector sort_spells(spells); + + if (!maxlvl_only) + { + switch (sort_by) + { + case 0: + if (!order) + stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByName); + else + stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByNameReverse); + break; + case 1: + if (!order) + stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByLevel); + else + stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByLevelReverse); + break; + case 2: + if (!order) + stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByCategory); + else + stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByCategoryReverse); + break; + } + } + + vector::iterator itr; + SpellBookEntry* spell = 0; + map tmpSpells; + vector resultSpells; + + int32 i = 0; + int8 page_book_count = 0; + int32 last_start_point = 0; + + for (itr = sort_spells.begin(); itr != sort_spells.end(); itr++) { + spell = *itr; + + if (spell->type != book_type) + continue; + + if (maxlvl_only) + { + Spell* actual_spell = 0; + actual_spell = master_spell_list.GetSpell(spell->spell_id, spell->tier); + if(!actual_spell) { + // we have a spell that doesn't exist here! + continue; + } + std::regex re("^(.*?)(\\s(I{1,}[VX]{0,}|V{1,}[IVX]{0,})|X{1,}[IVX]{0,})$"); + std::string output = std::regex_replace(string(actual_spell->GetName()), re, "$1", std::regex_constants::format_no_copy); + + if ( output.size() < 1 ) + output = string(actual_spell->GetName()); + + map::iterator tmpItr = tmpSpells.find(output); + if (tmpItr != tmpSpells.end()) + { + Spell* tmpSpell = master_spell_list.GetSpell(tmpItr->second->spell_id, tmpItr->second->tier); + if (actual_spell->GetLevelRequired(this) > tmpSpell->GetLevelRequired(this)) + { + tmpItr->second->visible = false; + tmpItr->second->slot = 0xFFFF; + + std::vector::iterator it; + it = find(resultSpells.begin(), resultSpells.end(), (SpellBookEntry*)tmpItr->second); + if (it != resultSpells.end()) + resultSpells.erase(it); + + tmpSpells.erase(tmpItr); + } + else + continue; // leave as-is we have the newer spell + } + + spell->visible = true; + tmpSpells.insert(make_pair(output, spell)); + resultSpells.push_back(spell); + } + spell->slot = i; + + GetSpellBookSlotSort(pattern, &i, &page_book_count, &last_start_point); + } // end for loop for setting slots + + if (maxlvl_only) + { + switch (sort_by) + { + case 0: + if (!order) + stable_sort(resultSpells.begin(), resultSpells.end(), SortSpellEntryByName); + else + stable_sort(resultSpells.begin(), resultSpells.end(), SortSpellEntryByNameReverse); + break; + case 1: + if (!order) + stable_sort(resultSpells.begin(), resultSpells.end(), SortSpellEntryByLevel); + else + stable_sort(resultSpells.begin(), resultSpells.end(), SortSpellEntryByLevelReverse); + break; + case 2: + if (!order) + stable_sort(resultSpells.begin(), resultSpells.end(), SortSpellEntryByCategory); + else + stable_sort(resultSpells.begin(), resultSpells.end(), SortSpellEntryByCategoryReverse); + break; + } + + i = 0; + page_book_count = 0; + last_start_point = 0; + vector::iterator tmpItr; + for (tmpItr = resultSpells.begin(); tmpItr != resultSpells.end(); tmpItr++) { + ((SpellBookEntry*)*tmpItr)->slot = i; + GetSpellBookSlotSort(pattern, &i, &page_book_count, &last_start_point); + } + } + + MSpellsBook.unlock(); +} + +bool Player::SortSpellEntryByName(SpellBookEntry* s1, SpellBookEntry* s2) +{ + Spell* spell1 = master_spell_list.GetSpell(s1->spell_id, s1->tier); + Spell* spell2 = master_spell_list.GetSpell(s2->spell_id, s2->tier); + + if (!spell1 || !spell2) + return false; + + return (string(spell1->GetName()) < string(spell2->GetName())); +} + +bool Player::SortSpellEntryByCategory(SpellBookEntry* s1, SpellBookEntry* s2) +{ + Spell* spell1 = master_spell_list.GetSpell(s1->spell_id, s1->tier); + Spell* spell2 = master_spell_list.GetSpell(s2->spell_id, s2->tier); + + if (!spell1 || !spell2) + return false; + + return (spell1->GetSpellIconBackdrop() < spell2->GetSpellIconBackdrop()); +} + +bool Player::SortSpellEntryByLevel(SpellBookEntry* s1, SpellBookEntry* s2) +{ + Spell* spell1 = master_spell_list.GetSpell(s1->spell_id, s1->tier); + Spell* spell2 = master_spell_list.GetSpell(s2->spell_id, s2->tier); + + if (!spell1 || !spell2) + return false; + + int16 lvl1 = spell1->GetLevelRequired(s1->player); + int16 lvl2 = spell2->GetLevelRequired(s2->player); + if (lvl1 == 0xFFFF) + lvl1 = 0; + if (lvl2 == 0xFFFF) + lvl2 = 0; + + return (lvl1 < lvl2); +} + +bool Player::SortSpellEntryByNameReverse(SpellBookEntry* s1, SpellBookEntry* s2) +{ + Spell* spell1 = master_spell_list.GetSpell(s1->spell_id, s1->tier); + Spell* spell2 = master_spell_list.GetSpell(s2->spell_id, s2->tier); + + if (!spell1 || !spell2) + return false; + + return (string(spell2->GetName()) < string(spell1->GetName())); +} + +bool Player::SortSpellEntryByCategoryReverse(SpellBookEntry* s1, SpellBookEntry* s2) +{ + Spell* spell1 = master_spell_list.GetSpell(s1->spell_id, s1->tier); + Spell* spell2 = master_spell_list.GetSpell(s2->spell_id, s2->tier); + if (!spell1 || !spell2) + return false; + return (spell2->GetSpellIconBackdrop() < spell1->GetSpellIconBackdrop()); +} + +bool Player::SortSpellEntryByLevelReverse(SpellBookEntry* s1, SpellBookEntry* s2) +{ + Spell* spell1 = master_spell_list.GetSpell(s1->spell_id, s1->tier); + Spell* spell2 = master_spell_list.GetSpell(s2->spell_id, s2->tier); + + if (!spell1 || !spell2) + return false; + + int16 lvl1 = spell1->GetLevelRequired(s1->player); + int16 lvl2 = spell2->GetLevelRequired(s2->player); + if (lvl1 == 0xFFFF) + lvl1 = 0; + if (lvl2 == 0xFFFF) + lvl2 = 0; + + return (lvl2 < lvl1); +} + +int8 Player::GetSpellSlot(int32 spell_id){ + MSpellsBook.lock(); + vector::iterator itr; + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = *itr; + if(spell->spell_id == spell_id) + { + int8 slot = spell->slot; + MSpellsBook.unlock(); + return slot; + } + } + MSpellsBook.unlock(); + return 0; +} + +void Player::AddSkill(int32 skill_id, int16 current_val, int16 max_val, bool save_needed){ + Skill* master_skill = master_skill_list.GetSkill(skill_id); + if (master_skill) { + Skill* skill = new Skill(master_skill); + skill->current_val = current_val; + skill->previous_val = current_val; + skill->max_val = max_val; + if (save_needed) + skill->save_needed = true; + skill_list.AddSkill(skill); + } +} + +void Player::RemovePlayerSkill(int32 skill_id, bool save) { + Skill* skill = skill_list.GetSkill(skill_id); + if (skill) + RemoveSkillFromDB(skill, save); +} + +void Player::RemoveSkillFromDB(Skill* skill, bool save) { + skill_list.RemoveSkill(skill); + if (save) + database.DeleteCharacterSkill(GetCharacterID(), skill); +} + +int16 Player::GetSpellSlotMappingCount(){ + int16 ret = 0; + MSpellsBook.lock(); + for(int32 i=0;islot >= 0 && spell->spell_id > 0 && spell->type != SPELL_BOOK_TYPE_NOT_SHOWN) + ret++; + } + MSpellsBook.unlock(); + return ret; +} + +int8 Player::GetSpellTier(int32 id){ + int8 ret = 0; + MSpellsBook.lock(); + for(int32 i=0;ispell_id == id){ + ret = spell->tier; + break; + } + } + MSpellsBook.unlock(); + return ret; +} + +int16 Player::GetSpellPacketCount(){ + int16 ret = 0; + MSpellsBook.lock(); + for(int32 i=0;ispell_id > 0 && spell->type != SPELL_BOOK_TYPE_NOT_SHOWN) + ret++; + } + MSpellsBook.unlock(); + return ret; +} + +void Player::LockAllSpells() { + vector::iterator itr; + + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + if ((*itr)->type != SPELL_BOOK_TYPE_TRADESKILL) + RemoveSpellStatus((*itr), SPELL_STATUS_LOCK, false); + } + + all_spells_locked = true; + + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::UnlockAllSpells(bool modify_recast, Spell* exception) { + vector::iterator itr; + int32 exception_spell_id = 0; + if (exception) + exception_spell_id = exception->GetSpellID(); + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + MaintainedEffects* effect = 0; + if((effect = GetMaintainedSpell((*itr)->spell_id)) && effect->spell->spell->GetSpellData()->duration_until_cancel) + continue; + + if ((*itr)->in_use == false && + (((*itr)->spell_id != exception_spell_id || + (*itr)->timer > 0 && (*itr)->timer != exception->GetSpellData()->linked_timer) + && (*itr)->type != SPELL_BOOK_TYPE_TRADESKILL)) { + AddSpellStatus((*itr), SPELL_STATUS_LOCK, modify_recast); + (*itr)->recast_available = 0; + } + else if((*itr)->in_remiss) + { + AddSpellStatus((*itr), SPELL_STATUS_LOCK); + (*itr)->recast_available = 0; + (*itr)->in_remiss = false; + } + } + + all_spells_locked = false; + + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::LockSpell(Spell* spell, int16 recast) { + vector::iterator itr; + SpellBookEntry* spell2; + + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + spell2 = *itr; + if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer)) + { + spell2->in_use = true; + RemoveSpellStatus(spell2, SPELL_STATUS_LOCK, true, recast); + } + else if(spell2->in_use) + RemoveSpellStatus(spell2, SPELL_STATUS_LOCK, false, 0); + } + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::UnlockSpell(Spell* spell) { + if (spell->GetStayLocked()) + return; + vector::iterator itr; + SpellBookEntry* spell2; + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + spell2 = *itr; + if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData() && spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer)) + { + spell2->in_use = false; + spell2->recast_available = 0; + if(all_spells_locked) + spell2->in_remiss = true; + else + AddSpellStatus(spell2, SPELL_STATUS_LOCK, false); + } + } + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + + +void Player::UnlockSpell(int32 spell_id, int32 linked_timer_id) { + vector::iterator itr; + SpellBookEntry* spell2; + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + spell2 = *itr; + if (spell2->spell_id == spell_id || (linked_timer_id > 0 && linked_timer_id == spell2->timer)) + { + spell2->in_use = false; + spell2->recast_available = 0; + if(all_spells_locked) + spell2->in_remiss = true; + else + AddSpellStatus(spell2, SPELL_STATUS_LOCK, false); + } + } + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::LockTSSpells() { + vector::iterator itr; + + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + if ((*itr)->type == SPELL_BOOK_TYPE_TRADESKILL) + RemoveSpellStatus(*itr, SPELL_STATUS_LOCK); + } + + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); + // Unlock all other types + UnlockAllSpells(); +} + +void Player::UnlockTSSpells() { + vector::iterator itr; + + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + if ((*itr)->type == SPELL_BOOK_TYPE_TRADESKILL) + AddSpellStatus(*itr, SPELL_STATUS_LOCK); + } + + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); + // Lock all other types + LockAllSpells(); +} + +void Player::QueueSpell(Spell* spell) { + vector::iterator itr; + SpellBookEntry* spell2; + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + spell2 = *itr; + if (spell2->spell_id == spell->GetSpellID()) + AddSpellStatus(spell2, SPELL_STATUS_QUEUE, false); + } + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::UnQueueSpell(Spell* spell) { + vector::iterator itr; + SpellBookEntry* spell2; + MSpellsBook.writelock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + spell2 = *itr; + if (spell2->spell_id == spell->GetSpellID()) + RemoveSpellStatus(spell2, SPELL_STATUS_QUEUE, false); + } + MSpellsBook.releasewritelock(__FUNCTION__, __LINE__); +} + +vector Player::GetSpellBookSpellsByTimer(Spell* spell, int32 timerID) { + vector ret; + vector::iterator itr; + MSpellsBook.readlock(__FUNCTION__, __LINE__); + for (itr = spells.begin(); itr != spells.end(); itr++) { + if ((*itr)->timer == timerID && spell->GetSpellID() != (*itr)->spell_id) + ret.push_back(master_spell_list.GetSpell((*itr)->spell_id, (*itr)->tier)); + } + MSpellsBook.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +void Player::ModifySpellStatus(SpellBookEntry* spell, sint16 value, bool modify_recast, int16 recast) { + SetSpellEntryRecast(spell, modify_recast, recast); + if (modify_recast || spell->recast_available <= Timer::GetCurrentTime2() || value == 4) { + spell->status += value; // use set/remove spell status now + } +} + +void Player::AddSpellStatus(SpellBookEntry* spell, sint16 value, bool modify_recast, int16 recast) { + SetSpellEntryRecast(spell, modify_recast, recast); + if (modify_recast || spell->recast_available <= Timer::GetCurrentTime2() || value == 4) { + spell->status = spell->status | value; + } +} + +void Player::RemoveSpellStatus(SpellBookEntry* spell, sint16 value, bool modify_recast, int16 recast) { + SetSpellEntryRecast(spell, modify_recast, recast); + if (modify_recast || spell->recast_available <= Timer::GetCurrentTime2() || value == 4) { + spell->status = spell->status & ~value; + } +} + +void Player::SetSpellStatus(Spell* spell, int8 status){ + MSpellsBook.lock(); + vector::iterator itr; + SpellBookEntry* spell2 = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell2 = *itr; + if(spell2->spell_id == spell->GetSpellData()->id){ + spell2->status = spell2->status | status; + break; + } + } + MSpellsBook.unlock(); +} + +void Player::SetSpellEntryRecast(SpellBookEntry* spell, bool modify_recast, int16 recast) { + if (modify_recast) { + spell->recast = recast / 100; + Spell* spell_ = master_spell_list.GetSpell(spell->spell_id, spell->tier); + if(spell_) { + float override_recast = 0.0f; + if(recast > 0) { + override_recast = static_cast(recast); + } + int32 recast_time = spell_->CalculateRecastTimer(this, override_recast); + + spell->recast = recast_time / 100; + spell->recast_available = Timer::GetCurrentTime2() + recast_time; + } + else { + spell->recast_available = Timer::GetCurrentTime2() + recast; + } + } +} + +vector* Player::GetSpellsSaveNeeded(){ + vector* ret = 0; + vector::iterator itr; + MSpellsBook.lock(); + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = *itr; + if(spell->save_needed){ + if(!ret) + ret = new vector; + ret->push_back(spell); + } + } + MSpellsBook.unlock(); + return ret; +} + +int16 Player::GetTierUp(int16 tier) +{ + switch(tier) + { + case 0: + break; + case 7: + case 9: + tier -= 2; + break; + default: + tier -= 1; + break; + } + + return tier; +} +bool Player::HasSpell(int32 spell_id, int8 tier, bool include_higher_tiers, bool include_possible_scribe){ + bool ret = false; + vector::iterator itr; + MSpellsBook.lock(); + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = *itr; + if(spell->spell_id == spell_id && (tier == 255 || spell->tier == tier || (include_higher_tiers && spell->tier > tier) || (include_possible_scribe && tier <= spell->tier))){ + ret = true; + break; + } + } + MSpellsBook.unlock(); + return ret; +} + +sint32 Player::GetFreeSpellBookSlot(int32 type){ + sint32 ret = 0; + MSpellsBook.lock(); + vector::iterator itr; + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = *itr; + if(spell->type == type && spell->slot > ret) //get last slot (add 1 to it on return) + ret = spell->slot; + } + MSpellsBook.unlock(); + return ret+1; +} + +SpellBookEntry* Player::GetSpellBookSpell(int32 spell_id){ + MSpellsBook.lock(); + vector::iterator itr; + SpellBookEntry* ret = 0; + SpellBookEntry* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = *itr; + if(spell->spell_id == spell_id){ + ret = spell; + break; + } + } + MSpellsBook.unlock(); + return ret; +} + +vector Player::GetSpellBookSpellIDBySkill(int32 skill_id) { + vector ret; + + MSpellsBook.readlock(__FUNCTION__, __LINE__); + vector::iterator itr; + Spell* spell = 0; + for(itr = spells.begin(); itr != spells.end(); itr++){ + spell = master_spell_list.GetSpell((*itr)->spell_id, (*itr)->tier); + if(spell && spell->GetSpellData()->mastery_skill == skill_id) + ret.push_back(spell->GetSpellData()->id); + } + MSpellsBook.releasereadlock(__FUNCTION__, __LINE__); + + return ret; +} + + +EQ2Packet* Player::GetSpellSlotMappingPacket(int16 version){ + PacketStruct* packet = configReader.getStruct("WS_SpellSlotMapping", version); + if(packet){ + int16 count = GetSpellSlotMappingCount(); + int16 ptr = 0; + if(count > 0){ + packet->setArrayLengthByName("spell_count", count); + MSpellsBook.lock(); + for(int32 i=0;itype == SPELL_BOOK_TYPE_NOT_SHOWN || spell->slot < 0 || spell->spell_id == 0) + continue; + packet->setArrayDataByName("spell_id", spell->spell_id, ptr); + packet->setArrayDataByName("slot_id", (int16)spell->slot, ptr); + ptr++; + } + MSpellsBook.unlock(); + EQ2Packet* ret = packet->serialize(); + safe_delete(packet); + return ret; + } + safe_delete(packet); + } + return 0; +} + +EQ2Packet* Player::GetSpellBookUpdatePacket(int16 version) { + std::unique_lock lock(spell_packet_update_mutex); + PacketStruct* packet = configReader.getStruct("WS_UpdateSpellBook", version); + EQ2Packet* ret = 0; + if (packet) { + Spell* spell = 0; + SpellBookEntry* spell_entry = 0; + int16 count = GetSpellPacketCount(); + int16 ptr = 0; + // Get the packet size + PacketStruct* packet2 = configReader.getStruct("SubStruct_UpdateSpellBook", version); + int32 total_bytes = packet2->GetTotalPacketSize(); + safe_delete(packet2); + packet->setArrayLengthByName("spell_count", count); + + LogWrite(PLAYER__DEBUG, 5, "Player", "%s: GetSpellBookUpdatePacket Spell Count: %u, Spell Entry Book Size: %u", GetName(), count, total_bytes); + + if (count > 0) { + if (count > spell_count) { + uchar* tmp = 0; + if (spell_orig_packet) { + tmp = new uchar[count * total_bytes]; + memset(tmp, 0, total_bytes * count); + memcpy(tmp, spell_orig_packet, spell_count * total_bytes); + safe_delete_array(spell_orig_packet); + safe_delete_array(spell_xor_packet); + spell_orig_packet = tmp; + } + else { + spell_orig_packet = new uchar[count * total_bytes]; + memset(spell_orig_packet, 0, total_bytes * count); + } + spell_xor_packet = new uchar[count * total_bytes]; + memset(spell_xor_packet, 0, count * total_bytes); + } + spell_count = count; + MSpellsBook.lock(); + for (int32 i = 0; i < spells.size(); i++) { + spell_entry = (SpellBookEntry*)spells[i]; + if (spell_entry->spell_id == 0 || spell_entry->type == SPELL_BOOK_TYPE_NOT_SHOWN) + continue; + spell = master_spell_list.GetSpell(spell_entry->spell_id, spell_entry->tier); + if (spell) { + if (spell_entry->recast_available == 0 || Timer::GetCurrentTime2() > spell_entry->recast_available) { + packet->setSubstructArrayDataByName("spells", "available", 1, 0, ptr); + } + LogWrite(PLAYER__DEBUG, 9, "Player", "%s: GetSpellBookUpdatePacket Send Spell %u in position %u\n",GetName(), spell_entry->spell_id, ptr); + packet->setSubstructArrayDataByName("spells", "spell_id", spell_entry->spell_id, 0, ptr); + packet->setSubstructArrayDataByName("spells", "type", spell_entry->type, 0, ptr); + packet->setSubstructArrayDataByName("spells", "recast_available", spell_entry->recast_available, 0, ptr); + packet->setSubstructArrayDataByName("spells", "recast_time", spell_entry->recast, 0, ptr); + packet->setSubstructArrayDataByName("spells", "status", spell_entry->status, 0, ptr); + packet->setSubstructArrayDataByName("spells", "icon", (spell->TranslateClientSpellIcon(version) * -1) - 1, 0, ptr); + packet->setSubstructArrayDataByName("spells", "icon_type", spell->GetSpellIconBackdrop(), 0, ptr); + packet->setSubstructArrayDataByName("spells", "icon2", spell->GetSpellIconHeroicOp(), 0, ptr); + packet->setSubstructArrayDataByName("spells", "unique_id", (spell_entry->tier + 1) * -1, 0, ptr); //this is actually GetSpellNameCrc(spell->GetName()), but hijacking it for spell tier + packet->setSubstructArrayDataByName("spells", "charges", 255, 0, ptr); + // Beastlord and Channeler spell support + if (spell->GetSpellData()->savage_bar == 1) + packet->setSubstructArrayDataByName("spells", "unknown6", 32, 0, ptr); // advantages + else if (spell->GetSpellData()->savage_bar == 2) + packet->setSubstructArrayDataByName("spells", "unknown6", 64, 0, ptr); // primal + else if (spell->GetSpellData()->savage_bar == 3) { + packet->setSubstructArrayDataByName("spells", "unknown6", 6, 1, ptr); // 6 = channeler + // Slot req for channelers + // bitmask for slots 1 = slot 1, 2 = slot 2, 4 = slot 3, 8 = slot 4, 16 = slot 5, 32 = slot 6, 64 = slot 7, 128 = slot 8 + packet->setSubstructArrayDataByName("spells", "savage_bar_slot", spell->GetSpellData()->savage_bar_slot, 0, ptr); + } + + ptr++; + } + } + MSpellsBook.unlock(); + } + ret = packet->serializeCountPacket(version, 0, spell_orig_packet, spell_xor_packet); + //packet->PrintPacket(); + //DumpPacket(ret); + safe_delete(packet); + } + return ret; +} +EQ2Packet* Player::GetRaidUpdatePacket(int16 version) { + std::unique_lock lock(raid_update_mutex); + + std::vector raidGroups; + PacketStruct* packet = configReader.getStruct("WS_RaidUpdate", version); + EQ2Packet* ret = 0; + Entity* member = 0; + int8 det_count = 0; + int8 total_groups = 0; + if (packet) { + int16 ptr = 0; + // Get the packet size + PacketStruct* packet2 = configReader.getStruct("Substruct_RaidMember", version); + int32 total_bytes = packet2->GetTotalPacketSize(); + safe_delete(packet2); + world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__); + if (GetGroupMemberInfo()) { + PlayerGroup* group = world.GetGroupManager()->GetGroup(GetGroupMemberInfo()->group_id); + if (group) + { + group->GetRaidGroups(&raidGroups); + std::vector::iterator raid_itr; + int32 group_pos = 0; + for(raid_itr = raidGroups.begin(); raid_itr != raidGroups.end(); raid_itr++) { + group = world.GetGroupManager()->GetGroup((*raid_itr)); + if(!group) + continue; + total_groups++; + group->MGroupMembers.readlock(__FUNCTION__, __LINE__); + deque* members = group->GetMembers(); + deque::iterator itr; + GroupMemberInfo* info = 0; + int x = 1; + int lastpos = 1; + bool gotleader = false; + for (itr = members->begin(); itr != members->end(); itr++) { + info = *itr; + + if(!info) + continue; + + member = info->member; + + std::string prop_name("group_member"); + if(!gotleader && info->leader) { + lastpos = x; + x = 0; + gotleader = true; + } + else if(lastpos) { + x = lastpos; + lastpos = 0; + } + prop_name.append(std::to_string(x) + "_" + std::to_string(group_pos)); + x++; + if (member && member->GetZone() == GetZone()) { + packet->setSubstructDataByName(prop_name.c_str(), "spawn_id", GetIDWithPlayerSpawn(member), 0); + + if (member->HasPet()) { + if (member->GetPet()) + packet->setSubstructDataByName(prop_name.c_str(), "pet_id", GetIDWithPlayerSpawn(member->GetPet()), 0); + else + packet->setSubstructDataByName(prop_name.c_str(), "pet_id", GetIDWithPlayerSpawn(member->GetCharmedPet()), 0); + } + else + packet->setSubstructDataByName(prop_name.c_str(), "pet_id", 0xFFFFFFFF, 0); + + //Send detriment counts as 255 if all dets of that type are incurable + det_count = member->GetTraumaCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_TRAUMA)) + det_count = 255; + } + packet->setSubstructDataByName(prop_name.c_str(), "trauma_count", det_count, 0); + + det_count = member->GetArcaneCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_ARCANE)) + det_count = 255; + } + packet->setSubstructDataByName(prop_name.c_str(), "arcane_count", det_count, 0); + + det_count = member->GetNoxiousCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_NOXIOUS)) + det_count = 255; + } + packet->setSubstructDataByName(prop_name.c_str(), "noxious_count", det_count, 0); + + det_count = member->GetElementalCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_ELEMENTAL)) + det_count = 255; + } + packet->setSubstructDataByName(prop_name.c_str(), "elemental_count", det_count, 0); + + det_count = member->GetCurseCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_CURSE)) + det_count = 255; + } + packet->setSubstructDataByName(prop_name.c_str(), "curse_count", det_count, 0); + + packet->setSubstructDataByName(prop_name.c_str(), "zone_status", 1, 0); + } + else { + packet->setSubstructDataByName(prop_name.c_str(), "pet_id", 0xFFFFFFFF, 0); + //packet->setSubstructDataByName(prop_name.c_str(), "unknown5", 1, 0, 1); // unknown5 > 1 = name is blue + packet->setSubstructDataByName(prop_name.c_str(), "zone_status", 2, 0); + } + + packet->setSubstructDataByName(prop_name.c_str(), "name", info->name.c_str(), 0); + packet->setSubstructDataByName(prop_name.c_str(), "hp_current", info->hp_current, 0); + packet->setSubstructDataByName(prop_name.c_str(), "hp_max", info->hp_max, 0); + packet->setSubstructDataByName(prop_name.c_str(), "hp_current2", info->hp_current, 0); + packet->setSubstructDataByName(prop_name.c_str(), "power_current", info->power_current, 0); + packet->setSubstructDataByName(prop_name.c_str(), "power_max", info->power_max, 0); + packet->setSubstructDataByName(prop_name.c_str(), "level_current", info->level_current, 0); + packet->setSubstructDataByName(prop_name.c_str(), "level_max", info->level_max, 0); + packet->setSubstructDataByName(prop_name.c_str(), "zone", info->zone.c_str(), 0); + packet->setSubstructDataByName(prop_name.c_str(), "race_id", info->race_id, 0); + packet->setSubstructDataByName(prop_name.c_str(), "class_id", info->class_id, 0); + } + + group->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__); + group_pos += 1; + } + } + } + world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__); + //packet->PrintPacket(); + + hassent_raid = true; + string* data = packet->serializeString(); + int32 size = data->length(); + + uchar* tmp = new uchar[size]; + if(!raid_xor_packet){ + raid_orig_packet = new uchar[size]; + raid_xor_packet = new uchar[size]; + memcpy(raid_orig_packet, (uchar*)data->c_str(), size); + size = Pack(tmp, (uchar*)data->c_str(), size, size, version); + } + else{ + memcpy(raid_xor_packet, (uchar*)data->c_str(), size); + Encode(raid_xor_packet, raid_orig_packet, size); + size = Pack(tmp, raid_xor_packet, size, size, version); + } + + ret = new EQ2Packet(OP_UpdateRaidMsg, tmp, size); + safe_delete_array(tmp); + safe_delete(packet); + //DumpPacket(ret); + } + return ret; +} + +PlayerInfo::~PlayerInfo(){ + RemoveOldPackets(); +} + +PlayerInfo::PlayerInfo(Player* in_player){ + orig_packet = 0; + changes = 0; + pet_orig_packet = 0; + pet_changes = 0; + player = in_player; + info_struct = player->GetInfoStruct(); + info_struct->set_name(std::string(player->GetName())); + info_struct->set_deity(std::string("None")); + + info_struct->set_class1(classes.GetBaseClass(player->GetAdventureClass())); + info_struct->set_class2(classes.GetSecondaryBaseClass(player->GetAdventureClass())); + info_struct->set_class3(player->GetAdventureClass()); + + info_struct->set_race(player->GetRace()); + info_struct->set_gender(player->GetGender()); + info_struct->set_level(player->GetLevel()); + info_struct->set_tradeskill_level(player->GetTSLevel()); + info_struct->set_tradeskill_class1(classes.GetTSBaseClass(player->GetTradeskillClass())); + info_struct->set_tradeskill_class2(classes.GetSecondaryTSBaseClass(player->GetTradeskillClass())); + info_struct->set_tradeskill_class3(player->GetTradeskillClass()); + + for(int i=0;i<45;i++){ + if(i<30){ + info_struct->maintained_effects[i].spell_id = 0xFFFFFFFF; + info_struct->maintained_effects[i].inherited_spell_id = 0; + info_struct->maintained_effects[i].icon = 0xFFFF; + info_struct->maintained_effects[i].spell = nullptr; + } + info_struct->spell_effects[i].spell_id = 0xFFFFFFFF; + info_struct->spell_effects[i].inherited_spell_id = 0; + info_struct->spell_effects[i].icon = 0; + info_struct->spell_effects[i].icon_backdrop = 0; + info_struct->spell_effects[i].tier = 0; + info_struct->spell_effects[i].total_time = 0.0f; + info_struct->spell_effects[i].expire_timestamp = 0; + info_struct->spell_effects[i].spell = nullptr; + } + + house_zone_id = 0; + bind_zone_id = 0; + bind_x = 0; + bind_y = 0; + bind_z = 0; + bind_heading = 0; + boat_x_offset = 0; + boat_y_offset = 0; + boat_z_offset = 0; + boat_spawn = 0; +} + +MaintainedEffects* Player::GetFreeMaintainedSpellSlot(){ + MaintainedEffects* ret = 0; + InfoStruct* info = GetInfoStruct(); + GetMaintainedMutex()->readlock(__FUNCTION__, __LINE__); + for(int i=0;imaintained_effects[i].spell_id == 0xFFFFFFFF){ + ret = &info->maintained_effects[i]; + ret->spell_id = 0; + ret->slot_pos = i; + break; + } + } + GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +MaintainedEffects* Player::GetMaintainedSpell(int32 id, bool on_char_load){ + MaintainedEffects* ret = 0; + InfoStruct* info = GetInfoStruct(); + GetMaintainedMutex()->readlock(__FUNCTION__, __LINE__); + for(int i=0;imaintained_effects[i].spell_id == id || (on_char_load && info->maintained_effects[i].inherited_spell_id == id)){ + ret = &info->maintained_effects[i]; + break; + } + } + GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +MaintainedEffects* Player::GetMaintainedSpellBySlot(int8 slot){ + MaintainedEffects* ret = 0; + InfoStruct* info = GetInfoStruct(); + GetMaintainedMutex()->readlock(__FUNCTION__, __LINE__); + for(int i=0;imaintained_effects[i].slot_pos == slot){ + ret = &info->maintained_effects[i]; + break; + } + } + GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +MaintainedEffects* Player::GetMaintainedSpells() { + return GetInfoStruct()->maintained_effects; +} + +SpellEffects* Player::GetFreeSpellEffectSlot(){ + SpellEffects* ret = 0; + InfoStruct* info = GetInfoStruct(); + GetSpellEffectMutex()->readlock(__FUNCTION__, __LINE__); + for(int i=0;i<45;i++){ + if(info->spell_effects[i].spell_id == 0xFFFFFFFF){ + ret = &info->spell_effects[i]; + ret->spell_id = 0; + break; + } + } + GetSpellEffectMutex()->releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +SpellEffects* Player::GetSpellEffects() { + return GetInfoStruct()->spell_effects; +} + +// call inside info_mutex +void Player::ClearRemovalTimers(){ + map::iterator itr; + for(itr = spawn_state_list.begin(); itr != spawn_state_list.end();) { + SpawnQueueState* sr = itr->second; + itr = spawn_state_list.erase(itr); + safe_delete(sr); + } +} + +void Player::ClearEverything(){ + index_mutex.writelock(__FUNCTION__, __LINE__); + player_spawn_id_map.clear(); + player_spawn_reverse_id_map.clear(); + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + map*>::iterator itr; + m_playerSpawnQuestsRequired.writelock(__FUNCTION__, __LINE__); + for (itr = player_spawn_quests_required.begin(); itr != player_spawn_quests_required.end(); itr++){ + safe_delete(itr->second); + } + player_spawn_quests_required.clear(); + m_playerSpawnQuestsRequired.releasewritelock(__FUNCTION__, __LINE__); + + m_playerSpawnHistoryRequired.writelock(__FUNCTION__, __LINE__); + for (itr = player_spawn_history_required.begin(); itr != player_spawn_history_required.end(); itr++){ + safe_delete(itr->second); + } + player_spawn_history_required.clear(); + m_playerSpawnHistoryRequired.releasewritelock(__FUNCTION__, __LINE__); + + spawn_mutex.writelock(__FUNCTION__, __LINE__); + ClearRemovalTimers(); + spawn_packet_sent.clear(); + spawn_mutex.releasewritelock(__FUNCTION__, __LINE__); + + info_mutex.writelock(__FUNCTION__, __LINE__); + spawn_info_packet_list.clear(); + info_mutex.releasewritelock(__FUNCTION__, __LINE__); + + vis_mutex.writelock(__FUNCTION__, __LINE__); + spawn_vis_packet_list.clear(); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); + + pos_mutex.writelock(__FUNCTION__, __LINE__); + spawn_pos_packet_list.clear(); + pos_mutex.releasewritelock(__FUNCTION__, __LINE__); +} +bool Player::IsResurrecting(){ + return resurrecting; +} +void Player::SetResurrecting(bool val){ + resurrecting = val; +} +void Player::AddMaintainedSpell(LuaSpell* luaspell){ + if(!luaspell) + return; + + if(luaspell->spell->GetSpellData()->not_maintained || luaspell->spell->GetSpellData()->duration1 == 0) { + LogWrite(PLAYER__INFO, 0, "NPC", "AddMaintainedSpell Spell ID: %u, Concentration: %u disallowed, not_maintained true (%u) or duration is 0 (%u).", luaspell->spell->GetSpellData()->id, luaspell->spell->GetSpellData()->req_concentration, luaspell->spell->GetSpellData()->not_maintained, luaspell->spell->GetSpellData()->duration1); + return; + } + + Spell* spell = luaspell->spell; + MaintainedEffects* effect = GetFreeMaintainedSpellSlot(); + int32 target_type = 0; + Spawn* spawn = 0; + + if(effect && luaspell->caster && luaspell->caster->GetZone()){ + GetMaintainedMutex()->writelock(__FUNCTION__, __LINE__); + strcpy(effect->name, spell->GetSpellData()->name.data.c_str()); + effect->target = luaspell->initial_target; + + spawn = luaspell->caster->GetZone()->GetSpawnByID(luaspell->initial_target); + if (spawn){ + if (spawn == this) + target_type = 0; + else if (GetPet() == spawn || GetCharmedPet() == spawn) + target_type = 1; + else + target_type = 2; + } + effect->target_type = target_type; + + effect->spell = luaspell; + if(!luaspell->slot_pos) + luaspell->slot_pos = effect->slot_pos; + effect->spell_id = spell->GetSpellData()->id; + LogWrite(PLAYER__DEBUG, 5, "Player", "AddMaintainedSpell Spell ID: %u, req concentration: %u", spell->GetSpellData()->id, spell->GetSpellData()->req_concentration); + effect->icon = spell->GetSpellData()->icon; + effect->icon_backdrop = spell->GetSpellData()->icon_backdrop; + effect->conc_used = spell->GetSpellData()->req_concentration; + effect->total_time = spell->GetSpellDuration()/10; + effect->tier = spell->GetSpellData()->tier; + if (spell->GetSpellData()->duration_until_cancel) + effect->expire_timestamp = 0xFFFFFFFF; + else + effect->expire_timestamp = Timer::GetCurrentTime2() + (spell->GetSpellDuration()*100); + GetMaintainedMutex()->releasewritelock(__FUNCTION__, __LINE__); + charsheet_changed = true; + } +} +void Player::AddSpellEffect(LuaSpell* luaspell, int32 override_expire_time){ + if(!luaspell || !luaspell->caster) + return; + + Spell* spell = luaspell->spell; + if(spell->GetSpellData() && spell->GetSpellData()->icon == 0 && spell->GetSpellData()->duration1 == 0 && spell->GetSpellData()->duration2 == 0) + return; + + SpellEffects* old_effect = GetSpellEffect(spell->GetSpellID(), luaspell->caster); + SpellEffects* effect = 0; + if (old_effect){ + GetZone()->RemoveTargetFromSpell(old_effect->spell, this); + RemoveSpellEffect(old_effect->spell); + } + + LogWrite(SPELL__DEBUG, 0, "Spell", "%s AddSpellEffect %s (%u).", spell->GetName(), GetName(), GetID()); + + effect = GetFreeSpellEffectSlot(); + + if(effect){ + GetSpellEffectMutex()->writelock(__FUNCTION__, __LINE__); + effect->spell = luaspell; + effect->spell_id = spell->GetSpellData()->id; + effect->caster = luaspell->caster; + effect->total_time = spell->GetSpellDuration()/10; + if (spell->GetSpellData()->duration_until_cancel) + effect->expire_timestamp = 0xFFFFFFFF; + else if(override_expire_time) + effect->expire_timestamp = Timer::GetCurrentTime2() + override_expire_time; + else + effect->expire_timestamp = Timer::GetCurrentTime2() + (spell->GetSpellDuration()*100); + effect->icon = spell->GetSpellData()->icon; + effect->icon_backdrop = spell->GetSpellData()->icon_backdrop; + effect->tier = spell->GetSpellTier(); + GetSpellEffectMutex()->releasewritelock(__FUNCTION__, __LINE__); + charsheet_changed = true; + + if(luaspell->caster && luaspell->caster->IsPlayer() && luaspell->caster != this) + { + if(GetClient()) { + GetClient()->TriggerSpellSave(); + } + if(((Player*)luaspell->caster)->GetClient()) { + ((Player*)luaspell->caster)->GetClient()->TriggerSpellSave(); + } + } + } +} + +void Player::RemoveMaintainedSpell(LuaSpell* luaspell){ + if(!luaspell) + return; + + bool found = false; + Client* client = GetClient(); + LuaSpell* old_spell = 0; + LuaSpell* current_spell = 0; + GetMaintainedMutex()->writelock(__FUNCTION__, __LINE__); + for(int i=0;i<30;i++){ + // If we already found the spell then we are bumping all other up one so there are no gaps in the ui + // This check needs to be first so found can never be true on the first iteration (i = 0) + if (found) { + old_spell = GetInfoStruct()->maintained_effects[i - 1].spell; + current_spell = GetInfoStruct()->maintained_effects[i].spell; + + //Update the maintained window uses_remaining and damage_remaining values + if (current_spell && current_spell->num_triggers > 0) + ClientPacketFunctions::SendMaintainedExamineUpdate(client, i - 1, current_spell->num_triggers, 0); + else if (current_spell && current_spell->damage_remaining > 0) + ClientPacketFunctions::SendMaintainedExamineUpdate(client, i - 1, current_spell->damage_remaining, 1); + else if (old_spell && old_spell->had_triggers) + ClientPacketFunctions::SendMaintainedExamineUpdate(client, i - 1, 0, 0); + else if (old_spell && old_spell->had_dmg_remaining) + ClientPacketFunctions::SendMaintainedExamineUpdate(client, i - 1, 0, 1); + + + GetInfoStruct()->maintained_effects[i].slot_pos = i - 1; + GetInfoStruct()->maintained_effects[i - 1] = GetInfoStruct()->maintained_effects[i]; + if (current_spell) + current_spell->slot_pos = i - 1; + } + // Compare spells, if we found a match set the found flag + if(GetInfoStruct()->maintained_effects[i].spell == luaspell) + found = true; + } + // if we found the spell in the array then we need to flag the char sheet as changed and set the last element to empty + if (found) { + memset(&GetInfoStruct()->maintained_effects[29], 0, sizeof(MaintainedEffects)); + GetInfoStruct()->maintained_effects[29].spell_id = 0xFFFFFFFF; + GetInfoStruct()->maintained_effects[29].inherited_spell_id = 0; + GetInfoStruct()->maintained_effects[29].icon = 0xFFFF; + GetInfoStruct()->maintained_effects[29].spell = nullptr; + charsheet_changed = true; + } + GetMaintainedMutex()->releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::RemoveSpellEffect(LuaSpell* spell){ + bool found = false; + GetSpellEffectMutex()->writelock(__FUNCTION__, __LINE__); + for(int i=0;i<45;i++){ + if (found) { + GetInfoStruct()->spell_effects[i-1] = GetInfoStruct()->spell_effects[i]; + } + if(GetInfoStruct()->spell_effects[i].spell == spell) + found = true; + } + if (found) { + memset(&GetInfoStruct()->spell_effects[44], 0, sizeof(SpellEffects)); + GetInfoStruct()->spell_effects[44].spell_id = 0xFFFFFFFF; + GetInfoStruct()->spell_effects[44].inherited_spell_id = 0; + GetInfoStruct()->spell_effects[44].spell = nullptr; + changed = true; + info_changed = true; + AddChangedZoneSpawn(); + charsheet_changed = true; + } + GetSpellEffectMutex()->releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::PrepareIncomingMovementPacket(int32 len, uchar* data, int16 version, bool dead_window_sent) +{ + if((GetClient() && GetClient()->IsReloadingZone()) || dead_window_sent) + return; + + LogWrite(PLAYER__DEBUG, 7, "Player", "Enter: %s", __FUNCTION__); // trace + + // XML structs may be to slow to use in this portion of the code as a single + // client sends a LOT of these packets when they are moving. I have commented + // out all the code for xml structs, to switch to it just uncomment + // the code and comment the 2 if/else if/else blocks, both have a comment + // above them to let you know wich ones they are. + + //PacketStruct* update = configReader.getStruct("WS_PlayerPosUpdate", version); + int16 total_bytes; // = update->GetTotalPacketSize(); + + // Comment out this if/else if/else block if you switch to xml structs + if (version >= 1144) + total_bytes = sizeof(Player_Update1144); + else if (version >= 1096) + total_bytes = sizeof(Player_Update1096); + else if (version <= 373) + total_bytes = sizeof(Player_Update283); + else + total_bytes = sizeof(Player_Update); + + if (!movement_packet) + movement_packet = new uchar[total_bytes]; + else if (!old_movement_packet) + old_movement_packet = new uchar[total_bytes]; + if (movement_packet && old_movement_packet) + memcpy(old_movement_packet, movement_packet, total_bytes); + bool reverse = version > 373; + Unpack(len, data, movement_packet, total_bytes, 0, reverse); + if (!movement_packet || !old_movement_packet) + return; + Decode(movement_packet, old_movement_packet, total_bytes); + + //update->LoadPacketData(movement_packet, total_bytes); + + int32 activity; // = update->getType_int32_ByName("activity"); + int32 grid_id; // = update->getType_int32_ByName("grid_location"); + float direction1; // = update->getType_float_ByName("direction1"); + float direction2; // = update->getType_float_ByName("direction2");; + float speed; // = update->getType_float_ByName("speed");; + float side_speed; + float vert_speed; + float x; // = update->getType_float_ByName("x");; + float y; // = update->getType_float_ByName("y");; + float z; // = update->getType_float_ByName("z");; + float x_speed; + float y_speed; + float z_speed; + float client_pitch; + + // comment out this if/else if/else block if you use xml structs + if (version >= 1144) { + Player_Update1144* update = (Player_Update1144*)movement_packet; + activity = update->activity; + grid_id = update->grid_location; + direction1 = update->direction1; + direction2 = update->direction2; + speed = update->speed; + side_speed = update->side_speed; + vert_speed = update->vert_speed; + x = update->x; + y = update->y; + z = update->z; + x_speed = update->speed_x; + y_speed = update->speed_y; + z_speed = update->speed_z; + client_pitch = update->pitch; + + SetPitch(180 + update->pitch); + } + else if (version >= 1096) { + Player_Update1096* update = (Player_Update1096*)movement_packet; + activity = update->activity; + grid_id = update->grid_location; + direction1 = update->direction1; + direction2 = update->direction2; + speed = update->speed; + side_speed = update->side_speed; + vert_speed = update->vert_speed; + x = update->x; + y = update->y; + z = update->z; + x_speed = update->speed_x; + y_speed = update->speed_y; + z_speed = update->speed_z; + client_pitch = update->pitch; + + SetPitch(180 + update->pitch); + } + else if (version <= 373) { + Player_Update283* update = (Player_Update283*)movement_packet; + activity = update->activity; + grid_id = update->grid_location; + direction1 = update->direction1; + direction2 = update->direction2; + speed = update->speed; + side_speed = update->side_speed; + vert_speed = update->vert_speed; + client_pitch = update->pitch; + + x = update->x; + y = update->y; + z = update->z; + x_speed = update->speed_x; + y_speed = update->speed_y; + z_speed = update->speed_z; + appearance.pos.X2 = update->orig_x; + appearance.pos.Y2 = update->orig_y; + appearance.pos.Z2 = update->orig_z; + appearance.pos.X3 = update->orig_x2; + appearance.pos.Y3 = update->orig_y2; + appearance.pos.Z3 = update->orig_z2; + if (update->pitch != 0) + SetPitch(180 + update->pitch); + } + else { + Player_Update* update = (Player_Update*)movement_packet; + activity = update->activity; + grid_id = update->grid_location; + direction1 = update->direction1; + direction2 = update->direction2; + speed = update->speed; + side_speed = update->side_speed; + vert_speed = update->vert_speed; + x = update->x; + y = update->y; + z = update->z; + x_speed = update->speed_x; + y_speed = update->speed_y; + z_speed = update->speed_z; + appearance.pos.X2 = update->orig_x; + appearance.pos.Y2 = update->orig_y; + appearance.pos.Z2 = update->orig_z; + appearance.pos.X3 = update->orig_x2; + appearance.pos.Y3 = update->orig_y2; + appearance.pos.Z3 = update->orig_z2; + client_pitch = update->pitch; + + SetPitch(180 + update->pitch); + } + + SetHeading((sint16)(direction1 * 64), (sint16)(direction2 * 64)); + + if (activity != last_movement_activity) { + switch(activity) { + case UPDATE_ACTIVITY_RUNNING: + case UPDATE_ACTIVITY_RUNNING_AOM: + case UPDATE_ACTIVITY_IN_WATER_ABOVE: + case UPDATE_ACTIVITY_IN_WATER_BELOW: + case UPDATE_ACTIVITY_MOVE_WATER_ABOVE_AOM: + case UPDATE_ACTIVITY_MOVE_WATER_BELOW_AOM: { + if(GetZone() && GetZone()->GetDrowningVictim(this)) + GetZone()->RemoveDrowningVictim(this); + + break; + } + case UPDATE_ACTIVITY_DROWNING: + case UPDATE_ACTIVITY_DROWNING2: + case UPDATE_ACTIVITY_DROWNING_AOM: + case UPDATE_ACTIVITY_DROWNING2_AOM: { + if(GetZone() && !GetInvulnerable()) { + GetZone()->AddDrowningVictim(this); + } + break; + } + case UPDATE_ACTIVITY_JUMPING: + case UPDATE_ACTIVITY_JUMPING_AOM: + case UPDATE_ACTIVITY_FALLING: + case UPDATE_ACTIVITY_FALLING_AOM: { + if(IsCasting()) { + GetZone()->Interrupted(this, 0, SPELL_ERROR_INTERRUPTED, false, true); + } + if(GetInitialState() != 1024) { + SetInitialState(1024); + } + else if(GetInitialState() == 1024) { + if(activity == UPDATE_ACTIVITY_JUMPING_AOM) { + SetInitialState(UPDATE_ACTIVITY_JUMPING_AOM); + } + else { + SetInitialState(16512); + } + } + break; + } + } + + last_movement_activity = activity; + } + //Player is riding a lift, update lift XYZ offsets and the lift's spawn pointer + if (activity & UPDATE_ACTIVITY_RIDING_BOAT) { + Spawn* boat = 0; + + float boat_x = x; + float boat_y = y; + float boat_z = z; + + if (GetBoatSpawn() == 0 && GetZone()) { + boat = GetZone()->GetClosestTransportSpawn(GetX(), GetY(), GetZ()); + SetBoatSpawn(boat); + if(boat) + { + LogWrite(PLAYER__DEBUG, 0, "Player", "Set Player %s (%u) on Boat: %s", + GetName(), GetCharacterID(), boat ? boat->GetName() : "notset"); + boat->AddRailPassenger(GetCharacterID()); + GetZone()->CallSpawnScript(boat, SPAWN_SCRIPT_BOARD, this); + } + } + + if (boat || (GetBoatSpawn() && GetZone())) { + if (!boat) + boat = GetZone()->GetSpawnByID(GetBoatSpawn()); + + if (boat && boat->IsWidget() && ((Widget*)boat)->GetMultiFloorLift()) { + boat_x -= boat->GetX(); + boat_y -= boat->GetY(); + boat_z -= boat->GetZ(); + } + } + + SetBoatX(boat_x); + SetBoatY(boat_y); + SetBoatZ(boat_z); + pos_packet_speed = speed; + grid_id = GetLocation(); + } + else if (GetBoatSpawn() > 0 && !lift_cooldown.Enabled()) + { + lift_cooldown.Start(100, true); + } + else if(lift_cooldown.Check()) + { + if(GetBoatSpawn()) + { + Spawn* boat = GetZone()->GetSpawnByID(GetBoatSpawn()); + if(boat) + { + LogWrite(PLAYER__DEBUG, 0, "Player", "Remove Player %s (%u) from Boat: %s", + GetName(), GetCharacterID(), boat ? boat->GetName() : "notset"); + boat->RemoveRailPassenger(GetCharacterID()); + GetZone()->CallSpawnScript(boat, SPAWN_SCRIPT_DEBOARD, this); + } + } + SetBoatSpawn(0); + lift_cooldown.Disable(); + } + + if (!IsResurrecting() && !GetBoatSpawn()) + { + if (!IsRooted() && !IsMezzedOrStunned()) { + SetX(x); + SetY(y, true, true); + SetZ(z); + SetSpeedX(x_speed); + SetSpeedY(y_speed); + SetSpeedZ(z_speed); + SetSideSpeed(side_speed); + SetVertSpeed(vert_speed); + SetClientHeading1(direction1); + SetClientHeading2(direction2); + SetClientPitch(client_pitch); + if(version > 373) { + pos_packet_speed = speed; + } + } + else { + SetSpeedX(0.0f); + SetSpeedY(0.0f); + SetSpeedZ(0.0f); + SetSideSpeed(0.0f); + SetVertSpeed(0.0f); + SetClientHeading1(direction1); + SetClientHeading2(direction2); + SetClientPitch(client_pitch); + pos_packet_speed = 0; + } + } + + if (GetLocation() != grid_id) + { + LogWrite(PLAYER__DEBUG, 0, "Player", "%s left grid %u and entered grid %u", appearance.name, GetLocation(), grid_id); + const char* zone_script = world.GetZoneScript(GetZone()->GetZoneID()); + + if (zone_script && lua_interface) { + lua_interface->RunZoneScript(zone_script, "leave_location", GetZone(), this, GetLocation()); + } + + SetLocation(grid_id); + + if (zone_script && lua_interface) { + lua_interface->RunZoneScript(zone_script, "enter_location", GetZone(), this, grid_id); + } + } + if (activity == UPDATE_ACTIVITY_IN_WATER_ABOVE || activity == UPDATE_ACTIVITY_IN_WATER_BELOW || + activity == UPDATE_ACTIVITY_MOVE_WATER_BELOW_AOM || activity == UPDATE_ACTIVITY_MOVE_WATER_ABOVE_AOM) { + if (MakeRandomFloat(0, 100) < 25 && InWater()) + GetSkillByName("Swimming", true); + } + // don't have to uncomment the print packet but you MUST uncomment the safe_delete() for xml structs + //update->PrintPacket(); + //safe_delete(update); + + LogWrite(PLAYER__DEBUG, 7, "Player", "Exit: %s", __FUNCTION__); // trace +} + +int16 Player::GetLastMovementActivity(){ + return last_movement_activity; +} + +void Player::AddSpawnInfoPacketForXOR(int32 spawn_id, uchar* packet, int16 packet_size){ + spawn_info_packet_list[spawn_id] = string((char*)packet, packet_size); +} + +void Player::AddSpawnPosPacketForXOR(int32 spawn_id, uchar* packet, int16 packet_size){ + spawn_pos_packet_list[spawn_id] = string((char*)packet, packet_size); +} + +uchar* Player::GetSpawnPosPacketForXOR(int32 spawn_id){ + uchar* ret = 0; + if(spawn_pos_packet_list.count(spawn_id) == 1) + ret = (uchar*)spawn_pos_packet_list[spawn_id].c_str(); + return ret; +} +uchar* Player::GetSpawnInfoPacketForXOR(int32 spawn_id){ + uchar* ret = 0; + if(spawn_info_packet_list.count(spawn_id) == 1) + ret = (uchar*)spawn_info_packet_list[spawn_id].c_str(); + return ret; +} +void Player::AddSpawnVisPacketForXOR(int32 spawn_id, uchar* packet, int16 packet_size){ + spawn_vis_packet_list[spawn_id] = string((char*)packet, packet_size); +} + +uchar* Player::GetSpawnVisPacketForXOR(int32 spawn_id){ + uchar* ret = 0; + if(spawn_vis_packet_list.count(spawn_id) == 1) + ret = (uchar*)spawn_vis_packet_list[spawn_id].c_str(); + return ret; +} + +uchar* Player::GetTempInfoPacketForXOR(){ + return spawn_tmp_info_xor_packet; +} + +uchar* Player::GetTempVisPacketForXOR(){ + return spawn_tmp_vis_xor_packet; +} + +uchar* Player::GetTempPosPacketForXOR(){ + return spawn_tmp_pos_xor_packet; +} + +uchar* Player::SetTempInfoPacketForXOR(int16 size){ + spawn_tmp_info_xor_packet = new uchar[size]; + info_xor_size = size; + return spawn_tmp_info_xor_packet; +} + +uchar* Player::SetTempVisPacketForXOR(int16 size){ + spawn_tmp_vis_xor_packet = new uchar[size]; + vis_xor_size = size; + return spawn_tmp_vis_xor_packet; +} + +uchar* Player::SetTempPosPacketForXOR(int16 size){ + spawn_tmp_pos_xor_packet = new uchar[size]; + pos_xor_size = size; + return spawn_tmp_pos_xor_packet; +} + +bool Player::CheckPlayerInfo(){ + return info != 0; +} + +bool Player::SetSpawnSentState(Spawn* spawn, SpawnState state) { + bool val = true; + spawn_mutex.writelock(__FUNCTION__, __LINE__); + int16 index = GetIndexForSpawn(spawn); + if(index > 0 && (state == SpawnState::SPAWN_STATE_SENDING)) { + LogWrite(PLAYER__WARNING, 0, "Player", "Spawn ALREADY INDEXED for Player %s (%u). Spawn %s (index %u) attempted to state %u.", + GetName(), GetCharacterID(), spawn->GetName(), index, state); + if(GetClient() && GetClient()->IsReloadingZone()) { + spawn_packet_sent.insert(make_pair(spawn->GetID(), state)); + val = false; + } + // we don't do anything this spawn is already populated by the player + } + else { + LogWrite(PLAYER__DEBUG, 0, "Player", "Spawn for Player %s (%u). Spawn %s (index %u) in state %u.", + GetName(), GetCharacterID(), spawn->GetName(), index, state); + + map::iterator itr = spawn_packet_sent.find(spawn->GetID()); + if(itr != spawn_packet_sent.end()) + itr->second = state; + else + spawn_packet_sent.insert(make_pair(spawn->GetID(), state)); + if(state == SPAWN_STATE_SENT_WAIT) { + map::iterator state_itr; + if((state_itr = spawn_state_list.find(spawn->GetID())) != spawn_state_list.end()) { + safe_delete(state_itr->second); + spawn_state_list.erase(state_itr); + } + + SpawnQueueState* removal = new SpawnQueueState; + removal->index_id = index; + removal->spawn_state_timer = Timer(500, true); + removal->spawn_state_timer.Start(); + spawn_state_list.insert(make_pair(spawn->GetID(),removal)); + } + else if(state == SpawnState::SPAWN_STATE_REMOVING && + spawn_state_list.count(spawn->GetID()) == 0) { + SpawnQueueState* removal = new SpawnQueueState; + removal->index_id = index; + removal->spawn_state_timer = Timer(1000, true); + removal->spawn_state_timer.Start(); + spawn_state_list.insert(make_pair(spawn->GetID(),removal)); + } + } + spawn_mutex.releasewritelock(__FUNCTION__, __LINE__); + return val; +} + +void Player::CheckSpawnStateQueue() { + if(!GetClient() || !GetClient()->IsReadyForUpdates()) + return; + + spawn_mutex.writelock(__FUNCTION__, __LINE__); + map::iterator itr; + for(itr = spawn_state_list.begin(); itr != spawn_state_list.end();) { + if(itr->second->spawn_state_timer.Check()) { + map::iterator sent_itr = spawn_packet_sent.find(itr->first); + LogWrite(PLAYER__DEBUG, 0, "Player", "Spawn for Player %s (%u). Spawn index %u in state %u.", + GetName(), GetCharacterID(), itr->second->index_id, sent_itr->second); + switch(sent_itr->second) { + case SpawnState::SPAWN_STATE_SENT_WAIT: { + sent_itr->second = SpawnState::SPAWN_STATE_SENT; + SpawnQueueState* sr = itr->second; + itr = spawn_state_list.erase(itr); + safe_delete(sr); + break; + } + case SpawnState::SPAWN_STATE_REMOVING: { + if(itr->first == GetID() && GetClient()->IsReloadingZone()) { + itr->second->spawn_state_timer.Disable(); + continue; + } + + if(itr->second->index_id) { + PacketStruct* packet = packet = configReader.getStruct("WS_DestroyGhostCmd", GetClient()->GetVersion()); + packet->setDataByName("spawn_index", itr->second->index_id); + packet->setDataByName("delete", 1); + GetClient()->QueuePacket(packet->serialize()); + safe_delete(packet); + } + sent_itr->second = SpawnState::SPAWN_STATE_REMOVING_SLEEP; + itr++; + break; + } + case SpawnState::SPAWN_STATE_REMOVING_SLEEP: { + map::iterator sent_itr = spawn_packet_sent.find(itr->first); + sent_itr->second = SpawnState::SPAWN_STATE_REMOVED; + SpawnQueueState* sr = itr->second; + itr = spawn_state_list.erase(itr); + safe_delete(sr); + break; + } + default: { + // reset + itr->second->spawn_state_timer.Disable(); + break; + } + } + } + else + itr++; + } + spawn_mutex.releasewritelock(__FUNCTION__, __LINE__); +} + +bool Player::WasSentSpawn(int32 spawn_id){ + if(GetID() == spawn_id) + return true; + + bool ret = false; + spawn_mutex.readlock(__FUNCTION__, __LINE__); + map::iterator itr = spawn_packet_sent.find(spawn_id); + if(itr != spawn_packet_sent.end() && itr->second == SpawnState::SPAWN_STATE_SENT) { + ret = true; + } + spawn_mutex.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +bool Player::IsSendingSpawn(int32 spawn_id){ + bool ret = false; + spawn_mutex.readlock(__FUNCTION__, __LINE__); + map::iterator itr = spawn_packet_sent.find(spawn_id); + if(itr != spawn_packet_sent.end() && (itr->second == SpawnState::SPAWN_STATE_SENDING || itr->second == SPAWN_STATE_SENT_WAIT)) { + ret = true; + } + spawn_mutex.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +bool Player::IsRemovingSpawn(int32 spawn_id){ + bool ret = false; + spawn_mutex.readlock(__FUNCTION__, __LINE__); + map::iterator itr = spawn_packet_sent.find(spawn_id); + if(itr != spawn_packet_sent.end() && + (itr->second == SpawnState::SPAWN_STATE_REMOVING || itr->second == SpawnState::SPAWN_STATE_REMOVING_SLEEP)) { + ret = true; + } + spawn_mutex.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +PlayerSkillList* Player::GetSkills(){ + return &skill_list; +} + +void Player::InCombat(bool val, bool range) { + if (val) + GetInfoStruct()->set_flags(GetInfoStruct()->get_flags() | (1 << (range?CF_RANGED_AUTO_ATTACK:CF_AUTO_ATTACK))); + else + GetInfoStruct()->set_flags(GetInfoStruct()->get_flags() & ~(1 << (range?CF_RANGED_AUTO_ATTACK:CF_AUTO_ATTACK))); + + bool changeCombatState = false; + + if((in_combat && !val) || (!in_combat && val)) + changeCombatState = true; + + in_combat = val; + if(in_combat) + AddIconValue(64); + else + RemoveIconValue(64); + + bool update_regen = false; + if(GetInfoStruct()->get_engaged_encounter()) { + if(!IsAggroed() || !IsEngagedInEncounter()) { + GetInfoStruct()->set_engaged_encounter(0); + update_regen = true; + } + } + + if(changeCombatState || update_regen) + SetRegenValues((GetInfoStruct()->get_effective_level() > 0) ? GetInfoStruct()->get_effective_level() : GetLevel()); + + charsheet_changed = true; + info_changed = true; +} + +void Player::SetCharSheetChanged(bool val){ + charsheet_changed = val; +} + +bool Player::GetCharSheetChanged(){ + return charsheet_changed; +} + +void Player::SetRaidSheetChanged(bool val){ + raidsheet_changed = val; +} + +bool Player::GetRaidSheetChanged(){ + return raidsheet_changed; +} + +bool Player::AdventureXPEnabled(){ + return (GetInfoStruct()->get_flags() & (1 << CF_COMBAT_EXPERIENCE_ENABLED)); +} + +bool Player::TradeskillXPEnabled() { + // TODO: need to identify the flag to togle tradeskill xp + return true; +} + +void Player::set_character_flag(int flag){ + LogWrite(PLAYER__DEBUG, 0, "Player", "Flag: %u", flag); + LogWrite(PLAYER__DEBUG, 0, "Player", "Flags before: %u, Flags2: %u", GetInfoStruct()->get_flags(), GetInfoStruct()->get_flags2()); + + if (flag > CF_MAXIMUM_FLAG) return; + if (flag < 32) GetInfoStruct()->set_flags(GetInfoStruct()->get_flags() | (1 << flag)); + else GetInfoStruct()->set_flags2(GetInfoStruct()->get_flags2() | (1 << (flag - 32))); + charsheet_changed = true; + info_changed = true; + + LogWrite(PLAYER__DEBUG, 0, "Player", "Flags after: %u, Flags2: %u", GetInfoStruct()->get_flags(), GetInfoStruct()->get_flags2()); +} + +void Player::reset_character_flag(int flag){ + LogWrite(PLAYER__DEBUG, 0, "Player", "Flag: %u", flag); + LogWrite(PLAYER__DEBUG, 0, "Player", "Flags before: %u, Flags2: %u", GetInfoStruct()->get_flags(), GetInfoStruct()->get_flags2()); + + if (flag > CF_MAXIMUM_FLAG) return; + if (flag < 32) + { + int8 origflag = GetInfoStruct()->get_flags(); + GetInfoStruct()->set_flags(origflag &= ~(1 << flag)); + } + else + { + int8 flag2 = GetInfoStruct()->get_flags2(); + GetInfoStruct()->set_flags2(flag2 &= ~(1 << (flag - 32))); + } + charsheet_changed = true; + info_changed = true; + + LogWrite(PLAYER__DEBUG, 0, "Player", "Flags after: %u, Flags2: %u", GetInfoStruct()->get_flags(), GetInfoStruct()->get_flags2()); +} + +void Player::toggle_character_flag(int flag){ + LogWrite(PLAYER__DEBUG, 0, "Player", "Flag: %u", flag); + LogWrite(PLAYER__DEBUG, 0, "Player", "Flags before: %u, Flags2: %u", GetInfoStruct()->get_flags(), GetInfoStruct()->get_flags2()); + + if (flag > CF_MAXIMUM_FLAG) return; + if (flag < 32) + { + int32 origflag = GetInfoStruct()->get_flags(); + GetInfoStruct()->set_flags(origflag ^= (1 << flag)); + } + else + { + int32 flag2 = GetInfoStruct()->get_flags2(); + GetInfoStruct()->set_flags2(flag2 ^= (1 << (flag - 32))); + } + charsheet_changed = true; + info_changed = true; + + LogWrite(PLAYER__DEBUG, 0, "Player", "Flags after: %u, Flags2: %u", GetInfoStruct()->get_flags(), GetInfoStruct()->get_flags2()); +} + +bool Player::get_character_flag(int flag){ + bool ret = false; + + if (flag > CF_MAXIMUM_FLAG){ + LogWrite(PLAYER__DEBUG, 0, "Player", "Player::get_character_flag error: attempted to check flag %i", flag); + return ret; + } + if (flag < 32) ret = ((GetInfoStruct()->get_flags()) >> flag & 1); + else ret = ((GetInfoStruct()->get_flags2()) >> (flag - 32) & 1); + + return ret; +} + +float Player::GetXPVitality(){ + return GetInfoStruct()->get_xp_vitality(); +} + +float Player::GetTSXPVitality() { + return GetInfoStruct()->get_tradeskill_xp_vitality(); +} + +bool Player::DoubleXPEnabled(){ + return GetInfoStruct()->get_xp_vitality() > 0; +} + +void Player::SetCharacterID(int32 new_id){ + char_id = new_id; +} + +int32 Player::GetCharacterID(){ + return char_id; +} + +float Player::CalculateXP(Spawn* victim){ + if(AdventureXPEnabled() == false || !victim) + return 0; + float multiplier = 0; + + float zone_xp_modifier = 1; // let's be safe!! + if( GetZone()->GetXPModifier() != 0 ) { + zone_xp_modifier = GetZone()->GetXPModifier(); + LogWrite(PLAYER__DEBUG, 5, "XP", "Zone XP Modifier = %.2f", zone_xp_modifier); + } + + switch(GetArrowColor(victim->GetLevel())){ + case ARROW_COLOR_GRAY: + LogWrite(PLAYER__DEBUG, 5, "XP", "Gray Arrow = No XP"); + return 0.0f; + break; + case ARROW_COLOR_GREEN: + multiplier = 3.25; + LogWrite(PLAYER__DEBUG, 5, "XP", "Green Arrow Multiplier = %.2f", multiplier); + break; + case ARROW_COLOR_BLUE: + multiplier = 3.5; + LogWrite(PLAYER__DEBUG, 5, "XP", "Blue Arrow Multiplier = %.2f", multiplier); + break; + case ARROW_COLOR_WHITE: + multiplier = 4; + LogWrite(PLAYER__DEBUG, 5, "XP", "White Arrow Multiplier = %.2f", multiplier); + break; + case ARROW_COLOR_YELLOW: + multiplier = 4.25; + LogWrite(PLAYER__DEBUG, 5, "XP", "Yellow Arrow Multiplier = %.2f", multiplier); + break; + case ARROW_COLOR_ORANGE: + multiplier = 4.5; + LogWrite(PLAYER__DEBUG, 5, "XP", "Orange Arrow Multiplier = %.2f", multiplier); + break; + case ARROW_COLOR_RED: + multiplier = 6; + LogWrite(PLAYER__DEBUG, 5, "XP", "Red Arrow Multiplier = %.2f", multiplier); + break; + } + float total = multiplier * 8; + LogWrite(PLAYER__DEBUG, 5, "XP", "Multiplier * 8 = %.2f", total); + + if(victim->GetDifficulty() > 6) { // no need to multiply by 1 if this is a normal mob + total *= (victim->GetDifficulty() - 5); + LogWrite(PLAYER__DEBUG, 5, "XP", "Encounter > 6, total = %.2f", total); + } + else if(victim->GetDifficulty() <= 5) { + total /= (7 - victim->GetDifficulty()); //1 down mobs are worth half credit, 2 down worth .25, etc + LogWrite(PLAYER__DEBUG, 5, "XP", "Encounter <= 5, total = %.2f", total); + } + + if(victim->GetHeroic() > 1) { + total *= victim->GetHeroic(); + LogWrite(PLAYER__DEBUG, 5, "XP", "Heroic, total = %.2f", total); + } + if(DoubleXPEnabled()) { + LogWrite(PLAYER__DEBUG, 5, "XP", "Calculating Double XP!"); + + float percent = (((float)(total))/GetNeededXP()) *100; + LogWrite(PLAYER__DEBUG, 5, "XP", "Percent of total / XP Needed * 100, percent = %.2f", percent); + float xp_vitality = GetXPVitality(); + if(xp_vitality >= percent) { + GetInfoStruct()->set_xp_vitality(xp_vitality - percent); + total *= 2; + LogWrite(PLAYER__DEBUG, 5, "XP", "Vitality >= Percent, total = %.2f", total); + } + else { + total += ((GetXPVitality() / percent) *2)*total; + GetInfoStruct()->set_xp_vitality(0); + LogWrite(PLAYER__DEBUG, 5, "XP", "Vitality < Percent, total = %.2f", total); + } + } + LogWrite(PLAYER__DEBUG, 5, "XP", "Final total = %.2f", (total * world.GetXPRate() * zone_xp_modifier)); + return total * world.GetXPRate() * zone_xp_modifier; +} + +float Player::CalculateTSXP(int8 level){ + if(TradeskillXPEnabled() == false) + return 0; + float multiplier = 0; + + float zone_xp_modifier = 1; // let's be safe!! + if( GetZone()->GetXPModifier() != 0 ) { + zone_xp_modifier = GetZone()->GetXPModifier(); + LogWrite(PLAYER__DEBUG, 5, "XP", "Zone XP Modifier = %.2f", zone_xp_modifier); + } + + sint16 diff = level - GetTSLevel(); + if(GetTSLevel() < 10) + diff *= 3; + else if(GetTSLevel() <= 20) + diff *= 2; + if(diff >= 9) + multiplier = 6; + else if(diff >= 5) + multiplier = 4.5; + else if(diff >= 1) + multiplier = 4.25; + else if(diff == 0) + multiplier = 4; + else if(diff <= -11) + multiplier = 0; + else if(diff <= -6) + multiplier = 3.25; + else //if(diff < 0) + multiplier = 3.5; + + + float total = multiplier * 8; + LogWrite(PLAYER__DEBUG, 5, "XP", "Multiplier * 8 = %.2f", total); + + if(DoubleXPEnabled()) { + LogWrite(PLAYER__DEBUG, 5, "XP", "Calculating Double XP!"); + + float percent = (((float)(total))/GetNeededTSXP()) *100; + LogWrite(PLAYER__DEBUG, 5, "XP", "Percent of total / XP Needed * 100, percent = %.2f", percent); + + float ts_xp_vitality = GetTSXPVitality(); + if(ts_xp_vitality >= percent) { + GetInfoStruct()->set_tradeskill_xp_vitality(ts_xp_vitality - percent); + total *= 2; + LogWrite(PLAYER__DEBUG, 5, "XP", "Vitality >= Percent, total = %.2f", total); + } + else { + total += ((GetTSXPVitality() / percent) *2)*total; + GetInfoStruct()->set_tradeskill_xp_vitality(0); + LogWrite(PLAYER__DEBUG, 5, "XP", "Vitality < Percent, total = %.2f", total); + } + } + LogWrite(PLAYER__DEBUG, 5, "XP", "Final total = %.2f", (total * world.GetXPRate() * zone_xp_modifier)); + return total * world.GetXPRate() * zone_xp_modifier; +} + +void Player::CalculateOfflineDebtRecovery(int32 unix_timestamp) +{ + float xpDebt = GetXPDebt(); + // not a real timestamp to work with + if(unix_timestamp < 1 || xpDebt == 0.0f) + return; + + uint32 diff = (Timer::GetUnixTimeStamp() - unix_timestamp)/1000; + + float recoveryDebtPercentage = rule_manager.GetGlobalRule(R_Combat, ExperienceDebtRecoveryPercent)->GetFloat()/100.0f; + int32 recoveryPeriodSeconds = rule_manager.GetGlobalRule(R_Combat, ExperienceDebtRecoveryPeriod)->GetInt32(); + if(recoveryDebtPercentage == 0.0f || recoveryPeriodSeconds < 1) + return; + + + float periodsPassed = (float)diff/(float)recoveryPeriodSeconds; + + // not enough time passed to calculate debt xp recovered + if(periodsPassed < 1.0f) + return; + + float debtToSubtract = xpDebt * ((recoveryDebtPercentage*periodsPassed)/100.0f); + + if(debtToSubtract >= xpDebt) + GetInfoStruct()->set_xp_debt(0.0f); + else + GetInfoStruct()->set_xp_debt(xpDebt - debtToSubtract); +} + +void Player::SetNeededXP(int32 val){ + GetInfoStruct()->set_xp_needed(val); +} + +void Player::SetNeededXP(){ + //GetInfoStruct()->xp_needed = GetLevel() * 100; + // Get xp needed to get to the next level + int16 level = GetLevel() + 1; + SetNeededXP(GetNeededXPByLevel(level)); +} + +int32 Player::GetNeededXPByLevel(int8 level) { + int32 exp_required = 0; + if (!Player::m_levelXPReq.count(level) && level > 95 && Player::m_levelXPReq.count(95)) { + exp_required = (Player::m_levelXPReq[95] * ((level - 95) + 1)); + } + else if(Player::m_levelXPReq.count(level)) + exp_required = Player::m_levelXPReq[level]; + else + exp_required = 0; + + return exp_required; +} + +void Player::SetXP(int32 val){ + GetInfoStruct()->set_xp(val); +} + +void Player::SetNeededTSXP(int32 val) { + GetInfoStruct()->set_ts_xp_needed(val); +} + +void Player::SetNeededTSXP() { + GetInfoStruct()->set_ts_xp_needed(GetTSLevel() * 100); +} + +void Player::SetTSXP(int32 val) { + GetInfoStruct()->set_ts_xp(val); +} + +float Player::GetXPDebt(){ + return GetInfoStruct()->get_xp_debt(); +} + +int32 Player::GetNeededXP(){ + return GetInfoStruct()->get_xp_needed(); +} + +int32 Player::GetXP(){ + return GetInfoStruct()->get_xp(); +} + +int32 Player::GetNeededTSXP() { + return GetInfoStruct()->get_ts_xp_needed(); +} + +int32 Player::GetTSXP() { + return GetInfoStruct()->get_ts_xp(); +} + +bool Player::AddXP(int32 xp_amount){ + if(!GetClient()) // potential linkdead player + return false; + + MStats.lock(); + xp_amount += (int32)(((float)xp_amount) * stats[ITEM_STAT_COMBATEXPMOD]) / 100; + MStats.unlock(); + + if(GetInfoStruct()->get_xp_debt()) + { + float expRatioToDebt = rule_manager.GetGlobalRule(R_Combat, ExperienceToDebt)->GetFloat()/100.0f; + int32 amountToTakeFromDebt = (int32)((float)expRatioToDebt * (float)xp_amount); + int32 amountRequiredClearDebt = (GetInfoStruct()->get_xp_debt()/100.0f) * xp_amount; + + if(amountToTakeFromDebt > amountRequiredClearDebt) + { + GetInfoStruct()->set_xp_debt(0.0f); + if(amountRequiredClearDebt > xp_amount) + xp_amount = 0; + else + xp_amount -= amountRequiredClearDebt; + } + else + { + float amountRemovedPct = ((float)amountToTakeFromDebt/(float)amountRequiredClearDebt); + GetInfoStruct()->set_xp_debt(GetInfoStruct()->get_xp_debt()-amountRemovedPct); + if(amountToTakeFromDebt > xp_amount) + xp_amount = 0; + else + xp_amount -= amountToTakeFromDebt; + } + } + + // used up in xp debt + if(!xp_amount) { + SetCharSheetChanged(true); + return true; + } + + int32 prev_level = GetLevel(); + float current_xp_percent = ((float)GetXP()/(float)GetNeededXP())*100; + int32 mini_ding_pct = rule_manager.GetGlobalRule(R_Player, MiniDingPercentage)->GetInt32(); + float miniding_min_percent = 0.0f; + if(mini_ding_pct < 10 || mini_ding_pct > 50) { + mini_ding_pct = 0; + } + else { + miniding_min_percent = ((int)(current_xp_percent/mini_ding_pct)+1)*mini_ding_pct; + } + while((xp_amount + GetXP()) >= GetNeededXP()){ + if (!CheckLevelStatus(GetLevel() + 1)) { + if(GetClient()) { + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "You do not have the required status to level up anymore!"); + } + SetCharSheetChanged(true); + return false; + } + int32 prev_xp_amount = xp_amount; + xp_amount -= GetNeededXP() - GetXP(); + if(GetClient()->ChangeLevel(GetLevel(), GetLevel()+1, prev_xp_amount)) + SetLevel(GetLevel() + 1); + else { + SetXP(GetXP() + prev_xp_amount); + SetCharSheetChanged(true); + return false; + } + } + + // set the actual end xp_amount result + SetXP(GetXP() + xp_amount); + + if(GetClient()) { + GetClient()->Message(CHANNEL_REWARD, "You gain %u experience!", (int32)xp_amount); + } + + GetPlayerInfo()->CalculateXPPercentages(); + current_xp_percent = ((float)GetXP()/(float)GetNeededXP())*100; + if(miniding_min_percent > 0.0f && current_xp_percent >= miniding_min_percent){ + if(GetClient() && rule_manager.GetGlobalRule(R_Spells, UseClassicSpellLevel)->GetInt8()) + GetClient()->SendNewAdventureSpells(); // mini ding involves checking spells again in classic level settings + SetHP(GetTotalHP()); + SetPower(GetTotalPower()); + GetZone()->SendCastSpellPacket(332, this, this); //send mini level up spell effect + } + + SetCharSheetChanged(true); + return true; +} + +bool Player::AddTSXP(int32 xp_amount){ + MStats.lock(); + xp_amount += ((xp_amount)*stats[ITEM_STAT_TRADESKILLEXPMOD]) / 100; + MStats.unlock(); + + float current_xp_percent = ((float)GetTSXP()/(float)GetNeededTSXP())*100; + + int32 mini_ding_pct = rule_manager.GetGlobalRule(R_Player, MiniDingPercentage)->GetInt32(); + float miniding_min_percent = 0.0f; + if(mini_ding_pct < 10 || mini_ding_pct > 50) { + mini_ding_pct = 0; + } + else { + miniding_min_percent = ((int)(current_xp_percent/mini_ding_pct)+1)*mini_ding_pct; + } + + while((xp_amount + GetTSXP()) >= GetNeededTSXP()){ + if (!CheckLevelStatus(GetTSLevel() + 1)) { + if(GetClient()) { + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "You do not have the required status to level up anymore!"); + } + return false; + } + int32 prev_xp_amount = xp_amount; + xp_amount -= GetNeededTSXP() - GetTSXP(); + if(GetClient()->ChangeTSLevel(GetLevel(), GetLevel()+1, prev_xp_amount)) { + SetTSLevel(GetTSLevel() + 1); + SetTSXP(0); + SetNeededTSXP(); + } + else { + SetTSXP(GetTSXP() + prev_xp_amount); + SetCharSheetChanged(true); + return false; + } + } + SetTSXP(GetTSXP() + xp_amount); + GetPlayerInfo()->CalculateXPPercentages(); + current_xp_percent = ((float)GetTSXP()/(float)GetNeededTSXP())*100; + if(current_xp_percent >= miniding_min_percent){ + SetHP(GetTotalHP()); + SetPower(GetTotalPower()); + } + + if (GetTradeskillClass() == 0){ + SetTradeskillClass(1); + GetInfoStruct()->set_tradeskill_class1(1); + GetInfoStruct()->set_tradeskill_class2(1); + GetInfoStruct()->set_tradeskill_class3(1); + } + + SetCharSheetChanged(true); + return true; +} + +void Player::CalculateLocation(){ + if(GetSpeed() > 0 ){ + if(GetHeading() >= 270 && GetHeading() <= 360){ + SetX(GetX() + (GetSpeed()*.5)*((360-GetHeading())/90)); + SetZ(GetZ() - (GetSpeed()*.5)*((GetHeading()-270)/90)); + } + else if(GetHeading() >= 180 && GetHeading() < 270){ + SetX(GetX() + (GetSpeed()*.5)*((GetHeading()-180)/90)); + SetZ(GetZ() + (GetSpeed()*.5)*((270-GetHeading())/90)); + } + else if(GetHeading() >= 90 && GetHeading() < 180){ + SetX(GetX() - (GetSpeed()*.5)*((180-GetHeading())/90)); + SetZ(GetZ() + (GetSpeed()*.5)*((GetHeading()-90)/90)); + } + else if(GetHeading() >= 0 && GetHeading() < 90){ + SetX(GetX() - (GetSpeed()*.5)*(GetHeading()/90)); + SetZ(GetZ() - (GetSpeed()*.5)*((90-GetHeading())/90)); + } + } +} + +Spawn* Player::GetSpawnByIndex(int16 index){ + Spawn* spawn = 0; + + index_mutex.readlock(__FUNCTION__, __LINE__); + if(player_spawn_id_map.count(index) > 0) + spawn = player_spawn_id_map[index]; + index_mutex.releasereadlock(__FUNCTION__, __LINE__); + + return spawn; +} + +int16 Player::GetIndexForSpawn(Spawn* spawn) { + int16 val = 0; + + index_mutex.readlock(__FUNCTION__, __LINE__); + if(player_spawn_reverse_id_map.count(spawn) > 0) + val = player_spawn_reverse_id_map[spawn]; + index_mutex.releasereadlock(__FUNCTION__, __LINE__); + + return val; +} + +bool Player::WasSpawnRemoved(Spawn* spawn){ + bool wasRemoved = false; + + if(IsRemovingSpawn(spawn->GetID())) + return false; + + spawn_mutex.readlock(__FUNCTION__, __LINE__); + map::iterator itr = spawn_packet_sent.find(spawn_id); + if(itr != spawn_packet_sent.end() && itr->second == SpawnState::SPAWN_STATE_REMOVED) { + wasRemoved = true; + } + spawn_mutex.releasereadlock(__FUNCTION__, __LINE__); + + return wasRemoved; +} + +void Player::ResetSpawnPackets(int32 id) { + info_mutex.writelock(__FUNCTION__, __LINE__); + vis_mutex.writelock(__FUNCTION__, __LINE__); + pos_mutex.writelock(__FUNCTION__, __LINE__); + index_mutex.writelock(__FUNCTION__, __LINE__); + + if (spawn_info_packet_list.count(id)) + spawn_info_packet_list.erase(id); + + if (spawn_pos_packet_list.count(id)) + spawn_pos_packet_list.erase(id); + + if (spawn_vis_packet_list.count(id)) + spawn_vis_packet_list.erase(id); + + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); + pos_mutex.releasewritelock(__FUNCTION__, __LINE__); + info_mutex.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::RemoveSpawn(Spawn* spawn, bool delete_spawn) +{ + LogWrite(PLAYER__DEBUG, 3, "Player", "Remove Spawn '%s' (%u)", spawn->GetName(), spawn->GetID()); + + SetSpawnSentState(spawn, delete_spawn ? SpawnState::SPAWN_STATE_REMOVING : SpawnState::SPAWN_STATE_REMOVING_SLEEP); + + info_mutex.writelock(__FUNCTION__, __LINE__); + vis_mutex.writelock(__FUNCTION__, __LINE__); + pos_mutex.writelock(__FUNCTION__, __LINE__); + + index_mutex.writelock(__FUNCTION__, __LINE__); + + if (player_spawn_reverse_id_map[spawn] && player_spawn_id_map.count(player_spawn_reverse_id_map[spawn]) > 0) + player_spawn_id_map.erase(player_spawn_reverse_id_map[spawn]); + + if (player_spawn_reverse_id_map.count(spawn) > 0) + player_spawn_reverse_id_map.erase(spawn); + + if (player_spawn_id_map.count(spawn->GetID()) && player_spawn_id_map[spawn->GetID()] == spawn) + player_spawn_id_map.erase(spawn->GetID()); + + int32 id = spawn->GetID(); + if (spawn_info_packet_list.count(id)) + spawn_info_packet_list.erase(id); + + if (spawn_pos_packet_list.count(id)) + spawn_pos_packet_list.erase(id); + + if (spawn_vis_packet_list.count(id)) + spawn_vis_packet_list.erase(id); + + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + pos_mutex.releasewritelock(__FUNCTION__, __LINE__); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); + info_mutex.releasewritelock(__FUNCTION__, __LINE__); +} + +vector Player::GetQuestIDs(){ + vector ret; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second) + ret.push_back(itr->second->GetQuestID()); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +vector* Player::CheckQuestsItemUpdate(Item* item){ + vector* quest_updates = 0; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second && itr->second->CheckQuestItemUpdate(item->details.item_id, item->details.count)){ + if(!quest_updates) + quest_updates = new vector(); + quest_updates->push_back(itr->second); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return quest_updates; +} + +void Player::CheckQuestsCraftUpdate(Item* item, int32 qty){ + map::iterator itr; + vector* update_list = new vector; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second){ + if(item && qty > 0){ + if(itr->second->CheckQuestRefIDUpdate(item->details.item_id, qty)){ + update_list->push_back(itr->second); + } + } + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + if(update_list && update_list->size() > 0){ + Client* client = GetClient(); + if(client){ + for(int8 i=0;isize(); i++){ + client->SendQuestUpdate(update_list->at(i)); + client->SendQuestFailure(update_list->at(i)); + } + } + } + update_list->clear(); + safe_delete(update_list); +} + +void Player::CheckQuestsHarvestUpdate(Item* item, int32 qty){ + map::iterator itr; + vector* update_list = new vector; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second){ + if(item && qty > 0){ + if(itr->second->CheckQuestRefIDUpdate(item->details.item_id, qty)){ + update_list->push_back(itr->second); + } + } + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + if(update_list && update_list->size() > 0){ + Client* client = GetClient(); + if(client){ + for(int8 i=0;isize(); i++){ + client->SendQuestUpdate(update_list->at(i)); + client->SendQuestFailure(update_list->at(i)); + } + } + } + update_list->clear(); + safe_delete(update_list); +} + +vector* Player::CheckQuestsSpellUpdate(Spell* spell) { + vector* quest_updates = 0; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for (itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if (itr->second && itr->second->CheckQuestSpellUpdate(spell)) { + if (!quest_updates) + quest_updates = new vector(); + quest_updates->push_back(itr->second); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return quest_updates; +} + +PacketStruct* Player::GetQuestJournalPacket(bool all_quests, int16 version, int32 crc, int32 current_quest_id, bool updated){ + PacketStruct* packet = configReader.getStruct("WS_QuestJournalUpdate", version); + Quest* quest = 0; + if(packet){ + int16 total_quests_num = 0; + int16 total_completed_quests = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + map total_quests = player_quests; + if(all_quests && completed_quests.size() > 0) + total_quests.insert(completed_quests.begin(), completed_quests.end()); + if(total_quests.size() > 0){ + map quest_types; + map::iterator itr; + int16 zone_id = 0; + for(itr = total_quests.begin(); itr != total_quests.end(); itr++){ + if(itr->first && itr->second){ + if(current_quest_id == 0 && itr->second->GetTurnedIn() == false) + current_quest_id = itr->first; + if(itr->second->GetTurnedIn()) + total_completed_quests++; + if(itr->second->GetType()){ + if(quest_types.count(itr->second->GetType()) == 0){ + quest_types[itr->second->GetType()] = zone_id; + zone_id++; + } + } + if(itr->second->GetZone()){ + if(quest_types.count(itr->second->GetZone()) == 0){ + quest_types[itr->second->GetZone()] = zone_id; // Fix #490 - incorrect ordering of quests in journal + zone_id++; + } + } + total_quests_num++; + } + else + continue; + } + packet->setArrayLengthByName("num_quests", total_quests_num); + int16 i = 0; + for(itr = total_quests.begin(); itr != total_quests.end(); itr++){ + if(i == 0 && quest_types.size() > 0){ + packet->setArrayLengthByName("num_quest_zones", quest_types.size()); + map::iterator type_itr; + int16 x = 0; + for(type_itr = quest_types.begin(); type_itr != quest_types.end(); type_itr++){ + packet->setArrayDataByName("quest_zones_zone", type_itr->first.c_str(), x); + packet->setArrayDataByName("quest_zones_zone_id", type_itr->second, x); + x++; + } + } + if(itr->first == 0 || !itr->second) + continue; + if(!all_quests && !itr->second->GetUpdateRequired()) + continue; + quest = itr->second; + if(!quest->GetDeleted()) + packet->setArrayDataByName("active", 1, i); + packet->setArrayDataByName("name", quest->GetName(), i); + packet->setArrayDataByName("quest_type", quest->GetType(), i); + packet->setArrayDataByName("quest_zone", quest->GetZone(), i); + int8 display_status = QUEST_DISPLAY_STATUS_SHOW; + if(itr->second->GetCompleted()) + packet->setArrayDataByName("completed", 1, i); + if(itr->second->GetTurnedIn()){ + packet->setArrayDataByName("turned_in", 1, i); + packet->setArrayDataByName("completed", 1, i); + packet->setArrayDataByName("visible", 1, i); + packet->setArrayDataByName("unknown3", 1, i); + display_status += QUEST_DISPLAY_STATUS_COMPLETED; + } + if (updated) { + packet->setArrayDataByName("quest_updated", 1, i); + packet->setArrayDataByName("journal_updated", 1, i); + } + packet->setArrayDataByName("quest_id", quest->GetQuestID(), i); + packet->setArrayDataByName("day", quest->GetDay(), i); + packet->setArrayDataByName("month", quest->GetMonth(), i); + packet->setArrayDataByName("year", quest->GetYear(), i); + packet->setArrayDataByName("level", quest->GetQuestLevel(), i); + int8 difficulty = 0; + string category = quest->GetType(); + if(category == "Tradeskill") + difficulty = GetTSArrowColor(quest->GetQuestLevel()); + else + difficulty = GetArrowColor(quest->GetQuestLevel()); + packet->setArrayDataByName("difficulty", difficulty, i); + if (itr->second->GetEncounterLevel() > 4) + packet->setArrayDataByName("encounter_level", quest->GetEncounterLevel(), i); + else + packet->setArrayDataByName("encounter_level", 4, i); + if(version >= 931 && quest_types.count(quest->GetType()) > 0) + packet->setArrayDataByName("zonetype_id", quest_types[quest->GetType()], i); + if(version >= 931 && quest_types.count(quest->GetZone()) > 0) + packet->setArrayDataByName("zone_id", quest_types[quest->GetZone()], i); + if(version >= 931 && quest->GetVisible()){ + if (quest->GetCompletedFlag()) + display_status += QUEST_DISPLAY_STATUS_COMPLETE_FLAG; + else if (quest->IsRepeatable()) + display_status += QUEST_DISPLAY_STATUS_REPEATABLE; + if (quest->GetYellowName() || quest->CheckCategoryYellow()) + display_status += QUEST_DISPLAY_STATUS_YELLOW; + + if (quest->IsTracked()) + display_status += QUEST_DISPLAY_STATUS_CHECK; + else + display_status += QUEST_DISPLAY_STATUS_NO_CHECK; + + if (quest->IsHidden() && !quest->GetTurnedIn()) { + display_status += QUEST_DISPLAY_STATUS_HIDDEN; + display_status -= QUEST_DISPLAY_STATUS_SHOW; + } + + if(quest->CanShareQuestCriteria(GetClient(),false)) { + display_status += QUEST_DISPLAY_STATUS_CAN_SHARE; + } + } + else + packet->setArrayDataByName("visible", quest->GetVisible(), i); + if (itr->second->IsRepeatable()) + packet->setArrayDataByName("repeatable", 1, i); + + packet->setArrayDataByName("display_status", display_status, i); + i++; + } + //packet->setDataByName("unknown4", 0); + packet->setDataByName("visible_quest_id", current_quest_id); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + packet->setDataByName("player_crc", crc); + packet->setDataByName("player_name", GetName()); + packet->setDataByName("used_quests", total_quests_num - total_completed_quests); + packet->setDataByName("max_quests", 75); + + LogWrite(PLAYER__PACKET, 0, "Player", "Dump/Print Packet in func: %s, line: %i", __FUNCTION__, __LINE__); +#if EQDEBUG >= 9 + packet->PrintPacket(); +#endif + } + return packet; +} + +PacketStruct* Player::GetQuestJournalPacket(Quest* quest, int16 version, int32 crc, bool updated) { + if (!quest) + return 0; + + PacketStruct* packet = configReader.getStruct("WS_QuestJournalUpdate", version); + if (packet) { + packet->setArrayLengthByName("num_quests", 1); + packet->setArrayLengthByName("num_quest_zones", 1); + packet->setArrayDataByName("quest_zones_zone", quest->GetType()); + packet->setArrayDataByName("quest_zones_zone_id", 0); + + if(!quest->GetDeleted() && !quest->GetCompleted()) + packet->setArrayDataByName("active", 1); + + packet->setArrayDataByName("name", quest->GetName()); + // don't see these two in the struct + packet->setArrayDataByName("quest_type", quest->GetType()); + packet->setArrayDataByName("quest_zone", quest->GetZone()); + + int8 display_status = QUEST_DISPLAY_STATUS_SHOW; + if(quest->GetCompleted()) + packet->setArrayDataByName("completed", 1); + if(quest->GetTurnedIn()) { + packet->setArrayDataByName("turned_in", 1); + packet->setArrayDataByName("completed", 1); + packet->setArrayDataByName("visible", 1); + display_status += QUEST_DISPLAY_STATUS_COMPLETED; + } + packet->setArrayDataByName("quest_id", quest->GetQuestID()); + packet->setArrayDataByName("day", quest->GetDay()); + packet->setArrayDataByName("month", quest->GetMonth()); + packet->setArrayDataByName("year", quest->GetYear()); + packet->setArrayDataByName("level", quest->GetQuestLevel()); + int8 difficulty = 0; + string category = quest->GetType(); + if(category == "Tradeskill") + difficulty = GetTSArrowColor(quest->GetQuestLevel()); + else + difficulty = GetArrowColor(quest->GetQuestLevel()); + + packet->setArrayDataByName("difficulty", difficulty); + if (quest->GetEncounterLevel() > 4) + packet->setArrayDataByName("encounter_level", quest->GetEncounterLevel()); + else + packet->setArrayDataByName("encounter_level", 4); + + if (version >= 931) { + packet->setArrayDataByName("zonetype_id", 0); + packet->setArrayDataByName("zone_id", 0); + } + if(version >= 931 && quest->GetVisible()){ + if (quest->GetCompletedFlag()) + display_status += QUEST_DISPLAY_STATUS_COMPLETE_FLAG; + else if (quest->IsRepeatable()) + display_status += QUEST_DISPLAY_STATUS_REPEATABLE; + if (quest->GetYellowName() || quest->CheckCategoryYellow()) + display_status += QUEST_DISPLAY_STATUS_YELLOW; + + if (quest->IsTracked()) + display_status += QUEST_DISPLAY_STATUS_CHECK; + else + display_status += QUEST_DISPLAY_STATUS_NO_CHECK; + + if (quest->IsHidden() && !quest->GetTurnedIn()) { + display_status += QUEST_DISPLAY_STATUS_HIDDEN; + display_status -= QUEST_DISPLAY_STATUS_SHOW; + } + + if(quest->CanShareQuestCriteria(GetClient(),false)) { + display_status += QUEST_DISPLAY_STATUS_CAN_SHARE; + } + } + else + packet->setArrayDataByName("visible", quest->GetVisible()); + if (quest->IsRepeatable()) + packet->setArrayDataByName("repeatable", 1); + + packet->setArrayDataByName("display_status", display_status); + if (updated) { + packet->setArrayDataByName("quest_updated", 1); + packet->setArrayDataByName("journal_updated", 1); + } + if(version >= 546) + packet->setDataByName("unknown3", 1); + packet->setDataByName("visible_quest_id", quest->GetQuestID()); + packet->setDataByName("player_crc", crc); + packet->setDataByName("player_name", GetName()); + packet->setDataByName("used_quests", player_quests.size()); + packet->setDataByName("unknown4a", 1); + packet->setDataByName("max_quests", 75); + } + + return packet; +} + +Quest* Player::SetStepComplete(int32 id, int32 step){ + Quest* ret = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(id) > 0){ + if(player_quests[id] && player_quests[id]->SetStepComplete(step)) + ret = player_quests[id]; + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +Quest* Player::AddStepProgress(int32 quest_id, int32 step, int32 progress) { + Quest* ret = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if (player_quests.count(quest_id) > 0) { + if (player_quests[quest_id] && player_quests[quest_id]->AddStepProgress(step, progress)) + ret = player_quests[quest_id]; + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +int32 Player::GetStepProgress(int32 quest_id, int32 step_id) { + int32 ret = 0; + + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if (player_quests.count(quest_id) > 0 && player_quests[quest_id]) + ret = player_quests[quest_id]->GetStepProgress(step_id); + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + return ret; +} + +void Player::RemoveQuest(int32 id, bool delete_quest){ + MPlayerQuests.writelock(__FUNCTION__, __LINE__); + map::iterator itr = player_quests.find(id); + if(itr != player_quests.end()) { + player_quests.erase(itr); + } + + if(delete_quest){ + safe_delete(player_quests[id]); + } + + MPlayerQuests.releasewritelock(__FUNCTION__, __LINE__); + SendQuestRequiredSpawns(id); +} + +vector* Player::CheckQuestsLocationUpdate(){ + vector* quest_updates = 0; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second && itr->second->CheckQuestLocationUpdate(GetX(), GetY(), GetZ(), (GetZoneID()))){ + if(!quest_updates) + quest_updates = new vector(); + quest_updates->push_back(itr->second); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return quest_updates; +} + +vector* Player::CheckQuestsFailures(){ + vector* quest_failures = 0; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second && itr->second->GetQuestFailures()->size() > 0){ + if(!quest_failures) + quest_failures = new vector(); + quest_failures->push_back(itr->second); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return quest_failures; +} + +vector* Player::CheckQuestsKillUpdate(Spawn* spawn, bool update){ + vector* quest_updates = 0; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second && itr->second->CheckQuestKillUpdate(spawn, update)){ + if(!quest_updates) + quest_updates = new vector(); + quest_updates->push_back(itr->second); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return quest_updates; +} + +bool Player::HasQuestUpdateRequirement(Spawn* spawn){ + bool reqMet = false; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second && itr->second->CheckQuestReferencedSpawns(spawn)){ + reqMet = true; + break; + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return reqMet; +} + +vector* Player::CheckQuestsChatUpdate(Spawn* spawn){ + vector* quest_updates = 0; + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + if(itr->second && itr->second->CheckQuestChatUpdate(spawn->GetDatabaseID())){ + if(!quest_updates) + quest_updates = new vector(); + quest_updates->push_back(itr->second); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return quest_updates; +} + +int16 Player::GetTaskGroupStep(int32 quest_id){ + Quest* quest = 0; + int16 step = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(quest_id) > 0){ + quest = player_quests[quest_id]; + if(quest) { + step = quest->GetTaskGroupStep(); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return step; +} + +bool Player::GetQuestStepComplete(int32 quest_id, int32 step_id){ + bool ret = false; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(quest_id) > 0){ + Quest* quest = player_quests[quest_id]; + if ( quest != NULL ) + ret = quest->GetQuestStepCompleted(step_id); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +int16 Player::GetQuestStep(int32 quest_id){ + Quest* quest = 0; + int16 step = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(quest_id) > 0){ + quest = player_quests[quest_id]; + if(quest) { + step = quest->GetQuestStep(); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return step; +} + +map* Player::GetPlayerQuests(){ + return &player_quests; +} + +map* Player::GetCompletedPlayerQuests(){ + return &completed_quests; +} + +Quest* Player::GetAnyQuest(int32 quest_id) { + if(player_quests.count(quest_id) > 0) + return player_quests[quest_id]; + if(completed_quests.count(quest_id) > 0) + return completed_quests[quest_id]; + + return 0; +} +Quest* Player::GetCompletedQuest(int32 quest_id){ + if(completed_quests.count(quest_id) > 0) + return completed_quests[quest_id]; + return 0; +} + +bool Player::HasQuestBeenCompleted(int32 quest_id){ + bool ret = false; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(completed_quests.count(quest_id) > 0 && completed_quests[quest_id]) + ret = true; + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + return ret; +} + +bool Player::HasActiveQuest(int32 quest_id){ + bool ret = false; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(quest_id) > 0 && player_quests[quest_id]) + ret = true; + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + return ret; +} + +bool Player::HasAnyQuest(int32 quest_id){ + bool ret = false; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(quest_id) > 0) + ret = true; + if(completed_quests.count(quest_id) > 0) + ret = true; + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + return ret; +} + +int32 Player::GetQuestCompletedCount(int32 quest_id) { + int32 count = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetCompletedQuest(quest_id); + if(quest) { + count = quest->GetCompleteCount(); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return count; +} + +Quest* Player::GetQuest(int32 quest_id){ + if(player_quests.count(quest_id) > 0) + return player_quests[quest_id]; + return 0; +} + +void Player::AddCompletedQuest(Quest* quest){ + Quest* existing = GetCompletedQuest(quest->GetQuestID()); + MPlayerQuests.writelock(__FUNCTION__, __LINE__); + completed_quests[quest->GetQuestID()] = quest; + if(existing && existing != quest) { + safe_delete(existing); + } + + quest->SetSaveNeeded(true); + quest->SetTurnedIn(true); + if(quest->GetCompletedDescription()) + quest->SetDescription(string(quest->GetCompletedDescription())); + quest->SetUpdateRequired(true); + MPlayerQuests.releasewritelock(__FUNCTION__, __LINE__); +} + +bool Player::CheckQuestRemoveFlag(Spawn* spawn){ + if(current_quest_flagged.count(spawn) > 0){ + current_quest_flagged.erase(spawn); + return true; + } + return false; +} + +bool Player::CheckQuestRequired(Spawn* spawn){ + if(spawn) + return spawn->MeetsSpawnAccessRequirements(this); + return false; +} + +int8 Player::CheckQuestFlag(Spawn* spawn){ + int8 ret = 0; + + if (!spawn) { + LogWrite(PLAYER__ERROR, 0, "Player", "CheckQuestFlag() called with an invalid spawn"); + return ret; + } + if(spawn->HasProvidedQuests()){ + vector* quests = spawn->GetProvidedQuests(); + Quest* quest = 0; + for(int32 i=0;isize();i++){ + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + if(player_quests.count(quests->at(i)) > 0){ + if(player_quests[quests->at(i)] && player_quests[quests->at(i)]->GetCompleted() && player_quests[quests->at(i)]->GetQuestReturnNPC() == spawn->GetDatabaseID()){ + ret = 2; + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + break; + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + int8 flag = 0; + if (CanReceiveQuest(quests->at(i), &flag)){ + if(flag) { + ret = flag; + break; + } + master_quest_list.LockQuests(); + quest = master_quest_list.GetQuest(quests->at(i), false); + master_quest_list.UnlockQuests(); + if(quest){ + int8 color = quest->GetFeatherColor(); + // purple + if (color == 1) + ret = 16; + // green + else if (color == 2) + ret = 32; + // blue + else if (color == 3) + ret = 64; + // normal + else + ret = 1; + break; + } + } + } + } + map::iterator itr; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + for(itr = player_quests.begin(); itr != player_quests.end(); itr++){ + // must make sure the quest ptr is alive or nullptr + if(itr->second && itr->second->CheckQuestChatUpdate(spawn->GetDatabaseID(), false)) + ret = 2; + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + if(ret > 0) + current_quest_flagged[spawn] = true; + return ret; +} + +bool Player::CanReceiveQuest(int32 quest_id, int8* ret){ + bool passed = true; + int32 x; + master_quest_list.LockQuests(); + Quest* quest = master_quest_list.GetQuest(quest_id, false); + master_quest_list.UnlockQuests(); + if (!quest) + passed = false; + //check if quest is already completed, and not repeatable + else if (HasQuestBeenCompleted(quest_id) && !quest->IsRepeatable()) + passed = false; + //check if the player already has this quest + else if (player_quests.count(quest_id) > 0) + passed = false; + //Check Prereq Adv Levels + else if (quest->GetPrereqLevel() > GetLevel()) + passed = false; + else if (quest->GetPrereqMaxLevel() > 0){ + if (GetLevel() > quest->GetPrereqMaxLevel()) + passed = false; + } + //Check Prereq TS Levels + else if (quest->GetPrereqTSLevel() > GetTSLevel()) + passed = false; + else if (quest->GetPrereqMaxTSLevel() > 0){ + if (GetTSLevel() > quest->GetPrereqMaxLevel()) + passed = false; + } + + + // Check quest pre req + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + vector* prereq_quests = quest->GetPrereqQuests(); + if(passed && prereq_quests && prereq_quests->size() > 0){ + for(int32 x=0;xsize();x++){ + if(completed_quests.count(prereq_quests->at(x)) == 0){ + passed = false; + break; + } + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + //Check Prereq Classes + vector* prereq_classes = quest->GetPrereqClasses(); + if(passed && prereq_classes && prereq_classes->size() > 0){ + for(int32 x=0;xsize();x++){ + if(prereq_classes->at(x) == GetAdventureClass()){ + passed = true; + break; + } + else + passed = false; + } + } + + //Check Prereq TS Classes + vector* prereq_tsclasses = quest->GetPrereqTradeskillClasses(); + if(passed && prereq_tsclasses && prereq_tsclasses->size() > 0){ + for( x=0;xsize();x++){ + if(prereq_tsclasses->at(x) == GetTradeskillClass()){ + passed = true; + break; + } + else + passed = false; + } + } + + + // Check model prereq + vector* prereq_model_types = quest->GetPrereqModelTypes(); + if(passed && prereq_model_types && prereq_model_types->size() > 0){ + for(x=0;xsize();x++){ + if(prereq_model_types->at(x) == GetModelType()){ + passed = true; + break; + } + else + passed = false; + } + } + + + // Check faction pre req + vector* prereq_factions = quest->GetPrereqFactions(); + if(passed && prereq_factions && prereq_factions->size() > 0){ + sint32 val = 0; + for(x=0;xsize();x++){ + val = GetFactions()->GetFactionValue(prereq_factions->at(x).faction_id); + if(val >= prereq_factions->at(x).min && (prereq_factions->at(x).max == 0 || val <= prereq_factions->at(x).max)){ + passed = true; + break; + } + else + passed = false; + } + } + + LogWrite(MISC__TODO, 1, "TODO", "Check prereq items\n\t(%s, function: %s, line #: %i)", __FILE__, __FUNCTION__, __LINE__); + + // Check race pre req + vector* prereq_races = quest->GetPrereqRaces(); + if(passed && prereq_races && prereq_races->size() > 0){ + for(x=0;xsize();x++){ + if(prereq_races->at(x) == GetRace()){ + passed = true; + break; + } + else + passed = false; + } + } + + int32 flag = 0; + if(lua_interface->CallQuestFunction(quest, "ReceiveQuestCriteria", this, 0xFFFFFFFF, &flag)) { + if(ret) + *ret = flag; + if(!flag) { + passed = false; + } + else { + passed = true; + } + } + + return passed; +} + +bool Player::UpdateQuestReward(int32 quest_id, QuestRewardData* qrd) { + if(!GetClient()) + return false; + + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetAnyQuest(quest_id); + + if(!quest) { + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return false; + } + + quest->SetQuestTemporaryState(qrd->is_temporary, qrd->description); + if(qrd->is_temporary) { + quest->SetStatusTmpReward(qrd->tmp_status); + quest->SetCoinTmpReward(qrd->tmp_coin); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + GetClient()->GiveQuestReward(quest, qrd->has_displayed); + SetActiveReward(true); + + return true; +} + + +Quest* Player::PendingQuestAcceptance(int32 quest_id, int32 item_id, bool* quest_exists) { + vector* items = 0; + bool ret = false; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetAnyQuest(quest_id); + if(!quest) { + if(quest_exists) { + *quest_exists = false; + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return nullptr; + } + + if(quest_exists) { + *quest_exists = true; + } + if(quest->GetQuestTemporaryState()) + items = quest->GetTmpRewardItems(); + else + items = quest->GetRewardItems(); + if (item_id == 0) { + ret = true; + } + else { + items = quest->GetSelectableRewardItems(); + if (items && items->size() > 0) { + for (int32 i = 0; i < items->size(); i++) { + if (items->at(i)->details.item_id == item_id) { + ret = true; + break; + } + } + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + return quest; +} + + +bool Player::AcceptQuestReward(int32 item_id, int32 selectable_item_id) { + if(!GetClient()) { + return false; + } + + Collection *collection = 0; + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = client->GetPendingQuestAcceptance(item_id); + if(quest){ + GetClient()->AcceptQuestReward(quest, item_id); + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return true; + } + bool collectedItems = false; + if (client->GetPlayer()->HasPendingItemRewards()) { + vector items = client->GetPlayer()->GetPendingItemRewards(); + if (items.size() > 0) { + collectedItems = true; + for (int i = 0; i < items.size(); i++) { + client->GetPlayer()->AddItem(new Item(items[i])); + } + client->GetPlayer()->ClearPendingItemRewards(); + client->GetPlayer()->SetActiveReward(false); + } + map selectable_item = client->GetPlayer()->GetPendingSelectableItemReward(item_id); + if (selectable_item.size() > 0) { + collectedItems = true; + map::iterator itr; + for (itr = selectable_item.begin(); itr != selectable_item.end(); itr++) { + client->GetPlayer()->AddItem(new Item(itr->second)); + client->GetPlayer()->ClearPendingSelectableItemRewards(itr->first); + } + client->GetPlayer()->SetActiveReward(false); + } + } + else if (collection = GetPendingCollectionReward()) + { + client->AcceptCollectionRewards(collection, (selectable_item_id > 0) ? selectable_item_id : item_id); + collectedItems = true; + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + return collectedItems; +} + + +bool Player::SendQuestStepUpdate(int32 quest_id, int32 quest_step_id, bool display_quest_helper) { + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetAnyQuest(quest_id); + if(!quest) { + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + return false; + } + + QuestStep* quest_step = quest->GetQuestStep(quest_step_id); + if (quest_step) { + if(GetClient()) { + GetClient()->QueuePacket(quest->QuestJournalReply(GetClient()->GetVersion(), GetClient()->GetNameCRC(), this, quest_step, 1, false, false, display_quest_helper)); + } + quest_step->WasUpdated(false); + } + bool turnedIn = quest->GetTurnedIn(); + + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); + + if(turnedIn && GetClient()) //update the journal so the old quest isn't the one displayed in the client's quest helper + GetClient()->SendQuestJournal(); + + return true; +} + +void Player::SendQuest(int32 quest_id) { + if(!GetClient()) { + return; + } + + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetQuest(quest_id); + if (quest) + GetClient()->QueuePacket(quest->QuestJournalReply(GetClient()->GetVersion(), GetClient()->GetNameCRC(), this)); + else { + quest = GetCompletedQuest(quest_id); + + if (quest) + GetClient()->QueuePacket(quest->QuestJournalReply(GetClient()->GetVersion(), GetClient()->GetNameCRC(), this, 0, 1, true)); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); +} + + +void Player::UpdateQuestCompleteCount(int32 quest_id) { + if(!GetClient()) { + return; + } + + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + // If character has already completed this quest once update the given date in the database + Quest* quest = GetQuest(id); + Quest* quest2 = GetCompletedQuest(id); + if (quest && quest2) { + quest->SetCompleteCount(quest2->GetCompleteCount()); + database.SaveCharRepeatableQuest(GetClient(), id, quest->GetCompleteCount()); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); +} + +void Player::GetQuestTemporaryRewards(int32 quest_id, std::vector* items) { + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetAnyQuest(quest_id); + if(quest) { + quest->GetTmpRewardItemsByID(items); + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); +} + +void Player::AddQuestTemporaryReward(int32 quest_id, int32 item_id, int16 item_count) { + MPlayerQuests.readlock(__FUNCTION__, __LINE__); + Quest* quest = GetAnyQuest(quest_id); + if(quest) { + Item* item = master_item_list.GetItem(item_id); + if(item) { + Item* tmpItem = new Item(item); + tmpItem->details.count = item_count; + quest->AddTmpRewardItem(tmpItem); + } + } + MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__); +} + +bool Player::ShouldSendSpawn(Spawn* spawn){ + if(spawn->IsDeletedSpawn() || IsSendingSpawn(spawn->GetID()) || IsRemovingSpawn(spawn->GetID())) + return false; + else if((WasSentSpawn(spawn->GetID()) == false) && (!spawn->IsPrivateSpawn() || spawn->AllowedAccess(this))) + return true; + + return false; +} + +int8 Player::GetTSArrowColor(int8 level){ + int8 color = 0; + sint16 diff = level - GetTSLevel(); + if(GetLevel() < 10) + diff *= 3; + else if(GetLevel() <= 20) + diff *= 2; + if(diff >= 9) + color = ARROW_COLOR_RED; + else if(diff >= 5) + color = ARROW_COLOR_ORANGE; + else if(diff >= 1) + color = ARROW_COLOR_YELLOW; + else if(diff == 0) + color = ARROW_COLOR_WHITE; + else if(diff <= -11) + color = ARROW_COLOR_GRAY; + else if(diff <= -6) + color = ARROW_COLOR_GREEN; + else //if(diff < 0) + color = ARROW_COLOR_BLUE; + return color; +} + +void Player::AddCoins(int64 val){ + int32 tmp = 0; + UpdatePlayerStatistic(STAT_PLAYER_TOTAL_WEALTH, (GetCoinsCopper() + (GetCoinsSilver() * 100) + (GetCoinsGold() * 10000) + (GetCoinsPlat() * 1000000)) + val, true); + if(val >= 1000000){ + tmp = val / 1000000; + val -= tmp*1000000; + GetInfoStruct()->add_coin_plat(tmp); + } + if(val >= 10000){ + tmp = val / 10000; + val -= tmp*10000; + GetInfoStruct()->add_coin_gold(tmp); + } + if(val >= 100){ + tmp = val / 100; + val -= tmp*100; + GetInfoStruct()->add_coin_silver(tmp); + } + GetInfoStruct()->add_coin_copper(val); + int32 new_copper_value = GetInfoStruct()->get_coin_copper(); + if(new_copper_value >= 100){ + tmp = new_copper_value/100; + GetInfoStruct()->set_coin_copper(new_copper_value - (100 * tmp)); + GetInfoStruct()->add_coin_silver(tmp); + } + int32 new_silver_value = GetInfoStruct()->get_coin_silver(); + if(new_silver_value >= 100){ + tmp = new_silver_value/100; + GetInfoStruct()->set_coin_silver(new_silver_value - (100 * tmp)); + GetInfoStruct()->add_coin_gold(tmp); + } + int32 new_gold_value = GetInfoStruct()->get_coin_gold(); + if(new_gold_value >= 100){ + tmp = new_gold_value/100; + GetInfoStruct()->set_coin_gold(new_gold_value - (100 * tmp)); + GetInfoStruct()->add_coin_plat(tmp); + } + charsheet_changed = true; +} + +bool Player::RemoveCoins(int64 val){ + int64 total_coins = GetInfoStruct()->get_coin_plat()*1000000; + total_coins += GetInfoStruct()->get_coin_gold()*10000; + total_coins += GetInfoStruct()->get_coin_silver()*100; + total_coins += GetInfoStruct()->get_coin_copper(); + if(total_coins >= val){ + total_coins -= val; + GetInfoStruct()->set_coin_plat(0); + GetInfoStruct()->set_coin_gold(0); + GetInfoStruct()->set_coin_silver(0); + GetInfoStruct()->set_coin_copper(0); + AddCoins(total_coins); + return true; + } + return false; +} + +bool Player::HasCoins(int64 val) { + int64 total_coins = GetInfoStruct()->get_coin_plat()*1000000; + total_coins += GetInfoStruct()->get_coin_gold()*10000; + total_coins += GetInfoStruct()->get_coin_silver()*100; + total_coins += GetInfoStruct()->get_coin_copper(); + if(total_coins >= val) + return true; + + return false; +} + +bool Player::HasPendingLootItems(int32 id){ + return (pending_loot_items.count(id) > 0 && pending_loot_items[id].size() > 0); +} + +bool Player::HasPendingLootItem(int32 id, int32 item_id){ + return (pending_loot_items.count(id) > 0 && pending_loot_items[id].count(item_id) > 0); +} +vector* Player::GetPendingLootItems(int32 id){ + vector* ret = 0; + if(pending_loot_items.count(id) > 0){ + ret = new vector(); + map::iterator itr; + for(itr = pending_loot_items[id].begin(); itr != pending_loot_items[id].end(); itr++){ + if(master_item_list.GetItem(itr->first)) + ret->push_back(master_item_list.GetItem(itr->first)); + } + } + return ret; +} + +void Player::RemovePendingLootItem(int32 id, int32 item_id){ + if(pending_loot_items.count(id) > 0){ + pending_loot_items[id].erase(item_id); + if(pending_loot_items[id].size() == 0) + pending_loot_items.erase(id); + } +} + +void Player::RemovePendingLootItems(int32 id){ + if(pending_loot_items.count(id) > 0) + pending_loot_items.erase(id); +} + +void Player::AddPendingLootItems(int32 id, vector* items){ + if(items){ + Item* item = 0; + for(int32 i=0;isize();i++){ + item = items->at(i); + if(item) + pending_loot_items[id][item->details.item_id] = true; + } + } +} + +void Player::AddPlayerStatistic(int32 stat_id, sint32 stat_value, int32 stat_date) { + if (statistics.count(stat_id) == 0) { + Statistic* stat = new Statistic; + stat->stat_id = stat_id; + stat->stat_value = stat_value; + stat->stat_date = stat_date; + stat->save_needed = false; + statistics[stat_id] = stat; + } +} + +void Player::UpdatePlayerStatistic(int32 stat_id, sint32 stat_value, bool overwrite) { + if (statistics.count(stat_id) == 0) + AddPlayerStatistic(stat_id, 0, 0); + Statistic* stat = statistics[stat_id]; + overwrite == true ? stat->stat_value = stat_value : stat->stat_value += stat_value; + stat->stat_date = Timer::GetUnixTimeStamp(); + stat->save_needed = true; +} + +void Player::WritePlayerStatistics() { + map::iterator stat_itr; + for (stat_itr = statistics.begin(); stat_itr != statistics.end(); stat_itr++) { + Statistic* stat = stat_itr->second; + if (stat->save_needed) { + stat->save_needed = false; + database.WritePlayerStatistic(this, stat); + } + } +} + +sint64 Player::GetPlayerStatisticValue(int32 stat_id) { + if (stat_id >= 0 && statistics.count(stat_id) > 0) + return statistics[stat_id]->stat_value; + return 0; +} + +void Player::RemovePlayerStatistics() { + map::iterator stat_itr; + for (stat_itr = statistics.begin(); stat_itr != statistics.end(); stat_itr++) + safe_delete(stat_itr->second); + statistics.clear(); +} + +void Player::SetGroup(PlayerGroup* new_group){ + group = new_group; +} + +/*PlayerGroup* Player::GetGroup(){ + return group; +}*/ + +bool Player::IsGroupMember(Entity* player) { + bool ret = false; + if (GetGroupMemberInfo() && player) { + //world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__); + ret = world.GetGroupManager()->IsInGroup(GetGroupMemberInfo()->group_id, player); + + /*deque::iterator itr; + deque* members = world.GetGroupManager()->GetGroupMembers(GetGroupMemberInfo()->group_id); + for (itr = members->begin(); itr != members->end(); itr++) { + GroupMemberInfo* gmi = *itr; + if (gmi->client && gmi->client->GetPlayer() == player) { + ret = true; + break; + } + } + + world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);*/ + } + return ret; +} + + + + + +void Player::SetGroupInformation(PacketStruct* packet){ + int8 det_count = 0; + Entity* member = 0; + + world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__); + if (GetGroupMemberInfo()) { + PlayerGroup* group = world.GetGroupManager()->GetGroup(GetGroupMemberInfo()->group_id); + if (group) + { + group->MGroupMembers.readlock(__FUNCTION__, __LINE__); + deque* members = group->GetMembers(); + deque::iterator itr; + GroupMemberInfo* info = 0; + int x = 0; + + for (itr = members->begin(); itr != members->end(); itr++) { + info = *itr; + + if (info == GetGroupMemberInfo()) { + if (info->leader) + packet->setDataByName("group_leader_id", 0xFFFFFFFF); // If this player is the group leader then fill this element with FF FF FF FF + + continue; + } + else { + if (info->leader) + packet->setDataByName("group_leader_id", x); // If leader is some one else then fill with the slot number they are in + } + + member = info->member; + + if (member && member->GetZone() == GetZone()) { + packet->setSubstructDataByName("group_members", "spawn_id", GetIDWithPlayerSpawn(member), x); + + if (member->HasPet()) { + if (member->GetPet()) + packet->setSubstructDataByName("group_members", "pet_id", GetIDWithPlayerSpawn(member->GetPet()), x); + else + packet->setSubstructDataByName("group_members", "pet_id", GetIDWithPlayerSpawn(member->GetCharmedPet()), x); + } + else + packet->setSubstructDataByName("group_members", "pet_id", 0xFFFFFFFF, x); + + //Send detriment counts as 255 if all dets of that type are incurable + det_count = member->GetTraumaCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_TRAUMA)) + det_count = 255; + } + packet->setSubstructDataByName("group_members", "trauma_count", det_count, x); + + det_count = member->GetArcaneCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_ARCANE)) + det_count = 255; + } + packet->setSubstructDataByName("group_members", "arcane_count", det_count, x); + + det_count = member->GetNoxiousCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_NOXIOUS)) + det_count = 255; + } + packet->setSubstructDataByName("group_members", "noxious_count", det_count, x); + + det_count = member->GetElementalCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_ELEMENTAL)) + det_count = 255; + } + packet->setSubstructDataByName("group_members", "elemental_count", det_count, x); + + det_count = member->GetCurseCount(); + if (det_count > 0) { + if (!member->HasCurableDetrimentType(DET_TYPE_CURSE)) + det_count = 255; + } + packet->setSubstructDataByName("group_members", "curse_count", det_count, x); + + packet->setSubstructDataByName("group_members", "zone_status", 1, x); + } + else { + packet->setSubstructDataByName("group_members", "pet_id", 0xFFFFFFFF, x); + //packet->setSubstructDataByName("group_members", "unknown5", 1, x, 1); // unknown5 > 1 = name is blue + packet->setSubstructDataByName("group_members", "zone_status", 2, x); + } + + packet->setSubstructDataByName("group_members", "name", info->name.c_str(), x); + packet->setSubstructDataByName("group_members", "hp_current", info->hp_current, x); + packet->setSubstructDataByName("group_members", "hp_max", info->hp_max, x); + packet->setSubstructDataByName("group_members", "power_current", info->power_current, x); + packet->setSubstructDataByName("group_members", "power_max", info->power_max, x); + packet->setSubstructDataByName("group_members", "level_current", info->level_current, x); + packet->setSubstructDataByName("group_members", "level_max", info->level_max, x); + packet->setSubstructDataByName("group_members", "zone", info->zone.c_str(), x); + packet->setSubstructDataByName("group_members", "race_id", info->race_id, x); + packet->setSubstructDataByName("group_members", "class_id", info->class_id, x); + + x++; + } + } + group->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__); + } + //packet->PrintPacket(); + world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__); +} + +PlayerItemList* Player::GetPlayerItemList(){ + return &item_list; +} + +void Player::ResetSavedSpawns(){ + spawn_mutex.writelock(__FUNCTION__, __LINE__); + ClearRemovalTimers(); + spawn_packet_sent.clear(); + spawn_mutex.releasewritelock(__FUNCTION__, __LINE__); + + info_mutex.writelock(__FUNCTION__, __LINE__); + spawn_info_packet_list.clear(); + info_mutex.releasewritelock(__FUNCTION__, __LINE__); + + vis_mutex.writelock(__FUNCTION__, __LINE__); + spawn_vis_packet_list.clear(); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); + + pos_mutex.writelock(__FUNCTION__, __LINE__); + spawn_pos_packet_list.clear(); + pos_mutex.releasewritelock(__FUNCTION__, __LINE__); + + index_mutex.writelock(__FUNCTION__, __LINE__); + player_spawn_reverse_id_map.clear(); + player_spawn_id_map.clear(); + player_spawn_id_map[1] = this; + player_spawn_reverse_id_map[this] = 1; + spawn_index = 1; + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + + m_playerSpawnQuestsRequired.writelock(__FUNCTION__, __LINE__); + player_spawn_quests_required.clear(); + m_playerSpawnQuestsRequired.releasewritelock(__FUNCTION__, __LINE__); + if(info) + info->RemoveOldPackets(); + + safe_delete_array(movement_packet); + safe_delete_array(old_movement_packet); +} + +void Player::SetReturningFromLD(bool val){ + std::unique_lock lock(spell_packet_update_mutex); + if(val && val != returning_from_ld) + { + if(GetPlayerItemList()) + GetPlayerItemList()->ResetPackets(); + + GetEquipmentList()->ResetPackets(); + GetAppearanceEquipmentList()->ResetPackets(); + skill_list.ResetPackets(); + safe_delete_array(spell_orig_packet); + safe_delete_array(spell_xor_packet); + spell_orig_packet=0; + spell_xor_packet=0; + spell_count = 0; + + safe_delete_array(raid_orig_packet); + safe_delete_array(raid_xor_packet); + raid_orig_packet=0; + raid_xor_packet=0; + + reset_character_flag(CF_IS_SITTING); + if (GetActivityStatus() & ACTIVITY_STATUS_CAMPING) + SetActivityStatus(GetActivityStatus() - ACTIVITY_STATUS_CAMPING); + + if (GetActivityStatus() & ACTIVITY_STATUS_LINKDEAD) + SetActivityStatus(GetActivityStatus() - ACTIVITY_STATUS_LINKDEAD); + + SetTempVisualState(0); + + safe_delete_array(spawn_tmp_info_xor_packet); + safe_delete_array(spawn_tmp_vis_xor_packet); + safe_delete_array(spawn_tmp_pos_xor_packet); + spawn_tmp_info_xor_packet = 0; + spawn_tmp_vis_xor_packet = 0; + spawn_tmp_pos_xor_packet = 0; + pos_xor_size = 0; + info_xor_size = 0; + vis_xor_size = 0; + + index_mutex.writelock(__FUNCTION__, __LINE__); + player_spawn_id_map[1] = this; + player_spawn_reverse_id_map[this] = 1; + spawn_index = 1; + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + } + + returning_from_ld = val; +} + +bool Player::IsReturningFromLD(){ + return returning_from_ld; +} + +void Player::AddFriend(const char* name, bool save){ + if(name){ + if(save) + friend_list[name] = 1; + else + friend_list[name] = 0; + } +} + +bool Player::IsFriend(const char* name){ + if(name && friend_list.count(name) > 0 && friend_list[name] < 2) + return true; + return false; +} + +void Player::RemoveFriend(const char* name){ + if(friend_list.count(name) > 0) + friend_list[name] = 2; +} + +map* Player::GetFriends(){ + return &friend_list; +} + +void Player::AddIgnore(const char* name, bool save){ + if(name){ + if(save) + ignore_list[name] = 1; + else + ignore_list[name] = 0; + } +} + +bool Player::IsIgnored(const char* name){ + if(name && ignore_list.count(name) > 0 && ignore_list[name] < 2) + return true; + return false; +} + +void Player::RemoveIgnore(const char* name){ + if(name && ignore_list.count(name) > 0) + ignore_list[name] = 2; +} + +map* Player::GetIgnoredPlayers(){ + return &ignore_list; +} + +bool Player::CheckLevelStatus(int16 new_level) { + int16 LevelCap = rule_manager.GetGlobalRule(R_Player, MaxLevel)->GetInt16(); + int16 LevelCapOverrideStatus = rule_manager.GetGlobalRule(R_Player, MaxLevelOverrideStatus)->GetInt16(); + int16 MaxLevelPlayer = GetInfoStruct()->get_max_level(); + if ( GetClient() && (LevelCap < new_level) && (LevelCapOverrideStatus > GetClient()->GetAdminStatus()) && (MaxLevelPlayer < 1 || MaxLevelPlayer < new_level) ) + return false; + return true; +} + +Skill* Player::GetSkillByName(const char* name, bool check_update){ + Skill* ret = skill_list.GetSkillByName(name); + if(check_update) + { + if(skill_list.CheckSkillIncrease(ret)) + CalculateBonuses(); + } + return ret; +} + +Skill* Player::GetSkillByID(int32 id, bool check_update){ + Skill* ret = skill_list.GetSkill(id); + if(check_update) + { + if(skill_list.CheckSkillIncrease(ret)) + CalculateBonuses(); + } + return ret; +} + +void Player::SetRangeAttack(bool val){ + range_attack = val; +} + +bool Player::GetRangeAttack(){ + return range_attack; +} + +bool Player::AddMail(Mail* mail) { + bool ret = false; + if (mail) { + mail_list.Put(mail->mail_id, mail); + ret = true; + } + return ret; +} + +MutexMap* Player::GetMail() { + return &mail_list; +} + +Mail* Player::GetMail(int32 mail_id) { + Mail* mail = 0; + if (mail_list.count(mail_id) > 0) + mail = mail_list.Get(mail_id); + return mail; +} + +void Player::DeleteMail(bool from_database) { + MutexMap::iterator itr = mail_list.begin(); + while (itr.Next()) + DeleteMail(itr->first, from_database); + mail_list.clear(); +} + +void Player::DeleteMail(int32 mail_id, bool from_database) { + if (mail_list.count(mail_id) > 0) { + if (from_database) + database.DeletePlayerMail(mail_list.Get(mail_id)); + mail_list.erase(mail_id, true, true); // need to delete the mail ptr + } +} + +/* CharacterInstances */ + +CharacterInstances::CharacterInstances() { + m_instanceList.SetName("Mutex::m_instanceList"); +} + +CharacterInstances::~CharacterInstances() { + RemoveInstances(); +} + +void CharacterInstances::AddInstance(int32 db_id, int32 instance_id, int32 last_success_timestamp, int32 last_failure_timestamp, int32 success_lockout_time, int32 failure_lockout_time, int32 zone_id, int8 zone_instancetype, string zone_name) { + InstanceData data; + data.db_id = db_id; + data.instance_id = instance_id; + data.zone_id = zone_id; + data.zone_instance_type = zone_instancetype; + data.zone_name = zone_name; + data.last_success_timestamp = last_success_timestamp; + data.last_failure_timestamp = last_failure_timestamp; + data.success_lockout_time = success_lockout_time; + data.failure_lockout_time = failure_lockout_time; + instanceList.push_back(data); +} + +void CharacterInstances::RemoveInstances() { + instanceList.clear(); +} + +bool CharacterInstances::RemoveInstanceByZoneID(int32 zone_id) { + vector::iterator itr; + m_instanceList.writelock(__FUNCTION__, __LINE__); + for(itr = instanceList.begin(); itr != instanceList.end(); itr++) { + InstanceData* data = &(*itr); + if (data->zone_id == zone_id) { + instanceList.erase(itr); + m_instanceList.releasewritelock(__FUNCTION__, __LINE__); + return true; + } + } + m_instanceList.releasewritelock(__FUNCTION__, __LINE__); + return false; +} + +bool CharacterInstances::RemoveInstanceByInstanceID(int32 instance_id) { + vector::iterator itr; + m_instanceList.writelock(__FUNCTION__, __LINE__); + for(itr = instanceList.begin(); itr != instanceList.end(); itr++) { + InstanceData* data = &(*itr); + if (data->instance_id == instance_id) { + instanceList.erase(itr); + m_instanceList.releasewritelock(__FUNCTION__, __LINE__); + return true; + } + } + m_instanceList.releasewritelock(__FUNCTION__, __LINE__); + return false; +} + +InstanceData* CharacterInstances::FindInstanceByZoneID(int32 zone_id) { + m_instanceList.readlock(__FUNCTION__, __LINE__); + for(int32 i = 0; i < instanceList.size(); i++) { + InstanceData* data = &instanceList.at(i); + if (data->zone_id == zone_id) { + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + return data; + } + } + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + + return 0; +} + +InstanceData* CharacterInstances::FindInstanceByDBID(int32 db_id) { + m_instanceList.readlock(__FUNCTION__, __LINE__); + for(int32 i = 0; i < instanceList.size(); i++) { + InstanceData* data = &instanceList.at(i); + if (data->db_id == db_id) { + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + return data; + } + } + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + + return 0; +} + +InstanceData* CharacterInstances::FindInstanceByInstanceID(int32 instance_id) { + m_instanceList.readlock(__FUNCTION__, __LINE__); + for(int32 i = 0; i < instanceList.size(); i++) { + InstanceData* data = &instanceList.at(i); + if (data->instance_id == instance_id) { + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + return data; + } + } + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + + return 0; +} +vector CharacterInstances::GetLockoutInstances() { + vector ret; + m_instanceList.readlock(__FUNCTION__, __LINE__); + for (int32 i = 0; i < instanceList.size(); i++) { + InstanceData* data = &instanceList.at(i); + if (data->zone_instance_type == SOLO_LOCKOUT_INSTANCE || data->zone_instance_type == GROUP_LOCKOUT_INSTANCE || data->zone_instance_type == RAID_LOCKOUT_INSTANCE) + ret.push_back(*data); + } + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +vector CharacterInstances::GetPersistentInstances() { + vector ret; + m_instanceList.readlock(__FUNCTION__, __LINE__); + for (int32 i = 0; i < instanceList.size(); i++) { + InstanceData* data = &instanceList.at(i); + if (data->zone_instance_type == SOLO_PERSIST_INSTANCE || data->zone_instance_type == GROUP_PERSIST_INSTANCE || data->zone_instance_type == RAID_PERSIST_INSTANCE) + ret.push_back(*data); + } + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + return ret; +} + +void CharacterInstances::ProcessInstanceTimers(Player* player) { + + // Only need to check persistent instances here, lockout should be handled by zone shutting down + + // Check instance id, if > 0 check timers, if timers expired set instance id to 0 and update the db `character_instance` to instance id 0, + // delete instance from `instances` if no more characters assigned to it + + m_instanceList.readlock(__FUNCTION__, __LINE__); + for (int32 i = 0; i < instanceList.size(); i++) { + InstanceData* data = &instanceList.at(i); + + // Check to see if we have a valid instance and if it is persistant + if (data->instance_id > 0) { + + if (data->zone_instance_type == SOLO_PERSIST_INSTANCE || data->zone_instance_type == GROUP_PERSIST_INSTANCE || data->zone_instance_type == RAID_PERSIST_INSTANCE) { + // Check max duration (last success + success time) + // if the zone does not have a success lockout time, we should not apply this logic + if (data->success_lockout_time > 0 && (Timer::GetUnixTimeStamp() >= (data->last_success_timestamp + data->success_lockout_time))) { + // Max duration has passed, instance has expired lets remove the player from it, + // this will also delete the instace if all players have been removed from it + database.DeleteCharacterFromInstance(player->GetCharacterID(), data->instance_id); + data->instance_id = 0; + } + } + + if (data->zone_instance_type == SOLO_LOCKOUT_INSTANCE || data->zone_instance_type == GROUP_LOCKOUT_INSTANCE || data->zone_instance_type == RAID_LOCKOUT_INSTANCE) { + // Need to check lockout instance ids to ensure they are still valid. + if (!database.VerifyInstanceID(player->GetCharacterID(), data->instance_id)) + data->instance_id = 0; + } + } + } + m_instanceList.releasereadlock(__FUNCTION__, __LINE__); + + /*for(int32 i=0;isize();i++) + { + bool valuesUpdated = false; + InstanceData data = instanceList->at(i); + if ( data.grant_reenter_time_left > 0 ) + { + if ( ( data.grant_reenter_time_left - msDiff ) < 1 ) + data.grant_reenter_time_left = 0; + else + data.grant_reenter_time_left -= msDiff; + + valuesUpdated = true; + } + if ( data.grant_reset_time_left > 0 ) + { + if ( ( data.grant_reset_time_left - msDiff ) < 1 ) + data.grant_reset_time_left = 0; + else + data.grant_reset_time_left -= msDiff; + + valuesUpdated = true; + } + if ( data.lockout_time > 0 ) + { + if ( ( data.lockout_time - msDiff ) < 1 ) + { + data.lockout_time = 0; + // this means that their timer ran out and we need to clear it from db and player + RemoveInstanceByInstanceID(data.instance_id); + database.DeleteCharacterFromInstance(player->GetCharacterID(),data.instance_id); + } + else + data.lockout_time -= msDiff; + + valuesUpdated = true; + } + + if ( valuesUpdated ) + database.UpdateCharacterInstanceTimers(player->GetCharacterID(),data.instance_id,data.lockout_time,data.grant_reset_time_left,data.grant_reenter_time_left); + }*/ +} + +int32 CharacterInstances::GetInstanceCount() { + return instanceList.size(); +} + +void Player::SetPlayerAdventureClass(int8 new_class, bool set_by_gm_command ){ + int8 old_class = GetAdventureClass(); + SetAdventureClass(new_class); + GetInfoStruct()->set_class1(classes.GetBaseClass(new_class)); + GetInfoStruct()->set_class2(classes.GetSecondaryBaseClass(new_class)); + GetInfoStruct()->set_class3(new_class); + charsheet_changed = true; + if(GetZone()) + GetZone()->TriggerCharSheetTimer(); + if(GetClient()) + GetClient()->UpdateTimeStampFlag ( CLASS_UPDATE_FLAG ); + + const char* playerScript = world.GetPlayerScript(0); // 0 = global script + const char* playerZoneScript = world.GetPlayerScript(GetZoneID()); // zone script + if(playerScript || playerZoneScript) { + std::vector args = { + LuaArg(GetZone()), + LuaArg(this), + LuaArg(old_class), + LuaArg(new_class) + }; + if(playerScript) { + lua_interface->RunPlayerScriptWithReturn(playerScript, "on_class_change", args); + } + if(playerZoneScript) { + lua_interface->RunPlayerScriptWithReturn(playerZoneScript, "on_class_change", args); + } + } +} + +void Player::AddSkillBonus(int32 spell_id, int32 skill_id, float value) { + GetSkills()->AddSkillBonus(spell_id, skill_id, value); +} + +SkillBonus* Player::GetSkillBonus(int32 spell_id) { + return GetSkills()->GetSkillBonus(spell_id); +} + +void Player::RemoveSkillBonus(int32 spell_id) { + GetSkills()->RemoveSkillBonus(spell_id); +} + +bool Player::HasFreeBankSlot() { + return item_list.HasFreeBankSlot(); +} + +int8 Player::FindFreeBankSlot() { + return item_list.FindFreeBankSlot(); +} + +void Player::AddTitle(sint32 title_id, const char *name, int8 prefix, bool save_needed){ + Title* new_title = new Title; + new_title->SetID(title_id); + new_title->SetName(name); + new_title->SetPrefix(prefix); + new_title->SetSaveNeeded(save_needed); + player_titles_list.Add(new_title); +} + +void Player::AddAAEntry(int16 template_id, int8 tab_id, int32 aa_id, int16 order,int8 treeid) { + + + +} +void Player::AddLanguage(int32 id, const char *name, bool save_needed){ + Skill* skill = master_skill_list.GetSkillByName(name); + if(skill && !GetSkills()->HasSkill(skill->skill_id)) { + AddSkill(skill->skill_id, 1, skill->max_val, true); + } + // Check to see if the player already has the language + if (HasLanguage(id)) + return; + + // Doesn't already have the language so add it + Language* new_language = new Language; + new_language->SetID(id); + new_language->SetName(name); + player_languages_list.Add(new_language); + + if (save_needed) + database.SaveCharacterLang(GetCharacterID(), id); +} + +bool Player::HasLanguage(int32 id){ + list* languages = player_languages_list.GetAllLanguages(); + list::iterator itr; + Language* language = 0; + bool ret = false; + for(itr = languages->begin(); itr != languages->end(); itr++){ + language = *itr; + if(language->GetID() == id){ + ret = true; + break; + } + } + return ret; +} + +bool Player::HasLanguage(const char* name){ + list* languages = player_languages_list.GetAllLanguages(); + list::iterator itr; + Language* language = 0; + bool ret = false; + for(itr = languages->begin(); itr != languages->end(); itr++){ + language = *itr; + if(!strncmp(language->GetName(), name, strlen(name))){ + ret = true; + break; + } + } + return ret; +} + +void Player::AddPassiveSpell(int32 id, int8 tier) +{ + // Add the spell to the list of passives this player currently has + // list is used for quickly going over only the passive spells the + // player has instead of going through all their spells. + passive_spells.push_back(id); + + Client* client = GetClient(); + + // Don not apply passives if the client is null, zoning, or reviving + if (client == NULL || client->IsZoning() || IsResurrecting()) + return; + + Spell* spell = 0; + spell = master_spell_list.GetSpell(id, tier); + if (spell) { + SpellProcess* spellProcess = 0; + // Get the current zones spell process + spellProcess = GetZone()->GetSpellProcess(); + // Cast the spell, CastPassives() bypasses the spell queue + spellProcess->CastPassives(spell, this); + } +} + +void Player::ApplyPassiveSpells() +{ + Spell* spell = 0; + SpellBookEntry* spell2 = 0; + vector::iterator itr; + SpellProcess* spellProcess = 0; + spellProcess = GetZone()->GetSpellProcess(); + + for (itr = passive_spells.begin(); itr != passive_spells.end(); itr++) + { + spell2 = GetSpellBookSpell((*itr)); + spell = master_spell_list.GetSpell(spell2->spell_id, spell2->tier); + if (spell) { + spellProcess->CastPassives(spell, this); + } + } +} + +void Player::RemovePassive(int32 id, int8 tier, bool remove_from_list) +{ + Spell* spell = 0; + spell = master_spell_list.GetSpell(id, tier); + if (spell) { + SpellProcess* spellProcess = 0; + spellProcess = GetZone()->GetSpellProcess(); + spellProcess->CastPassives(spell, this, true); + + if (remove_from_list) { + vector::iterator remove; + remove = find(passive_spells.begin(), passive_spells.end(), id); + if (remove != passive_spells.end()) + passive_spells.erase(remove); + } + + database.DeleteCharacterSpell(GetCharacterID(), spell->GetSpellID()); + } +} + +void Player::RemoveAllPassives() +{ + vector::iterator itr; + for (itr = passive_spells.begin(); itr != passive_spells.end(); itr++) + RemoveSpellBookEntry((*itr), false); + + passive_spells.clear(); +} + +void Player::ResetPetInfo() { + GetInfoStruct()->set_pet_id(0xFFFFFFFF); + GetInfoStruct()->set_pet_movement(0); + GetInfoStruct()->set_pet_behavior(0); + GetInfoStruct()->set_pet_health_pct(0.0f); + GetInfoStruct()->set_pet_power_pct(0.0f); + // Make sure the values get sent to the client + SetCharSheetChanged(true); +} + +bool Player::HasRecipeBook(int32 recipe_id){ + return recipebook_list.HasRecipeBook(recipe_id); +} + +bool Player::DiscoveredLocation(int32 locationID) { + bool ret = false; + + // No discovery type entry then return false + if (m_characterHistory.count(HISTORY_TYPE_DISCOVERY) == 0) + return false; + + // Is a discovery type entry but not a location subtype return false + if (m_characterHistory[HISTORY_TYPE_DISCOVERY].count(HISTORY_SUBTYPE_LOCATION) == 0) + return false; + + vector::iterator itr; + + for (itr = m_characterHistory[HISTORY_TYPE_DISCOVERY][HISTORY_SUBTYPE_LOCATION].begin(); itr != m_characterHistory[HISTORY_TYPE_DISCOVERY][HISTORY_SUBTYPE_LOCATION].end(); itr++) { + if ((*itr)->Value == locationID) { + ret = true; + break; + } + } + + return ret; +} + +void Player::UpdatePlayerHistory(int8 type, int8 subtype, int32 value, int32 value2) { + switch (type) { + case HISTORY_TYPE_NONE: + HandleHistoryNone(subtype, value, value2); + break; + case HISTORY_TYPE_DEATH: + HandleHistoryDeath(subtype, value, value2); + break; + case HISTORY_TYPE_DISCOVERY: + HandleHistoryDiscovery(subtype, value, value2); + break; + case HISTORY_TYPE_XP: + HandleHistoryXP(subtype, value, value2); + break; + default: + // Not a valid history event so return out before trying to save into the db + return; + } +} + +void Player::HandleHistoryNone(int8 subtype, int32 value, int32 value2) { +} + +void Player::HandleHistoryDeath(int8 subtype, int32 value, int32 value2) { +} + +void Player::HandleHistoryDiscovery(int8 subtype, int32 value, int32 value2) { + switch (subtype) { + case HISTORY_SUBTYPE_NONE: + break; + case HISTORY_SUBTYPE_ADVENTURE: + break; + case HISTORY_SUBTYPE_TRADESKILL: + break; + case HISTORY_SUBTYPE_QUEST: + break; + case HISTORY_SUBTYPE_AA: + break; + case HISTORY_SUBTYPE_ITEM: + break; + case HISTORY_SUBTYPE_LOCATION: { + HistoryData* hd = new HistoryData; + hd->Value = value; + hd->Value2 = value2; + hd->EventDate = Timer::GetUnixTimeStamp(); + strcpy(hd->Location, GetZone()->GetZoneName()); + hd->needs_save = true; + + m_characterHistory[HISTORY_TYPE_DISCOVERY][HISTORY_SUBTYPE_LOCATION].push_back(hd); + break; + } + default: + // Invalid subtype, default to NONE + break; + } +} + +void Player::HandleHistoryXP(int8 subtype, int32 value, int32 value2) { + switch (subtype) { + case HISTORY_SUBTYPE_NONE: + break; + case HISTORY_SUBTYPE_ADVENTURE: { + HistoryData* hd = new HistoryData; + hd->Value = value; + hd->Value2 = value2; + hd->EventDate = Timer::GetUnixTimeStamp(); + strcpy(hd->Location, GetZone()->GetZoneName()); + hd->needs_save = true; + + m_characterHistory[HISTORY_TYPE_XP][HISTORY_SUBTYPE_ADVENTURE].push_back(hd); + } + break; + case HISTORY_SUBTYPE_TRADESKILL: + break; + case HISTORY_SUBTYPE_QUEST: + break; + case HISTORY_SUBTYPE_AA: + break; + case HISTORY_SUBTYPE_ITEM: + break; + case HISTORY_SUBTYPE_LOCATION: + break; + default: + // Invalid subtype, default to NONE + break; + } +} + +void Player::LoadPlayerHistory(int8 type, int8 subtype, HistoryData* hd) { + m_characterHistory[type][subtype].push_back(hd); +} + +void Player::SaveHistory() { + LogWrite(PLAYER__DEBUG, 0, "Player", "Saving History for Player: '%s'", GetName()); + + map > >::iterator itr; + map >::iterator itr2; + vector::iterator itr3; + for (itr = m_characterHistory.begin(); itr != m_characterHistory.end(); itr++) { + for (itr2 = itr->second.begin(); itr2 != itr->second.end(); itr2++) { + for (itr3 = itr2->second.begin(); itr3 != itr2->second.end(); itr3++) { + + if((*itr3)->needs_save) { + database.SaveCharacterHistory(this, itr->first, itr2->first, (*itr3)->Value, (*itr3)->Value2, (*itr3)->Location, (*itr3)->EventDate); + (*itr3)->needs_save = false; + } + } + } + } +} + +void Player::InitXPTable() { + int i = 2; + while (i >= 2 && i <= 95) { + m_levelXPReq[i] = database.GetMysqlExpCurve(i); + i++; + } +} + +void Player::SendQuestRequiredSpawns(int32 quest_id){ + bool locked = true; + m_playerSpawnQuestsRequired.readlock(__FUNCTION__, __LINE__); + if (player_spawn_quests_required.size() > 0 ) { + ZoneServer* zone = GetZone(); + Client* client = GetClient(); + if (!client){ + m_playerSpawnQuestsRequired.releasereadlock(__FUNCTION__, __LINE__); + return; + } + int xxx = player_spawn_quests_required.count(quest_id); + if (player_spawn_quests_required.count(quest_id) > 0){ + vector spawns = *player_spawn_quests_required[quest_id]; + m_playerSpawnQuestsRequired.releasereadlock(__FUNCTION__, __LINE__); + Spawn* spawn = 0; + vector::iterator itr; + for (itr = spawns.begin(); itr != spawns.end();){ + spawn = zone->GetSpawnByID(*itr); + if (spawn) + zone->SendSpawnChanges(spawn, client, false, true); + else { + itr = spawns.erase(itr); + continue; + } + itr++; + } + locked = false; + } + } + if (locked) + m_playerSpawnQuestsRequired.releasereadlock(__FUNCTION__, __LINE__); +} + +void Player::SendHistoryRequiredSpawns(int32 event_id){ + bool locked = true; + m_playerSpawnHistoryRequired.readlock(__FUNCTION__, __LINE__); + if (player_spawn_history_required.size() > 0) { + ZoneServer* zone = GetZone(); + Client* client = GetClient(); + if (!client){ + m_playerSpawnHistoryRequired.releasereadlock(__FUNCTION__, __LINE__); + return; + } + if (player_spawn_history_required.count(event_id) > 0){ + vector spawns = *player_spawn_history_required[event_id]; + m_playerSpawnHistoryRequired.releasereadlock(__FUNCTION__, __LINE__); + Spawn* spawn = 0; + vector::iterator itr; + for (itr = spawns.begin(); itr != spawns.end();){ + spawn = zone->GetSpawnByID(*itr); + if (spawn) + zone->SendSpawnChanges(spawn, client, false, true); + else { + itr = spawns.erase(itr); + continue; + } + itr++; + } + locked = false; + } + } + if (locked) + m_playerSpawnHistoryRequired.releasereadlock(__FUNCTION__, __LINE__); +} + +void Player::AddQuestRequiredSpawn(Spawn* spawn, int32 quest_id){ + if(!spawn || !quest_id) + return; + m_playerSpawnQuestsRequired.writelock(__FUNCTION__, __LINE__); + if(player_spawn_quests_required.count(quest_id) == 0) + player_spawn_quests_required[quest_id] = new vector; + vector* quest_spawns = player_spawn_quests_required[quest_id]; + int32 current_spawn = 0; + for(int32 i=0;isize();i++){ + current_spawn = quest_spawns->at(i); + if (current_spawn == spawn->GetID()){ + m_playerSpawnQuestsRequired.releasewritelock(__FUNCTION__, __LINE__); + return; + } + } + player_spawn_quests_required[quest_id]->push_back(spawn->GetID()); + m_playerSpawnQuestsRequired.releasewritelock(__FUNCTION__, __LINE__); +} + +void Player::AddHistoryRequiredSpawn(Spawn* spawn, int32 event_id){ + if (!spawn || !event_id) + return; + m_playerSpawnHistoryRequired.writelock(__FUNCTION__, __LINE__); + if (player_spawn_history_required.count(event_id) == 0) + player_spawn_history_required[event_id] = new vector; + vector* history_spawns = player_spawn_history_required[event_id]; + vector::iterator itr = find(history_spawns->begin(), history_spawns->end(), spawn->GetID()); + if (itr == history_spawns->end()) + history_spawns->push_back(spawn->GetID()); + m_playerSpawnHistoryRequired.releasewritelock(__FUNCTION__, __LINE__); +} + +int32 PlayerInfo::GetBoatSpawn(){ + return boat_spawn; +} + +void PlayerInfo::SetBoatSpawn(Spawn* spawn){ + if(spawn) + boat_spawn = spawn->GetID(); + else + boat_spawn = 0; +} + +void PlayerInfo::RemoveOldPackets(){ + safe_delete_array(changes); + safe_delete_array(orig_packet); + safe_delete_array(pet_changes); + safe_delete_array(pet_orig_packet); + changes = 0; + orig_packet = 0; + pet_changes = 0; + pet_orig_packet = 0; +} + +PlayerControlFlags::PlayerControlFlags(){ + MControlFlags.SetName("PlayerControlFlags::MControlFlags"); + MFlagChanges.SetName("PlayerControlFlags::MFlagChanges"); + flags_changed = false; + flag_changes.clear(); + current_flags.clear(); +} + +PlayerControlFlags::~PlayerControlFlags(){ + flag_changes.clear(); + current_flags.clear(); +} + +bool PlayerControlFlags::ControlFlagsChanged(){ + bool ret; + MFlagChanges.writelock(__FUNCTION__, __LINE__); + ret = flags_changed; + MFlagChanges.releasewritelock(__FUNCTION__, __LINE__); + return ret; +} + +void PlayerControlFlags::SetPlayerControlFlag(int8 param, int8 param_value, bool is_active){ + if (!param || !param_value) + return; + + bool active_changed = false; + MControlFlags.writelock(__FUNCTION__, __LINE__); + active_changed = (current_flags[param][param_value] != is_active); + if (active_changed){ + current_flags[param][param_value] = is_active; + MFlagChanges.writelock(__FUNCTION__, __LINE__); + flag_changes[param][param_value] = is_active ? 1 : 0; + flags_changed = true; + MFlagChanges.releasewritelock(__FUNCTION__, __LINE__); + } + MControlFlags.releasewritelock(__FUNCTION__, __LINE__); +} + +void PlayerControlFlags::SendControlFlagUpdates(Client* client){ + if (!client) + return; + + map* ptr = 0; + map >::iterator itr; + map::iterator itr2; + + MFlagChanges.writelock(__FUNCTION__, __LINE__); + for (itr = flag_changes.begin(); itr != flag_changes.end(); itr++){ + ptr = &itr->second; + for (itr2 = ptr->begin(); itr2 != ptr->end(); itr2++){ + int32 param = itr2->first; + if(client->GetVersion() <= 561) { + if(itr->first == 1) { // first set of flags DoF only supports these + bool skip = false; + switch(itr2->first) { + case 1: // flymode for DoF + case 2: // no clip mode for DoF + case 4: // we don't know + case 32: { // safe fall (DoF is low gravity for this parameter) + skip = true; + break; + } + } + + if(skip) { + continue; + } + } + + bool bypassFlag = true; + // remap control effects to old DoF from AoM + switch(itr->first) { + case 4: { + if(itr2->first == 64) { // stun + ClientPacketFunctions::SendServerControlFlagsClassic(client, 8, itr2->second); + param = 16; + bypassFlag = false; + } + break; + } + } + if(itr->first > 1 && bypassFlag) { + continue; // we don't support flag sets higher than 1 for DoF + } + ClientPacketFunctions::SendServerControlFlagsClassic(client, param, itr2->second); + } + else { + ClientPacketFunctions::SendServerControlFlags(client, itr->first, itr2->first, itr2->second); + } + } + } + flag_changes.clear(); + flags_changed = false; + MFlagChanges.releasewritelock(__FUNCTION__, __LINE__); +} + +bool Player::ControlFlagsChanged(){ + return control_flags.ControlFlagsChanged(); +} + +void Player::SetPlayerControlFlag(int8 param, int8 param_value, bool is_active){ + control_flags.SetPlayerControlFlag(param, param_value, is_active); +} + +void Player::SendControlFlagUpdates(Client* client){ + control_flags.SendControlFlagUpdates(client); +} + +void Player::LoadLUAHistory(int32 event_id, LUAHistory* history) { + mLUAHistory.writelock(); + if (m_charLuaHistory.count(event_id) > 0) { + LogWrite(PLAYER__ERROR, 0, "Player", "Attempted to added a dupicate event (%u) to character LUA history", event_id); + safe_delete(history); + mLUAHistory.releasewritelock(); + return; + } + + m_charLuaHistory.insert(make_pair(event_id,history)); + mLUAHistory.releasewritelock(); +} + +void Player::SaveLUAHistory() { + mLUAHistory.readlock(); + LogWrite(PLAYER__DEBUG, 0, "Player", "Saving LUA History for Player: '%s'", GetName()); + + map::iterator itr; + for (itr = m_charLuaHistory.begin(); itr != m_charLuaHistory.end(); itr++) { + if (itr->second->SaveNeeded) { + database.SaveCharacterLUAHistory(this, itr->first, itr->second->Value, itr->second->Value2); + itr->second->SaveNeeded = false; + } + } + mLUAHistory.releasereadlock(); +} + +void Player::UpdateLUAHistory(int32 event_id, int32 value, int32 value2) { + mLUAHistory.writelock(); + LUAHistory* hd = 0; + + if (m_charLuaHistory.count(event_id) > 0) + hd = m_charLuaHistory[event_id]; + else { + hd = new LUAHistory; + m_charLuaHistory.insert(make_pair(event_id,hd)); + } + + hd->Value = value; + hd->Value2 = value2; + hd->SaveNeeded = true; + mLUAHistory.releasewritelock(); + // release the mLUAHistory lock, we will maintain a readlock to avoid any further writes until we complete SendHistoryRequiredSpawns + // through Spawn::SendSpawnChanges -> Spawn::InitializeVisPacketData -> Spawn::MeetsSpawnAccessRequirements-> Player::GetLUAHistory (this was causing a deadlock) + mLUAHistory.readlock(); + SendHistoryRequiredSpawns(event_id); + mLUAHistory.releasereadlock(); +} + +LUAHistory* Player::GetLUAHistory(int32 event_id) { + LUAHistory* ret = 0; + + mLUAHistory.readlock(); + + if (m_charLuaHistory.count(event_id) > 0) + ret = m_charLuaHistory[event_id]; + + mLUAHistory.releasereadlock(); + + return ret; +} + +bool Player::CanSeeInvis(Entity* target) +{ + if (!target->IsStealthed() && !target->IsInvis()) + return true; + if (target->IsStealthed() && HasSeeHideSpell()) + return true; + else if (target->IsInvis() && HasSeeInvisSpell()) + return true; + + sint32 radius = rule_manager.GetZoneRule(GetZoneID(), R_PVP, InvisPlayerDiscoveryRange)->GetSInt32(); + + if (radius == 0) // radius of 0 is always seen + return true; + // radius of -1 is never seen except through items/spells, radius > -1 means we will show the player if they get into the inner radius + else if (radius > -1 && this->GetDistance((Spawn*)target) < (float)radius) + return true; + + // TODO: Implement See Invis Spells! http://cutpon.com:3000/devn00b/EQ2EMu/issues/43 + + Item* item = 0; + vector* equipped_list = GetEquippedItemList(); + bool seeInvis = false; + bool seeStealth = false; + for (int32 i = 0; i < equipped_list->size(); i++) + { + item = equipped_list->at(i); + seeInvis = item->HasStat(ITEM_STAT_SEEINVIS); + seeStealth = item->HasStat(ITEM_STAT_SEESTEALTH); + if (target->IsStealthed() && seeStealth) + return true; + else if (target->IsInvis() && seeInvis) + return true; + } + + return false; +} + +// returns true if we need to update target info due to see invis status change +bool Player::CheckChangeInvisHistory(Entity* target) +{ + std::map::iterator it; + + it = target_invis_history.find(target->GetID()); + if (it != target_invis_history.end()) + { + //canSeeStatus + if (it->second) + { + if (!this->CanSeeInvis(target)) + { + UpdateTargetInvisHistory(target->GetID(), false); + return true; + } + else + return false; + } + else + { + if (this->CanSeeInvis(target)) + { + UpdateTargetInvisHistory(target->GetID(), true); + return true; + } + else + return false; + } + } + + if (!this->CanSeeInvis(target)) + UpdateTargetInvisHistory(target->GetID(), false); + else + UpdateTargetInvisHistory(target->GetID(), true); + + return true; +} + +void Player::UpdateTargetInvisHistory(int32 targetID, bool canSeeStatus) +{ + target_invis_history[targetID] = canSeeStatus; +} + +void Player::RemoveTargetInvisHistory(int32 targetID) +{ + target_invis_history.erase(targetID); +} + +int16 Player::GetNextSpawnIndex(Spawn* spawn, bool set_lock) +{ + if(set_lock) + index_mutex.writelock(__FUNCTION__, __LINE__); + int16 next_index = 0; + int16 max_count = 0; + bool not_found = true; + do { + next_index = (spawn_index++); + max_count++; + if(max_count > 0xFFFE) { + LogWrite(PLAYER__ERROR, 0, "Player", "%s: This is bad we ran out of spawn indexes!", GetName()); + break; + } + if(next_index == 1 && spawn != this) { // only self can occupy index 1 + continue; + } + if(next_index == 0 || next_index == 255) { // avoid 0 and overloads (255) + continue; + } + Spawn* tmp_spawn = nullptr; + if(player_spawn_id_map.count(next_index) > 0) + tmp_spawn = player_spawn_id_map[next_index]; + + if(tmp_spawn && tmp_spawn != spawn) { // spawn index already taken and it is not this spawn + continue; + } + not_found = false; + } + while(not_found); + + if(set_lock) + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + + return next_index; +} + +bool Player::SetSpawnMap(Spawn* spawn) +{ + if(!client->GetPlayer()->SetSpawnSentState(spawn, SpawnState::SPAWN_STATE_SENDING)) { + return false; + } + + index_mutex.writelock(__FUNCTION__, __LINE__); + int32 tmp_id = GetNextSpawnIndex(spawn, false); + + player_spawn_id_map[tmp_id] = spawn; + + if(player_spawn_reverse_id_map.count(spawn)) + player_spawn_reverse_id_map.erase(spawn); + + player_spawn_reverse_id_map.insert(make_pair(spawn,tmp_id)); + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + return true; +} + +int16 Player::SetSpawnMapAndIndex(Spawn* spawn) +{ + index_mutex.writelock(__FUNCTION__, __LINE__); + int32 new_index = GetNextSpawnIndex(spawn, false); + + player_spawn_id_map[new_index] = spawn; + player_spawn_reverse_id_map[spawn] = new_index; + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + + return new_index; +} + +NPC* Player::InstantiateSpiritShard(float origX, float origY, float origZ, float origHeading, int32 origGridID, ZoneServer* origZone) +{ + NPC* npc = new NPC(); + string newName(GetName()); + newName.append("'s spirit shard"); + + strcpy(npc->appearance.name, newName.c_str()); + /*vector* primary_command_list = zone->GetEntityCommandList(result.GetInt32(9)); + vector* secondary_command_list = zone->GetEntityCommandList(result.GetInt32(10)); + if(primary_command_list){ + npc->SetPrimaryCommands(primary_command_list); + npc->primary_command_list_id = result.GetInt32(9); + } + if(secondary_command_list){ + npc->SetSecondaryCommands(secondary_command_list); + npc->secondary_command_list_id = result.GetInt32(10); + }*/ + npc->appearance.level = GetLevel(); + npc->appearance.race = GetRace(); + npc->appearance.gender = GetGender(); + npc->appearance.adventure_class = GetAdventureClass(); + + npc->appearance.model_type = GetModelType(); + npc->appearance.soga_model_type = GetSogaModelType(); + npc->appearance.display_name = 1; + npc->features.hair_type = GetHairType(); + npc->features.hair_face_type = GetFacialHairType(); + npc->features.wing_type = GetWingType(); + npc->features.chest_type = GetChestType(); + npc->features.legs_type = GetLegsType(); + npc->features.soga_hair_type = GetSogaHairType(); + npc->features.soga_hair_face_type = GetSogaFacialHairType(); + npc->appearance.attackable = 0; + npc->appearance.show_level = 1; + npc->appearance.targetable = 1; + npc->appearance.show_command_icon = 1; + npc->appearance.display_hand_icon = 0; + npc->appearance.hide_hood = GetHideHood(); + npc->size = GetSize(); + npc->appearance.pos.collision_radius = appearance.pos.collision_radius; + npc->appearance.action_state = appearance.action_state; + npc->appearance.visual_state = 6193; // ghostly look + npc->appearance.mood_state = appearance.mood_state; + npc->appearance.emote_state = appearance.emote_state; + npc->appearance.pos.state = appearance.pos.state; + npc->appearance.activity_status = appearance.activity_status; + strncpy(npc->appearance.sub_title, appearance.sub_title, sizeof(npc->appearance.sub_title)); + npc->SetPrefixTitle(GetPrefixTitle()); + npc->SetSuffixTitle(GetSuffixTitle()); + npc->SetLastName(GetLastName()); + npc->SetX(origX); + npc->SetY(origY); + npc->SetZ(origZ); + npc->SetHeading(origHeading); + npc->SetSpawnOrigX(origX); + npc->SetSpawnOrigY(origY); + npc->SetSpawnOrigZ(origZ); + npc->SetSpawnOrigHeading(origHeading); + npc->SetLocation(origGridID); + npc->SetAlive(false); + const char* script = rule_manager.GetGlobalRule(R_Combat, SpiritShardSpawnScript)->GetString(); + + int32 dbid = database.CreateSpiritShard(newName.c_str(), GetLevel(), GetRace(), GetGender(), GetAdventureClass(), GetModelType(), GetSogaModelType(), + GetHairType(), GetFacialHairType(), GetWingType(), GetChestType(), GetLegsType(), GetSogaHairType(), GetSogaFacialHairType(), GetHideHood(), + GetSize(), npc->appearance.pos.collision_radius, npc->appearance.action_state, npc->appearance.visual_state, npc->appearance.mood_state, + npc->appearance.emote_state, npc->appearance.pos.state, npc->appearance.activity_status, npc->appearance.sub_title, GetPrefixTitle(), GetSuffixTitle(), + GetLastName(), origX, origY, origZ, origHeading, origGridID, GetCharacterID(), origZone->GetZoneID(), origZone->GetInstanceID()); + + npc->SetShardID(dbid); + npc->SetShardCharID(GetCharacterID()); + npc->SetShardCreatedTimestamp(Timer::GetCurrentTime2()); + + if(script) + npc->SetSpawnScript(script); + + return npc; +} + +void Player::SaveCustomSpellFields(LuaSpell* luaspell) { + if (!luaspell || !luaspell->spell || !luaspell->spell->IsCopiedSpell()) + return; + + auto spell_data = luaspell->spell->GetSpellData(); + std::unordered_set modified_fields = luaspell->GetModifiedFieldsCopy(); + + Query savedEffects; + for (const std::string& field : modified_fields) { + auto it = SpellDataFieldAccessors.find(field); + if (it == SpellDataFieldAccessors.end()) + continue; + + const auto& [type, getter] = it->second; + std::string value = getter(spell_data); + + std::string type_str; + switch (type) { + case SpellFieldType::Integer: type_str = "int"; break; + case SpellFieldType::Float: type_str = "float"; break; + case SpellFieldType::Boolean: type_str = "bool"; break; + case SpellFieldType::String: type_str = "string"; break; + default: continue; + } + + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, "INSERT IGNORE INTO character_custom_spell_data (charid, spell_id, field, type, value) VALUES (%u, %u, '%s', '%s', '%s')", + GetCharacterID(), + luaspell->spell->GetSpellData()->inherited_spell_id, + database.getSafeEscapeString(field.c_str()).c_str(), + type_str.c_str(), + database.getSafeEscapeString(value.c_str()).c_str()); + } +} + + +void Player::SaveCustomSpellDataIndex(LuaSpell* luaspell) { + if (!luaspell || !luaspell->spell || !luaspell->spell->IsCopiedSpell()) + return; + + auto& vec = luaspell->spell->lua_data; + + Query savedEffects; + for (int i = 0; i < vec.size(); ++i) { + LUAData* data = vec[i]; + if (!data || !data->needs_db_save) + continue; + + std::string value1, value2, type; + switch (data->type) { + case 0: + value1 = std::to_string(data->int_value); + value2 = std::to_string(data->int_value2); + type = "int"; + break; + case 1: + value1 = std::to_string(data->float_value); + value2 = std::to_string(data->float_value2); + type = "float"; + break; + case 2: + value1 = data->bool_value ? "1" : "0"; + type = "bool"; + break; + case 3: + value1 = database.getSafeEscapeString(data->string_value.c_str()); + value2 = database.getSafeEscapeString(data->string_value2.c_str()); + type = "string"; + break; + default: + continue; + } + + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, "INSERT IGNORE INTO character_custom_spell_dataindex (charid, spell_id, idx, type, value1, value2) VALUES (%u, %u, %d, '%s', '%s', '%s')", GetCharacterID(), + luaspell->spell->GetSpellData()->inherited_spell_id, + i, + type.c_str(), value1.c_str(), value2.c_str()); + } +} + +void Player::SaveCustomSpellEffectsDisplay(LuaSpell* luaspell) { + if (!luaspell || !luaspell->spell || !luaspell->spell->IsCopiedSpell()) + return; + + auto& vec = luaspell->spell->effects; + + Query savedEffects; + for (int i = 0; i < vec.size(); ++i) { + SpellDisplayEffect* eff = vec[i]; + if (!eff || !eff->needs_db_save) + continue; + + std::string charid = std::to_string(GetCharacterID()); + + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, "INSERT IGNORE INTO character_custom_spell_display (charid, spell_id, idx, field, value) VALUES (%u, %u, %d, 'description', '%s')", + GetCharacterID(), luaspell->spell->GetSpellData()->inherited_spell_id, i, + database.getSafeEscapeString(eff->description.c_str()).c_str()); + + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, "INSERT IGNORE INTO character_custom_spell_display (charid, spell_id, idx, field, value) VALUES (%u, %u, %d, 'bullet', '%d')", + GetCharacterID(), luaspell->spell->GetSpellData()->inherited_spell_id, i, eff->subbullet); + + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, "INSERT IGNORE INTO character_custom_spell_display (charid, spell_id, idx, field, value) VALUES (%u, %u, %d, 'percentage', '%d')", + GetCharacterID(), luaspell->spell->GetSpellData()->inherited_spell_id, i, eff->percentage); + } +} +void Player::SaveSpellEffects() +{ + if(stop_save_spell_effects) + { + LogWrite(PLAYER__WARNING, 0, "Player", "%s: SaveSpellEffects called while player constructing / deconstructing!", GetName()); + return; + } + + SpellProcess* spellProcess = 0; + // Get the current zones spell process + spellProcess = GetZone()->GetSpellProcess(); + + Query savedEffects; + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_spell_effects where charid=%u", GetCharacterID()); + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_spell_effect_targets where caster_char_id=%u", GetCharacterID()); + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_custom_spell_dataindex where charid=%u", GetCharacterID()); + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_custom_spell_display where charid=%u", GetCharacterID()); + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_custom_spell_data where charid=%u", GetCharacterID()); + InfoStruct* info = GetInfoStruct(); + MMaintainedSpells.readlock(__FUNCTION__, __LINE__); + MSpellEffects.readlock(__FUNCTION__, __LINE__); + for(int i = 0; i < 45; i++) { + if(info->spell_effects[i].spell_id != 0xFFFFFFFF) + { + Spawn* spawn = nullptr; + int32 target_char_id = 0; + if(info->spell_effects[i].spell->initial_target_char_id != 0) + target_char_id = info->spell_effects[i].spell->initial_target_char_id; + else if((spawn = GetZone()->GetSpawnByID(info->spell_effects[i].spell->initial_target)) != nullptr && spawn->IsPlayer()) + target_char_id = ((Player*)spawn)->GetCharacterID(); + + int32 timestamp = 0xFFFFFFFF; + if(info->spell_effects[i].spell->spell->GetSpellData() && !info->spell_effects[i].spell->spell->GetSpellData()->duration_until_cancel) + timestamp = info->spell_effects[i].expire_timestamp - Timer::GetCurrentTime2(); + + int32 caster_char_id = info->spell_effects[i].spell->initial_caster_char_id; + + if(caster_char_id == 0) + continue; + + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, + "insert into character_spell_effects (name, caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, charid, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, has_damaged, custom_function, caster_level) values ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %f, %u, '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s', %u)", + database.getSafeEscapeString(info->spell_effects[i].spell->spell->GetName()).c_str(), caster_char_id, + target_char_id, 0 /*no target_type for spell_effects*/, DB_TYPE_SPELLEFFECTS /* db_effect_type for spell_effects */, info->spell_effects[i].spell->spell->IsCopiedSpell() ? info->spell_effects[i].spell->spell->GetSpellData()->inherited_spell_id : info->spell_effects[i].spell_id, i, info->spell_effects[i].spell->slot_pos, + info->spell_effects[i].icon, info->spell_effects[i].icon_backdrop, 0 /* no conc_used for spell_effects */, info->spell_effects[i].tier, + info->spell_effects[i].total_time, timestamp, database.getSafeEscapeString(info->spell_effects[i].spell->file_name.c_str()).c_str(), info->spell_effects[i].spell->spell->IsCopiedSpell(), GetCharacterID(), + info->spell_effects[i].spell->damage_remaining, info->spell_effects[i].spell->effect_bitmask, info->spell_effects[i].spell->num_triggers, info->spell_effects[i].spell->had_triggers, info->spell_effects[i].spell->cancel_after_all_triggers, + info->spell_effects[i].spell->crit, info->spell_effects[i].spell->last_spellattack_hit, info->spell_effects[i].spell->interrupted, info->spell_effects[i].spell->resisted, info->spell_effects[i].spell->has_damaged, (info->maintained_effects[i].expire_timestamp) == 0xFFFFFFFF ? "" : database.getSafeEscapeString(spellProcess->SpellScriptTimerCustomFunction(info->spell_effects[i].spell).c_str()).c_str(), info->spell_effects[i].spell->initial_caster_level); + + SaveCustomSpellFields(info->spell_effects[i].spell); + SaveCustomSpellDataIndex(info->spell_effects[i].spell); + SaveCustomSpellEffectsDisplay(info->spell_effects[i].spell); + } + if (i < NUM_MAINTAINED_EFFECTS && info->maintained_effects[i].spell && info->maintained_effects[i].spell_id != 0xFFFFFFFF){ + LogWrite(PLAYER__INFO, 0, "Player", "Saving slot %u maintained effect %u", i, info->maintained_effects[i].spell_id); + Spawn* spawn = GetZone()->GetSpawnByID(info->maintained_effects[i].spell->initial_target); + + int32 target_char_id = 0; + + if(info->maintained_effects[i].spell->initial_target_char_id != 0) + target_char_id = info->maintained_effects[i].spell->initial_target_char_id; + else if(!info->maintained_effects[i].spell->initial_target) + target_char_id = GetCharacterID(); + else if(spawn && spawn->IsPlayer()) + target_char_id = ((Player*)spawn)->GetCharacterID(); + else if (spawn && spawn->IsPet() && ((Entity*)spawn)->GetOwner() == (Entity*)this) + target_char_id = 0xFFFFFFFF; + + int32 caster_char_id = info->maintained_effects[i].spell->initial_caster_char_id; + + int32 timestamp = 0xFFFFFFFF; + if(info->maintained_effects[i].spell->spell->GetSpellData() && !info->maintained_effects[i].spell->spell->GetSpellData()->duration_until_cancel) + timestamp = info->maintained_effects[i].expire_timestamp - Timer::GetCurrentTime2(); + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, + "insert into character_spell_effects (name, caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, charid, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, has_damaged, custom_function, caster_level) values ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %f, %u, '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s', %u)", + database.getSafeEscapeString(info->maintained_effects[i].name).c_str(), caster_char_id, target_char_id, info->maintained_effects[i].target_type, DB_TYPE_MAINTAINEDEFFECTS /* db_effect_type for maintained_effects */, info->maintained_effects[i].spell->spell->IsCopiedSpell() ? info->maintained_effects[i].spell->spell->GetSpellData()->inherited_spell_id : info->maintained_effects[i].spell_id, i, info->maintained_effects[i].slot_pos, + info->maintained_effects[i].icon, info->maintained_effects[i].icon_backdrop, info->maintained_effects[i].conc_used, info->maintained_effects[i].tier, + info->maintained_effects[i].total_time, timestamp, database.getSafeEscapeString(info->maintained_effects[i].spell->file_name.c_str()).c_str(), info->maintained_effects[i].spell->spell->IsCopiedSpell(), GetCharacterID(), + info->maintained_effects[i].spell->damage_remaining, info->maintained_effects[i].spell->effect_bitmask, info->maintained_effects[i].spell->num_triggers, info->maintained_effects[i].spell->had_triggers, info->maintained_effects[i].spell->cancel_after_all_triggers, + info->maintained_effects[i].spell->crit, info->maintained_effects[i].spell->last_spellattack_hit, info->maintained_effects[i].spell->interrupted, info->maintained_effects[i].spell->resisted, info->maintained_effects[i].spell->has_damaged, (info->maintained_effects[i].expire_timestamp) == 0xFFFFFFFF ? "" : database.getSafeEscapeString(spellProcess->SpellScriptTimerCustomFunction(info->maintained_effects[i].spell).c_str()).c_str(), info->maintained_effects[i].spell->initial_caster_level); + + SaveCustomSpellFields(info->maintained_effects[i].spell); + SaveCustomSpellDataIndex(info->maintained_effects[i].spell); + SaveCustomSpellEffectsDisplay(info->maintained_effects[i].spell); + + std::string insertTargets = string("insert into character_spell_effect_targets (caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos) values "); + bool firstTarget = true; + map targetsInserted; + for (int32 id : info->maintained_effects[i].spell->GetTargets()) { + Spawn* spawn = GetZone()->GetSpawnByID(id); + LogWrite(SPELL__DEBUG, 0, "Spell", "%s has target %u to identify for spell %s", GetName(), spawn_id, info->maintained_effects[i].spell->spell->GetName()); + if(spawn && (spawn->IsPlayer() || spawn->IsPet())) + { + int32 tmpCharID = 0; + int8 type = 0; + + if(targetsInserted.find(spawn) != targetsInserted.end()) + continue; + + if(spawn->IsPlayer()) + tmpCharID = ((Player*)spawn)->GetCharacterID(); + else if (spawn->IsPet() && ((Entity*)spawn)->GetOwner() == (Entity*)this) + { + tmpCharID = 0xFFFFFFFF; + } + else if(spawn->IsPet() && ((Entity*)spawn)->GetOwner() && + ((Entity*)spawn)->GetOwner()->IsPlayer()) + { + type = ((Entity*)spawn)->GetPetType(); + Player* petOwner = (Player*)((Entity*)spawn)->GetOwner(); + tmpCharID = petOwner->GetCharacterID(); + } + + if(!firstTarget) + insertTargets.append(", "); + + targetsInserted.insert(make_pair(spawn, true)); + + + LogWrite(SPELL__DEBUG, 0, "Spell", "%s has target %s (%u) added to spell %s", GetName(), spawn ? spawn->GetName() : "NA", tmpCharID, info->maintained_effects[i].spell->spell->GetName()); + insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(tmpCharID) + ", " + std::to_string(type) + ", " + + std::to_string(DB_TYPE_MAINTAINEDEFFECTS) + ", " + std::to_string(info->maintained_effects[i].spell_id) + ", " + std::to_string(i) + + ", " + std::to_string(info->maintained_effects[i].slot_pos) + ")"); + firstTarget = false; + } + } + for (const auto& [char_id, pet_type] : info->maintained_effects[i].spell->GetCharIDTargets()) { + { + if(!firstTarget) + insertTargets.append(", "); + + LogWrite(SPELL__DEBUG, 0, "Spell", "%s has target (%u) added to spell %s", GetName(), char_id, info->maintained_effects[i].spell->spell->GetName()); + insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(char_id) + ", " + std::to_string(pet_type) + ", " + + std::to_string(DB_TYPE_MAINTAINEDEFFECTS) + ", " + std::to_string(info->maintained_effects[i].spell_id) + ", " + std::to_string(i) + + ", " + std::to_string(info->maintained_effects[i].slot_pos) + ")"); + + firstTarget = false; + } + if(!firstTarget) { + savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, insertTargets.c_str()); + } + } + } + } + MSpellEffects.releasereadlock(__FUNCTION__, __LINE__); + MMaintainedSpells.releasereadlock(__FUNCTION__, __LINE__); +} + +void Player::MentorTarget() +{ + if(client->GetPlayer()->GetGroupMemberInfo() && client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id) + { + client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id = 0; + reset_mentorship = true; + client->Message(CHANNEL_COMMAND_TEXT, "You stop mentoring, and return to level %u.", client->GetPlayer()->GetLevel()); + } + else if(!reset_mentorship && client->GetPlayer()->GetTarget()) + { + if(client->GetPlayer()->GetTarget()->IsPlayer()) + { + Player* tmpPlayer = (Player*)client->GetPlayer()->GetTarget(); + if(tmpPlayer->GetGroupMemberInfo() && tmpPlayer->GetGroupMemberInfo()->mentor_target_char_id) + { + client->Message(CHANNEL_COMMAND_TEXT, "You cannot mentor %s at this time.",tmpPlayer->GetName()); + return; + } + if(client->GetPlayer()->group_id > 0 && client->GetPlayer()->GetTarget()->group_id == client->GetPlayer()->group_id) + { + if(client->GetPlayer()->GetGroupMemberInfo() && !client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id && client->GetPlayer()->GetZone() == client->GetPlayer()->GetTarget()->GetZone() && client->GetPlayer()->GetTarget()->GetName() != client->GetPlayer()->GetName()) + { + SetMentorStats(client->GetPlayer()->GetTarget()->GetLevel(), tmpPlayer->GetCharacterID()); + client->Message(CHANNEL_COMMAND_TEXT, "You are now mentoring %s, reducing your effective level to %u.",client->GetPlayer()->GetTarget()->GetName(), client->GetPlayer()->GetTarget()->GetLevel()); + } + if(client->GetPlayer()->GetTarget()->GetName() == client->GetPlayer()->GetName()) { + client->Message(CHANNEL_COMMAND_TEXT, "You cannot mentor yourself."); + } + } + } + } +} + +void Player::SetMentorStats(int32 effective_level, int32 target_char_id, bool update_stats) +{ + if(update_stats) { + RemoveSpells(); + } + if(client->GetPlayer()->GetGroupMemberInfo()) + client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id = target_char_id; + InfoStruct* info = GetInfoStruct(); + info->set_effective_level(effective_level); + CalculatePlayerHPPower(effective_level); + client->GetPlayer()->CalculateBonuses(); + if(update_stats) { + client->GetPlayer()->SetHP(GetTotalHP()); + client->GetPlayer()->SetPower(GetTotalPower()); + } + /*info->set_agi_base(effective_level * 2 + 15); + info->set_intel_base(effective_level * 2 + 15); + info->set_wis_base(effective_level * 2 + 15); + info->set_str_base(effective_level * 2 + 15); + info->set_sta_base(effective_level * 2 + 15); + info->set_cold_base((int16)(effective_level * 1.5 + 10)); + info->set_heat_base((int16)(effective_level * 1.5 + 10)); + info->set_disease_base((int16)(effective_level * 1.5 + 10)); + info->set_mental_base((int16)(effective_level * 1.5 + 10)); + info->set_magic_base((int16)(effective_level * 1.5 + 10)); + info->set_divine_base((int16)(effective_level * 1.5 + 10)); + info->set_poison_base((int16)(effective_level * 1.5 + 10));*/ + GetClient()->ClearSentItemDetails(); + if(GetClient()) + { + EQ2Packet* app = GetEquipmentList()->serialize(GetClient()->GetVersion(), this); + if (app) { + GetClient()->QueuePacket(app); + } + } + GetEquipmentList()->SendEquippedItems(this); +} + +void Player::SetLevel(int16 level, bool setUpdateFlags) { + if(!GetGroupMemberInfo() || GetGroupMemberInfo()->mentor_target_char_id == 0) { + GetInfoStruct()->set_effective_level(level); + } + SetInfo(&appearance.level, level, setUpdateFlags); + SetXP(0); + SetNeededXP(); +} + +bool Player::SerializeItemPackets(EquipmentItemList* equipList, vector* packets, Item* item, int16 version, Item* to_item) { + if(item_list.AddItem(item)) { + item->save_needed = true; + SetEquippedItemAppearances(); + packets->push_back(equipList->serialize(version, this)); + packets->push_back(item->serialize(version, false)); + if(to_item) + packets->push_back(to_item->serialize(version, false, this)); + packets->push_back(item_list.serialize(this, version)); + return true; + } + else { + LogWrite(PLAYER__ERROR, 0, "Player", "failed to add item to item_list"); + } + return false; +} + +void Player::AddGMVisualFilter(int32 filter_type, int32 filter_value, char* filter_search_str, int16 visual_tag) { + if(MatchGMVisualFilter(filter_type, filter_value, filter_search_str) > 0) + return; + + vis_mutex.writelock(__FUNCTION__, __LINE__); + GMTagFilter filter; + filter.filter_type = filter_type; + filter.filter_value = filter_value; + memset(filter.filter_search_criteria, 0, sizeof(filter.filter_search_criteria)); + if(filter_search_str) + memcpy(&filter.filter_search_criteria, filter_search_str, strnlen(filter_search_str, sizeof(filter.filter_search_criteria))); + + filter.visual_tag = visual_tag; + gm_visual_filters.push_back(filter); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); +} + +int16 Player::MatchGMVisualFilter(int32 filter_type, int32 filter_value, char* filter_search_str, bool in_vismutex_lock) { + if(!in_vismutex_lock) + vis_mutex.readlock(__FUNCTION__, __LINE__); + int16 tag_id = 0; + vector::iterator itr = gm_visual_filters.begin(); + for(;itr != gm_visual_filters.end();itr++) { + if(itr->filter_type == filter_type && itr->filter_value == filter_value) { + if(filter_search_str && !strcasecmp(filter_search_str, itr->filter_search_criteria)) { + tag_id = itr->visual_tag; + break; + } + } + } + if(!in_vismutex_lock) + vis_mutex.releasereadlock(__FUNCTION__, __LINE__); + return tag_id; +} +void Player::ClearGMVisualFilters() { + vis_mutex.writelock(__FUNCTION__, __LINE__); + gm_visual_filters.clear(); + vis_mutex.releasewritelock(__FUNCTION__, __LINE__); +} + +int Player::GetPVPAlignment(){ + int bind_zone = GetPlayerInfo()->GetBindZoneID(); + int alignment = 0; + + if(bind_zone && bind_zone != 0){ + //0 is good. + //1 is evil. + //2 is neutral aka haven players. + switch(bind_zone){ + //good zones + case 114: //Gfay + case 221: //Qeynos Harbor + case 222: //North Qeynos + case 231: //South Qeynos + case 233: //Nettleville + case 234: //Starcrest + case 235: //Graystone + case 236: //CastleView + case 237: //Willowood + case 238: //Baubbleshire + case 470: //Frostfang + case 589: //Qeynos Combined 1 + case 660: //Qeynos Combined 2 + alignment = 0; //good + break; + //evil zones + case 128: //East Freeport + case 134: //Big Bend + case 135: //Stonestair + case 136: //Temple St. + case 137: //Beggars Ct. + case 138: //Longshadow + case 139: //Scale Yard + case 144: //North Freeport + case 166: //South Freeport + case 168: //West Freeport + case 184: //Neriak + case 644: //BigBend2 + case 645: //Stonestair2 + case 646: //Temple St2 + case 647: //Beggars Ct2 + case 648: //LongShadow2 + case 649: //Scale Yard2 + alignment = 1; //evil + break; + //Neutral (MajDul?) + case 45: //haven + case 46: //MajDul + alignment = 2; + break; + + default: + alignment = -1; //error + } + //return -1 (error), 0 (good), 1 (evil), or 2 (Neutral) + return alignment; + } + return -1; //error +} + +void Player::GetSpellBookSlotSort(int32 pattern, int32* i, int8* page_book_count, int32* last_start_point) { + switch(pattern) { + case 1: { // down + *i = (*i) + 2; + (*page_book_count)++; + if(*page_book_count > 3) { + if(((*i) % 2) == 0) { + (*i) = (*last_start_point) + 1; + } + else { + (*last_start_point) = (*last_start_point) + 8; + (*i) = (*last_start_point); + } + (*page_book_count) = 0; + } + break; + } + case 2: { // across + (*page_book_count)++; + switch(*page_book_count) { + case 1: + case 3: { + (*i)++; + break; + } + case 2: { + (*i) = (*i) + 7; + break; + } + case 4: { + (*last_start_point) = (*last_start_point) + 2; + (*i) = (*last_start_point); + (*page_book_count) = 0; + break; + } + } + break; + } + default: { // zig-zag + (*i)++; + break; + } + } +} + + +bool Player::IsSpawnInRangeList(int32 spawn_id) { + std::shared_lock lock(spawn_aggro_range_mutex); + map::iterator spawn_itr = player_aggro_range_spawns.find(spawn_id); + if(spawn_itr != player_aggro_range_spawns.end()) { + return spawn_itr->second; + } + return false; +} + +void Player::SetSpawnInRangeList(int32 spawn_id, bool in_range) { + std::unique_lock lock(spawn_aggro_range_mutex); + player_aggro_range_spawns[spawn_id] = in_range; +} + +void Player::ProcessSpawnRangeUpdates() { + std::unique_lock lock(spawn_aggro_range_mutex); + if(GetClient()->GetCurrentZone() == nullptr) { + return; + } + + map::iterator spawn_itr; + for(spawn_itr = player_aggro_range_spawns.begin(); spawn_itr != player_aggro_range_spawns.end();) { + if(spawn_itr->second) { + Spawn* spawn = GetClient()->GetCurrentZone()->GetSpawnByID(spawn_itr->first); + if(spawn && spawn->IsNPC() && (GetDistance(spawn)) > ((NPC*)spawn)->GetAggroRadius()) { + GetClient()->GetCurrentZone()->SendSpawnChanges((NPC*)spawn, GetClient(), true, true); + spawn_itr->second = false; + spawn_itr = player_aggro_range_spawns.erase(spawn_itr); + continue; + } + } + spawn_itr++; + } +} + +void Player::CalculatePlayerHPPower(int16 new_level) { + if(IsPlayer()) { + int16 effective_level = GetInfoStruct()->get_effective_level() != 0 ? GetInfoStruct()->get_effective_level() : GetLevel(); + if(new_level < 1) { + new_level = effective_level; + } + + float hp_rule_mod = rule_manager.GetGlobalRule(R_Player, StartHPLevelMod)->GetFloat(); + float power_rule_mod = rule_manager.GetGlobalRule(R_Player, StartPowerLevelMod)->GetFloat(); + + sint32 base_hp = rule_manager.GetGlobalRule(R_Player, StartHPBase)->GetFloat(); + sint32 base_power = rule_manager.GetGlobalRule(R_Player, StartPowerBase)->GetSInt32(); + + sint32 new_hp = (sint32)((float)new_level * (float)new_level * hp_rule_mod + base_hp); + sint32 new_power = (sint32)((float)new_level * (float)new_level * power_rule_mod + base_power); + + if(new_hp < 1) { + LogWrite(PLAYER__WARNING, 0, "Player", "Player HP Calculation for %s too low at level %u due to ruleset, StartPowerLevelMod %f, BasePower %i", GetName(), new_level, hp_rule_mod, base_hp); + new_hp = 1; + } + if(new_power < 1) { + LogWrite(PLAYER__WARNING, 0, "Player", "Player Power Calculations for %s too low at level %u due to ruleset, StartPowerLevelMod %f, BasePower %i", GetName(), new_level, power_rule_mod, base_power); + new_power = 1; + } + + SetTotalHPBase(new_hp); + SetTotalHPBaseInstance(new_hp); // we need the hp base to override the instance as the new default + + SetTotalPowerBase(new_power); + SetTotalPowerBaseInstance(new_power); // we need the hp base to override the instance as the new default + + LogWrite(PLAYER__INFO, 0, "Player", "Player %s: Level %u, Set Base HP %i, Set Base Power: %i", GetName(), new_level, new_hp, new_power); + } +} + +bool Player::IsAllowedCombatEquip(int8 slot, bool send_message) { + bool rule_pass = true; + if(EngagedInCombat() && rule_manager.GetZoneRule(GetZoneID(), R_Player, AllowPlayerEquipCombat)->GetInt8() == 0) { + switch(slot) { + case EQ2_PRIMARY_SLOT: + case EQ2_SECONDARY_SLOT: + case EQ2_RANGE_SLOT: + case EQ2_AMMO_SLOT: { + // good to go! + break; + } + default: { + if(send_message && GetClient()) { + GetClient()->SimpleMessage(CHANNEL_COLOR_RED, "You may not unequip/equip items while in combat."); + } + rule_pass = false; + break; + } + } + } + return rule_pass; +} + +void Player::SetActiveFoodUniqueID(int32 unique_id, bool update_db) { + active_food_unique_id = unique_id; + if(update_db) { + database.insertCharacterProperty(client, CHAR_PROPERTY_SETACTIVEFOOD, (char*)std::to_string(unique_id).c_str()); + } +} + +void Player::SetActiveDrinkUniqueID(int32 unique_id, bool update_db) { + active_drink_unique_id = unique_id; + if(update_db) { + database.insertCharacterProperty(client, CHAR_PROPERTY_SETACTIVEDRINK, (char*)std::to_string(unique_id).c_str()); + } +} + \ No newline at end of file diff --git a/internal/Player.h b/internal/Player.h new file mode 100644 index 0000000..305fa58 --- /dev/null +++ b/internal/Player.h @@ -0,0 +1,1276 @@ +/* + EQ2Emulator: Everquest II Server Emulator + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + + This file is part of EQ2Emulator. + + EQ2Emulator 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, either version 3 of the License, or + (at your option) any later version. + + EQ2Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; 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 EQ2Emulator. If not, see . +*/ + +#ifndef __EQ2_PLAYER__ +#define __EQ2_PLAYER__ + +#include "Entity.h" +#include "Items/Items.h" +#include "Factions.h" +#include "Skills.h" +#include "Quests.h" +#include "MutexMap.h" +#include "Guilds/Guild.h" +#include "Collections/Collections.h" +#include "Recipes/Recipe.h" +#include "Titles.h" +#include "Languages.h" +#include "Achievements/Achievements.h" +#include "Traits/Traits.h" +#include +#include + +#define CF_COMBAT_EXPERIENCE_ENABLED 0 +#define CF_ENABLE_CHANGE_LASTNAME 1 +#define CF_FOOD_AUTO_CONSUME 2 +#define CF_DRINK_AUTO_CONSUME 3 +#define CF_AUTO_ATTACK 4 +#define CF_RANGED_AUTO_ATTACK 5 +#define CF_QUEST_EXPERIENCE_ENABLED 6 +#define CF_CHASE_CAMERA_MAYBE 7 +#define CF_100 8 +#define CF_200 9 +#define CF_IS_SITTING 10 /*CAN'T CAST OR ATTACK*/ +#define CF_800 11 +#define CF_ANONYMOUS 12 +#define CF_ROLEPLAYING 13 +#define CF_AFK 14 +#define CF_LFG 15 +#define CF_LFW 16 +#define CF_HIDE_HOOD 17 +#define CF_HIDE_HELM 18 +#define CF_SHOW_ILLUSION 19 +#define CF_ALLOW_DUEL_INVITES 20 +#define CF_ALLOW_TRADE_INVITES 21 +#define CF_ALLOW_GROUP_INVITES 22 +#define CF_ALLOW_RAID_INVITES 23 +#define CF_ALLOW_GUILD_INVITES 24 +#define CF_2000000 25 +#define CF_4000000 26 +#define CF_DEFENSE_SKILLS_AT_MAX_QUESTIONABLE 27 +#define CF_SHOW_GUILD_HERALDRY 28 +#define CF_SHOW_CLOAK 29 +#define CF_IN_PVP 30 +#define CF_IS_HATED 31 +#define CF2_1 32 +#define CF2_2 33 +#define CF2_4 34 +#define CF2_ALLOW_LON_INVITES 35 +#define CF2_SHOW_RANGED 36 +#define CF2_ALLOW_VOICE_INVITES 37 +#define CF2_CHARACTER_BONUS_EXPERIENCE_ENABLED 38 +#define CF2_80 39 +#define CF2_100 40 /* hide achievments*/ +#define CF2_200 41 +#define CF2_400 42 +#define CF2_800 43 /* enable facebook updates*/ +#define CF2_1000 44 /* enable twitter updates*/ +#define CF2_2000 45 /* enable eq2 player updates */ +#define CF2_4000 46 /*eq2 players, link to alt chars */ +#define CF2_8000 47 +#define CF2_10000 48 +#define CF2_20000 49 +#define CF2_40000 50 +#define CF2_80000 51 +#define CF2_100000 52 +#define CF2_200000 53 +#define CF2_400000 54 +#define CF2_800000 55 +#define CF2_1000000 56 +#define CF2_2000000 57 +#define CF2_4000000 58 +#define CF2_8000000 59 +#define CF2_10000000 60 +#define CF2_20000000 61 +#define CF2_40000000 62 +#define CF2_80000000 63 +#define CF_MAXIMUM_FLAG 63 +#define CF_HIDE_STATUS 49 /* !!FORTESTING ONLY!! */ +#define CF_GM_HIDDEN 50 /* !!FOR TESTING ONLY!! */ + +#define UPDATE_ACTIVITY_FALLING 0 +#define UPDATE_ACTIVITY_RUNNING 128 +#define UPDATE_ACTIVITY_RIDING_BOAT 256 +#define UPDATE_ACTIVITY_JUMPING 1024 +#define UPDATE_ACTIVITY_IN_WATER_ABOVE 6144 +#define UPDATE_ACTIVITY_IN_WATER_BELOW 6272 +#define UPDATE_ACTIVITY_SITING 6336 +#define UPDATE_ACTIVITY_DROWNING 14464 +#define UPDATE_ACTIVITY_DROWNING2 14336 + + + +#define UPDATE_ACTIVITY_FALLING_AOM 16384 +#define UPDATE_ACTIVITY_RIDING_BOAT_AOM 256 +#define UPDATE_ACTIVITY_RUNNING_AOM 16512 +#define UPDATE_ACTIVITY_JUMPING_AOM 17408 +#define UPDATE_ACTIVITY_MOVE_WATER_BELOW_AOM 22528 +#define UPDATE_ACTIVITY_MOVE_WATER_ABOVE_AOM 22656 +#define UPDATE_ACTIVITY_SITTING_AOM 22720 +#define UPDATE_ACTIVITY_DROWNING_AOM 30720 +#define UPDATE_ACTIVITY_DROWNING2_AOM 30848 + +#define NUM_MAINTAINED_EFFECTS 30 +#define NUM_SPELL_EFFECTS 45 + +/* Character History Type Defines */ +#define HISTORY_TYPE_NONE 0 +#define HISTORY_TYPE_DEATH 1 +#define HISTORY_TYPE_DISCOVERY 2 +#define HISTORY_TYPE_XP 3 + +/* Spell Status */ +#define SPELL_STATUS_QUEUE 4 +#define SPELL_STATUS_LOCK 66 + +/* Character History Sub Type Defines */ +#define HISTORY_SUBTYPE_NONE 0 +#define HISTORY_SUBTYPE_ADVENTURE 1 +#define HISTORY_SUBTYPE_TRADESKILL 2 +#define HISTORY_SUBTYPE_QUEST 3 +#define HISTORY_SUBTYPE_AA 4 +#define HISTORY_SUBTYPE_ITEM 5 +#define HISTORY_SUBTYPE_LOCATION 6 + +/// Character history data, should match the `character_history` table in the DB +struct HistoryData { + int32 Value; + int32 Value2; + char Location[200]; + int32 EventID; + int32 EventDate; + bool needs_save; +}; + +/// History set through the LUA system +struct LUAHistory { + int32 Value; + int32 Value2; + bool SaveNeeded; +}; + +struct SpellBookEntry{ + int32 spell_id; + int8 tier; + int32 type; + sint32 slot; + int32 recast_available; + int8 status; + int16 recast; + int32 timer; + bool save_needed; + bool in_use; + bool in_remiss; + Player* player; + bool visible; +}; + +struct GMTagFilter { + int32 filter_type; + int32 filter_value; + char filter_search_criteria[256]; + int16 visual_tag; +}; + +enum GMTagFilterType { + GMFILTERTYPE_NONE=0, + GMFILTERTYPE_FACTION=1, + GMFILTERTYPE_SPAWNGROUP=2, + GMFILTERTYPE_RACE=3, + GMFILTERTYPE_GROUNDSPAWN=4 +}; +enum SpawnState{ + SPAWN_STATE_NONE=0, + SPAWN_STATE_SENDING=1, + SPAWN_STATE_SENT_WAIT=2, + SPAWN_STATE_SENT=3, + SPAWN_STATE_REMOVING=4, + SPAWN_STATE_REMOVING_SLEEP=5, + SPAWN_STATE_REMOVED=6 +}; +#define QUICKBAR_NORMAL 1 +#define QUICKBAR_INV_SLOT 2 +#define QUICKBAR_MACRO 3 +#define QUICKBAR_TEXT_CMD 4 +#define QUICKBAR_ITEM 6 + +#define EXP_DISABLED_STATE 0 +#define EXP_ENABLED_STATE 1 +#define MELEE_COMBAT_STATE 16 +#define RANGE_COMBAT_STATE 32 + +struct QuickBarItem{ + bool deleted; + int32 hotbar; + int32 slot; + int32 type; + int16 icon; + int16 icon_type; + int32 id; + int8 tier; + int64 unique_id; + EQ2_16BitString text; +}; + +struct LoginAppearances { + bool deleted; + int16 equip_type; + int8 red; + int8 green; + int8 blue; + int8 h_red; + int8 h_green; + int8 h_blue; + bool update_needed; +}; + +struct SpawnQueueState { + Timer spawn_state_timer; + int16 index_id; +}; + +class PlayerLoginAppearance { +public: + PlayerLoginAppearance() { appearanceList = new map; } + ~PlayerLoginAppearance() { } + + void AddEquipmentToUpdate(int8 slot_id, LoginAppearances* equip) + { + //LoginAppearances data; + //data.equip_type = equip->equip_type; + //appearanceList[slot_id] = data; + } + + void DeleteEquipmentFromUpdate(int8 slot_id, LoginAppearances* equip) + { + //LoginAppearances data; + //data.deleted = equip->deleted; + //data.update_needed = true; + //appearanceList[slot_id] = data; + } + + void RemoveEquipmentUpdates() + { + appearanceList->clear(); + safe_delete(appearanceList); + } + +private: + map* appearanceList; +}; + + +struct InstanceData{ + int32 db_id; + int32 instance_id; + int32 zone_id; + int8 zone_instance_type; + string zone_name; + int32 last_success_timestamp; + int32 last_failure_timestamp; + int32 success_lockout_time; + int32 failure_lockout_time; +}; + +class CharacterInstances { +public: + CharacterInstances(); + ~CharacterInstances(); + + ///Adds an instance data to the player with the given data + ///The unique id for this record in the database + ///The id of the instance + ///The success timestamp + ///The failure timestamp + ///The lockout time, in secs, for completing the instance + ///The lockout time, in secs, for failing the instance + ///The id of the zone + ///The type of instance of the zone + ///The name of the zone + void AddInstance(int32 db_id, int32 instance_id, int32 last_success_timestamp, int32 last_failure_timestamp, int32 success_lockout_time, int32 failure_lockout_time, int32 zone_id, int8 zone_instancetype, string zone_name); + + ///Clears all instance data + void RemoveInstances(); + + ///Removes the instace with the given zone id + ///The zone id of the instance to remove + ///True if the instance was found and removed + bool RemoveInstanceByZoneID(int32 zone_id); + + ///Removes the instance with the given instance id + ///the instance id of the instance to remove + ///True if instance was found and removed + bool RemoveInstanceByInstanceID(int32 instance_id); + + ///Gets the instance with the given zone id + ///The zone id of the instance to get + ///InstanceData* of the instance record for the given zone id + InstanceData* FindInstanceByZoneID(int32 zone_id); + + ///Gets the instance with the given database id + ///The database id of the instance to get + ///InstanceData* of the instance record for the given database id + InstanceData* FindInstanceByDBID(int32 db_id); + + ///Gets the instance with the given instance id + ///The instance id of the instance to get + ///InstanceData* of the instance record for the given instance id + InstanceData* FindInstanceByInstanceID(int32 instance_id); + + ///Gets a list of all the lockout instances + vector GetLockoutInstances(); + + ///Gets a list of all the persistent instances + vector GetPersistentInstances(); + + ///Check the timers for the instances + ///player we are checking the timers for + void ProcessInstanceTimers(Player* player); + + ///Gets the total number of instances + int32 GetInstanceCount(); +private: + vector instanceList; + Mutex m_instanceList; +}; + +class Player; +struct PlayerGroup; +struct GroupMemberInfo; +struct Statistic; +struct Mail; +class PlayerInfo { +public: + ~PlayerInfo(); + PlayerInfo(Player* in_player); + + EQ2Packet* serialize(int16 version, int16 modifyPos = 0, int32 modifyValue = 0); + PacketStruct* serialize2(int16 version); + EQ2Packet* serialize3(PacketStruct* packet, int16 version); + EQ2Packet* serializePet(int16 version); + void CalculateXPPercentages(); + void CalculateTSXPPercentages(); + void SetHouseZone(int32 id); + void SetBindZone(int32 id); + void SetBindX(float x); + void SetBindY(float y); + void SetBindZ(float z); + void SetBindHeading(float heading); + void SetAccountAge(int32 days); + int32 GetHouseZoneID(); + int32 GetBindZoneID(); + float GetBindZoneX(); + float GetBindZoneY(); + float GetBindZoneZ(); + float GetBindZoneHeading(); + float GetBoatX() { return boat_x_offset; } + float GetBoatY() { return boat_y_offset; } + float GetBoatZ() { return boat_z_offset; } + int32 GetBoatSpawn(); + void SetBoatX(float x) { boat_x_offset = x; } + void SetBoatY(float y) { boat_y_offset = y; } + void SetBoatZ(float z) { boat_z_offset = z; } + void SetBoatSpawn(Spawn* boat); + void RemoveOldPackets(); + +private: + int32 house_zone_id; + int32 bind_zone_id; + float bind_x; + float bind_y; + float bind_z; + float bind_heading; + uchar* changes; + uchar* orig_packet; + uchar* pet_changes; + uchar* pet_orig_packet; + InfoStruct* info_struct; + Player* player; + float boat_x_offset; + float boat_y_offset; + float boat_z_offset; + int32 boat_spawn; +}; + +class PlayerControlFlags{ +public: + PlayerControlFlags(); + ~PlayerControlFlags(); + + void SetPlayerControlFlag(int8 param, int8 param_value, bool is_active); + bool ControlFlagsChanged(); + void SendControlFlagUpdates(Client* client); +private: + bool flags_changed; + map > flag_changes; + map > current_flags; + Mutex MControlFlags; + Mutex MFlagChanges; +}; + +class Player : public Entity{ +public: + Player(); + virtual ~Player(); + EQ2Packet* serialize(Player* player, int16 version); + //int8 GetMaxArtLevel(){ return info->GetInfo()->max_art_level; } + //int8 GetArtLevel(){ return info->GetInfo()->art_level; } + + Client* GetClient() { return client; } + void SetClient(Client* client) { this->client = client; } + PlayerInfo* GetPlayerInfo(); + void SetCharSheetChanged(bool val); + bool GetCharSheetChanged(); + void SetRaidSheetChanged(bool val); + bool GetRaidSheetChanged(); + void AddFriend(const char* name, bool save); + bool IsFriend(const char* name); + void RemoveFriend(const char* name); + map* GetFriends(); + void AddIgnore(const char* name, bool save); + bool IsIgnored(const char* name); + void RemoveIgnore(const char* name); + map* GetIgnoredPlayers(); + + // JA: POI Discoveries + map >* GetPlayerDiscoveredPOIs(); + void AddPlayerDiscoveredPOI(int32 location_id); + // + + EQ2Packet* Move(float x, float y, float z, int16 version, float heading = -1.0f); + + /*void SetMaxArtLevel(int8 new_max){ + max_art_level = new_max; + } + void SetArtLevel(int8 new_lvl){ + art_level = new_lvl; + }*/ + bool WasSentSpawn(int32 spawn_id); + bool IsSendingSpawn(int32 spawn_id); + bool IsRemovingSpawn(int32 spawn_id); + bool SetSpawnSentState(Spawn* spawn, SpawnState state); + void CheckSpawnStateQueue(); + void SetSideSpeed(float side_speed, bool updateFlags = true) { + SetPos(&appearance.pos.SideSpeed, side_speed, updateFlags); + } + float GetSideSpeed() { + return appearance.pos.SideSpeed; + } + void SetVertSpeed(float vert_speed, bool updateFlags = true) { + SetPos(&appearance.pos.VertSpeed, vert_speed, updateFlags); + } + float GetVertSpeed() { + return appearance.pos.VertSpeed; + } + + void SetClientHeading1(float heading, bool updateFlags = true) { + SetPos(&appearance.pos.ClientHeading1, heading, updateFlags); + } + float GetClientHeading1() { + return appearance.pos.ClientHeading1; + } + + void SetClientHeading2(float heading, bool updateFlags = true) { + SetPos(&appearance.pos.ClientHeading2, heading, updateFlags); + } + float GetClientHeading2() { + return appearance.pos.ClientHeading2; + } + + void SetClientPitch(float pitch, bool updateFlags = true) { + SetPos(&appearance.pos.ClientPitch, pitch, updateFlags); + } + float GetClientPitch() { + return appearance.pos.ClientPitch; + } + + int8 GetTutorialStep() { + return tutorial_step; + } + void SetTutorialStep(int8 val) { + tutorial_step = val; + } + void AddMaintainedSpell(LuaSpell* spell); + void AddSpellEffect(LuaSpell* spell, int32 override_expire_time = 0); + void RemoveMaintainedSpell(LuaSpell* spell); + void RemoveSpellEffect(LuaSpell* spell); + void AddQuickbarItem(int32 bar, int32 slot, int32 type, int16 icon, int16 icon_type, int32 id, int8 tier, int32 unique_id, const char* text, bool update = true); + void RemoveQuickbarItem(int32 bar, int32 slot, bool update = true); + void MoveQuickbarItem(int32 id, int32 new_slot); + void ClearQuickbarItems(); + PlayerItemList* GetPlayerItemList(); + PlayerItemList item_list; + PlayerSkillList skill_list; + Skill* GetSkillByName(const char* name, bool check_update = false); + Skill* GetSkillByID(int32 skill_id, bool check_update = false); + PlayerSkillList* GetSkills(); + bool DamageEquippedItems(int8 amount = 10, Client* client = 0); + vector EquipItem(int16 index, int16 version, int8 appearance_type, int8 slot_id = 255); + bool CanEquipItem(Item* item, int8 slot); + void SetEquippedItemAppearances(); + vector UnequipItem(int16 index, sint32 bag_id, int8 slot, int16 version, int8 appearance_type = 0, bool send_item_updates = true); + int16 ConvertSlotToClient(int8 slot, int16 version); + int16 ConvertSlotFromClient(int8 slot, int16 version); + int16 GetNumSlotsEquip(int16 version); + int8 GetMaxBagSlots(int16 version); + EQ2Packet* SwapEquippedItems(int8 slot1, int8 slot2, int16 version, int16 equiptype); + EQ2Packet* RemoveInventoryItem(int8 bag_slot, int8 slot); + EQ2Packet* SendInventoryUpdate(int16 version); + EQ2Packet* SendBagUpdate(int32 bag_unique_id, int16 version); + void SendQuestRequiredSpawns(int32 quest_id); + void SendHistoryRequiredSpawns(int32 event_id); + map* GetItemList(); + map* GetBankItemList(); + vector* GetEquippedItemList(); + vector* GetAppearanceEquippedItemList(); + Quest* SetStepComplete(int32 quest_id, int32 step); + Quest* AddStepProgress(int32 quest_id, int32 step, int32 progress); + int32 GetStepProgress(int32 quest_id, int32 step_id); + Quest* GetQuestByPositionID(int32 list_position_id); + bool AddItem(Item* item, AddItemType type = AddItemType::NOT_SET); + bool AddItemToBank(Item* item); + int16 GetSpellSlotMappingCount(); + int16 GetSpellPacketCount(); + Quest* GetQuest(int32 quest_id); + bool GetQuestStepComplete(int32 quest_id, int32 step_id); + int16 GetQuestStep(int32 quest_id); + int16 GetTaskGroupStep(int32 quest_id); + int8 GetSpellTier(int32 id); + void SetSpellStatus(Spell* spell, int8 status); + void RemoveSpellStatus(Spell* spell, int8 status); + EQ2Packet* GetSpellSlotMappingPacket(int16 version); + EQ2Packet* GetSpellBookUpdatePacket(int16 version); + EQ2Packet* GetRaidUpdatePacket(int16 version); + int32 GetCharacterID(); + void SetCharacterID(int32 new_id); + EQ2Packet* GetQuickbarPacket(int16 version); + vector* GetQuickbar(); + bool UpdateQuickbarNeeded(); + void ResetQuickbarNeeded(); + void set_character_flag(int flag); + void reset_character_flag(int flag); + void toggle_character_flag(int flag); + bool get_character_flag(int flag); + void AddCoins(int64 val); + bool RemoveCoins(int64 val); + /// Checks to see if the player has the given amount of coins + /// Amount of coins to check + /// True if the player has enough coins + bool HasCoins(int64 val); + void AddSkill(int32 skill_id, int16 current_val, int16 max_val, bool save_needed = false); + void RemovePlayerSkill(int32 skill_id, bool save = false); + void RemoveSkillFromDB(Skill* skill, bool save = false); + void AddSpellBookEntry(int32 spell_id, int8 tier, sint32 slot, int32 type, int32 timer, bool save_needed = false); + SpellBookEntry* GetSpellBookSpell(int32 spell_id); + vector* GetSpellsSaveNeeded(); + sint32 GetFreeSpellBookSlot(int32 type); + /// Get a vector of spell ids for all spells in the spell book for the given skill + /// The id of the skill to check + /// A vector of int32's of the spell id's + vector GetSpellBookSpellIDBySkill(int32 skill_id); + void UpdateInventory(int32 bag_id); + EQ2Packet* MoveInventoryItem(sint32 to_bag_id, int16 from_index, int8 new_slot, int8 charges, int8 appearance_type, bool* item_deleted, int16 version = 1); + bool IsPlayer(){ return true; } + MaintainedEffects* GetFreeMaintainedSpellSlot(); + MaintainedEffects* GetMaintainedSpell(int32 id, bool on_char_load = false); + MaintainedEffects* GetMaintainedSpellBySlot(int8 slot); + MaintainedEffects* GetMaintainedSpells(); + SpellEffects* GetFreeSpellEffectSlot(); + SpellEffects* GetSpellEffects(); + int32 GetCoinsCopper(); + int32 GetCoinsSilver(); + int32 GetCoinsGold(); + int32 GetCoinsPlat(); + int32 GetBankCoinsCopper(); + int32 GetBankCoinsSilver(); + int32 GetBankCoinsGold(); + int32 GetBankCoinsPlat(); + int32 GetStatusPoints(); + float GetXPVitality(); + float GetTSXPVitality(); + bool AdventureXPEnabled(); + bool TradeskillXPEnabled(); + void SetNeededXP(int32 val); + void SetNeededXP(); + static int32 GetNeededXPByLevel(int8 level); + void SetXP(int32 val); + void SetNeededTSXP(int32 val); + void SetNeededTSXP(); + void SetTSXP(int32 val); + int32 GetNeededXP(); + float GetXPDebt(); + int32 GetXP(); + int32 GetNeededTSXP(); + int32 GetTSXP(); + bool AddXP(int32 xp_amount); + bool AddTSXP(int32 xp_amount); + bool DoubleXPEnabled(); + float CalculateXP(Spawn* victim); + float CalculateTSXP(int8 level); + void CalculateOfflineDebtRecovery(int32 unix_timestamp); + void InCombat(bool val, bool range = false); + void PrepareIncomingMovementPacket(int32 len, uchar* data, int16 version, bool dead_window_sent = false); + uchar* GetMovementPacketData(){ + return movement_packet; + } + void AddSpawnInfoPacketForXOR(int32 spawn_id, uchar* packet, int16 packet_size); + uchar* GetSpawnInfoPacketForXOR(int32 spawn_id); + void AddSpawnVisPacketForXOR(int32 spawn_id, uchar* packet, int16 packet_size); + uchar* GetSpawnVisPacketForXOR(int32 spawn_id); + void AddSpawnPosPacketForXOR(int32 spawn_id, uchar* packet, int16 packet_size); + uchar* GetSpawnPosPacketForXOR(int32 spawn_id); + uchar* GetTempInfoPacketForXOR(); + uchar* GetTempVisPacketForXOR(); + uchar* GetTempPosPacketForXOR(); + uchar* SetTempInfoPacketForXOR(int16 size); + uchar* SetTempVisPacketForXOR(int16 size); + uchar* SetTempPosPacketForXOR(int16 size); + int32 GetTempInfoXorSize() { return info_xor_size; } + int32 GetTempVisXorSize() { return vis_xor_size; } + int32 GetTempPosXorSize() { return pos_xor_size; } + bool CheckPlayerInfo(); + void CalculateLocation(); + void SetSpawnDeleteTime(int32 id, int32 time); + int32 GetSpawnDeleteTime(int32 id); + void ClearRemovalTimers(); + void ClearEverything(); + bool IsResurrecting(); + void SetResurrecting(bool val); + int8 GetTSArrowColor(int8 level); + Spawn* GetSpawnByIndex(int16 index); + int16 GetIndexForSpawn(Spawn* spawn); + bool WasSpawnRemoved(Spawn* spawn); + void ResetSpawnPackets(int32 id); + void RemoveSpawn(Spawn* spawn, bool delete_spawn = true); + bool ShouldSendSpawn(Spawn* spawn); + Client* client = 0; + void SetLevel(int16 level, bool setUpdateFlags = true); + + Spawn* GetSpawnWithPlayerID(int32 id){ + Spawn* spawn = 0; + + index_mutex.readlock(__FUNCTION__, __LINE__); + if (player_spawn_id_map.count(id) > 0) + spawn = player_spawn_id_map[id]; + index_mutex.releasereadlock(__FUNCTION__, __LINE__); + return spawn; + } + int32 GetIDWithPlayerSpawn(Spawn* spawn){ + int32 id = 0; + + index_mutex.readlock(__FUNCTION__, __LINE__); + if (player_spawn_reverse_id_map.count(spawn) > 0) + id = player_spawn_reverse_id_map[spawn]; + index_mutex.releasereadlock(__FUNCTION__, __LINE__); + + return id; + } + + int16 GetNextSpawnIndex(Spawn* spawn, bool set_lock = true); + bool SetSpawnMap(Spawn* spawn); + + void SetSpawnMapIndex(Spawn* spawn, int32 index) + { + index_mutex.writelock(__FUNCTION__, __LINE__); + if (player_spawn_id_map.count(index)) + player_spawn_id_map[index] = spawn; + else + player_spawn_id_map[index] = spawn; + index_mutex.releasewritelock(__FUNCTION__, __LINE__); + } + + int16 SetSpawnMapAndIndex(Spawn* spawn); + + PacketStruct* GetQuestJournalPacket(bool all_quests, int16 version, int32 crc, int32 current_quest_id, bool updated = true); + void RemoveQuest(int32 id, bool delete_quest); + vector* CheckQuestsChatUpdate(Spawn* spawn); + vector* CheckQuestsItemUpdate(Item* item); + vector* CheckQuestsLocationUpdate(); + vector* CheckQuestsKillUpdate(Spawn* spawn,bool update = true); + bool HasQuestUpdateRequirement(Spawn* spawn); + vector* CheckQuestsSpellUpdate(Spell* spell); + void CheckQuestsCraftUpdate(Item* item, int32 qty); + void CheckQuestsHarvestUpdate(Item* item, int32 qty); + vector* CheckQuestsFailures(); + bool CheckQuestRemoveFlag(Spawn* spawn); + int8 CheckQuestFlag(Spawn* spawn); + bool UpdateQuestReward(int32 quest_id, QuestRewardData* qrd); + Quest* PendingQuestAcceptance(int32 quest_id, int32 item_id, bool* quest_exists); + bool AcceptQuestReward(int32 item_id, int32 selectable_item_id); + + bool SendQuestStepUpdate(int32 quest_id, int32 quest_step_id, bool display_quest_helper); + void SendQuest(int32 quest_id); + void UpdateQuestCompleteCount(int32 quest_id); + void GetQuestTemporaryRewards(int32 quest_id, std::vector* items); + void AddQuestTemporaryReward(int32 quest_id, int32 item_id, int16 item_count); + + bool CheckQuestRequired(Spawn* spawn); + void AddQuestRequiredSpawn(Spawn* spawn, int32 quest_id); + void AddHistoryRequiredSpawn(Spawn* spawn, int32 event_id); + int16 spawn_index; + int32 spawn_id; + int8 tutorial_step; + map*> player_spawn_quests_required; + map*> player_spawn_history_required; + Mutex m_playerSpawnQuestsRequired; + Mutex m_playerSpawnHistoryRequired; + bool HasQuestBeenCompleted(int32 quest_id); + int32 GetQuestCompletedCount(int32 quest_id); + void AddCompletedQuest(Quest* quest); + bool HasActiveQuest(int32 quest_id); + bool HasAnyQuest(int32 quest_id); + map pending_quests; + map player_quests; + map* GetPlayerQuests(); + map* GetCompletedPlayerQuests(); + void SetFactionValue(int32 faction_id, sint32 value){ + factions.SetFactionValue(faction_id, value); + } + PlayerFaction* GetFactions(){ + return &factions; + } + vector GetQuestIDs(); + map macro_icons; + + bool HasPendingLootItems(int32 id); + bool HasPendingLootItem(int32 id, int32 item_id); + vector* GetPendingLootItems(int32 id); + void RemovePendingLootItem(int32 id, int32 item_id); + void RemovePendingLootItems(int32 id); + void AddPendingLootItems(int32 id, vector* items); + int16 GetTierUp(int16 tier); + bool HasSpell(int32 spell_id, int8 tier = 255, bool include_higher_tiers = false, bool include_possible_scribe = false); + bool HasRecipeBook(int32 recipe_id); + void AddPlayerStatistic(int32 stat_id, sint32 stat_value, int32 stat_date); + void UpdatePlayerStatistic(int32 stat_id, sint32 stat_value, bool overwrite = false); + sint64 GetPlayerStatisticValue(int32 stat_id); + void WritePlayerStatistics(); + + + + //PlayerGroup* GetGroup(); + void SetGroup(PlayerGroup* group); + bool IsGroupMember(Entity* player); + void SetGroupInformation(PacketStruct* packet); + + + void ResetSavedSpawns(); + bool IsReturningFromLD(); + void SetReturningFromLD(bool val); + bool CheckLevelStatus(int16 new_level); + int16 GetLastMovementActivity(); + void DestroyQuests(); + string GetAwayMessage() const { return away_message; } + void SetAwayMessage(string val) { away_message = val; } + void SetRangeAttack(bool val); + bool GetRangeAttack(); + bool AddMail(Mail* mail); + MutexMap* GetMail(); + Mail* GetMail(int32 mail_id); + void DeleteMail(bool from_database = false); + void DeleteMail(int32 mail_id, bool from_database = false); + CharacterInstances* GetCharacterInstances() { return &character_instances; } + void SetIsTracking(bool val) { is_tracking = val; } + bool GetIsTracking() const { return is_tracking; } + void SetBiography(string new_biography) { biography = new_biography; } + string GetBiography() const { return biography; } + void SetPlayerAdventureClass(int8 new_class, bool set_by_gm_command = false); + void SetGuild(Guild* new_guild) { guild = new_guild; } + Guild* GetGuild() { return guild; } + void AddSkillBonus(int32 spell_id, int32 skill_id, float value); + SkillBonus* GetSkillBonus(int32 spell_id); + virtual void RemoveSkillBonus(int32 spell_id); + + virtual bool CanSeeInvis(Entity* target); + bool CheckChangeInvisHistory(Entity* target); + void UpdateTargetInvisHistory(int32 targetID, bool canSeeStatus); + void RemoveTargetInvisHistory(int32 targetID); + + bool HasFreeBankSlot(); + int8 FindFreeBankSlot(); + PlayerCollectionList * GetCollectionList() { return &collection_list; } + PlayerRecipeList * GetRecipeList() { return &recipe_list; } + PlayerRecipeBookList * GetRecipeBookList() { return &recipebook_list; } + PlayerAchievementList * GetAchievementList() { return &achievement_list; } + PlayerAchievementUpdateList * GetAchievementUpdateList() { return &achievement_update_list; } + void SetPendingCollectionReward(Collection *collection) { pending_collection_reward = collection; } + Collection * GetPendingCollectionReward() { return pending_collection_reward; } + void AddPendingSelectableItemReward(int32 source_id, Item* item) { + if (pending_selectable_item_rewards.count(source_id) == 0) + pending_selectable_item_rewards[source_id] = vector(); + pending_selectable_item_rewards[source_id].push_back(item); + } + void AddPendingItemReward(Item* item) { + pending_item_rewards.push_back(item); + } + bool HasPendingItemRewards() { return (pending_item_rewards.size() > 0 || pending_selectable_item_rewards.size() > 0); } + vector GetPendingItemRewards() { return pending_item_rewards; } + map GetPendingSelectableItemReward(int32 item_id) { //since the client sends the selected item id, we need to have the associated source and remove all of them. Yes, there is an edge case if multiple sources have the same Item in them, but limited on what the client sends (just a single item id) + map ret; + if (pending_selectable_item_rewards.size() > 0) { + map>::iterator map_itr; + for (map_itr = pending_selectable_item_rewards.begin(); map_itr != pending_selectable_item_rewards.end(); map_itr++) { + vector::iterator itr; + for (itr = map_itr->second.begin(); itr != map_itr->second.end(); itr++) { + if ((*itr)->details.item_id == item_id) { + ret[map_itr->first] = *itr; + break; + } + } + if (ret.size() > 0) + break; + } + } + return map(); + } + void ClearPendingSelectableItemRewards(int32 source_id, bool all = false) { + if (pending_selectable_item_rewards.size() > 0) { + map>::iterator map_itr; + if (all) { + for (map_itr = pending_selectable_item_rewards.begin(); map_itr != pending_selectable_item_rewards.end(); map_itr++) { + vector::iterator itr; + for (itr = map_itr->second.begin(); itr != map_itr->second.end(); itr++) { + safe_delete(*itr); + } + } + pending_selectable_item_rewards.clear(); + } + else { + if (pending_selectable_item_rewards.count(source_id) > 0) { + vector::iterator itr; + for (itr = pending_selectable_item_rewards[source_id].begin(); itr != pending_selectable_item_rewards[source_id].end(); itr++) { + safe_delete(*itr); + } + pending_selectable_item_rewards.erase(source_id); + } + } + } + } + void ClearPendingItemRewards() { //the client doesn't send any reference to where the pending rewards came from, so if they collect one, we should just them all of them at once + if (pending_item_rewards.size() > 0) { + vector::iterator itr; + for (itr = pending_item_rewards.begin(); itr != pending_item_rewards.end(); itr++) { + safe_delete(*itr); + } + pending_item_rewards.clear(); + } + } + + enum DELETE_BOOK_TYPE { + DELETE_TRADESKILLS = 1, + DELETE_SPELLS = 2, + DELETE_COMBAT_ART = 4, + DELETE_ABILITY = 8, + DELETE_NOT_SHOWN = 16 + }; + void DeleteSpellBook(int8 type_selection = 0); + void RemoveSpellBookEntry(int32 spell_id, bool remove_passives_from_list = true); + void ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 maxlvl_only, int32 book_type); + void GetSpellBookSlotSort(int32 pattern, int32* i, int8* page_book_count, int32* last_start_point); + static bool SortSpellEntryByName(SpellBookEntry* s1, SpellBookEntry* s2); + static bool SortSpellEntryByCategory(SpellBookEntry* s1, SpellBookEntry* s2); + static bool SortSpellEntryByLevel(SpellBookEntry* s1, SpellBookEntry* s2); + static bool SortSpellEntryByNameReverse(SpellBookEntry* s1, SpellBookEntry* s2); + static bool SortSpellEntryByCategoryReverse(SpellBookEntry* s1, SpellBookEntry* s2); + static bool SortSpellEntryByLevelReverse(SpellBookEntry* s1, SpellBookEntry* s2); + + int8 GetSpellSlot(int32 spell_id); + void AddTitle(sint32 title_id, const char *name, int8 prefix, bool save_needed = false); + void AddAAEntry(int16 template_id, int8 tab_id, int32 aa_id, int16 order, int8 treeid); + PlayerTitlesList* GetPlayerTitles() { return &player_titles_list; } + void AddLanguage(int32 id, const char *name, bool save_needed = false); + PlayerLanguagesList* GetPlayerLanguages() { return &player_languages_list; } + bool HasLanguage(int32 id); + bool HasLanguage(const char* name); + bool CanReceiveQuest(int32 quest_id, int8* ret = 0); + float GetBoatX() { if (info) return info->GetBoatX(); return 0; } + float GetBoatY() { if (info) return info->GetBoatY(); return 0; } + float GetBoatZ() { if (info) return info->GetBoatZ(); return 0; } + int32 GetBoatSpawn() { if (info) return info->GetBoatSpawn(); return 0; } + void SetBoatX(float x) { if (info) info->SetBoatX(x); } + void SetBoatY(float y) { if (info) info->SetBoatY(y); } + void SetBoatZ(float z) { if (info) info->SetBoatZ(z); } + void SetBoatSpawn(Spawn* boat) { if (info) info->SetBoatSpawn(boat); } + Mutex* GetGroupBuffMutex(); + void SetPendingDeletion(bool val) { pending_deletion = val; } + bool GetPendingDeletion() { return pending_deletion; } + float GetPosPacketSpeed() { return pos_packet_speed; } + bool ControlFlagsChanged(); + void SetPlayerControlFlag(int8 param, int8 param_value, bool is_active); + void SendControlFlagUpdates(Client* client); + + /// Casts all the passive spells for the player, only call after zoning is complete. + void ApplyPassiveSpells(); + + /// Removes all passive spell effects from the player and clears the passive list + void RemoveAllPassives(); + + /// Gets the current recipie ID + int32 GetCurrentRecipe() { return current_recipe; } + + /// Sets the current recipie ID + /// Id of the new recipe + void SetCurrentRecipe(int32 val) { current_recipe = val; } + + /// Reset the pet window info + void ResetPetInfo(); + + void ProcessCombat(); + + /* Character history stuff */ + + /// Adds a new history event to the player + /// The history type + /// The history sub type + /// The first history value + /// The second history value + void UpdatePlayerHistory(int8 type, int8 subtype, int32 value, int32 value2 = 0); + + /// Checks to see if the player has discovered the location + /// The ID of the location to check + /// True if the player has discovered the location + bool DiscoveredLocation(int32 locationID); + + /// Load the players history from the database + /// The history type + /// The history sub type + /// The history data + void LoadPlayerHistory(int8 type, int8 subtype, HistoryData* hd); + + /// Save the player's history to the database + void SaveHistory(); + + + /* New functions for spell locking and unlocking*/ + /// Lock all Spells, Combat arts, and Abilities (not trade skill spells) + void LockAllSpells(); + + /// Unlocks all Spells, Combat arts, and Abilities (not trade skill spells) + void UnlockAllSpells(bool modify_recast = false, Spell* exception = 0); + + /// Locks the given spell as well as all spells with a shared timer + void LockSpell(Spell* spell, int16 recast); + + /// Unlocks the given spell as well as all spells with shared timers + void UnlockSpell(Spell* spell); + void UnlockSpell(int32 spell_id, int32 linked_timer_id); + + /// Locks all ts spells and unlocks all normal spells + void LockTSSpells(); + + /// Unlocks all ts spells and locks all normal spells + void UnlockTSSpells(); + + /// Queue the given spell + void QueueSpell(Spell* spell); + + /// Unqueue the given spell + void UnQueueSpell(Spell* spell); + + ///Get all the spells the player has with the given id + vector GetSpellBookSpellsByTimer(Spell* spell, int32 timerID); + + PacketStruct* GetQuestJournalPacket(Quest* quest, int16 version, int32 crc, bool updated = true); + + void SetSpawnInfoStruct(PacketStruct* packet) { safe_delete(spawn_info_struct); spawn_info_struct = packet; } + void SetSpawnVisStruct(PacketStruct* packet) { safe_delete(spawn_vis_struct); spawn_vis_struct = packet; } + void SetSpawnPosStruct(PacketStruct* packet) { safe_delete(spawn_pos_struct); spawn_pos_struct = packet; } + void SetSpawnHeaderStruct(PacketStruct* packet) { safe_delete(spawn_header_struct); spawn_header_struct = packet; } + void SetSpawnFooterStruct(PacketStruct* packet) { safe_delete(spawn_footer_struct); spawn_footer_struct = packet; } + void SetSignFooterStruct(PacketStruct* packet) { safe_delete(sign_footer_struct); sign_footer_struct = packet; } + void SetWidgetFooterStruct(PacketStruct* packet) { safe_delete(widget_footer_struct); widget_footer_struct = packet; } + + PacketStruct* GetSpawnInfoStruct() { return spawn_info_struct; } + PacketStruct* GetSpawnVisStruct() { return spawn_vis_struct; } + PacketStruct* GetSpawnPosStruct() { return spawn_pos_struct; } + PacketStruct* GetSpawnHeaderStruct() { return spawn_header_struct; } + PacketStruct* GetSpawnFooterStruct() { return spawn_footer_struct; } + PacketStruct* GetSignFooterStruct() { return sign_footer_struct; } + PacketStruct* GetWidgetFooterStruct() { return widget_footer_struct; } + + Mutex info_mutex; + Mutex pos_mutex; + Mutex vis_mutex; + Mutex index_mutex; + Mutex spawn_mutex; + mutable std::shared_mutex spawn_aggro_range_mutex; + + void SetTempMount(int32 id) { tmp_mount_model = id; } + int32 GetTempMount() { return tmp_mount_model; } + + void SetTempMountColor(EQ2_Color* color) { tmp_mount_color = *color; } + EQ2_Color GetTempMountColor() { return tmp_mount_color; } + + void SetTempMountSaddleColor(EQ2_Color* color) { tmp_mount_saddle_color = *color; } + EQ2_Color GetTempMountSaddleColor() { return tmp_mount_saddle_color; } + + + void LoadLUAHistory(int32 event_id, LUAHistory* history); + void SaveLUAHistory(); + void UpdateLUAHistory(int32 event_id, int32 value, int32 value2); + LUAHistory* GetLUAHistory(int32 event_id); + + bool HasGMVision() { return gm_vision; } + void SetGMVision(bool val) { gm_vision = val; } + + void StopCombat(int8 type=0) { + switch(type) + { + case 2: + SetRangeAttack(false); + InCombat(false, true); + break; + default: + InCombat(false); + InCombat(false, true); + SetRangeAttack(false); + break; + } + } + + NPC* InstantiateSpiritShard(float origX, float origY, float origZ, float origHeading, int32 origGridID, ZoneServer* origZone); + + void DismissAllPets(); + + void SaveSpellEffects(); + void SaveCustomSpellFields(LuaSpell* luaspell); + void SaveCustomSpellDataIndex(LuaSpell* luaspell); + void SaveCustomSpellEffectsDisplay(LuaSpell* luaspell); + + void SetSaveSpellEffects(bool val) { stop_save_spell_effects = val; } + AppearanceData SavedApp; + CharFeatures SavedFeatures; + bool custNPC; + Entity* custNPCTarget; + // bot index, spawn id + map SpawnedBots; + bool StopSaveSpellEffects() { return stop_save_spell_effects; } + + void MentorTarget(); + void SetMentorStats(int32 effective_level, int32 target_char_id = 0, bool update_stats = true); + + bool ResetMentorship() { + bool mentorship_status = reset_mentorship; + if(mentorship_status) + { + SetMentorStats(GetLevel()); + } + reset_mentorship = false; + return mentorship_status; + } + + void EnableResetMentorship() { + reset_mentorship = true; + } + + bool SerializeItemPackets(EquipmentItemList* equipList, vector* packets, Item* item, int16 version, Item* to_item = 0); + + void AddGMVisualFilter(int32 filter_type, int32 filter_value, char* filter_search_str, int16 visual_tag); + int16 MatchGMVisualFilter(int32 filter_type, int32 filter_value, char* filter_search_str, bool in_vismutex_lock = false); + void ClearGMVisualFilters(); + int GetPVPAlignment(); + + int32 GetCurrentLanguage() { return current_language_id; } + void SetCurrentLanguage(int32 language_id) { current_language_id = language_id; } + + void SetActiveReward(bool val) { active_reward = val; } + bool IsActiveReward() { return active_reward; } + + + bool IsSpawnInRangeList(int32 spawn_id); + void SetSpawnInRangeList(int32 spawn_id, bool in_range); + void ProcessSpawnRangeUpdates(); + void CalculatePlayerHPPower(int16 new_level = 0); + bool IsAllowedCombatEquip(int8 slot = 255, bool send_message = false); + + void SetActiveFoodUniqueID(int32 unique_id, bool update_db = true); + void SetActiveDrinkUniqueID(int32 unique_id, bool update_db = true); + + int64 GetActiveFoodUniqueID() { return active_food_unique_id; } + int64 GetActiveDrinkUniqueID() { return active_drink_unique_id; } + + void SetHouseVaultSlots(int8 allowed_slots) { house_vault_slots = allowed_slots; } + int8 GetHouseVaultSlots() { return house_vault_slots; } + + Mutex MPlayerQuests; + float pos_packet_speed; + + map > >* SortedTraitList; + map >* ClassTraining; + map >* RaceTraits; + map >* InnateRaceTraits; + map >* FocusEffects; + mutable std::shared_mutex trait_mutex; + std::atomic need_trait_update; + + static void InitXPTable(); + static map m_levelXPReq; + + mutable std::shared_mutex spell_packet_update_mutex; + mutable std::shared_mutex raid_update_mutex; +private: + bool reset_mentorship; + bool range_attack; + int16 last_movement_activity; + bool returning_from_ld; + PlayerGroup* group; + + float test_x; + float test_y; + float test_z; + int32 test_time; + map > pending_loot_items; + Mutex MSpellsBook; + Mutex MRecipeBook; + map current_quest_flagged; + PlayerFaction factions; + map completed_quests; + std::atomic charsheet_changed; + std::atomic raidsheet_changed; + std::atomic hassent_raid; + map spawn_vis_packet_list; + map spawn_info_packet_list; + map spawn_pos_packet_list; + map spawn_packet_sent; + map spawn_state_list; + uchar* movement_packet; + uchar* old_movement_packet; + uchar* spell_orig_packet; + uchar* spell_xor_packet; + int16 spell_count; + + uchar* raid_orig_packet; + uchar* raid_xor_packet; + //float speed; + int16 target_id; + Spawn* combat_target; + int32 char_id; + bool quickbar_updated; + bool resurrecting; + PlayerInfo* info; + vector spells; + vector quickbar_items; + map statistics; + void RemovePlayerStatistics(); + map friend_list; + map ignore_list; + bool pending_deletion; + PlayerControlFlags control_flags; + + map target_invis_history; + + // JA: POI Discoveries + map > players_poi_list; + + // Jabantiz: Passive spell list, just stores spell id's + vector passive_spells; + + /// Adds a new passive spell to the list + /// Spell id to add + /// Tier of spell to add + void AddPassiveSpell(int32 id, int8 tier); + + /// Removes a passive spell from the list + /// Spell id to remove + /// Tier of spell to remove + /// Remove the spell from this players passive list, default true + void RemovePassive(int32 id, int8 tier, bool remove_from_list = true); + + CharacterInstances character_instances; + string away_message; + string biography; + MutexMap mail_list; + bool is_tracking; + Guild* guild; + PlayerCollectionList collection_list; + Collection * pending_collection_reward; + vector pending_item_rewards; + map> pending_selectable_item_rewards; + PlayerTitlesList player_titles_list; + PlayerRecipeList recipe_list; + PlayerLanguagesList player_languages_list; + PlayerRecipeBookList recipebook_list; + PlayerAchievementList achievement_list; + PlayerAchievementUpdateList achievement_update_list; + // Need to keep track of the recipe the player is crafting as not all crafting packets have this info + int32 current_recipe; + + void HandleHistoryNone(int8 subtype, int32 value, int32 value2); + void HandleHistoryDeath(int8 subtype, int32 value, int32 value2); + void HandleHistoryDiscovery(int8 subtype, int32 value, int32 value2); + void HandleHistoryXP(int8 subtype, int32 value, int32 value2); + + /// + void ModifySpellStatus(SpellBookEntry* spell, sint16 value, bool modify_recast = true, int16 recast = 0); + void AddSpellStatus(SpellBookEntry* spell, sint16 value, bool modify_recast = true, int16 recast = 0); + void RemoveSpellStatus(SpellBookEntry* spell, sint16 value, bool modify_recast = true, int16 recast = 0); + void SetSpellEntryRecast(SpellBookEntry* spell, bool modify_recast, int16 recast); + + //The following variables are for serializing spawn packets + PacketStruct* spawn_pos_struct; + PacketStruct* spawn_info_struct; + PacketStruct* spawn_vis_struct; + PacketStruct* spawn_header_struct; + PacketStruct* spawn_footer_struct; + PacketStruct* sign_footer_struct; + PacketStruct* widget_footer_struct; + uchar* spawn_tmp_vis_xor_packet; + uchar* spawn_tmp_pos_xor_packet; + uchar* spawn_tmp_info_xor_packet; + int32 vis_xor_size; + int32 pos_xor_size; + int32 info_xor_size; + + // Character history, map > > + map > > m_characterHistory; + + map m_charLuaHistory; + Mutex mLUAHistory; + + int32 tmp_mount_model; + EQ2_Color tmp_mount_color; + EQ2_Color tmp_mount_saddle_color; + + bool gm_vision; + bool stop_save_spell_effects; + + map player_spawn_id_map; + map player_spawn_reverse_id_map; + map player_aggro_range_spawns; + + bool all_spells_locked; + Timer lift_cooldown; + + vector gm_visual_filters; + + int32 current_language_id; + + bool active_reward; + + Quest* GetAnyQuest(int32 quest_id); + Quest* GetCompletedQuest(int32 quest_id); + + std::atomic active_food_unique_id; + std::atomic active_drink_unique_id; + + int8 house_vault_slots; +}; +#pragma pack() +#endif diff --git a/internal/languages/constants.go b/internal/languages/constants.go new file mode 100644 index 0000000..319b821 --- /dev/null +++ b/internal/languages/constants.go @@ -0,0 +1,39 @@ +package languages + +// Language system constants +const ( + // Maximum language name length + MaxLanguageNameLength = 50 + + // Special language IDs (common in EQ2) + LanguageIDCommon = 0 // Common tongue (default) + LanguageIDElvish = 1 // Elvish + LanguageIDDwarven = 2 // Dwarven + LanguageIDHalfling = 3 // Halfling + LanguageIDGnomish = 4 // Gnomish + LanguageIDIksar = 5 // Iksar + LanguageIDTrollish = 6 // Trollish + LanguageIDOgrish = 7 // Ogrish + LanguageIDFae = 8 // Fae + LanguageIDArasai = 9 // Arasai + LanguageIDSarnak = 10 // Sarnak + LanguageIDFroglok = 11 // Froglok +) + +// Language validation constants +const ( + MinLanguageID = 0 + MaxLanguageID = 999999 // Reasonable upper bound +) + +// Database operation constants +const ( + SaveStatusUnchanged = false + SaveStatusNeeded = true +) + +// System limits +const ( + MaxLanguagesPerPlayer = 100 // Reasonable limit to prevent abuse + MaxTotalLanguages = 1000 // System-wide language limit +) \ No newline at end of file diff --git a/internal/languages/interfaces.go b/internal/languages/interfaces.go new file mode 100644 index 0000000..747be52 --- /dev/null +++ b/internal/languages/interfaces.go @@ -0,0 +1,396 @@ +package languages + +// Database interface for language persistence +type Database interface { + LoadAllLanguages() ([]*Language, error) + SaveLanguage(language *Language) error + DeleteLanguage(languageID int32) error + LoadPlayerLanguages(playerID int32) ([]*Language, error) + SavePlayerLanguage(playerID int32, languageID int32) error + DeletePlayerLanguage(playerID int32, languageID int32) error +} + +// Logger interface for language logging +type Logger interface { + LogInfo(message string, args ...interface{}) + LogError(message string, args ...interface{}) + LogDebug(message string, args ...interface{}) + LogWarning(message string, args ...interface{}) +} + +// Player interface for language-related player operations +type Player interface { + GetCharacterID() int32 + GetName() string + GetLanguages() *PlayerLanguagesList + KnowsLanguage(languageID int32) bool + LearnLanguage(languageID int32) error + ForgetLanguage(languageID int32) error + SendMessage(message string) +} + +// Client interface for language-related client operations +type Client interface { + GetPlayer() *Player + GetVersion() int16 + SendLanguageUpdate(languageData []byte) error +} + +// LanguageProvider interface for systems that provide language functionality +type LanguageProvider interface { + GetMasterLanguagesList() *MasterLanguagesList + GetLanguage(languageID int32) *Language + GetLanguageByName(name string) *Language + CreatePlayerLanguagesList() *PlayerLanguagesList +} + +// LanguageAware interface for entities that can understand languages +type LanguageAware interface { + GetKnownLanguages() *PlayerLanguagesList + KnowsLanguage(languageID int32) bool + GetPrimaryLanguage() int32 + SetPrimaryLanguage(languageID int32) + CanUnderstand(languageID int32) bool +} + +// LanguageHandler interface for handling language events +type LanguageHandler interface { + OnLanguageLearned(player *Player, languageID int32) error + OnLanguageForgotten(player *Player, languageID int32) error + OnLanguageUsed(player *Player, languageID int32, message string) error +} + +// ChatProcessor interface for processing multilingual chat +type ChatProcessor interface { + ProcessMessage(speaker *Player, message string, languageID int32) (string, error) + FilterMessage(listener *Player, message string, languageID int32) string + GetLanguageSkramble(message string, comprehension float32) string +} + +// PlayerLanguageAdapter provides language functionality for players +type PlayerLanguageAdapter struct { + player *Player + languages *PlayerLanguagesList + primaryLang int32 + manager *Manager + logger Logger +} + +// NewPlayerLanguageAdapter creates a new player language adapter +func NewPlayerLanguageAdapter(player *Player, manager *Manager, logger Logger) *PlayerLanguageAdapter { + return &PlayerLanguageAdapter{ + player: player, + languages: manager.CreatePlayerLanguagesList(), + primaryLang: LanguageIDCommon, // Default to common + manager: manager, + logger: logger, + } +} + +// GetKnownLanguages returns the player's known languages +func (pla *PlayerLanguageAdapter) GetKnownLanguages() *PlayerLanguagesList { + return pla.languages +} + +// KnowsLanguage checks if the player knows a specific language +func (pla *PlayerLanguageAdapter) KnowsLanguage(languageID int32) bool { + return pla.languages.HasLanguage(languageID) +} + +// GetPrimaryLanguage returns the player's primary language +func (pla *PlayerLanguageAdapter) GetPrimaryLanguage() int32 { + return pla.primaryLang +} + +// SetPrimaryLanguage sets the player's primary language +func (pla *PlayerLanguageAdapter) SetPrimaryLanguage(languageID int32) { + // Only allow setting to a known language + if pla.languages.HasLanguage(languageID) { + pla.primaryLang = languageID + + if pla.logger != nil { + lang := pla.manager.GetLanguage(languageID) + langName := "Unknown" + if lang != nil { + langName = lang.GetName() + } + pla.logger.LogDebug("Player %s set primary language to %s (%d)", + pla.player.GetName(), langName, languageID) + } + } +} + +// CanUnderstand checks if the player can understand a language +func (pla *PlayerLanguageAdapter) CanUnderstand(languageID int32) bool { + // Common language is always understood + if languageID == LanguageIDCommon { + return true + } + + // Check if player knows the language + return pla.languages.HasLanguage(languageID) +} + +// LearnLanguage teaches the player a new language +func (pla *PlayerLanguageAdapter) LearnLanguage(languageID int32) error { + // Get the language from master list + language := pla.manager.GetLanguage(languageID) + if language == nil { + return fmt.Errorf("language with ID %d does not exist", languageID) + } + + // Check if already known + if pla.languages.HasLanguage(languageID) { + return fmt.Errorf("player already knows language %s", language.GetName()) + } + + // Create a copy for the player + playerLang := language.Copy() + playerLang.SetSaveNeeded(true) + + // Add to player's languages + if err := pla.languages.Add(playerLang); err != nil { + return fmt.Errorf("failed to add language to player: %w", err) + } + + // Record usage statistics + pla.manager.RecordLanguageUsage(languageID) + + if pla.logger != nil { + pla.logger.LogInfo("Player %s learned language %s (%d)", + pla.player.GetName(), language.GetName(), languageID) + } + + return nil +} + +// ForgetLanguage makes the player forget a language +func (pla *PlayerLanguageAdapter) ForgetLanguage(languageID int32) error { + // Cannot forget common language + if languageID == LanguageIDCommon { + return fmt.Errorf("cannot forget common language") + } + + // Check if player knows the language + if !pla.languages.HasLanguage(languageID) { + return fmt.Errorf("player does not know language %d", languageID) + } + + // Get language name for logging + language := pla.manager.GetLanguage(languageID) + langName := "Unknown" + if language != nil { + langName = language.GetName() + } + + // Remove from player's languages + if !pla.languages.RemoveLanguage(languageID) { + return fmt.Errorf("failed to remove language from player") + } + + // Reset primary language if this was it + if pla.primaryLang == languageID { + pla.primaryLang = LanguageIDCommon + } + + if pla.logger != nil { + pla.logger.LogInfo("Player %s forgot language %s (%d)", + pla.player.GetName(), langName, languageID) + } + + return nil +} + +// LoadPlayerLanguages loads the player's languages from database +func (pla *PlayerLanguageAdapter) LoadPlayerLanguages(database Database) error { + if database == nil { + return fmt.Errorf("database is nil") + } + + playerID := pla.player.GetCharacterID() + languages, err := database.LoadPlayerLanguages(playerID) + if err != nil { + return fmt.Errorf("failed to load player languages: %w", err) + } + + // Clear current languages + pla.languages.Clear() + + // Add loaded languages + for _, lang := range languages { + if err := pla.languages.Add(lang); err != nil && pla.logger != nil { + pla.logger.LogWarning("Failed to add loaded language %d to player %s: %v", + lang.GetID(), pla.player.GetName(), err) + } + } + + // Ensure player knows common language + if !pla.languages.HasLanguage(LanguageIDCommon) { + commonLang := pla.manager.GetLanguage(LanguageIDCommon) + if commonLang != nil { + playerCommon := commonLang.Copy() + pla.languages.Add(playerCommon) + } + } + + if pla.logger != nil { + pla.logger.LogDebug("Loaded %d languages for player %s", + len(languages), pla.player.GetName()) + } + + return nil +} + +// SavePlayerLanguages saves the player's languages to database +func (pla *PlayerLanguageAdapter) SavePlayerLanguages(database Database) error { + if database == nil { + return fmt.Errorf("database is nil") + } + + playerID := pla.player.GetCharacterID() + languages := pla.languages.GetAllLanguages() + + // Save each language that needs saving + for _, lang := range languages { + if lang.GetSaveNeeded() { + if err := database.SavePlayerLanguage(playerID, lang.GetID()); err != nil { + return fmt.Errorf("failed to save player language %d: %w", lang.GetID(), err) + } + lang.SetSaveNeeded(false) + } + } + + if pla.logger != nil { + pla.logger.LogDebug("Saved languages for player %s", pla.player.GetName()) + } + + return nil +} + +// ChatLanguageProcessor handles multilingual chat processing +type ChatLanguageProcessor struct { + manager *Manager + logger Logger +} + +// NewChatLanguageProcessor creates a new chat language processor +func NewChatLanguageProcessor(manager *Manager, logger Logger) *ChatLanguageProcessor { + return &ChatLanguageProcessor{ + manager: manager, + logger: logger, + } +} + +// ProcessMessage processes a chat message in a specific language +func (clp *ChatLanguageProcessor) ProcessMessage(speaker *Player, message string, languageID int32) (string, error) { + if speaker == nil { + return "", fmt.Errorf("speaker cannot be nil") + } + + // Validate language exists + language := clp.manager.GetLanguage(languageID) + if language == nil { + return "", fmt.Errorf("language %d does not exist", languageID) + } + + // Check if speaker knows the language + if !speaker.KnowsLanguage(languageID) { + return "", fmt.Errorf("speaker does not know language %s", language.GetName()) + } + + // Record language usage + clp.manager.RecordLanguageUsage(languageID) + + return message, nil +} + +// FilterMessage filters a message for a listener based on language comprehension +func (clp *ChatLanguageProcessor) FilterMessage(listener *Player, message string, languageID int32) string { + if listener == nil { + return message + } + + // Common language is always understood + if languageID == LanguageIDCommon { + return message + } + + // Check if listener knows the language + if listener.KnowsLanguage(languageID) { + return message + } + + // Scramble the message for unknown languages + return clp.GetLanguageSkramble(message, 0.0) +} + +// GetLanguageSkramble scrambles a message based on comprehension level +func (clp *ChatLanguageProcessor) GetLanguageSkramble(message string, comprehension float32) string { + if comprehension >= 1.0 { + return message + } + + if comprehension <= 0.0 { + // Complete scramble - replace with gibberish + runes := []rune(message) + scrambled := make([]rune, len(runes)) + + for i, r := range runes { + if r == ' ' { + scrambled[i] = ' ' + } else if r >= 'a' && r <= 'z' { + scrambled[i] = 'a' + rune((int(r-'a')+7)%26) + } else if r >= 'A' && r <= 'Z' { + scrambled[i] = 'A' + rune((int(r-'A')+7)%26) + } else { + scrambled[i] = r + } + } + + return string(scrambled) + } + + // Partial comprehension - scramble some words + // This is a simplified implementation + return message +} + +// LanguageEventAdapter handles language-related events +type LanguageEventAdapter struct { + handler LanguageHandler + logger Logger +} + +// NewLanguageEventAdapter creates a new language event adapter +func NewLanguageEventAdapter(handler LanguageHandler, logger Logger) *LanguageEventAdapter { + return &LanguageEventAdapter{ + handler: handler, + logger: logger, + } +} + +// ProcessLanguageEvent processes a language-related event +func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *Player, languageID int32, data interface{}) { + if lea.handler == nil { + return + } + + switch eventType { + case "language_learned": + if err := lea.handler.OnLanguageLearned(player, languageID); err != nil && lea.logger != nil { + lea.logger.LogError("Language learned handler failed: %v", err) + } + + case "language_forgotten": + if err := lea.handler.OnLanguageForgotten(player, languageID); err != nil && lea.logger != nil { + lea.logger.LogError("Language forgotten handler failed: %v", err) + } + + case "language_used": + if message, ok := data.(string); ok { + if err := lea.handler.OnLanguageUsed(player, languageID, message); err != nil && lea.logger != nil { + lea.logger.LogError("Language used handler failed: %v", err) + } + } + } +} \ No newline at end of file diff --git a/internal/languages/manager.go b/internal/languages/manager.go new file mode 100644 index 0000000..0b68205 --- /dev/null +++ b/internal/languages/manager.go @@ -0,0 +1,525 @@ +package languages + +import ( + "fmt" + "sync" +) + +// Manager provides high-level management of the language system +type Manager struct { + masterLanguagesList *MasterLanguagesList + database Database + logger Logger + mutex sync.RWMutex + + // Statistics + languageLookups int64 + playersWithLanguages int64 + languageUsageCount map[int32]int64 // Language ID -> usage count +} + +// NewManager creates a new language manager +func NewManager(database Database, logger Logger) *Manager { + return &Manager{ + masterLanguagesList: NewMasterLanguagesList(), + database: database, + logger: logger, + languageUsageCount: make(map[int32]int64), + } +} + +// Initialize loads languages from database +func (m *Manager) Initialize() error { + if m.logger != nil { + m.logger.LogInfo("Initializing language manager...") + } + + if m.database == nil { + if m.logger != nil { + m.logger.LogWarning("No database provided, starting with empty language list") + } + return nil + } + + // Load languages from database + languages, err := m.database.LoadAllLanguages() + if err != nil { + return fmt.Errorf("failed to load languages from database: %w", err) + } + + for _, language := range languages { + if err := m.masterLanguagesList.AddLanguage(language); err != nil { + if m.logger != nil { + m.logger.LogError("Failed to add language %d (%s): %v", language.GetID(), language.GetName(), err) + } + } + } + + if m.logger != nil { + m.logger.LogInfo("Loaded %d languages from database", len(languages)) + } + + return nil +} + +// GetMasterLanguagesList returns the master language list +func (m *Manager) GetMasterLanguagesList() *MasterLanguagesList { + return m.masterLanguagesList +} + +// CreatePlayerLanguagesList creates a new player language list +func (m *Manager) CreatePlayerLanguagesList() *PlayerLanguagesList { + m.mutex.Lock() + m.playersWithLanguages++ + m.mutex.Unlock() + + return NewPlayerLanguagesList() +} + +// GetLanguage returns a language by ID +func (m *Manager) GetLanguage(id int32) *Language { + m.mutex.Lock() + m.languageLookups++ + m.mutex.Unlock() + + return m.masterLanguagesList.GetLanguage(id) +} + +// GetLanguageByName returns a language by name +func (m *Manager) GetLanguageByName(name string) *Language { + m.mutex.Lock() + m.languageLookups++ + m.mutex.Unlock() + + return m.masterLanguagesList.GetLanguageByName(name) +} + +// AddLanguage adds a new language to the system +func (m *Manager) AddLanguage(language *Language) error { + if language == nil { + return fmt.Errorf("language cannot be nil") + } + + // Add to master list + if err := m.masterLanguagesList.AddLanguage(language); err != nil { + return fmt.Errorf("failed to add language to master list: %w", err) + } + + // Save to database if available + if m.database != nil { + if err := m.database.SaveLanguage(language); err != nil { + // Remove from master list if database save failed + m.masterLanguagesList.RemoveLanguage(language.GetID()) + return fmt.Errorf("failed to save language to database: %w", err) + } + } + + if m.logger != nil { + m.logger.LogInfo("Added language %d: %s", language.GetID(), language.GetName()) + } + + return nil +} + +// UpdateLanguage updates an existing language +func (m *Manager) UpdateLanguage(language *Language) error { + if language == nil { + return fmt.Errorf("language cannot be nil") + } + + // Update in master list + if err := m.masterLanguagesList.UpdateLanguage(language); err != nil { + return fmt.Errorf("failed to update language in master list: %w", err) + } + + // Save to database if available + if m.database != nil { + if err := m.database.SaveLanguage(language); err != nil { + return fmt.Errorf("failed to save language to database: %w", err) + } + } + + if m.logger != nil { + m.logger.LogInfo("Updated language %d: %s", language.GetID(), language.GetName()) + } + + return nil +} + +// RemoveLanguage removes a language from the system +func (m *Manager) RemoveLanguage(id int32) error { + // Check if language exists + if !m.masterLanguagesList.HasLanguage(id) { + return fmt.Errorf("language with ID %d does not exist", id) + } + + // Remove from database first if available + if m.database != nil { + if err := m.database.DeleteLanguage(id); err != nil { + return fmt.Errorf("failed to delete language from database: %w", err) + } + } + + // Remove from master list + if !m.masterLanguagesList.RemoveLanguage(id) { + return fmt.Errorf("failed to remove language from master list") + } + + if m.logger != nil { + m.logger.LogInfo("Removed language %d", id) + } + + return nil +} + +// RecordLanguageUsage records language usage for statistics +func (m *Manager) RecordLanguageUsage(languageID int32) { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.languageUsageCount[languageID]++ +} + +// GetStatistics returns language system statistics +func (m *Manager) GetStatistics() *LanguageStatistics { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // Create language name mapping + languagesByName := make(map[string]int32) + allLanguages := m.masterLanguagesList.GetAllLanguages() + for _, lang := range allLanguages { + languagesByName[lang.GetName()] = lang.GetID() + } + + // Copy usage count + usageCount := make(map[int32]int64) + for id, count := range m.languageUsageCount { + usageCount[id] = count + } + + return &LanguageStatistics{ + TotalLanguages: len(allLanguages), + PlayersWithLanguages: int(m.playersWithLanguages), + LanguageUsageCount: usageCount, + LanguageLookups: m.languageLookups, + LanguagesByName: languagesByName, + } +} + +// ResetStatistics resets all statistics +func (m *Manager) ResetStatistics() { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.languageLookups = 0 + m.playersWithLanguages = 0 + m.languageUsageCount = make(map[int32]int64) +} + +// ValidateAllLanguages validates all languages in the system +func (m *Manager) ValidateAllLanguages() []string { + allLanguages := m.masterLanguagesList.GetAllLanguages() + var issues []string + + for _, lang := range allLanguages { + if !lang.IsValid() { + issues = append(issues, fmt.Sprintf("Language %d (%s) is invalid", lang.GetID(), lang.GetName())) + } + } + + return issues +} + +// ReloadFromDatabase reloads all languages from database +func (m *Manager) ReloadFromDatabase() error { + if m.database == nil { + return fmt.Errorf("no database available") + } + + // Clear current languages + m.masterLanguagesList.Clear() + + // Reload from database + return m.Initialize() +} + +// GetLanguageCount returns the total number of languages +func (m *Manager) GetLanguageCount() int32 { + return m.masterLanguagesList.Size() +} + +// ProcessCommand handles language-related commands +func (m *Manager) ProcessCommand(command string, args []string) (string, error) { + switch command { + case "stats": + return m.handleStatsCommand(args) + case "validate": + return m.handleValidateCommand(args) + case "list": + return m.handleListCommand(args) + case "info": + return m.handleInfoCommand(args) + case "reload": + return m.handleReloadCommand(args) + case "add": + return m.handleAddCommand(args) + case "remove": + return m.handleRemoveCommand(args) + case "search": + return m.handleSearchCommand(args) + default: + return "", fmt.Errorf("unknown language command: %s", command) + } +} + +// handleStatsCommand shows language system statistics +func (m *Manager) handleStatsCommand(args []string) (string, error) { + stats := m.GetStatistics() + + result := "Language System Statistics:\n" + result += fmt.Sprintf("Total Languages: %d\n", stats.TotalLanguages) + result += fmt.Sprintf("Players with Languages: %d\n", stats.PlayersWithLanguages) + result += fmt.Sprintf("Language Lookups: %d\n", stats.LanguageLookups) + + if len(stats.LanguageUsageCount) > 0 { + result += "\nMost Used Languages:\n" + // Show top 5 most used languages + count := 0 + for langID, usage := range stats.LanguageUsageCount { + if count >= 5 { + break + } + lang := m.GetLanguage(langID) + name := "Unknown" + if lang != nil { + name = lang.GetName() + } + result += fmt.Sprintf(" %s (ID: %d): %d uses\n", name, langID, usage) + count++ + } + } + + return result, nil +} + +// handleValidateCommand validates all languages +func (m *Manager) handleValidateCommand(args []string) (string, error) { + issues := m.ValidateAllLanguages() + + if len(issues) == 0 { + return "All languages are valid.", nil + } + + result := fmt.Sprintf("Found %d issues with languages:\n", len(issues)) + for i, issue := range issues { + if i >= 10 { // Limit output + result += "... (and more)\n" + break + } + result += fmt.Sprintf("%d. %s\n", i+1, issue) + } + + return result, nil +} + +// handleListCommand lists languages +func (m *Manager) handleListCommand(args []string) (string, error) { + languages := m.masterLanguagesList.GetAllLanguages() + + if len(languages) == 0 { + return "No languages loaded.", nil + } + + result := fmt.Sprintf("Languages (%d):\n", len(languages)) + count := 0 + for _, language := range languages { + if count >= 20 { // Limit output + result += "... (and more)\n" + break + } + result += fmt.Sprintf(" %d: %s\n", language.GetID(), language.GetName()) + count++ + } + + return result, nil +} + +// handleInfoCommand shows information about a specific language +func (m *Manager) handleInfoCommand(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("language ID or name required") + } + + var language *Language + + // Try to parse as ID first + var languageID int32 + if _, err := fmt.Sscanf(args[0], "%d", &languageID); err == nil { + language = m.GetLanguage(languageID) + } else { + // Try as name + language = m.GetLanguageByName(args[0]) + } + + if language == nil { + return fmt.Sprintf("Language '%s' not found.", args[0]), nil + } + + result := fmt.Sprintf("Language Information:\n") + result += fmt.Sprintf("ID: %d\n", language.GetID()) + result += fmt.Sprintf("Name: %s\n", language.GetName()) + result += fmt.Sprintf("Save Needed: %v\n", language.GetSaveNeeded()) + + // Show usage statistics if available + m.mutex.RLock() + if usage, exists := m.languageUsageCount[language.GetID()]; exists { + result += fmt.Sprintf("Usage Count: %d\n", usage) + } else { + result += "Usage Count: 0\n" + } + m.mutex.RUnlock() + + return result, nil +} + +// handleReloadCommand reloads languages from database +func (m *Manager) handleReloadCommand(args []string) (string, error) { + if err := m.ReloadFromDatabase(); err != nil { + return "", fmt.Errorf("failed to reload languages: %w", err) + } + + count := m.GetLanguageCount() + return fmt.Sprintf("Successfully reloaded %d languages from database.", count), nil +} + +// handleAddCommand adds a new language +func (m *Manager) handleAddCommand(args []string) (string, error) { + if len(args) < 2 { + return "", fmt.Errorf("usage: add ") + } + + var id int32 + if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil { + return "", fmt.Errorf("invalid language ID: %s", args[0]) + } + + name := args[1] + + language := NewLanguage() + language.SetID(id) + language.SetName(name) + language.SetSaveNeeded(true) + + if err := m.AddLanguage(language); err != nil { + return "", fmt.Errorf("failed to add language: %w", err) + } + + return fmt.Sprintf("Successfully added language %d: %s", id, name), nil +} + +// handleRemoveCommand removes a language +func (m *Manager) handleRemoveCommand(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("language ID required") + } + + var id int32 + if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil { + return "", fmt.Errorf("invalid language ID: %s", args[0]) + } + + if err := m.RemoveLanguage(id); err != nil { + return "", fmt.Errorf("failed to remove language: %w", err) + } + + return fmt.Sprintf("Successfully removed language %d", id), nil +} + +// handleSearchCommand searches for languages by name +func (m *Manager) handleSearchCommand(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("search term required") + } + + searchTerm := args[0] + languages := m.masterLanguagesList.GetAllLanguages() + var results []*Language + + // Search by name (case-insensitive partial match) + for _, language := range languages { + if contains(language.GetName(), searchTerm) { + results = append(results, language) + } + } + + if len(results) == 0 { + return fmt.Sprintf("No languages found matching '%s'.", searchTerm), nil + } + + result := fmt.Sprintf("Found %d languages matching '%s':\n", len(results), searchTerm) + for i, language := range results { + if i >= 20 { // Limit output + result += "... (and more)\n" + break + } + result += fmt.Sprintf(" %d: %s\n", language.GetID(), language.GetName()) + } + + return result, nil +} + +// Shutdown gracefully shuts down the manager +func (m *Manager) Shutdown() { + if m.logger != nil { + m.logger.LogInfo("Shutting down language manager...") + } + + // Clear languages + m.masterLanguagesList.Clear() +} + +// contains checks if a string contains a substring (case-insensitive) +func contains(str, substr string) bool { + if len(substr) == 0 { + return true + } + if len(str) < len(substr) { + return false + } + + // Convert to lowercase for case-insensitive comparison + strLower := make([]byte, len(str)) + substrLower := make([]byte, len(substr)) + + for i := 0; i < len(str); i++ { + if str[i] >= 'A' && str[i] <= 'Z' { + strLower[i] = str[i] + 32 + } else { + strLower[i] = str[i] + } + } + + for i := 0; i < len(substr); i++ { + if substr[i] >= 'A' && substr[i] <= 'Z' { + substrLower[i] = substr[i] + 32 + } else { + substrLower[i] = substr[i] + } + } + + for i := 0; i <= len(strLower)-len(substrLower); i++ { + match := true + for j := 0; j < len(substrLower); j++ { + if strLower[i+j] != substrLower[j] { + match = false + break + } + } + if match { + return true + } + } + + return false +} \ No newline at end of file diff --git a/internal/languages/types.go b/internal/languages/types.go new file mode 100644 index 0000000..7656322 --- /dev/null +++ b/internal/languages/types.go @@ -0,0 +1,460 @@ +package languages + +import ( + "fmt" + "sync" +) + +// Language represents a single language that can be learned by players +type Language struct { + id int32 // Unique language identifier + name string // Language name + saveNeeded bool // Whether this language needs to be saved to database + mutex sync.RWMutex // Thread safety +} + +// NewLanguage creates a new language with default values +func NewLanguage() *Language { + return &Language{ + id: 0, + name: "", + saveNeeded: false, + } +} + +// NewLanguageFromExisting creates a copy of an existing language +func NewLanguageFromExisting(source *Language) *Language { + if source == nil { + return NewLanguage() + } + + source.mutex.RLock() + defer source.mutex.RUnlock() + + return &Language{ + id: source.id, + name: source.name, + saveNeeded: source.saveNeeded, + } +} + +// GetID returns the language's unique identifier +func (l *Language) GetID() int32 { + l.mutex.RLock() + defer l.mutex.RUnlock() + + return l.id +} + +// SetID updates the language's unique identifier +func (l *Language) SetID(id int32) { + l.mutex.Lock() + defer l.mutex.Unlock() + + l.id = id +} + +// GetName returns the language name +func (l *Language) GetName() string { + l.mutex.RLock() + defer l.mutex.RUnlock() + + return l.name +} + +// SetName updates the language name +func (l *Language) SetName(name string) { + l.mutex.Lock() + defer l.mutex.Unlock() + + // Truncate if too long + if len(name) > MaxLanguageNameLength { + name = name[:MaxLanguageNameLength] + } + + l.name = name +} + +// GetSaveNeeded returns whether this language needs database saving +func (l *Language) GetSaveNeeded() bool { + l.mutex.RLock() + defer l.mutex.RUnlock() + + return l.saveNeeded +} + +// SetSaveNeeded updates the save status +func (l *Language) SetSaveNeeded(needed bool) { + l.mutex.Lock() + defer l.mutex.Unlock() + + l.saveNeeded = needed +} + +// IsValid validates the language data +func (l *Language) IsValid() bool { + l.mutex.RLock() + defer l.mutex.RUnlock() + + if l.id < MinLanguageID || l.id > MaxLanguageID { + return false + } + + if len(l.name) == 0 || len(l.name) > MaxLanguageNameLength { + return false + } + + return true +} + +// String returns a string representation of the language +func (l *Language) String() string { + l.mutex.RLock() + defer l.mutex.RUnlock() + + return fmt.Sprintf("Language{ID: %d, Name: %s, SaveNeeded: %v}", l.id, l.name, l.saveNeeded) +} + +// Copy creates a deep copy of the language +func (l *Language) Copy() *Language { + return NewLanguageFromExisting(l) +} + +// MasterLanguagesList manages the global list of all available languages +type MasterLanguagesList struct { + languages map[int32]*Language // Languages indexed by ID for fast lookup + nameIndex map[string]*Language // Languages indexed by name for name lookups + mutex sync.RWMutex // Thread safety +} + +// NewMasterLanguagesList creates a new master languages list +func NewMasterLanguagesList() *MasterLanguagesList { + return &MasterLanguagesList{ + languages: make(map[int32]*Language), + nameIndex: make(map[string]*Language), + } +} + +// Clear removes all languages from the list +func (mll *MasterLanguagesList) Clear() { + mll.mutex.Lock() + defer mll.mutex.Unlock() + + mll.languages = make(map[int32]*Language) + mll.nameIndex = make(map[string]*Language) +} + +// Size returns the number of languages in the list +func (mll *MasterLanguagesList) Size() int32 { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + return int32(len(mll.languages)) +} + +// AddLanguage adds a new language to the master list +func (mll *MasterLanguagesList) AddLanguage(language *Language) error { + if language == nil { + return fmt.Errorf("language cannot be nil") + } + + if !language.IsValid() { + return fmt.Errorf("language is not valid: %s", language.String()) + } + + mll.mutex.Lock() + defer mll.mutex.Unlock() + + // Check for duplicate ID + if _, exists := mll.languages[language.GetID()]; exists { + return fmt.Errorf("language with ID %d already exists", language.GetID()) + } + + // Check for duplicate name + name := language.GetName() + if _, exists := mll.nameIndex[name]; exists { + return fmt.Errorf("language with name '%s' already exists", name) + } + + // Add to both indexes + mll.languages[language.GetID()] = language + mll.nameIndex[name] = language + + return nil +} + +// GetLanguage retrieves a language by ID +func (mll *MasterLanguagesList) GetLanguage(id int32) *Language { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + return mll.languages[id] +} + +// GetLanguageByName retrieves a language by name +func (mll *MasterLanguagesList) GetLanguageByName(name string) *Language { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + return mll.nameIndex[name] +} + +// GetAllLanguages returns a copy of all languages +func (mll *MasterLanguagesList) GetAllLanguages() []*Language { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + result := make([]*Language, 0, len(mll.languages)) + for _, lang := range mll.languages { + result = append(result, lang) + } + + return result +} + +// HasLanguage checks if a language exists by ID +func (mll *MasterLanguagesList) HasLanguage(id int32) bool { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + _, exists := mll.languages[id] + return exists +} + +// HasLanguageByName checks if a language exists by name +func (mll *MasterLanguagesList) HasLanguageByName(name string) bool { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + _, exists := mll.nameIndex[name] + return exists +} + +// RemoveLanguage removes a language by ID +func (mll *MasterLanguagesList) RemoveLanguage(id int32) bool { + mll.mutex.Lock() + defer mll.mutex.Unlock() + + language, exists := mll.languages[id] + if !exists { + return false + } + + // Remove from both indexes + delete(mll.languages, id) + delete(mll.nameIndex, language.GetName()) + + return true +} + +// UpdateLanguage updates an existing language +func (mll *MasterLanguagesList) UpdateLanguage(language *Language) error { + if language == nil { + return fmt.Errorf("language cannot be nil") + } + + if !language.IsValid() { + return fmt.Errorf("language is not valid: %s", language.String()) + } + + mll.mutex.Lock() + defer mll.mutex.Unlock() + + id := language.GetID() + oldLanguage, exists := mll.languages[id] + if !exists { + return fmt.Errorf("language with ID %d does not exist", id) + } + + // Remove old name index if name changed + oldName := oldLanguage.GetName() + newName := language.GetName() + if oldName != newName { + delete(mll.nameIndex, oldName) + + // Check for name conflicts + if _, exists := mll.nameIndex[newName]; exists { + return fmt.Errorf("language with name '%s' already exists", newName) + } + + mll.nameIndex[newName] = language + } + + // Update language + mll.languages[id] = language + + return nil +} + +// GetLanguageNames returns all language names +func (mll *MasterLanguagesList) GetLanguageNames() []string { + mll.mutex.RLock() + defer mll.mutex.RUnlock() + + names := make([]string, 0, len(mll.nameIndex)) + for name := range mll.nameIndex { + names = append(names, name) + } + + return names +} + +// PlayerLanguagesList manages languages known by a specific player +type PlayerLanguagesList struct { + languages map[int32]*Language // Player's languages indexed by ID + nameIndex map[string]*Language // Player's languages indexed by name + mutex sync.RWMutex // Thread safety +} + +// NewPlayerLanguagesList creates a new player languages list +func NewPlayerLanguagesList() *PlayerLanguagesList { + return &PlayerLanguagesList{ + languages: make(map[int32]*Language), + nameIndex: make(map[string]*Language), + } +} + +// Clear removes all languages from the player's list +func (pll *PlayerLanguagesList) Clear() { + pll.mutex.Lock() + defer pll.mutex.Unlock() + + pll.languages = make(map[int32]*Language) + pll.nameIndex = make(map[string]*Language) +} + +// Add adds a language to the player's known languages +func (pll *PlayerLanguagesList) Add(language *Language) error { + if language == nil { + return fmt.Errorf("language cannot be nil") + } + + pll.mutex.Lock() + defer pll.mutex.Unlock() + + id := language.GetID() + name := language.GetName() + + // Check if already known + if _, exists := pll.languages[id]; exists { + return fmt.Errorf("player already knows language with ID %d", id) + } + + // Check player language limit + if len(pll.languages) >= MaxLanguagesPerPlayer { + return fmt.Errorf("player has reached maximum language limit (%d)", MaxLanguagesPerPlayer) + } + + // Add to both indexes + pll.languages[id] = language + pll.nameIndex[name] = language + + return nil +} + +// GetLanguage retrieves a language the player knows by ID +func (pll *PlayerLanguagesList) GetLanguage(id int32) *Language { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + return pll.languages[id] +} + +// GetLanguageByName retrieves a language the player knows by name +func (pll *PlayerLanguagesList) GetLanguageByName(name string) *Language { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + return pll.nameIndex[name] +} + +// GetAllLanguages returns a copy of all languages the player knows +func (pll *PlayerLanguagesList) GetAllLanguages() []*Language { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + result := make([]*Language, 0, len(pll.languages)) + for _, lang := range pll.languages { + result = append(result, lang) + } + + return result +} + +// HasLanguage checks if the player knows a language by ID +func (pll *PlayerLanguagesList) HasLanguage(id int32) bool { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + _, exists := pll.languages[id] + return exists +} + +// HasLanguageByName checks if the player knows a language by name +func (pll *PlayerLanguagesList) HasLanguageByName(name string) bool { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + _, exists := pll.nameIndex[name] + return exists +} + +// RemoveLanguage removes a language from the player's known languages +func (pll *PlayerLanguagesList) RemoveLanguage(id int32) bool { + pll.mutex.Lock() + defer pll.mutex.Unlock() + + language, exists := pll.languages[id] + if !exists { + return false + } + + // Remove from both indexes + delete(pll.languages, id) + delete(pll.nameIndex, language.GetName()) + + return true +} + +// Size returns the number of languages the player knows +func (pll *PlayerLanguagesList) Size() int32 { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + return int32(len(pll.languages)) +} + +// GetLanguageIDs returns all language IDs the player knows +func (pll *PlayerLanguagesList) GetLanguageIDs() []int32 { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + ids := make([]int32, 0, len(pll.languages)) + for id := range pll.languages { + ids = append(ids, id) + } + + return ids +} + +// GetLanguageNames returns all language names the player knows +func (pll *PlayerLanguagesList) GetLanguageNames() []string { + pll.mutex.RLock() + defer pll.mutex.RUnlock() + + names := make([]string, 0, len(pll.nameIndex)) + for name := range pll.nameIndex { + names = append(names, name) + } + + return names +} + +// LanguageStatistics contains language system statistics +type LanguageStatistics struct { + TotalLanguages int `json:"total_languages"` + PlayersWithLanguages int `json:"players_with_languages"` + LanguageUsageCount map[int32]int64 `json:"language_usage_count"` + LanguageLookups int64 `json:"language_lookups"` + LanguagesByName map[string]int32 `json:"languages_by_name"` +} \ No newline at end of file diff --git a/internal/npc/ai/brain.go b/internal/npc/ai/brain.go new file mode 100644 index 0000000..1e63144 --- /dev/null +++ b/internal/npc/ai/brain.go @@ -0,0 +1,635 @@ +package ai + +import ( + "fmt" + "sync" + "time" +) + +// Brain interface defines the core AI functionality +type Brain interface { + // Core AI methods + Think() error + GetBrainType() int8 + + // State management + IsActive() bool + SetActive(bool) + GetState() int32 + SetState(int32) + + // Timing + GetThinkTick() int32 + SetThinkTick(int32) + GetLastThink() int64 + SetLastThink(int64) + + // Body management + GetBody() NPC + SetBody(NPC) + + // Hate management + AddHate(entityID int32, hate int32) + GetHate(entityID int32) int32 + ClearHate() + ClearHateForEntity(entityID int32) + GetMostHated() int32 + GetHatePercentage(entityID int32) int8 + GetHateList() map[int32]*HateEntry + + // Encounter management + AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool + IsEntityInEncounter(entityID int32) bool + IsPlayerInEncounter(characterID int32) bool + HasPlayerInEncounter() bool + GetEncounterSize() int + ClearEncounter() + CheckLootAllowed(entityID int32) bool + + // Combat methods + ProcessSpell(target Entity, distance float32) bool + ProcessMelee(target Entity, distance float32) + CheckBuffs() bool + HasRecovered() bool + MoveCloser(target Spawn) + + // Statistics + GetStatistics() *BrainStatistics + ResetStatistics() +} + +// BaseBrain provides the default AI implementation +type BaseBrain struct { + npc NPC // The NPC this brain controls + brainType int8 // Type of brain + state *BrainState // Brain state management + hateList *HateList // Hate management + encounterList *EncounterList // Encounter management + statistics *BrainStatistics // Performance statistics + logger Logger // Logger interface + mutex sync.RWMutex // Thread safety +} + +// NewBaseBrain creates a new base brain +func NewBaseBrain(npc NPC, logger Logger) *BaseBrain { + return &BaseBrain{ + npc: npc, + brainType: BrainTypeDefault, + state: NewBrainState(), + hateList: NewHateList(), + encounterList: NewEncounterList(), + statistics: NewBrainStatistics(), + logger: logger, + } +} + +// Think implements the main AI logic +func (bb *BaseBrain) Think() error { + if !bb.IsActive() { + return nil + } + + startTime := time.Now() + defer func() { + // Update statistics + bb.mutex.Lock() + bb.statistics.ThinkCycles++ + thinkTime := float64(time.Since(startTime).Nanoseconds()) / 1000000.0 // Convert to milliseconds + bb.statistics.AverageThinkTime = (bb.statistics.AverageThinkTime + thinkTime) / 2.0 + bb.statistics.LastThinkTime = time.Now().UnixMilli() + bb.mutex.Unlock() + + bb.state.SetLastThink(time.Now().UnixMilli()) + }() + + if bb.npc == nil { + return fmt.Errorf("brain has no body") + } + + // Handle pet ID registration for players + if bb.npc.IsPet() && bb.npc.GetOwner() != nil && bb.npc.GetOwner().IsPlayer() { + // TODO: Register pet ID with player's info struct + } + + // Get the most hated target + mostHatedID := bb.hateList.GetMostHated() + var target Entity + + if mostHatedID > 0 { + target = bb.getEntityByID(mostHatedID) + // Remove dead targets from hate list + if target != nil && target.GetHP() <= 0 { + bb.hateList.RemoveHate(mostHatedID) + target = nil + // Try again to get most hated + mostHatedID = bb.hateList.GetMostHated() + if mostHatedID > 0 { + target = bb.getEntityByID(mostHatedID) + } + } + } + + // Skip if mezzed, stunned, or feared + if bb.npc.IsMezzedOrStunned() { + return nil + } + + // Get runback distance + runbackDistance := bb.npc.GetRunbackDistance() + + if target != nil { + // We have a target to fight + if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { + bb.logger.LogDebug("NPC %s has target %s", bb.npc.GetName(), target.GetName()) + } + + // Set target if not already set + if bb.npc.GetTarget() != target { + bb.npc.SetTarget(target) + } + + // Face the target + bb.npc.FaceTarget(target, false) + + // Enter combat if not already in combat + if !bb.npc.GetInCombat() { + bb.npc.ClearRunningLocations() + bb.npc.InCombat(true) + bb.npc.SetCastOnAggroCompleted(false) + // TODO: Call spawn script for aggro + } + + // Check chase distance and water restrictions + if bb.shouldBreakPursuit(target, runbackDistance) { + // Break pursuit - clear hate and encounter + if bb.logger != nil { + bb.logger.LogDebug("NPC %s breaking pursuit (distance: %.2f)", bb.npc.GetName(), runbackDistance) + } + + // TODO: Send encounter break messages to players + bb.hateList.Clear() + bb.encounterList.Clear() + } else { + // Continue combat + distance := bb.npc.GetDistance(target) + + // Try to cast spells first, then melee + if !bb.npc.IsCasting() && (!bb.HasRecovered() || !bb.ProcessSpell(target, distance)) { + if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { + bb.logger.LogDebug("NPC %s attempting melee on %s", bb.npc.GetName(), target.GetName()) + } + bb.npc.FaceTarget(target, false) + bb.ProcessMelee(target, distance) + } + } + } else { + // No target - handle out of combat behavior + wasInCombat := bb.npc.GetInCombat() + + if bb.npc.GetInCombat() { + bb.npc.InCombat(false) + + // Restore HP for non-player pets + if !bb.npc.IsPet() || (bb.npc.IsPet() && bb.npc.GetOwner() != nil && !bb.npc.GetOwner().IsPlayer()) { + bb.npc.SetHP(bb.npc.GetTotalHP()) + } + } + + // Check for buffs when not in combat + bb.CheckBuffs() + + // Handle runback if needed + if !bb.npc.GetInCombat() && !bb.npc.IsPauseMovementTimerActive() { + if runbackDistance > RunbackThreshold || (bb.npc.ShouldCallRunback() && !bb.npc.IsFollowing()) { + bb.npc.SetEncounterState(EncounterStateBroken) + bb.npc.Runback(runbackDistance) + bb.npc.SetCallRunback(false) + } else if bb.npc.GetRunbackLocation() != nil { + bb.handleRunbackStages() + } + } + + // Clear encounter if any entities remain + if bb.encounterList.Size() > 0 { + bb.encounterList.Clear() + } + } + + return nil +} + +// GetBrainType returns the brain type +func (bb *BaseBrain) GetBrainType() int8 { + return bb.brainType +} + +// IsActive returns whether the brain is active +func (bb *BaseBrain) IsActive() bool { + return bb.state.IsActive() +} + +// SetActive sets the brain's active state +func (bb *BaseBrain) SetActive(active bool) { + bb.state.SetActive(active) +} + +// GetState returns the current AI state +func (bb *BaseBrain) GetState() int32 { + return bb.state.GetState() +} + +// SetState sets the current AI state +func (bb *BaseBrain) SetState(state int32) { + bb.state.SetState(state) +} + +// GetThinkTick returns the think tick interval +func (bb *BaseBrain) GetThinkTick() int32 { + return bb.state.GetThinkTick() +} + +// SetThinkTick sets the think tick interval +func (bb *BaseBrain) SetThinkTick(tick int32) { + bb.state.SetThinkTick(tick) +} + +// GetLastThink returns the timestamp of the last think cycle +func (bb *BaseBrain) GetLastThink() int64 { + return bb.state.GetLastThink() +} + +// SetLastThink sets the timestamp of the last think cycle +func (bb *BaseBrain) SetLastThink(timestamp int64) { + bb.state.SetLastThink(timestamp) +} + +// GetBody returns the NPC this brain controls +func (bb *BaseBrain) GetBody() NPC { + bb.mutex.RLock() + defer bb.mutex.RUnlock() + return bb.npc +} + +// SetBody sets the NPC this brain controls +func (bb *BaseBrain) SetBody(npc NPC) { + bb.mutex.Lock() + defer bb.mutex.Unlock() + bb.npc = npc +} + +// AddHate adds hate for an entity +func (bb *BaseBrain) AddHate(entityID int32, hate int32) { + // Don't add hate while running back + if bb.npc != nil && bb.npc.IsRunningBack() { + return + } + + // Don't add hate if owner is attacking pet + if bb.npc != nil && bb.npc.IsPet() && bb.npc.GetOwner() != nil { + if bb.npc.GetOwner().GetID() == entityID { + return + } + } + + // Check for taunt immunity + // TODO: Implement immunity checking + + bb.hateList.AddHate(entityID, hate) + + // Update statistics + bb.mutex.Lock() + bb.statistics.HateEvents++ + bb.mutex.Unlock() + + // TODO: Add to entity's HatedBy list + + // Add pet owner to hate list if not already present + entity := bb.getEntityByID(entityID) + if entity != nil && entity.IsPet() && entity.GetOwner() != nil { + ownerID := entity.GetOwner().GetID() + if bb.hateList.GetHate(ownerID) == 0 { + bb.hateList.AddHate(ownerID, 0) + } + } +} + +// GetHate returns the hate value for an entity +func (bb *BaseBrain) GetHate(entityID int32) int32 { + return bb.hateList.GetHate(entityID) +} + +// ClearHate removes all hate entries +func (bb *BaseBrain) ClearHate() { + bb.hateList.Clear() + // TODO: Update entities' HatedBy lists +} + +// ClearHateForEntity removes hate for a specific entity +func (bb *BaseBrain) ClearHateForEntity(entityID int32) { + bb.hateList.RemoveHate(entityID) + // TODO: Update entity's HatedBy list +} + +// GetMostHated returns the ID of the most hated entity +func (bb *BaseBrain) GetMostHated() int32 { + return bb.hateList.GetMostHated() +} + +// GetHatePercentage returns the hate percentage for an entity +func (bb *BaseBrain) GetHatePercentage(entityID int32) int8 { + return bb.hateList.GetHatePercentage(entityID) +} + +// GetHateList returns a copy of the hate list +func (bb *BaseBrain) GetHateList() map[int32]*HateEntry { + return bb.hateList.GetAllEntries() +} + +// AddToEncounter adds an entity to the encounter list +func (bb *BaseBrain) AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool { + success := bb.encounterList.AddEntity(entityID, characterID, isPlayer, isBot) + if success { + bb.mutex.Lock() + bb.statistics.EncounterEvents++ + bb.mutex.Unlock() + } + return success +} + +// IsEntityInEncounter checks if an entity is in the encounter +func (bb *BaseBrain) IsEntityInEncounter(entityID int32) bool { + return bb.encounterList.IsEntityInEncounter(entityID) +} + +// IsPlayerInEncounter checks if a player is in the encounter +func (bb *BaseBrain) IsPlayerInEncounter(characterID int32) bool { + return bb.encounterList.IsPlayerInEncounter(characterID) +} + +// HasPlayerInEncounter returns whether any player is in the encounter +func (bb *BaseBrain) HasPlayerInEncounter() bool { + return bb.encounterList.HasPlayerInEncounter() +} + +// GetEncounterSize returns the size of the encounter list +func (bb *BaseBrain) GetEncounterSize() int { + return bb.encounterList.Size() +} + +// ClearEncounter removes all entities from the encounter +func (bb *BaseBrain) ClearEncounter() { + bb.encounterList.Clear() + // TODO: Remove spells from NPC +} + +// CheckLootAllowed checks if an entity can loot this NPC +func (bb *BaseBrain) CheckLootAllowed(entityID int32) bool { + // TODO: Implement loot method checking, chest timers, etc. + + // Basic check - is entity in encounter? + return bb.encounterList.IsEntityInEncounter(entityID) +} + +// ProcessSpell attempts to cast a spell +func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool { + if bb.npc == nil { + return false + } + + // Check cast percentage and conditions + castChance := bb.npc.GetCastPercentage() + if castChance <= 0 { + return false + } + + // TODO: Implement random chance checking + // TODO: Check for stifled, feared conditions + + // Get next spell to cast + spell := bb.npc.GetNextSpell(target, distance) + if spell == nil { + return false + } + + // Determine spell target + var spellTarget Spawn = target + if spell.IsFriendlySpell() { + // TODO: Find best friendly target (lowest HP group member) + spellTarget = bb.npc + } + + // Cast the spell + success := bb.castSpell(spell, spellTarget, false) + if success { + bb.mutex.Lock() + bb.statistics.SpellsCast++ + bb.mutex.Unlock() + } + + return success +} + +// ProcessMelee handles melee combat +func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) { + if bb.npc == nil || target == nil { + return + } + + maxCombatRange := bb.getMaxCombatRange() + + if distance > maxCombatRange { + bb.MoveCloser(target) + } else { + if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { + bb.logger.LogDebug("NPC %s is within melee range of %s", bb.npc.GetName(), target.GetName()) + } + + // Check if attack is allowed + if !bb.npc.AttackAllowed(target) { + return + } + + currentTime := time.Now().UnixMilli() + + // Primary weapon attack + if bb.npc.PrimaryWeaponReady() && !bb.npc.IsDazed() && !bb.npc.IsFeared() { + if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelVerbose { + bb.logger.LogDebug("NPC %s swings primary weapon at %s", bb.npc.GetName(), target.GetName()) + } + + bb.npc.SetPrimaryLastAttackTime(currentTime) + bb.npc.MeleeAttack(target, distance, true) + + bb.mutex.Lock() + bb.statistics.MeleeAttacks++ + bb.mutex.Unlock() + + // TODO: Call spawn script for auto attack tick + } + + // Secondary weapon attack + if bb.npc.SecondaryWeaponReady() && !bb.npc.IsDazed() { + bb.npc.SetSecondaryLastAttackTime(currentTime) + bb.npc.MeleeAttack(target, distance, false) + + bb.mutex.Lock() + bb.statistics.MeleeAttacks++ + bb.mutex.Unlock() + } + } +} + +// CheckBuffs checks and casts buff spells +func (bb *BaseBrain) CheckBuffs() bool { + if bb.npc == nil { + return false + } + + // Don't buff in combat, while casting, stunned, etc. + if bb.npc.GetInCombat() || bb.npc.IsCasting() || bb.npc.IsMezzedOrStunned() || + !bb.npc.IsAlive() || bb.npc.IsStifled() || !bb.HasRecovered() { + return false + } + + // Get next buff spell + buffSpell := bb.npc.GetNextBuffSpell(bb.npc) + if buffSpell == nil { + return false + } + + // Try to cast on self first + if bb.castSpell(buffSpell, bb.npc, false) { + return true + } + + // TODO: Try to buff group members + + return false +} + +// HasRecovered checks if the brain has recovered from spell casting +func (bb *BaseBrain) HasRecovered() bool { + return bb.state.HasRecovered() +} + +// MoveCloser moves the NPC closer to a target +func (bb *BaseBrain) MoveCloser(target Spawn) { + if bb.npc == nil || target == nil { + return + } + + maxCombatRange := bb.getMaxCombatRange() + + if bb.npc.GetFollowTarget() != target { + bb.npc.SetFollowTarget(target, maxCombatRange) + } + + if bb.npc.GetFollowTarget() != nil && !bb.npc.IsFollowing() { + bb.npc.CalculateRunningLocation(true) + bb.npc.SetFollowing(true) + } +} + +// GetStatistics returns brain performance statistics +func (bb *BaseBrain) GetStatistics() *BrainStatistics { + bb.mutex.RLock() + defer bb.mutex.RUnlock() + + // Return a copy + return &BrainStatistics{ + ThinkCycles: bb.statistics.ThinkCycles, + SpellsCast: bb.statistics.SpellsCast, + MeleeAttacks: bb.statistics.MeleeAttacks, + HateEvents: bb.statistics.HateEvents, + EncounterEvents: bb.statistics.EncounterEvents, + AverageThinkTime: bb.statistics.AverageThinkTime, + LastThinkTime: bb.statistics.LastThinkTime, + TotalActiveTime: bb.statistics.TotalActiveTime, + } +} + +// ResetStatistics resets all performance statistics +func (bb *BaseBrain) ResetStatistics() { + bb.mutex.Lock() + defer bb.mutex.Unlock() + + bb.statistics = NewBrainStatistics() +} + +// Helper methods + +// shouldBreakPursuit checks if the NPC should break pursuit of a target +func (bb *BaseBrain) shouldBreakPursuit(target Entity, runbackDistance float32) bool { + if target == nil { + return false + } + + // Check max chase distance + maxChase := bb.getMaxChaseDistance() + if runbackDistance > maxChase { + return true + } + + // Check water creature restrictions + if bb.npc != nil && bb.npc.IsWaterCreature() && !bb.npc.IsFlyingCreature() && !target.InWater() { + return true + } + + return false +} + +// castSpell casts a spell on a target +func (bb *BaseBrain) castSpell(spell Spell, target Spawn, calculateRunLoc bool) bool { + if spell == nil || bb.npc == nil { + return false + } + + if calculateRunLoc { + bb.npc.CalculateRunningLocation(true) + } + + // TODO: Process spell through zone + // bb.npc.GetZone().ProcessSpell(spell, bb.npc, target) + + // Set spell recovery time + castTime := spell.GetCastTime() * RecoveryTimeMultiple + recoveryTime := spell.GetRecoveryTime() * RecoveryTimeMultiple + totalRecovery := time.Now().UnixMilli() + int64(castTime) + int64(recoveryTime) + int64(SpellRecoveryBuffer) + + bb.state.SetSpellRecovery(totalRecovery) + + return true +} + +// handleRunbackStages handles the various stages of runback +func (bb *BaseBrain) handleRunbackStages() { + if bb.npc == nil { + return + } + + runbackLoc := bb.npc.GetRunbackLocation() + if runbackLoc == nil { + return + } + + // TODO: Implement runback stage handling + // This would involve movement management and position updates +} + +// getEntityByID retrieves an entity by ID (placeholder) +func (bb *BaseBrain) getEntityByID(entityID int32) Entity { + // TODO: Implement entity lookup through zone + return nil +} + +// getMaxChaseDistance returns the maximum chase distance +func (bb *BaseBrain) getMaxChaseDistance() float32 { + // TODO: Check NPC info struct and zone rules + return MaxChaseDistance +} + +// getMaxCombatRange returns the maximum combat range +func (bb *BaseBrain) getMaxCombatRange() float32 { + // TODO: Check zone rules + return MaxCombatRange +} \ No newline at end of file diff --git a/internal/npc/ai/constants.go b/internal/npc/ai/constants.go new file mode 100644 index 0000000..b5e64f2 --- /dev/null +++ b/internal/npc/ai/constants.go @@ -0,0 +1,90 @@ +package ai + +// AI tick constants +const ( + DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second) + FastThinkTick int32 = 100 // Fast think tick for active AI + SlowThinkTick int32 = 1000 // Slow think tick for idle AI + BlankBrainTick int32 = 50000 // Very slow tick for blank brain + MaxThinkTick int32 = 60000 // Maximum think tick (1 minute) +) + +// Combat constants +const ( + MaxChaseDistance float32 = 150.0 // Default max chase distance + MaxCombatRange float32 = 25.0 // Default max combat range + RunbackThreshold float32 = 1.0 // Distance threshold for runback +) + +// Hate system constants +const ( + MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid) + MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX) + DefaultHateValue int32 = 100 // Default hate amount + MaxHateListSize int = 100 // Maximum entities in hate list +) + +// Encounter system constants +const ( + MaxEncounterSize int = 50 // Maximum entities in encounter list +) + +// Spell recovery constants +const ( + SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds) +) + +// Brain type constants for identification +const ( + BrainTypeDefault int8 = 0 + BrainTypeCombatPet int8 = 1 + BrainTypeNonCombatPet int8 = 2 + BrainTypeBlank int8 = 3 + BrainTypeLua int8 = 4 + BrainTypeDumbFire int8 = 5 +) + +// Pet movement constants +const ( + PetMovementFollow int8 = 0 + PetMovementStay int8 = 1 + PetMovementGuard int8 = 2 +) + +// Encounter state constants +const ( + EncounterStateAvailable int8 = 0 + EncounterStateLocked int8 = 1 + EncounterStateBroken int8 = 2 +) + +// Combat decision constants +const ( + MeleeAttackChance int = 70 // Base chance for melee attack + SpellCastChance int = 30 // Base chance for spell casting + BuffCheckChance int = 50 // Chance to check for buffs +) + +// AI state flags +const ( + AIStateIdle int32 = 0 + AIStateCombat int32 = 1 + AIStateFollowing int32 = 2 + AIStateRunback int32 = 3 + AIStateCasting int32 = 4 + AIStateMoving int32 = 5 +) + +// Debug levels +const ( + DebugLevelNone int8 = 0 + DebugLevelBasic int8 = 1 + DebugLevelDetailed int8 = 2 + DebugLevelVerbose int8 = 3 +) + +// Timer constants +const ( + MillisecondsPerSecond int32 = 1000 + RecoveryTimeMultiple int32 = 10 // Multiply cast/recovery times by 10 +) \ No newline at end of file diff --git a/internal/npc/ai/interfaces.go b/internal/npc/ai/interfaces.go new file mode 100644 index 0000000..cb20f05 --- /dev/null +++ b/internal/npc/ai/interfaces.go @@ -0,0 +1,483 @@ +package ai + +import "fmt" + +// Logger interface for AI logging +type Logger interface { + LogInfo(message string, args ...interface{}) + LogError(message string, args ...interface{}) + LogDebug(message string, args ...interface{}) + LogWarning(message string, args ...interface{}) +} + +// NPC interface defines the required NPC functionality for AI +type NPC interface { + // Basic NPC information + GetID() int32 + GetName() string + GetHP() int32 + GetTotalHP() int32 + SetHP(int32) + IsAlive() bool + + // Combat state + GetInCombat() bool + InCombat(bool) + GetTarget() Entity + SetTarget(Entity) + + // Pet functionality + IsPet() bool + GetOwner() Entity + + // Movement and positioning + GetX() float32 + GetY() float32 + GetZ() float32 + GetDistance(Entity) float32 + FaceTarget(Entity, bool) + IsFollowing() bool + SetFollowing(bool) + GetFollowTarget() Spawn + SetFollowTarget(Spawn, float32) + CalculateRunningLocation(bool) + ClearRunningLocations() + + // Runback functionality + IsRunningBack() bool + GetRunbackLocation() *MovementLocation + GetRunbackDistance() float32 + Runback(float32) + ShouldCallRunback() bool + SetCallRunback(bool) + + // Status effects + IsMezzedOrStunned() bool + IsCasting() bool + IsDazed() bool + IsFeared() bool + IsStifled() bool + InWater() bool + IsWaterCreature() bool + IsFlyingCreature() bool + + // Combat mechanics + AttackAllowed(Entity) bool + PrimaryWeaponReady() bool + SecondaryWeaponReady() bool + SetPrimaryLastAttackTime(int64) + SetSecondaryLastAttackTime(int64) + MeleeAttack(Entity, float32, bool) + + // Spell casting + GetCastPercentage() int8 + GetNextSpell(Entity, float32) Spell + GetNextBuffSpell(Spawn) Spell + SetCastOnAggroCompleted(bool) + CheckLoS(Entity) bool + + // Movement pausing + IsPauseMovementTimerActive() bool + + // Encounter state + SetEncounterState(int8) + + // Scripts + GetSpawnScript() string + + // Utility + KillSpawn(NPC) +} + +// Entity interface for combat entities +type Entity interface { + Spawn + GetID() int32 + GetName() string + GetHP() int32 + GetTotalHP() int32 + IsPlayer() bool + IsBot() bool + IsPet() bool + GetOwner() Entity + InWater() bool +} + +// Spawn interface for basic spawn functionality +type Spawn interface { + GetID() int32 + GetName() string + GetX() float32 + GetY() float32 + GetZ() float32 +} + +// Spell interface for spell data +type Spell interface { + GetSpellID() int32 + GetName() string + IsFriendlySpell() bool + GetCastTime() int32 + GetRecoveryTime() int32 + GetRange() float32 + GetMinRange() float32 +} + +// MovementLocation represents a location for movement/runback +type MovementLocation struct { + X float32 + Y float32 + Z float32 + GridID int32 + Stage int32 +} + +// LuaInterface defines Lua script integration +type LuaInterface interface { + RunSpawnScript(script, function string, npc NPC, target Entity) error +} + +// Zone interface for zone-related AI operations +type Zone interface { + GetSpawnByID(int32) Spawn + ProcessSpell(spell Spell, caster NPC, target Spawn) error + CallSpawnScript(npc NPC, scriptType string, args ...interface{}) error +} + +// AIManager provides high-level management of the AI system +type AIManager struct { + brains map[int32]Brain // Map of NPC ID to brain + activeCount int64 // Number of active brains + totalThinks int64 // Total think cycles processed + logger Logger // Logger for AI events + luaInterface LuaInterface // Lua script interface +} + +// NewAIManager creates a new AI manager +func NewAIManager(logger Logger, luaInterface LuaInterface) *AIManager { + return &AIManager{ + brains: make(map[int32]Brain), + activeCount: 0, + totalThinks: 0, + logger: logger, + luaInterface: luaInterface, + } +} + +// AddBrain adds a brain for an NPC +func (am *AIManager) AddBrain(npcID int32, brain Brain) error { + if brain == nil { + return fmt.Errorf("brain cannot be nil") + } + + if _, exists := am.brains[npcID]; exists { + return fmt.Errorf("brain already exists for NPC %d", npcID) + } + + am.brains[npcID] = brain + if brain.IsActive() { + am.activeCount++ + } + + if am.logger != nil { + am.logger.LogDebug("Added brain for NPC %d (type: %d)", npcID, brain.GetBrainType()) + } + + return nil +} + +// RemoveBrain removes a brain for an NPC +func (am *AIManager) RemoveBrain(npcID int32) { + if brain, exists := am.brains[npcID]; exists { + if brain.IsActive() { + am.activeCount-- + } + delete(am.brains, npcID) + + if am.logger != nil { + am.logger.LogDebug("Removed brain for NPC %d", npcID) + } + } +} + +// GetBrain retrieves a brain for an NPC +func (am *AIManager) GetBrain(npcID int32) Brain { + return am.brains[npcID] +} + +// CreateBrainForNPC creates and adds the appropriate brain for an NPC +func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...interface{}) error { + if npc == nil { + return fmt.Errorf("NPC cannot be nil") + } + + npcID := npc.GetID() + + // Create brain based on type + var brain Brain + switch brainType { + case BrainTypeCombatPet: + brain = NewCombatPetBrain(npc, am.logger) + + case BrainTypeNonCombatPet: + brain = NewNonCombatPetBrain(npc, am.logger) + + case BrainTypeBlank: + brain = NewBlankBrain(npc, am.logger) + + case BrainTypeLua: + brain = NewLuaBrain(npc, am.logger, am.luaInterface) + + case BrainTypeDumbFire: + if len(options) >= 2 { + if target, ok := options[0].(Entity); ok { + if expireTime, ok := options[1].(int32); ok { + brain = NewDumbFirePetBrain(npc, target, expireTime, am.logger) + } + } + } + if brain == nil { + return fmt.Errorf("invalid options for dumbfire brain") + } + + default: + brain = NewBaseBrain(npc, am.logger) + } + + return am.AddBrain(npcID, brain) +} + +// ProcessAllBrains runs think cycles for all active brains +func (am *AIManager) ProcessAllBrains() { + currentTime := currentTimeMillis() + + for npcID, brain := range am.brains { + if !brain.IsActive() { + continue + } + + // Check if it's time to think + lastThink := brain.GetLastThink() + thinkTick := brain.GetThinkTick() + + if currentTime-lastThink >= int64(thinkTick) { + if err := brain.Think(); err != nil { + if am.logger != nil { + am.logger.LogError("Brain think error for NPC %d: %v", npcID, err) + } + } + am.totalThinks++ + } + } +} + +// SetBrainActive sets the active state of a brain +func (am *AIManager) SetBrainActive(npcID int32, active bool) { + if brain := am.brains[npcID]; brain != nil { + wasActive := brain.IsActive() + brain.SetActive(active) + + // Update active count + if wasActive && !active { + am.activeCount-- + } else if !wasActive && active { + am.activeCount++ + } + } +} + +// GetActiveCount returns the number of active brains +func (am *AIManager) GetActiveCount() int64 { + return am.activeCount +} + +// GetTotalThinks returns the total number of think cycles processed +func (am *AIManager) GetTotalThinks() int64 { + return am.totalThinks +} + +// GetBrainCount returns the total number of brains +func (am *AIManager) GetBrainCount() int { + return len(am.brains) +} + +// GetBrainsByType returns all brains of a specific type +func (am *AIManager) GetBrainsByType(brainType int8) []Brain { + var result []Brain + for _, brain := range am.brains { + if brain.GetBrainType() == brainType { + result = append(result, brain) + } + } + return result +} + +// ClearAllBrains removes all brains +func (am *AIManager) ClearAllBrains() { + am.brains = make(map[int32]Brain) + am.activeCount = 0 + + if am.logger != nil { + am.logger.LogInfo("Cleared all AI brains") + } +} + +// GetStatistics returns overall AI system statistics +func (am *AIManager) GetStatistics() *AIStatistics { + return &AIStatistics{ + TotalBrains: len(am.brains), + ActiveBrains: int(am.activeCount), + TotalThinks: am.totalThinks, + BrainsByType: am.getBrainCountsByType(), + } +} + +// getBrainCountsByType returns counts of brains by type +func (am *AIManager) getBrainCountsByType() map[string]int { + counts := make(map[string]int) + + for _, brain := range am.brains { + typeName := getBrainTypeName(brain.GetBrainType()) + counts[typeName]++ + } + + return counts +} + +// AIStatistics contains AI system statistics +type AIStatistics struct { + TotalBrains int `json:"total_brains"` + ActiveBrains int `json:"active_brains"` + TotalThinks int64 `json:"total_thinks"` + BrainsByType map[string]int `json:"brains_by_type"` +} + +// AIBrainAdapter provides NPC functionality for brains +type AIBrainAdapter struct { + npc NPC + logger Logger +} + +// NewAIBrainAdapter creates a new brain adapter +func NewAIBrainAdapter(npc NPC, logger Logger) *AIBrainAdapter { + return &AIBrainAdapter{ + npc: npc, + logger: logger, + } +} + +// GetNPC returns the adapted NPC +func (aba *AIBrainAdapter) GetNPC() NPC { + return aba.npc +} + +// ProcessAI processes AI for the NPC using its brain +func (aba *AIBrainAdapter) ProcessAI(brain Brain) error { + if brain == nil { + return fmt.Errorf("brain is nil") + } + + if !brain.IsActive() { + return nil + } + + return brain.Think() +} + +// SetupDefaultBrain sets up a default brain for the NPC +func (aba *AIBrainAdapter) SetupDefaultBrain() Brain { + return NewBaseBrain(aba.npc, aba.logger) +} + +// SetupPetBrain sets up a pet brain based on pet type +func (aba *AIBrainAdapter) SetupPetBrain(combatPet bool) Brain { + if combatPet { + return NewCombatPetBrain(aba.npc, aba.logger) + } + return NewNonCombatPetBrain(aba.npc, aba.logger) +} + +// Utility functions + +// getBrainTypeName returns the string name for a brain type +func getBrainTypeName(brainType int8) string { + switch brainType { + case BrainTypeDefault: + return "default" + case BrainTypeCombatPet: + return "combat_pet" + case BrainTypeNonCombatPet: + return "non_combat_pet" + case BrainTypeBlank: + return "blank" + case BrainTypeLua: + return "lua" + case BrainTypeDumbFire: + return "dumbfire" + default: + return "unknown" + } +} + +// currentTimeMillis returns current time in milliseconds +func currentTimeMillis() int64 { + return time.Now().UnixMilli() +} + +// HateListDebugger provides debugging functionality for hate lists +type HateListDebugger struct { + logger Logger +} + +// NewHateListDebugger creates a new hate list debugger +func NewHateListDebugger(logger Logger) *HateListDebugger { + return &HateListDebugger{ + logger: logger, + } +} + +// PrintHateList prints a formatted hate list +func (hld *HateListDebugger) PrintHateList(npcName string, hateList map[int32]*HateEntry) { + if hld.logger == nil { + return + } + + hld.logger.LogInfo("%s's Hate List", npcName) + hld.logger.LogInfo("-------------------") + + if len(hateList) == 0 { + hld.logger.LogInfo("(empty)") + } else { + for entityID, entry := range hateList { + hld.logger.LogInfo("Entity %d: %d hate", entityID, entry.HateValue) + } + } + + hld.logger.LogInfo("-------------------") +} + +// PrintEncounterList prints a formatted encounter list +func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList map[int32]*EncounterEntry) { + if hld.logger == nil { + return + } + + hld.logger.LogInfo("%s's Encounter List", npcName) + hld.logger.LogInfo("-------------------") + + if len(encounterList) == 0 { + hld.logger.LogInfo("(empty)") + } else { + for entityID, entry := range encounterList { + entryType := "NPC" + if entry.IsPlayer { + entryType = "Player" + } else if entry.IsBot { + entryType = "Bot" + } + hld.logger.LogInfo("Entity %d (%s)", entityID, entryType) + } + } + + hld.logger.LogInfo("-------------------") +} \ No newline at end of file diff --git a/internal/npc/ai/types.go b/internal/npc/ai/types.go new file mode 100644 index 0000000..b96627b --- /dev/null +++ b/internal/npc/ai/types.go @@ -0,0 +1,504 @@ +package ai + +import ( + "sync" + "time" +) + +// HateEntry represents a single hate entry in the hate list +type HateEntry struct { + EntityID int32 // ID of the hated entity + HateValue int32 // Amount of hate (must be >= 1) + LastUpdated int64 // Timestamp of last hate update +} + +// NewHateEntry creates a new hate entry +func NewHateEntry(entityID, hateValue int32) *HateEntry { + if hateValue < MinHateValue { + hateValue = MinHateValue + } + + return &HateEntry{ + EntityID: entityID, + HateValue: hateValue, + LastUpdated: time.Now().UnixMilli(), + } +} + +// HateList manages the hate list for an NPC brain +type HateList struct { + entries map[int32]*HateEntry // Map of entity ID to hate entry + mutex sync.RWMutex // Thread safety +} + +// NewHateList creates a new hate list +func NewHateList() *HateList { + return &HateList{ + entries: make(map[int32]*HateEntry), + } +} + +// AddHate adds or updates hate for an entity +func (hl *HateList) AddHate(entityID, hateValue int32) { + hl.mutex.Lock() + defer hl.mutex.Unlock() + + if len(hl.entries) >= MaxHateListSize { + // Remove oldest entry if at capacity + hl.removeOldestEntry() + } + + if entry, exists := hl.entries[entityID]; exists { + // Update existing entry + entry.HateValue += hateValue + if entry.HateValue < MinHateValue { + entry.HateValue = MinHateValue + } + entry.LastUpdated = time.Now().UnixMilli() + } else { + // Create new entry + if hateValue < MinHateValue { + hateValue = MinHateValue + } + hl.entries[entityID] = NewHateEntry(entityID, hateValue) + } +} + +// GetHate returns the hate value for an entity +func (hl *HateList) GetHate(entityID int32) int32 { + hl.mutex.RLock() + defer hl.mutex.RUnlock() + + if entry, exists := hl.entries[entityID]; exists { + return entry.HateValue + } + return 0 +} + +// RemoveHate removes hate for a specific entity +func (hl *HateList) RemoveHate(entityID int32) { + hl.mutex.Lock() + defer hl.mutex.Unlock() + + delete(hl.entries, entityID) +} + +// Clear removes all hate entries +func (hl *HateList) Clear() { + hl.mutex.Lock() + defer hl.mutex.Unlock() + + hl.entries = make(map[int32]*HateEntry) +} + +// GetMostHated returns the entity ID with the highest hate +func (hl *HateList) GetMostHated() int32 { + hl.mutex.RLock() + defer hl.mutex.RUnlock() + + var mostHated int32 = 0 + var highestHate int32 = 0 + + for entityID, entry := range hl.entries { + if entry.HateValue > highestHate { + highestHate = entry.HateValue + mostHated = entityID + } + } + + return mostHated +} + +// GetHatePercentage returns the percentage of hate for an entity +func (hl *HateList) GetHatePercentage(entityID int32) int8 { + hl.mutex.RLock() + defer hl.mutex.RUnlock() + + entry, exists := hl.entries[entityID] + if !exists || entry.HateValue <= 0 { + return 0 + } + + // Calculate total hate + var totalHate int32 = 0 + for _, e := range hl.entries { + totalHate += e.HateValue + } + + if totalHate <= 0 { + return 0 + } + + percentage := float32(entry.HateValue) / float32(totalHate) * 100.0 + return int8(percentage) +} + +// GetAllEntries returns a copy of all hate entries +func (hl *HateList) GetAllEntries() map[int32]*HateEntry { + hl.mutex.RLock() + defer hl.mutex.RUnlock() + + result := make(map[int32]*HateEntry) + for id, entry := range hl.entries { + result[id] = &HateEntry{ + EntityID: entry.EntityID, + HateValue: entry.HateValue, + LastUpdated: entry.LastUpdated, + } + } + return result +} + +// Size returns the number of entries in the hate list +func (hl *HateList) Size() int { + hl.mutex.RLock() + defer hl.mutex.RUnlock() + + return len(hl.entries) +} + +// removeOldestEntry removes the oldest hate entry (internal use only) +func (hl *HateList) removeOldestEntry() { + if len(hl.entries) == 0 { + return + } + + var oldestID int32 + var oldestTime int64 = time.Now().UnixMilli() + + for id, entry := range hl.entries { + if entry.LastUpdated < oldestTime { + oldestTime = entry.LastUpdated + oldestID = id + } + } + + if oldestID != 0 { + delete(hl.entries, oldestID) + } +} + +// EncounterEntry represents a single encounter participant +type EncounterEntry struct { + EntityID int32 // ID of the entity + CharacterID int32 // Character ID for players (0 for NPCs) + AddedTime int64 // When entity was added to encounter + IsPlayer bool // Whether this is a player entity + IsBot bool // Whether this is a bot entity +} + +// NewEncounterEntry creates a new encounter entry +func NewEncounterEntry(entityID, characterID int32, isPlayer, isBot bool) *EncounterEntry { + return &EncounterEntry{ + EntityID: entityID, + CharacterID: characterID, + AddedTime: time.Now().UnixMilli(), + IsPlayer: isPlayer, + IsBot: isBot, + } +} + +// EncounterList manages the encounter list for an NPC brain +type EncounterList struct { + entries map[int32]*EncounterEntry // Map of entity ID to encounter entry + playerEntries map[int32]int32 // Map of character ID to entity ID + playerInEncounter bool // Whether any player is in encounter + mutex sync.RWMutex // Thread safety +} + +// NewEncounterList creates a new encounter list +func NewEncounterList() *EncounterList { + return &EncounterList{ + entries: make(map[int32]*EncounterEntry), + playerEntries: make(map[int32]int32), + playerInEncounter: false, + } +} + +// AddEntity adds an entity to the encounter list +func (el *EncounterList) AddEntity(entityID, characterID int32, isPlayer, isBot bool) bool { + el.mutex.Lock() + defer el.mutex.Unlock() + + if len(el.entries) >= MaxEncounterSize { + return false + } + + // Check if already in encounter + if _, exists := el.entries[entityID]; exists { + return false + } + + // Add entry + entry := NewEncounterEntry(entityID, characterID, isPlayer, isBot) + el.entries[entityID] = entry + + // Track player entries separately + if isPlayer && characterID > 0 { + el.playerEntries[characterID] = entityID + el.playerInEncounter = true + } + + return true +} + +// RemoveEntity removes an entity from the encounter list +func (el *EncounterList) RemoveEntity(entityID int32) { + el.mutex.Lock() + defer el.mutex.Unlock() + + if entry, exists := el.entries[entityID]; exists { + // Remove from player entries if it's a player + if entry.IsPlayer && entry.CharacterID > 0 { + delete(el.playerEntries, entry.CharacterID) + } + + // Remove main entry + delete(el.entries, entityID) + + // Update player in encounter flag + el.updatePlayerInEncounter() + } +} + +// Clear removes all entities from the encounter list +func (el *EncounterList) Clear() { + el.mutex.Lock() + defer el.mutex.Unlock() + + el.entries = make(map[int32]*EncounterEntry) + el.playerEntries = make(map[int32]int32) + el.playerInEncounter = false +} + +// IsEntityInEncounter checks if an entity is in the encounter +func (el *EncounterList) IsEntityInEncounter(entityID int32) bool { + el.mutex.RLock() + defer el.mutex.RUnlock() + + _, exists := el.entries[entityID] + return exists +} + +// IsPlayerInEncounter checks if a player (by character ID) is in the encounter +func (el *EncounterList) IsPlayerInEncounter(characterID int32) bool { + el.mutex.RLock() + defer el.mutex.RUnlock() + + _, exists := el.playerEntries[characterID] + return exists +} + +// HasPlayerInEncounter returns whether any player is in the encounter +func (el *EncounterList) HasPlayerInEncounter() bool { + el.mutex.RLock() + defer el.mutex.RUnlock() + + return el.playerInEncounter +} + +// Size returns the number of entities in the encounter +func (el *EncounterList) Size() int { + el.mutex.RLock() + defer el.mutex.RUnlock() + + return len(el.entries) +} + +// CountPlayerBots returns the number of players and bots in encounter +func (el *EncounterList) CountPlayerBots() int { + el.mutex.RLock() + defer el.mutex.RUnlock() + + count := 0 + for _, entry := range el.entries { + if entry.IsPlayer || entry.IsBot { + count++ + } + } + return count +} + +// GetAllEntityIDs returns a copy of all entity IDs in the encounter +func (el *EncounterList) GetAllEntityIDs() []int32 { + el.mutex.RLock() + defer el.mutex.RUnlock() + + result := make([]int32, 0, len(el.entries)) + for entityID := range el.entries { + result = append(result, entityID) + } + return result +} + +// GetAllEntries returns a copy of all encounter entries +func (el *EncounterList) GetAllEntries() map[int32]*EncounterEntry { + el.mutex.RLock() + defer el.mutex.RUnlock() + + result := make(map[int32]*EncounterEntry) + for id, entry := range el.entries { + result[id] = &EncounterEntry{ + EntityID: entry.EntityID, + CharacterID: entry.CharacterID, + AddedTime: entry.AddedTime, + IsPlayer: entry.IsPlayer, + IsBot: entry.IsBot, + } + } + return result +} + +// updatePlayerInEncounter updates the player in encounter flag (internal use only) +func (el *EncounterList) updatePlayerInEncounter() { + el.playerInEncounter = len(el.playerEntries) > 0 +} + +// BrainState represents the current state of a brain +type BrainState struct { + State int32 // Current AI state + LastThink int64 // Timestamp of last think cycle + ThinkTick int32 // Time between think cycles in milliseconds + SpellRecovery int64 // Timestamp when spell recovery completes + IsActive bool // Whether the brain is active + DebugLevel int8 // Debug output level + mutex sync.RWMutex +} + +// NewBrainState creates a new brain state +func NewBrainState() *BrainState { + return &BrainState{ + State: AIStateIdle, + LastThink: time.Now().UnixMilli(), + ThinkTick: DefaultThinkTick, + SpellRecovery: 0, + IsActive: true, + DebugLevel: DebugLevelNone, + } +} + +// GetState returns the current AI state +func (bs *BrainState) GetState() int32 { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + return bs.State +} + +// SetState sets the current AI state +func (bs *BrainState) SetState(state int32) { + bs.mutex.Lock() + defer bs.mutex.Unlock() + bs.State = state +} + +// GetLastThink returns the timestamp of the last think cycle +func (bs *BrainState) GetLastThink() int64 { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + return bs.LastThink +} + +// SetLastThink sets the timestamp of the last think cycle +func (bs *BrainState) SetLastThink(timestamp int64) { + bs.mutex.Lock() + defer bs.mutex.Unlock() + bs.LastThink = timestamp +} + +// GetThinkTick returns the think tick interval +func (bs *BrainState) GetThinkTick() int32 { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + return bs.ThinkTick +} + +// SetThinkTick sets the think tick interval +func (bs *BrainState) SetThinkTick(tick int32) { + bs.mutex.Lock() + defer bs.mutex.Unlock() + + if tick < 1 { + tick = 1 + } else if tick > MaxThinkTick { + tick = MaxThinkTick + } + + bs.ThinkTick = tick +} + +// GetSpellRecovery returns the spell recovery timestamp +func (bs *BrainState) GetSpellRecovery() int64 { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + return bs.SpellRecovery +} + +// SetSpellRecovery sets the spell recovery timestamp +func (bs *BrainState) SetSpellRecovery(timestamp int64) { + bs.mutex.Lock() + defer bs.mutex.Unlock() + bs.SpellRecovery = timestamp +} + +// HasRecovered checks if the brain has recovered from spell casting +func (bs *BrainState) HasRecovered() bool { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + + currentTime := time.Now().UnixMilli() + return bs.SpellRecovery <= currentTime +} + +// IsActive returns whether the brain is active +func (bs *BrainState) IsActive() bool { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + return bs.IsActive +} + +// SetActive sets the brain's active state +func (bs *BrainState) SetActive(active bool) { + bs.mutex.Lock() + defer bs.mutex.Unlock() + bs.IsActive = active +} + +// GetDebugLevel returns the debug level +func (bs *BrainState) GetDebugLevel() int8 { + bs.mutex.RLock() + defer bs.mutex.RUnlock() + return bs.DebugLevel +} + +// SetDebugLevel sets the debug level +func (bs *BrainState) SetDebugLevel(level int8) { + bs.mutex.Lock() + defer bs.mutex.Unlock() + bs.DebugLevel = level +} + +// BrainStatistics contains brain performance statistics +type BrainStatistics struct { + ThinkCycles int64 `json:"think_cycles"` + SpellsCast int64 `json:"spells_cast"` + MeleeAttacks int64 `json:"melee_attacks"` + HateEvents int64 `json:"hate_events"` + EncounterEvents int64 `json:"encounter_events"` + AverageThinkTime float64 `json:"average_think_time_ms"` + LastThinkTime int64 `json:"last_think_time"` + TotalActiveTime int64 `json:"total_active_time_ms"` +} + +// NewBrainStatistics creates new brain statistics +func NewBrainStatistics() *BrainStatistics { + return &BrainStatistics{ + ThinkCycles: 0, + SpellsCast: 0, + MeleeAttacks: 0, + HateEvents: 0, + EncounterEvents: 0, + AverageThinkTime: 0.0, + LastThinkTime: time.Now().UnixMilli(), + TotalActiveTime: 0, + } +} \ No newline at end of file diff --git a/internal/npc/ai/variants.go b/internal/npc/ai/variants.go new file mode 100644 index 0000000..f318aac --- /dev/null +++ b/internal/npc/ai/variants.go @@ -0,0 +1,324 @@ +package ai + +import ( + "fmt" + "time" +) + +// CombatPetBrain extends the base brain for combat pets +type CombatPetBrain struct { + *BaseBrain +} + +// NewCombatPetBrain creates a new combat pet brain +func NewCombatPetBrain(npc NPC, logger Logger) *CombatPetBrain { + brain := &CombatPetBrain{ + BaseBrain: NewBaseBrain(npc, logger), + } + brain.brainType = BrainTypeCombatPet + return brain +} + +// Think implements pet-specific AI logic +func (cpb *CombatPetBrain) Think() error { + // Call parent Think() for default combat behavior + if err := cpb.BaseBrain.Think(); err != nil { + return err + } + + // Additional pet-specific logic + if cpb.npc.GetInCombat() || !cpb.npc.IsPet() || cpb.npc.IsMezzedOrStunned() { + return nil + } + + if cpb.logger != nil && cpb.state.GetDebugLevel() >= DebugLevelDetailed { + cpb.logger.LogDebug("Combat pet AI for %s", cpb.npc.GetName()) + } + + // Check if owner has stay command set + owner := cpb.npc.GetOwner() + if owner != nil && owner.IsPlayer() { + // TODO: Check player's pet movement setting + // if player.GetInfoStruct().GetPetMovement() == PetMovementStay { + // return nil + // } + } + + // Follow owner + if owner != nil { + cpb.npc.SetTarget(owner) + distance := cpb.npc.GetDistance(owner) + + maxRange := cpb.getMaxCombatRange() + if distance > maxRange { + cpb.MoveCloser(owner) + } + } + + return nil +} + +// NonCombatPetBrain handles non-combat pets (cosmetic pets) +type NonCombatPetBrain struct { + *BaseBrain +} + +// NewNonCombatPetBrain creates a new non-combat pet brain +func NewNonCombatPetBrain(npc NPC, logger Logger) *NonCombatPetBrain { + brain := &NonCombatPetBrain{ + BaseBrain: NewBaseBrain(npc, logger), + } + brain.brainType = BrainTypeNonCombatPet + return brain +} + +// Think implements non-combat pet AI (just following) +func (ncpb *NonCombatPetBrain) Think() error { + // Non-combat pets don't do combat AI + if !ncpb.npc.IsPet() || ncpb.npc.IsMezzedOrStunned() { + return nil + } + + if ncpb.logger != nil && ncpb.state.GetDebugLevel() >= DebugLevelDetailed { + ncpb.logger.LogDebug("Non-combat pet AI for %s", ncpb.npc.GetName()) + } + + // Just follow owner + owner := ncpb.npc.GetOwner() + if owner != nil { + ncpb.npc.SetTarget(owner) + distance := ncpb.npc.GetDistance(owner) + + maxRange := ncpb.getMaxCombatRange() + if distance > maxRange { + ncpb.MoveCloser(owner) + } + } + + return nil +} + +// BlankBrain provides a minimal AI that does nothing +type BlankBrain struct { + *BaseBrain +} + +// NewBlankBrain creates a new blank brain +func NewBlankBrain(npc NPC, logger Logger) *BlankBrain { + brain := &BlankBrain{ + BaseBrain: NewBaseBrain(npc, logger), + } + brain.brainType = BrainTypeBlank + brain.SetThinkTick(BlankBrainTick) // Very slow tick + return brain +} + +// Think does nothing for blank brains +func (bb *BlankBrain) Think() error { + // Blank brain does nothing + return nil +} + +// LuaBrain allows AI to be controlled by Lua scripts +type LuaBrain struct { + *BaseBrain + scriptInterface LuaInterface +} + +// NewLuaBrain creates a new Lua-controlled brain +func NewLuaBrain(npc NPC, logger Logger, luaInterface LuaInterface) *LuaBrain { + brain := &LuaBrain{ + BaseBrain: NewBaseBrain(npc, logger), + scriptInterface: luaInterface, + } + brain.brainType = BrainTypeLua + return brain +} + +// Think calls the Lua script's Think function +func (lb *LuaBrain) Think() error { + if lb.scriptInterface == nil { + return fmt.Errorf("no Lua interface available") + } + + if lb.npc == nil { + return fmt.Errorf("brain has no body") + } + + script := lb.npc.GetSpawnScript() + if script == "" { + if lb.logger != nil { + lb.logger.LogError("Lua brain set on spawn without script") + } + return fmt.Errorf("no spawn script available") + } + + // Call the Lua Think function + target := lb.npc.GetTarget() + err := lb.scriptInterface.RunSpawnScript(script, "Think", lb.npc, target) + if err != nil { + if lb.logger != nil { + lb.logger.LogError("Lua script Think function failed: %v", err) + } + return fmt.Errorf("Lua Think function failed: %w", err) + } + + return nil +} + +// DumbFirePetBrain handles dumbfire pets (temporary combat pets) +type DumbFirePetBrain struct { + *BaseBrain + expireTime int64 +} + +// NewDumbFirePetBrain creates a new dumbfire pet brain +func NewDumbFirePetBrain(npc NPC, target Entity, expireTimeMS int32, logger Logger) *DumbFirePetBrain { + brain := &DumbFirePetBrain{ + BaseBrain: NewBaseBrain(npc, logger), + expireTime: time.Now().UnixMilli() + int64(expireTimeMS), + } + brain.brainType = BrainTypeDumbFire + + // Add maximum hate for the target + if target != nil { + brain.AddHate(target.GetID(), MaxHateValue) + } + + return brain +} + +// AddHate only allows hate for the initial target +func (dfpb *DumbFirePetBrain) AddHate(entityID int32, hate int32) { + // Only add hate if we don't already have a target + if dfpb.GetMostHated() == 0 { + dfpb.BaseBrain.AddHate(entityID, hate) + } +} + +// Think implements dumbfire pet AI +func (dfpb *DumbFirePetBrain) Think() error { + // Check if expired + if time.Now().UnixMilli() > dfpb.expireTime { + if dfpb.npc != nil && dfpb.npc.GetHP() > 0 { + if dfpb.logger != nil { + dfpb.logger.LogDebug("Dumbfire pet %s expired", dfpb.npc.GetName()) + } + dfpb.npc.KillSpawn(dfpb.npc) + } + return nil + } + + // Get target + targetID := dfpb.GetMostHated() + if targetID == 0 { + // No target, kill self + if dfpb.npc != nil && dfpb.npc.GetHP() > 0 { + if dfpb.logger != nil { + dfpb.logger.LogDebug("Dumbfire pet %s has no target", dfpb.npc.GetName()) + } + dfpb.npc.KillSpawn(dfpb.npc) + } + return nil + } + + target := dfpb.getEntityByID(targetID) + if target == nil { + // Target no longer exists, kill self + if dfpb.npc != nil && dfpb.npc.GetHP() > 0 { + dfpb.npc.KillSpawn(dfpb.npc) + } + return nil + } + + // Skip if mezzed or stunned + if dfpb.npc.IsMezzedOrStunned() { + return nil + } + + // Set target if not already set + if dfpb.npc.GetTarget() != target { + dfpb.npc.SetTarget(target) + dfpb.npc.FaceTarget(target, false) + } + + // Enter combat if not already + if !dfpb.npc.GetInCombat() { + dfpb.npc.CalculateRunningLocation(true) + dfpb.npc.InCombat(true) + } + + distance := dfpb.npc.GetDistance(target) + + // Try to cast spells if we have line of sight + if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() && + (!dfpb.HasRecovered() || !dfpb.ProcessSpell(target, distance)) { + + if dfpb.logger != nil && dfpb.state.GetDebugLevel() >= DebugLevelDetailed { + dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s", + dfpb.npc.GetName(), target.GetName()) + } + + dfpb.npc.FaceTarget(target, false) + dfpb.ProcessMelee(target, distance) + } + + return nil +} + +// GetExpireTime returns when this dumbfire pet will expire +func (dfpb *DumbFirePetBrain) GetExpireTime() int64 { + return dfpb.expireTime +} + +// SetExpireTime sets when this dumbfire pet will expire +func (dfpb *DumbFirePetBrain) SetExpireTime(expireTime int64) { + dfpb.expireTime = expireTime +} + +// IsExpired checks if the dumbfire pet has expired +func (dfpb *DumbFirePetBrain) IsExpired() bool { + return time.Now().UnixMilli() > dfpb.expireTime +} + +// ExtendExpireTime extends the expire time by the given duration +func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) { + dfpb.expireTime += int64(durationMS) +} + +// Brain factory functions + +// CreateBrain creates the appropriate brain type for an NPC +func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{}) Brain { + switch brainType { + case BrainTypeCombatPet: + return NewCombatPetBrain(npc, logger) + + case BrainTypeNonCombatPet: + return NewNonCombatPetBrain(npc, logger) + + case BrainTypeBlank: + return NewBlankBrain(npc, logger) + + case BrainTypeLua: + if len(options) > 0 { + if luaInterface, ok := options[0].(LuaInterface); ok { + return NewLuaBrain(npc, logger, luaInterface) + } + } + return NewBaseBrain(npc, logger) // Fallback to default + + case BrainTypeDumbFire: + if len(options) >= 2 { + if target, ok := options[0].(Entity); ok { + if expireTime, ok := options[1].(int32); ok { + return NewDumbFirePetBrain(npc, target, expireTime, logger) + } + } + } + return NewBaseBrain(npc, logger) // Fallback to default + + default: + return NewBaseBrain(npc, logger) + } +} \ No newline at end of file diff --git a/internal/npc/constants.go b/internal/npc/constants.go new file mode 100644 index 0000000..e10441d --- /dev/null +++ b/internal/npc/constants.go @@ -0,0 +1,93 @@ +package npc + +// AI Strategy constants +const ( + AIStrategyBalanced int8 = 1 + AIStrategyOffensive int8 = 2 + AIStrategyDefensive int8 = 3 +) + +// Randomize Appearances constants +const ( + RandomizeGender int32 = 1 + RandomizeRace int32 = 2 + RandomizeModelType int32 = 4 + RandomizeFacialHairType int32 = 8 + RandomizeHairType int32 = 16 + RandomizeWingType int32 = 64 + RandomizeCheekType int32 = 128 + RandomizeChinType int32 = 256 + RandomizeEarType int32 = 512 + RandomizeEyeBrowType int32 = 1024 + RandomizeEyeType int32 = 2048 + RandomizeLipType int32 = 4096 + RandomizeNoseType int32 = 8192 + RandomizeEyeColor int32 = 16384 + RandomizeHairColor1 int32 = 32768 + RandomizeHairColor2 int32 = 65536 + RandomizeHairHighlight int32 = 131072 + RandomizeHairFaceColor int32 = 262144 + RandomizeHairFaceHigh int32 = 524288 + RandomizeHairTypeColor int32 = 1048576 + RandomizeHairTypeHigh int32 = 2097152 + RandomizeSkinColor int32 = 4194304 + RandomizeWingColor1 int32 = 8388608 + RandomizeWingColor2 int32 = 16777216 + RandomizeAll int32 = 33554431 +) + +// Pet Type constants +const ( + PetTypeCombat int8 = 1 + PetTypeCharmed int8 = 2 + PetTypeDeity int8 = 3 + PetTypeCosmetic int8 = 4 + PetTypeDumbfire int8 = 5 +) + +// Cast Type constants +const ( + CastOnSpawn int8 = 0 + CastOnAggro int8 = 1 + MaxCastTypes int8 = 2 +) + +// Default values +const ( + DefaultCastPercentage int8 = 25 + DefaultAggroRadius float32 = 10.0 + DefaultRunbackSpeed float32 = 2.0 + MaxSkillBonuses int = 100 + MaxNPCSpells int = 50 + MaxPauseTime int32 = 300000 // 5 minutes max pause +) + +// NPC validation constants +const ( + MinNPCLevel int8 = 1 + MaxNPCLevel int8 = 100 + MaxNPCNameLen int = 64 + MinAppearanceID int32 = 0 + MaxAppearanceID int32 = 999999 +) + +// Combat constants +const ( + RunbackDistanceThreshold float32 = 5.0 + HPRatioMin int8 = -100 + HPRatioMax int8 = 100 + DefaultMaxPetLevel int8 = 20 +) + +// Color randomization constants +const ( + ColorRandomMin int8 = 0 + ColorRandomMax int8 = 255 + ColorVariation int8 = 30 +) + +// Movement constants +const ( + DefaultPauseCheckMS int32 = 100 + RunbackCheckMS int32 = 250 +) \ No newline at end of file diff --git a/internal/npc/interfaces.go b/internal/npc/interfaces.go new file mode 100644 index 0000000..52306ef --- /dev/null +++ b/internal/npc/interfaces.go @@ -0,0 +1,570 @@ +package npc + +// Database interface for NPC persistence +type Database interface { + LoadAllNPCs() ([]*NPC, error) + SaveNPC(npc *NPC) error + DeleteNPC(npcID int32) error + LoadNPCSpells(npcID int32) ([]*NPCSpell, error) + SaveNPCSpells(npcID int32, spells []*NPCSpell) error + LoadNPCSkills(npcID int32) (map[string]*Skill, error) + SaveNPCSkills(npcID int32, skills map[string]*Skill) error +} + +// Logger interface for NPC logging +type Logger interface { + LogInfo(message string, args ...interface{}) + LogError(message string, args ...interface{}) + LogDebug(message string, args ...interface{}) + LogWarning(message string, args ...interface{}) +} + +// Client interface for NPC-related client operations +type Client interface { + GetPlayer() Player + GetVersion() int16 + SendNPCUpdate(npcData []byte) error + SendCombatUpdate(combatData []byte) error + SendSpellCast(spellData []byte) error +} + +// Player interface for NPC-related player operations +type Player interface { + GetCharacterID() int32 + GetName() string + GetLevel() int8 + GetZoneID() int32 + GetX() float32 + GetY() float32 + GetZ() float32 + IsInCombat() bool + GetTarget() *NPC + SendMessage(message string) +} + +// Zone interface for NPC zone operations +type Zone interface { + GetZoneID() int32 + GetNPCs() []*NPC + AddNPC(npc *NPC) error + RemoveNPC(npcID int32) error + GetPlayersInRange(x, y, z, radius float32) []Player + ProcessEntityCommand(command string, client Client, target *NPC) error + CallSpawnScript(npc *NPC, scriptType string, args ...interface{}) error +} + +// SpellManager interface for spell system integration +type SpellManager interface { + GetSpell(spellID int32, tier int8) Spell + CastSpell(caster *NPC, target interface{}, spell Spell) error + GetSpellEffect(entity interface{}, spellID int32) SpellEffect + ProcessSpell(spell Spell, caster *NPC, target interface{}) error +} + +// Spell interface for spell data +type Spell interface { + GetSpellID() int32 + GetName() string + GetTier() int8 + GetRange() float32 + GetMinRange() float32 + GetPowerRequired() int32 + IsFriendlySpell() bool + IsToggleSpell() bool + GetCastTime() int32 + GetRecastTime() int32 +} + +// SpellEffect interface for active spell effects +type SpellEffect interface { + GetSpellID() int32 + GetTier() int8 + GetDuration() int32 + GetRemainingTime() int32 + IsExpired() bool +} + +// SkillManager interface for skill system integration +type SkillManager interface { + GetSkill(skillID int32) MasterSkill + GetSkillByName(name string) MasterSkill + ApplySkillBonus(entity interface{}, skillID int32, bonus float32) error + RemoveSkillBonus(entity interface{}, skillID int32, bonus float32) error +} + +// MasterSkill interface for skill definitions +type MasterSkill interface { + GetSkillID() int32 + GetName() string + GetDescription() string + GetMaxValue() int16 +} + +// AppearanceManager interface for appearance system integration +type AppearanceManager interface { + GetAppearance(appearanceID int32) Appearance + GetAppearancesByName(name string) []Appearance + RandomizeAppearance(npc *NPC, flags int32) error +} + +// Appearance interface for appearance data +type Appearance interface { + GetAppearanceID() int32 + GetName() string + GetModelType() int16 + GetRace() int16 + GetGender() int8 +} + +// MovementManager interface for movement system integration +type MovementManager interface { + StartMovement(npc *NPC, x, y, z float32) error + StopMovement(npc *NPC) error + SetSpeed(npc *NPC, speed float32) error + NavigateToLocation(npc *NPC, x, y, z float32) error + IsMoving(npc *NPC) bool +} + +// CombatManager interface for combat system integration +type CombatManager interface { + StartCombat(npc *NPC, target interface{}) error + EndCombat(npc *NPC) error + ProcessCombatRound(npc *NPC) error + CalculateDamage(attacker *NPC, target interface{}) int32 + ApplyDamage(target interface{}, damage int32) error +} + +// NPCAware interface for entities that can interact with NPCs +type NPCAware interface { + GetNPC() *NPC + IsNPC() bool + HandleNPCInteraction(npc *NPC, interactionType string) error + ReceiveNPCCommand(npc *NPC, command string) error +} + +// EntityAdapter provides NPC functionality for entities +type EntityAdapter struct { + npc *NPC + logger Logger +} + +// NewEntityAdapter creates a new entity adapter +func NewEntityAdapter(npc *NPC, logger Logger) *EntityAdapter { + return &EntityAdapter{ + npc: npc, + logger: logger, + } +} + +// GetNPC returns the associated NPC +func (ea *EntityAdapter) GetNPC() *NPC { + return ea.npc +} + +// IsNPC always returns true for entity adapters +func (ea *EntityAdapter) IsNPC() bool { + return true +} + +// HandleNPCInteraction processes interactions with other NPCs +func (ea *EntityAdapter) HandleNPCInteraction(otherNPC *NPC, interactionType string) error { + if ea.npc == nil || otherNPC == nil { + return fmt.Errorf("invalid NPC for interaction") + } + + // Handle different interaction types + switch interactionType { + case "aggro": + return ea.handleAggroInteraction(otherNPC) + case "assist": + return ea.handleAssistInteraction(otherNPC) + case "trade": + return ea.handleTradeInteraction(otherNPC) + default: + if ea.logger != nil { + ea.logger.LogWarning("Unknown NPC interaction type: %s", interactionType) + } + return fmt.Errorf("unknown interaction type: %s", interactionType) + } +} + +// ReceiveNPCCommand processes commands from other NPCs +func (ea *EntityAdapter) ReceiveNPCCommand(otherNPC *NPC, command string) error { + if ea.npc == nil || otherNPC == nil { + return fmt.Errorf("invalid NPC for command") + } + + // Process the command + switch command { + case "follow": + return ea.handleFollowCommand(otherNPC) + case "attack": + return ea.handleAttackCommand(otherNPC) + case "retreat": + return ea.handleRetreatCommand(otherNPC) + default: + if ea.logger != nil { + ea.logger.LogWarning("Unknown NPC command: %s", command) + } + return fmt.Errorf("unknown command: %s", command) + } +} + +// handleAggroInteraction processes aggro interactions +func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error { + // TODO: Implement aggro logic between NPCs + if ea.logger != nil { + ea.logger.LogDebug("NPC %d received aggro from NPC %d", + ea.npc.GetNPCID(), otherNPC.GetNPCID()) + } + return nil +} + +// handleAssistInteraction processes assist interactions +func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error { + // TODO: Implement assist logic between NPCs + if ea.logger != nil { + ea.logger.LogDebug("NPC %d received assist request from NPC %d", + ea.npc.GetNPCID(), otherNPC.GetNPCID()) + } + return nil +} + +// handleTradeInteraction processes trade interactions +func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error { + // TODO: Implement trade logic between NPCs + if ea.logger != nil { + ea.logger.LogDebug("NPC %d received trade request from NPC %d", + ea.npc.GetNPCID(), otherNPC.GetNPCID()) + } + return nil +} + +// handleFollowCommand processes follow commands +func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error { + // TODO: Implement follow logic + if ea.logger != nil { + ea.logger.LogDebug("NPC %d received follow command from NPC %d", + ea.npc.GetNPCID(), otherNPC.GetNPCID()) + } + return nil +} + +// handleAttackCommand processes attack commands +func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error { + // TODO: Implement attack logic + if ea.logger != nil { + ea.logger.LogDebug("NPC %d received attack command from NPC %d", + ea.npc.GetNPCID(), otherNPC.GetNPCID()) + } + return nil +} + +// handleRetreatCommand processes retreat commands +func (ea *EntityAdapter) handleRetreatCommand(otherNPC *NPC) error { + // TODO: Implement retreat logic + if ea.logger != nil { + ea.logger.LogDebug("NPC %d received retreat command from NPC %d", + ea.npc.GetNPCID(), otherNPC.GetNPCID()) + } + return nil +} + +// SpellCasterAdapter provides spell casting functionality for NPCs +type SpellCasterAdapter struct { + npc *NPC + spellManager SpellManager + logger Logger +} + +// NewSpellCasterAdapter creates a new spell caster adapter +func NewSpellCasterAdapter(npc *NPC, spellManager SpellManager, logger Logger) *SpellCasterAdapter { + return &SpellCasterAdapter{ + npc: npc, + spellManager: spellManager, + logger: logger, + } +} + +// GetNextSpell selects the next spell to cast based on AI strategy +func (sca *SpellCasterAdapter) GetNextSpell(target interface{}, distance float32) Spell { + if sca.npc == nil || sca.spellManager == nil { + return nil + } + + // Check cast-on-aggro spells first + if !sca.npc.castOnAggroCompleted { + spell := sca.getNextCastOnAggroSpell(target) + if spell != nil { + return spell + } + sca.npc.castOnAggroCompleted = true + } + + // Get spells based on AI strategy + strategy := sca.npc.GetAIStrategy() + return sca.getNextSpellByStrategy(target, distance, strategy) +} + +// GetNextBuffSpell selects the next buff spell to cast +func (sca *SpellCasterAdapter) GetNextBuffSpell(target interface{}) Spell { + if sca.npc == nil || sca.spellManager == nil { + return nil + } + + // Check cast-on-spawn spells first + castOnSpells := sca.npc.castOnSpells[CastOnSpawn] + for _, npcSpell := range castOnSpells { + spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier()) + if spell != nil { + // Check if target already has this effect + if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect != nil { + if effect.GetTier() < spell.GetTier() { + return spell // Upgrade existing effect + } + } else { + return spell // New effect + } + } + } + + // Check regular spells for buffs + for _, npcSpell := range sca.npc.spells { + spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier()) + if spell != nil && spell.IsFriendlySpell() && spell.IsToggleSpell() { + // Check if target already has this effect + if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect != nil { + if effect.GetTier() < spell.GetTier() { + return spell // Upgrade existing effect + } + } else { + return spell // New effect + } + } + } + + return nil +} + +// CastSpell attempts to cast a spell +func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error { + if sca.npc == nil || sca.spellManager == nil || spell == nil { + return fmt.Errorf("invalid parameters for spell casting") + } + + // Check casting conditions + if err := sca.checkCastingConditions(spell); err != nil { + return fmt.Errorf("casting conditions not met: %w", err) + } + + // Cast the spell + if err := sca.spellManager.CastSpell(sca.npc, target, spell); err != nil { + return fmt.Errorf("failed to cast spell: %w", err) + } + + if sca.logger != nil { + sca.logger.LogDebug("NPC %d cast spell %s (%d)", + sca.npc.GetNPCID(), spell.GetName(), spell.GetSpellID()) + } + + return nil +} + +// getNextCastOnAggroSpell selects cast-on-aggro spells +func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell { + castOnSpells := sca.npc.castOnSpells[CastOnAggro] + for _, npcSpell := range castOnSpells { + spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier()) + if spell != nil { + // Check if target doesn't already have this effect + if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect == nil { + return spell + } + } + } + return nil +} + +// getNextSpellByStrategy selects spells based on AI strategy +func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell { + // TODO: Implement more sophisticated spell selection based on strategy + + for _, npcSpell := range sca.npc.spells { + // Check HP ratio requirements + if npcSpell.GetRequiredHPRatio() != 0 { + // TODO: Implement HP ratio checking + } + + spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier()) + if spell == nil { + continue + } + + // Check strategy compatibility + if strategy == AIStrategyOffensive && spell.IsFriendlySpell() { + continue + } + if strategy == AIStrategyDefensive && !spell.IsFriendlySpell() { + continue + } + + // Check range and power requirements + if distance <= spell.GetRange() && distance >= spell.GetMinRange() { + // TODO: Check power requirements + return spell + } + } + + return nil +} + +// checkCastingConditions validates spell casting conditions +func (sca *SpellCasterAdapter) checkCastingConditions(spell Spell) error { + if sca.npc.Entity == nil { + return fmt.Errorf("NPC entity is nil") + } + + // TODO: Implement power checking, cooldown checking, etc. + + return nil +} + +// CombatAdapter provides combat functionality for NPCs +type CombatAdapter struct { + npc *NPC + combatManager CombatManager + logger Logger +} + +// NewCombatAdapter creates a new combat adapter +func NewCombatAdapter(npc *NPC, combatManager CombatManager, logger Logger) *CombatAdapter { + return &CombatAdapter{ + npc: npc, + combatManager: combatManager, + logger: logger, + } +} + +// EnterCombat handles entering combat state +func (ca *CombatAdapter) EnterCombat(target interface{}) error { + if ca.npc == nil { + return fmt.Errorf("NPC is nil") + } + + // Start combat through combat manager + if ca.combatManager != nil { + if err := ca.combatManager.StartCombat(ca.npc, target); err != nil { + return fmt.Errorf("failed to start combat: %w", err) + } + } + + // Update NPC state + ca.npc.InCombat(true) + + if ca.logger != nil { + ca.logger.LogDebug("NPC %d entered combat", ca.npc.GetNPCID()) + } + + return nil +} + +// ExitCombat handles exiting combat state +func (ca *CombatAdapter) ExitCombat() error { + if ca.npc == nil { + return fmt.Errorf("NPC is nil") + } + + // End combat through combat manager + if ca.combatManager != nil { + if err := ca.combatManager.EndCombat(ca.npc); err != nil { + return fmt.Errorf("failed to end combat: %w", err) + } + } + + // Update NPC state + ca.npc.InCombat(false) + + if ca.logger != nil { + ca.logger.LogDebug("NPC %d exited combat", ca.npc.GetNPCID()) + } + + return nil +} + +// ProcessCombat handles combat processing +func (ca *CombatAdapter) ProcessCombat() error { + if ca.npc == nil { + return fmt.Errorf("NPC is nil") + } + + if ca.combatManager != nil { + return ca.combatManager.ProcessCombatRound(ca.npc) + } + + return nil +} + +// MovementAdapter provides movement functionality for NPCs +type MovementAdapter struct { + npc *NPC + movementManager MovementManager + logger Logger +} + +// NewMovementAdapter creates a new movement adapter +func NewMovementAdapter(npc *NPC, movementManager MovementManager, logger Logger) *MovementAdapter { + return &MovementAdapter{ + npc: npc, + movementManager: movementManager, + logger: logger, + } +} + +// MoveToLocation moves the NPC to a specific location +func (ma *MovementAdapter) MoveToLocation(x, y, z float32) error { + if ma.npc == nil { + return fmt.Errorf("NPC is nil") + } + + if ma.movementManager != nil { + return ma.movementManager.NavigateToLocation(ma.npc, x, y, z) + } + + return fmt.Errorf("movement manager not available") +} + +// StopMovement stops the NPC's movement +func (ma *MovementAdapter) StopMovement() error { + if ma.npc == nil { + return fmt.Errorf("NPC is nil") + } + + if ma.movementManager != nil { + return ma.movementManager.StopMovement(ma.npc) + } + + return fmt.Errorf("movement manager not available") +} + +// IsMoving checks if the NPC is currently moving +func (ma *MovementAdapter) IsMoving() bool { + if ma.npc == nil || ma.movementManager == nil { + return false + } + + return ma.movementManager.IsMoving(ma.npc) +} + +// RunbackToSpawn moves the NPC back to its spawn location +func (ma *MovementAdapter) RunbackToSpawn() error { + if ma.npc == nil { + return fmt.Errorf("NPC is nil") + } + + runbackLocation := ma.npc.GetRunbackLocation() + if runbackLocation == nil { + return fmt.Errorf("no runback location set") + } + + return ma.MoveToLocation(runbackLocation.X, runbackLocation.Y, runbackLocation.Z) +} \ No newline at end of file diff --git a/internal/npc/manager.go b/internal/npc/manager.go new file mode 100644 index 0000000..68f15d1 --- /dev/null +++ b/internal/npc/manager.go @@ -0,0 +1,762 @@ +package npc + +import ( + "fmt" + "math/rand" + "strings" + "sync" + "time" +) + +// Manager provides high-level management of the NPC system +type Manager struct { + npcs map[int32]*NPC // NPCs indexed by ID + npcsByZone map[int32][]*NPC // NPCs indexed by zone ID + npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID + database Database // Database interface + logger Logger // Logger interface + spellManager SpellManager // Spell system interface + skillManager SkillManager // Skill system interface + appearanceManager AppearanceManager // Appearance system interface + mutex sync.RWMutex // Thread safety + + // Statistics + totalNPCs int64 + npcsInCombat int64 + spellCastCount int64 + skillUsageCount int64 + runbackCount int64 + aiStrategyCounts map[int8]int64 + + // Configuration + maxNPCs int32 + defaultAggroRadius float32 + enableAI bool +} + +// NewManager creates a new NPC manager +func NewManager(database Database, logger Logger) *Manager { + return &Manager{ + npcs: make(map[int32]*NPC), + npcsByZone: make(map[int32][]*NPC), + npcsByAppearance: make(map[int32][]*NPC), + database: database, + logger: logger, + aiStrategyCounts: make(map[int8]int64), + maxNPCs: 10000, // Default limit + defaultAggroRadius: DefaultAggroRadius, + enableAI: true, + } +} + +// Initialize loads NPCs from database and sets up the system +func (m *Manager) Initialize() error { + if m.logger != nil { + m.logger.LogInfo("Initializing NPC manager...") + } + + if m.database == nil { + if m.logger != nil { + m.logger.LogWarning("No database provided, starting with empty NPC list") + } + return nil + } + + // Load NPCs from database + npcs, err := m.database.LoadAllNPCs() + if err != nil { + return fmt.Errorf("failed to load NPCs from database: %w", err) + } + + for _, npc := range npcs { + if err := m.addNPCInternal(npc); err != nil { + if m.logger != nil { + m.logger.LogError("Failed to add NPC %d: %v", npc.GetNPCID(), err) + } + } + } + + if m.logger != nil { + m.logger.LogInfo("Loaded %d NPCs from database", len(npcs)) + } + + return nil +} + +// AddNPC adds a new NPC to the system +func (m *Manager) AddNPC(npc *NPC) error { + if npc == nil { + return fmt.Errorf("NPC cannot be nil") + } + + if !npc.IsValid() { + return fmt.Errorf("NPC is not valid: %s", npc.String()) + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + if len(m.npcs) >= int(m.maxNPCs) { + return fmt.Errorf("maximum NPC limit reached (%d)", m.maxNPCs) + } + + return m.addNPCInternal(npc) +} + +// addNPCInternal adds an NPC without locking (internal use) +func (m *Manager) addNPCInternal(npc *NPC) error { + npcID := npc.GetNPCID() + + // Check for duplicate ID + if _, exists := m.npcs[npcID]; exists { + return fmt.Errorf("NPC with ID %d already exists", npcID) + } + + // Add to main index + m.npcs[npcID] = npc + + // Add to zone index + if npc.Entity != nil { + zoneID := npc.Entity.GetZoneID() + m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) + } + + // Add to appearance index + appearanceID := npc.GetAppearanceID() + m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc) + + // Update statistics + m.totalNPCs++ + strategy := npc.GetAIStrategy() + m.aiStrategyCounts[strategy]++ + + // Save to database if available + if m.database != nil { + if err := m.database.SaveNPC(npc); err != nil { + // Remove from indexes if database save failed + delete(m.npcs, npcID) + m.removeFromZoneIndex(npc) + m.removeFromAppearanceIndex(npc) + m.totalNPCs-- + m.aiStrategyCounts[strategy]-- + return fmt.Errorf("failed to save NPC to database: %w", err) + } + } + + if m.logger != nil { + m.logger.LogInfo("Added NPC %d: %s", npcID, npc.String()) + } + + return nil +} + +// GetNPC retrieves an NPC by ID +func (m *Manager) GetNPC(id int32) *NPC { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return m.npcs[id] +} + +// GetNPCsByZone retrieves all NPCs in a specific zone +func (m *Manager) GetNPCsByZone(zoneID int32) []*NPC { + m.mutex.RLock() + defer m.mutex.RUnlock() + + npcs := m.npcsByZone[zoneID] + result := make([]*NPC, len(npcs)) + copy(result, npcs) + return result +} + +// GetNPCsByAppearance retrieves all NPCs with a specific appearance +func (m *Manager) GetNPCsByAppearance(appearanceID int32) []*NPC { + m.mutex.RLock() + defer m.mutex.RUnlock() + + npcs := m.npcsByAppearance[appearanceID] + result := make([]*NPC, len(npcs)) + copy(result, npcs) + return result +} + +// RemoveNPC removes an NPC from the system +func (m *Manager) RemoveNPC(id int32) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + npc, exists := m.npcs[id] + if !exists { + return fmt.Errorf("NPC with ID %d does not exist", id) + } + + // Remove from database first if available + if m.database != nil { + if err := m.database.DeleteNPC(id); err != nil { + return fmt.Errorf("failed to delete NPC from database: %w", err) + } + } + + // Remove from all indexes + delete(m.npcs, id) + m.removeFromZoneIndex(npc) + m.removeFromAppearanceIndex(npc) + + // Update statistics + m.totalNPCs-- + strategy := npc.GetAIStrategy() + if count := m.aiStrategyCounts[strategy]; count > 0 { + m.aiStrategyCounts[strategy]-- + } + + if m.logger != nil { + m.logger.LogInfo("Removed NPC %d", id) + } + + return nil +} + +// UpdateNPC updates an existing NPC +func (m *Manager) UpdateNPC(npc *NPC) error { + if npc == nil { + return fmt.Errorf("NPC cannot be nil") + } + + if !npc.IsValid() { + return fmt.Errorf("NPC is not valid: %s", npc.String()) + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + npcID := npc.GetNPCID() + oldNPC, exists := m.npcs[npcID] + if !exists { + return fmt.Errorf("NPC with ID %d does not exist", npcID) + } + + // Update indexes if zone or appearance changed + if npc.Entity != nil && oldNPC.Entity != nil { + if npc.Entity.GetZoneID() != oldNPC.Entity.GetZoneID() { + m.removeFromZoneIndex(oldNPC) + zoneID := npc.Entity.GetZoneID() + m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) + } + } + + if npc.GetAppearanceID() != oldNPC.GetAppearanceID() { + m.removeFromAppearanceIndex(oldNPC) + appearanceID := npc.GetAppearanceID() + m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc) + } + + // Update AI strategy statistics + oldStrategy := oldNPC.GetAIStrategy() + newStrategy := npc.GetAIStrategy() + if oldStrategy != newStrategy { + if count := m.aiStrategyCounts[oldStrategy]; count > 0 { + m.aiStrategyCounts[oldStrategy]-- + } + m.aiStrategyCounts[newStrategy]++ + } + + // Update main index + m.npcs[npcID] = npc + + // Save to database if available + if m.database != nil { + if err := m.database.SaveNPC(npc); err != nil { + return fmt.Errorf("failed to save NPC to database: %w", err) + } + } + + if m.logger != nil { + m.logger.LogInfo("Updated NPC %d: %s", npcID, npc.String()) + } + + return nil +} + +// CreateNPCFromTemplate creates a new NPC from an existing template +func (m *Manager) CreateNPCFromTemplate(templateID, newID int32) (*NPC, error) { + template := m.GetNPC(templateID) + if template == nil { + return nil, fmt.Errorf("template NPC with ID %d not found", templateID) + } + + // Create new NPC from template + newNPC := NewNPCFromExisting(template) + newNPC.SetNPCID(newID) + + // Add to system + if err := m.AddNPC(newNPC); err != nil { + return nil, fmt.Errorf("failed to add new NPC: %w", err) + } + + return newNPC, nil +} + +// GetRandomNPCByAppearance returns a random NPC with the specified appearance +func (m *Manager) GetRandomNPCByAppearance(appearanceID int32) *NPC { + npcs := m.GetNPCsByAppearance(appearanceID) + if len(npcs) == 0 { + return nil + } + + return npcs[rand.Intn(len(npcs))] +} + +// ProcessCombat handles combat processing for all NPCs +func (m *Manager) ProcessCombat() { + m.mutex.RLock() + npcs := make([]*NPC, 0, len(m.npcs)) + for _, npc := range m.npcs { + if npc.Entity != nil && npc.Entity.GetInCombat() { + npcs = append(npcs, npc) + } + } + m.mutex.RUnlock() + + // Process combat for each NPC in combat + for _, npc := range npcs { + npc.ProcessCombat() + } + + // Update combat statistics + m.mutex.Lock() + m.npcsInCombat = int64(len(npcs)) + m.mutex.Unlock() +} + +// ProcessAI handles AI processing for all NPCs +func (m *Manager) ProcessAI() { + if !m.enableAI { + return + } + + m.mutex.RLock() + npcs := make([]*NPC, 0, len(m.npcs)) + for _, npc := range m.npcs { + npcs = append(npcs, npc) + } + m.mutex.RUnlock() + + // Process AI for each NPC + for _, npc := range npcs { + if brain := npc.GetBrain(); brain != nil && brain.IsActive() { + if err := brain.Think(); err != nil && m.logger != nil { + m.logger.LogError("AI brain error for NPC %d: %v", npc.GetNPCID(), err) + } + } + } +} + +// ProcessMovement handles movement processing for all NPCs +func (m *Manager) ProcessMovement() { + m.mutex.RLock() + npcs := make([]*NPC, 0, len(m.npcs)) + for _, npc := range m.npcs { + npcs = append(npcs, npc) + } + m.mutex.RUnlock() + + // Process movement for each NPC + for _, npc := range npcs { + // Check pause timer + if npc.IsPauseMovementTimerActive() { + continue + } + + // Handle runback if needed + if npc.callRunback && npc.GetRunbackLocation() != nil { + npc.callRunback = false + npc.Runback(0, true) + m.mutex.Lock() + m.runbackCount++ + m.mutex.Unlock() + } + } +} + +// GetStatistics returns NPC system statistics +func (m *Manager) GetStatistics() *NPCStatistics { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // Create AI strategy counts by name + aiCounts := make(map[string]int) + for strategy, count := range m.aiStrategyCounts { + switch strategy { + case AIStrategyBalanced: + aiCounts["balanced"] = int(count) + case AIStrategyOffensive: + aiCounts["offensive"] = int(count) + case AIStrategyDefensive: + aiCounts["defensive"] = int(count) + default: + aiCounts[fmt.Sprintf("unknown_%d", strategy)] = int(count) + } + } + + // Calculate average aggro radius + var totalAggro float32 + npcCount := 0 + for _, npc := range m.npcs { + totalAggro += npc.GetAggroRadius() + npcCount++ + } + var avgAggro float32 + if npcCount > 0 { + avgAggro = totalAggro / float32(npcCount) + } + + // Count NPCs with spells and skills + npcsWithSpells := 0 + npcsWithSkills := 0 + for _, npc := range m.npcs { + if npc.HasSpells() { + npcsWithSpells++ + } + if len(npc.skills) > 0 { + npcsWithSkills++ + } + } + + return &NPCStatistics{ + TotalNPCs: int(m.totalNPCs), + NPCsInCombat: int(m.npcsInCombat), + NPCsWithSpells: npcsWithSpells, + NPCsWithSkills: npcsWithSkills, + AIStrategyCounts: aiCounts, + SpellCastCount: m.spellCastCount, + SkillUsageCount: m.skillUsageCount, + RunbackCount: m.runbackCount, + AverageAggroRadius: avgAggro, + } +} + +// ValidateAllNPCs validates all NPCs in the system +func (m *Manager) ValidateAllNPCs() []string { + m.mutex.RLock() + npcs := make([]*NPC, 0, len(m.npcs)) + for _, npc := range m.npcs { + npcs = append(npcs, npc) + } + m.mutex.RUnlock() + + var issues []string + for _, npc := range npcs { + if !npc.IsValid() { + issues = append(issues, fmt.Sprintf("NPC %d is invalid: %s", npc.GetNPCID(), npc.String())) + } + } + + return issues +} + +// ProcessCommand handles NPC-related commands +func (m *Manager) ProcessCommand(command string, args []string) (string, error) { + switch command { + case "stats": + return m.handleStatsCommand(args) + case "validate": + return m.handleValidateCommand(args) + case "list": + return m.handleListCommand(args) + case "info": + return m.handleInfoCommand(args) + case "create": + return m.handleCreateCommand(args) + case "remove": + return m.handleRemoveCommand(args) + case "search": + return m.handleSearchCommand(args) + case "combat": + return m.handleCombatCommand(args) + default: + return "", fmt.Errorf("unknown NPC command: %s", command) + } +} + +// Command handlers +func (m *Manager) handleStatsCommand(args []string) (string, error) { + stats := m.GetStatistics() + + result := "NPC System Statistics:\n" + result += fmt.Sprintf("Total NPCs: %d\n", stats.TotalNPCs) + result += fmt.Sprintf("NPCs in Combat: %d\n", stats.NPCsInCombat) + result += fmt.Sprintf("NPCs with Spells: %d\n", stats.NPCsWithSpells) + result += fmt.Sprintf("NPCs with Skills: %d\n", stats.NPCsWithSkills) + result += fmt.Sprintf("Average Aggro Radius: %.2f\n", stats.AverageAggroRadius) + result += fmt.Sprintf("Spell Casts: %d\n", stats.SpellCastCount) + result += fmt.Sprintf("Skill Uses: %d\n", stats.SkillUsageCount) + result += fmt.Sprintf("Runbacks: %d\n", stats.RunbackCount) + + if len(stats.AIStrategyCounts) > 0 { + result += "\nAI Strategy Distribution:\n" + for strategy, count := range stats.AIStrategyCounts { + result += fmt.Sprintf(" %s: %d\n", strategy, count) + } + } + + return result, nil +} + +func (m *Manager) handleValidateCommand(args []string) (string, error) { + issues := m.ValidateAllNPCs() + + if len(issues) == 0 { + return "All NPCs are valid.", nil + } + + result := fmt.Sprintf("Found %d issues with NPCs:\n", len(issues)) + for i, issue := range issues { + if i >= 10 { // Limit output + result += "... (and more)\n" + break + } + result += fmt.Sprintf("%d. %s\n", i+1, issue) + } + + return result, nil +} + +func (m *Manager) handleListCommand(args []string) (string, error) { + m.mutex.RLock() + npcs := make([]*NPC, 0, len(m.npcs)) + for _, npc := range m.npcs { + npcs = append(npcs, npc) + } + m.mutex.RUnlock() + + if len(npcs) == 0 { + return "No NPCs loaded.", nil + } + + result := fmt.Sprintf("NPCs (%d):\n", len(npcs)) + count := 0 + for _, npc := range npcs { + if count >= 20 { // Limit output + result += "... (and more)\n" + break + } + result += fmt.Sprintf(" %d: %s\n", npc.GetNPCID(), npc.String()) + count++ + } + + return result, nil +} + +func (m *Manager) handleInfoCommand(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("NPC ID required") + } + + var npcID int32 + if _, err := fmt.Sscanf(args[0], "%d", &npcID); err != nil { + return "", fmt.Errorf("invalid NPC ID: %s", args[0]) + } + + npc := m.GetNPC(npcID) + if npc == nil { + return fmt.Sprintf("NPC %d not found.", npcID), nil + } + + result := fmt.Sprintf("NPC Information:\n") + result += fmt.Sprintf("ID: %d\n", npc.GetNPCID()) + result += fmt.Sprintf("Appearance ID: %d\n", npc.GetAppearanceID()) + result += fmt.Sprintf("AI Strategy: %d\n", npc.GetAIStrategy()) + result += fmt.Sprintf("Cast Percentage: %d%%\n", npc.GetCastPercentage()) + result += fmt.Sprintf("Aggro Radius: %.2f\n", npc.GetAggroRadius()) + result += fmt.Sprintf("Has Spells: %v\n", npc.HasSpells()) + result += fmt.Sprintf("Running Back: %v\n", npc.IsRunningBack()) + + if npc.Entity != nil { + result += fmt.Sprintf("Name: %s\n", npc.Entity.GetName()) + result += fmt.Sprintf("Level: %d\n", npc.Entity.GetLevel()) + result += fmt.Sprintf("Zone: %d\n", npc.Entity.GetZoneID()) + result += fmt.Sprintf("In Combat: %v\n", npc.Entity.GetInCombat()) + } + + return result, nil +} + +func (m *Manager) handleCreateCommand(args []string) (string, error) { + if len(args) < 2 { + return "", fmt.Errorf("usage: create ") + } + + var templateID, newID int32 + if _, err := fmt.Sscanf(args[0], "%d", &templateID); err != nil { + return "", fmt.Errorf("invalid template ID: %s", args[0]) + } + if _, err := fmt.Sscanf(args[1], "%d", &newID); err != nil { + return "", fmt.Errorf("invalid new ID: %s", args[1]) + } + + npc, err := m.CreateNPCFromTemplate(templateID, newID) + if err != nil { + return "", fmt.Errorf("failed to create NPC: %w", err) + } + + return fmt.Sprintf("Successfully created NPC %d from template %d", newID, templateID), nil +} + +func (m *Manager) handleRemoveCommand(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("NPC ID required") + } + + var npcID int32 + if _, err := fmt.Sscanf(args[0], "%d", &npcID); err != nil { + return "", fmt.Errorf("invalid NPC ID: %s", args[0]) + } + + if err := m.RemoveNPC(npcID); err != nil { + return "", fmt.Errorf("failed to remove NPC: %w", err) + } + + return fmt.Sprintf("Successfully removed NPC %d", npcID), nil +} + +func (m *Manager) handleSearchCommand(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("search term required") + } + + searchTerm := strings.ToLower(args[0]) + + m.mutex.RLock() + var results []*NPC + for _, npc := range m.npcs { + if npc.Entity != nil { + name := strings.ToLower(npc.Entity.GetName()) + if strings.Contains(name, searchTerm) { + results = append(results, npc) + } + } + } + m.mutex.RUnlock() + + if len(results) == 0 { + return fmt.Sprintf("No NPCs found matching '%s'.", args[0]), nil + } + + result := fmt.Sprintf("Found %d NPCs matching '%s':\n", len(results), args[0]) + for i, npc := range results { + if i >= 20 { // Limit output + result += "... (and more)\n" + break + } + result += fmt.Sprintf(" %d: %s\n", npc.GetNPCID(), npc.String()) + } + + return result, nil +} + +func (m *Manager) handleCombatCommand(args []string) (string, error) { + result := "Combat Processing Status:\n" + result += fmt.Sprintf("NPCs in Combat: %d\n", m.npcsInCombat) + result += fmt.Sprintf("Total Spell Casts: %d\n", m.spellCastCount) + result += fmt.Sprintf("Total Runbacks: %d\n", m.runbackCount) + + return result, nil +} + +// Helper methods +func (m *Manager) removeFromZoneIndex(npc *NPC) { + if npc.Entity == nil { + return + } + + zoneID := npc.Entity.GetZoneID() + npcs := m.npcsByZone[zoneID] + + for i, n := range npcs { + if n == npc { + // Remove from slice + m.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...) + break + } + } + + // Clean up empty slices + if len(m.npcsByZone[zoneID]) == 0 { + delete(m.npcsByZone, zoneID) + } +} + +func (m *Manager) removeFromAppearanceIndex(npc *NPC) { + appearanceID := npc.GetAppearanceID() + npcs := m.npcsByAppearance[appearanceID] + + for i, n := range npcs { + if n == npc { + // Remove from slice + m.npcsByAppearance[appearanceID] = append(npcs[:i], npcs[i+1:]...) + break + } + } + + // Clean up empty slices + if len(m.npcsByAppearance[appearanceID]) == 0 { + delete(m.npcsByAppearance, appearanceID) + } +} + +// SetManagers sets the external system managers +func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.spellManager = spellMgr + m.skillManager = skillMgr + m.appearanceManager = appearanceMgr +} + +// Configuration methods +func (m *Manager) SetMaxNPCs(max int32) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.maxNPCs = max +} + +func (m *Manager) SetDefaultAggroRadius(radius float32) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.defaultAggroRadius = radius +} + +func (m *Manager) SetAIEnabled(enabled bool) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.enableAI = enabled +} + +// GetNPCCount returns the total number of NPCs +func (m *Manager) GetNPCCount() int32 { + m.mutex.RLock() + defer m.mutex.RUnlock() + return int32(len(m.npcs)) +} + +// Shutdown gracefully shuts down the manager +func (m *Manager) Shutdown() { + if m.logger != nil { + m.logger.LogInfo("Shutting down NPC manager...") + } + + // Stop all AI brains + m.mutex.Lock() + for _, npc := range m.npcs { + if brain := npc.GetBrain(); brain != nil { + brain.SetActive(false) + } + } + + // Clear all data + m.npcs = make(map[int32]*NPC) + m.npcsByZone = make(map[int32][]*NPC) + m.npcsByAppearance = make(map[int32][]*NPC) + m.mutex.Unlock() +} \ No newline at end of file diff --git a/internal/npc/npc.go b/internal/npc/npc.go new file mode 100644 index 0000000..3dbde0a --- /dev/null +++ b/internal/npc/npc.go @@ -0,0 +1,806 @@ +package npc + +import ( + "fmt" + "math" + "math/rand" + "sync" + "time" + + "eq2emu/internal/common" + "eq2emu/internal/entity" + "eq2emu/internal/spawn" +) + +// NewNPC creates a new NPC with default values +func NewNPC() *NPC { + npc := &NPC{ + Entity: entity.NewEntity(), + appearanceID: 0, + npcID: 0, + aiStrategy: AIStrategyBalanced, + attackType: 0, + castPercentage: DefaultCastPercentage, + maxPetLevel: DefaultMaxPetLevel, + aggroRadius: DefaultAggroRadius, + baseAggroRadius: DefaultAggroRadius, + runback: nil, + runningBack: false, + runbackHeadingDir1: 0, + runbackHeadingDir2: 0, + pauseTimer: NewTimer(), + primarySpellList: 0, + secondarySpellList: 0, + primarySkillList: 0, + secondarySkillList: 0, + equipmentListID: 0, + skills: make(map[string]*Skill), + spells: make([]*NPCSpell, 0), + castOnSpells: make(map[int8][]*NPCSpell), + skillBonuses: make(map[int32]*SkillBonus), + hasSpells: false, + castOnAggroCompleted: false, + shardID: 0, + shardCharID: 0, + shardCreatedTimestamp: 0, + callRunback: false, + } + + // Initialize cast-on spell arrays + npc.castOnSpells[CastOnSpawn] = make([]*NPCSpell, 0) + npc.castOnSpells[CastOnAggro] = make([]*NPCSpell, 0) + + // Create default brain + npc.brain = NewDefaultBrain(npc) + + return npc +} + +// NewNPCFromExisting creates a copy of an existing NPC with randomization +func NewNPCFromExisting(oldNPC *NPC) *NPC { + if oldNPC == nil { + return NewNPC() + } + + npc := NewNPC() + + // Copy basic properties + npc.npcID = oldNPC.npcID + npc.appearanceID = oldNPC.appearanceID + npc.aiStrategy = oldNPC.aiStrategy + npc.attackType = oldNPC.attackType + npc.castPercentage = oldNPC.castPercentage + npc.maxPetLevel = oldNPC.maxPetLevel + npc.baseAggroRadius = oldNPC.baseAggroRadius + npc.aggroRadius = oldNPC.baseAggroRadius + + // Copy spell lists + npc.primarySpellList = oldNPC.primarySpellList + npc.secondarySpellList = oldNPC.secondarySpellList + npc.primarySkillList = oldNPC.primarySkillList + npc.secondarySkillList = oldNPC.secondarySkillList + npc.equipmentListID = oldNPC.equipmentListID + + // Copy entity data (stats, appearance, etc.) + if oldNPC.Entity != nil { + npc.Entity = oldNPC.Entity.Copy().(*entity.Entity) + } + + // Handle level randomization + if oldNPC.Entity != nil { + minLevel := oldNPC.Entity.GetMinLevel() + maxLevel := oldNPC.Entity.GetMaxLevel() + if minLevel < maxLevel { + randomLevel := minLevel + int8(rand.Intn(int(maxLevel-minLevel)+1)) + npc.Entity.SetLevel(randomLevel) + } + } + + // Copy skills (deep copy) + npc.copySkills(oldNPC) + + // Copy spells (deep copy) + npc.copySpells(oldNPC) + + // Handle appearance randomization + if oldNPC.Entity != nil && oldNPC.Entity.GetRandomize() > 0 { + npc.randomizeAppearance(oldNPC.Entity.GetRandomize()) + } + + return npc +} + +// IsNPC always returns true for NPC instances +func (n *NPC) IsNPC() bool { + return true +} + +// GetAppearanceID returns the appearance ID +func (n *NPC) GetAppearanceID() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.appearanceID +} + +// SetAppearanceID sets the appearance ID +func (n *NPC) SetAppearanceID(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.appearanceID = id +} + +// GetNPCID returns the NPC database ID +func (n *NPC) GetNPCID() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.npcID +} + +// SetNPCID sets the NPC database ID +func (n *NPC) SetNPCID(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.npcID = id +} + +// AI Strategy methods +func (n *NPC) GetAIStrategy() int8 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.aiStrategy +} + +func (n *NPC) SetAIStrategy(strategy int8) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.aiStrategy = strategy +} + +// Attack Type methods +func (n *NPC) GetAttackType() int8 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.attackType +} + +func (n *NPC) SetAttackType(attackType int8) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.attackType = attackType +} + +// Cast Percentage methods +func (n *NPC) GetCastPercentage() int8 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.castPercentage +} + +func (n *NPC) SetCastPercentage(percentage int8) { + n.mutex.Lock() + defer n.mutex.Unlock() + if percentage < 0 { + percentage = 0 + } else if percentage > 100 { + percentage = 100 + } + n.castPercentage = percentage +} + +// Aggro Radius methods +func (n *NPC) GetAggroRadius() float32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.aggroRadius +} + +func (n *NPC) SetAggroRadius(radius float32, overrideBase bool) { + n.mutex.Lock() + defer n.mutex.Unlock() + + if n.baseAggroRadius == 0.0 || overrideBase { + n.baseAggroRadius = radius + } + n.aggroRadius = radius +} + +func (n *NPC) GetBaseAggroRadius() float32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.baseAggroRadius +} + +// Pet Level methods +func (n *NPC) GetMaxPetLevel() int8 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.maxPetLevel +} + +func (n *NPC) SetMaxPetLevel(level int8) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.maxPetLevel = level +} + +// Spell List methods +func (n *NPC) GetPrimarySpellList() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.primarySpellList +} + +func (n *NPC) SetPrimarySpellList(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.primarySpellList = id +} + +func (n *NPC) GetSecondarySpellList() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.secondarySpellList +} + +func (n *NPC) SetSecondarySpellList(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.secondarySpellList = id +} + +// Skill List methods +func (n *NPC) GetPrimarySkillList() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.primarySkillList +} + +func (n *NPC) SetPrimarySkillList(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.primarySkillList = id +} + +func (n *NPC) GetSecondarySkillList() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.secondarySkillList +} + +func (n *NPC) SetSecondarySkillList(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.secondarySkillList = id +} + +// Equipment List methods +func (n *NPC) GetEquipmentListID() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.equipmentListID +} + +func (n *NPC) SetEquipmentListID(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.equipmentListID = id +} + +// HasSpells returns whether the NPC has any spells +func (n *NPC) HasSpells() bool { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.hasSpells +} + +// GetSpells returns a copy of all spells +func (n *NPC) GetSpells() []*NPCSpell { + n.mutex.RLock() + defer n.mutex.RUnlock() + + result := make([]*NPCSpell, len(n.spells)) + for i, spell := range n.spells { + result[i] = spell.Copy() + } + return result +} + +// SetSpells sets the NPC's spell list +func (n *NPC) SetSpells(spells []*NPCSpell) { + n.mutex.Lock() + defer n.mutex.Unlock() + + // Clear existing cast-on spells + for i := int8(0); i < MaxCastTypes; i++ { + n.castOnSpells[i] = make([]*NPCSpell, 0) + } + + // Clear existing spells + n.spells = make([]*NPCSpell, 0) + + if spells == nil || len(spells) == 0 { + n.hasSpells = false + return + } + + n.hasSpells = true + + // Process spells and separate cast-on types + for _, spell := range spells { + if spell == nil { + continue + } + + spellCopy := spell.Copy() + + if spellCopy.GetCastOnSpawn() { + n.castOnSpells[CastOnSpawn] = append(n.castOnSpells[CastOnSpawn], spellCopy) + } else if spellCopy.GetCastOnInitialAggro() { + n.castOnSpells[CastOnAggro] = append(n.castOnSpells[CastOnAggro], spellCopy) + } else { + n.spells = append(n.spells, spellCopy) + } + } +} + +// GetSkillByName returns a skill by name +func (n *NPC) GetSkillByName(name string, checkUpdate bool) *Skill { + n.mutex.RLock() + defer n.mutex.RUnlock() + + skill, exists := n.skills[name] + if !exists { + return nil + } + + // Random skill increase (10% chance) + if checkUpdate && skill.GetCurrentVal() < skill.MaxVal && rand.Intn(100) >= 90 { + skill.IncreaseSkill() + } + + return skill +} + +// GetSkillByID returns a skill by ID (requires master skill list lookup) +func (n *NPC) GetSkillByID(id int32, checkUpdate bool) *Skill { + // TODO: Implement skill lookup by ID using master skill list + // For now, return nil as we need the master skill list integration + return nil +} + +// SetSkills sets the NPC's skills +func (n *NPC) SetSkills(skills map[string]*Skill) { + n.mutex.Lock() + defer n.mutex.Unlock() + + // Clear existing skills + n.skills = make(map[string]*Skill) + + // Copy skills + if skills != nil { + for name, skill := range skills { + if skill != nil { + n.skills[name] = &Skill{ + SkillID: skill.SkillID, + Name: skill.Name, + CurrentVal: skill.CurrentVal, + MaxVal: skill.MaxVal, + } + } + } + } +} + +// AddSkillBonus adds a skill bonus from a spell +func (n *NPC) AddSkillBonus(spellID, skillID int32, value float32) { + if value == 0 { + return + } + + n.mutex.Lock() + defer n.mutex.Unlock() + + // Get or create skill bonus + skillBonus, exists := n.skillBonuses[spellID] + if !exists { + skillBonus = NewSkillBonus(spellID) + n.skillBonuses[spellID] = skillBonus + } + + // Add the skill bonus + skillBonus.AddSkill(skillID, value) + + // Apply bonus to existing skills + for _, skill := range n.skills { + if skill.SkillID == skillID { + skill.CurrentVal += int16(value) + skill.MaxVal += int16(value) + } + } +} + +// RemoveSkillBonus removes skill bonuses from a spell +func (n *NPC) RemoveSkillBonus(spellID int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + + skillBonus, exists := n.skillBonuses[spellID] + if !exists { + return + } + + // Remove bonuses from skills + bonuses := skillBonus.GetSkills() + for _, bonus := range bonuses { + for _, skill := range n.skills { + if skill.SkillID == bonus.SkillID { + skill.CurrentVal -= int16(bonus.Value) + skill.MaxVal -= int16(bonus.Value) + } + } + } + + // Remove the skill bonus + delete(n.skillBonuses, spellID) +} + +// Runback Location methods +func (n *NPC) SetRunbackLocation(x, y, z float32, gridID int32, resetHP bool) { + n.mutex.Lock() + defer n.mutex.Unlock() + + n.runback = &MovementLocation{ + X: x, + Y: y, + Z: z, + GridID: gridID, + Stage: 0, + ResetHPOnRunback: resetHP, + UseNavPath: false, + Mapped: false, + } +} + +func (n *NPC) GetRunbackLocation() *MovementLocation { + n.mutex.RLock() + defer n.mutex.RUnlock() + + if n.runback == nil { + return nil + } + return n.runback.Copy() +} + +func (n *NPC) GetRunbackDistance() float32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + + if n.runback == nil || n.Entity == nil { + return 0 + } + + // Calculate distance using basic distance formula + dx := n.Entity.GetX() - n.runback.X + dy := n.Entity.GetY() - n.runback.Y + dz := n.Entity.GetZ() - n.runback.Z + + return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz))) +} + +func (n *NPC) ClearRunback() { + n.mutex.Lock() + defer n.mutex.Unlock() + + n.runback = nil + n.runningBack = false + n.runbackHeadingDir1 = 0 + n.runbackHeadingDir2 = 0 +} + +// StartRunback sets the current location as the runback point +func (n *NPC) StartRunback(resetHP bool) { + if n.GetRunbackLocation() != nil { + return + } + + if n.Entity == nil { + return + } + + n.mutex.Lock() + defer n.mutex.Unlock() + + n.runback = &MovementLocation{ + X: n.Entity.GetX(), + Y: n.Entity.GetY(), + Z: n.Entity.GetZ(), + GridID: n.Entity.GetLocation(), + Stage: 0, + ResetHPOnRunback: resetHP, + UseNavPath: false, + Mapped: false, + } + + // Store original heading + n.runbackHeadingDir1 = n.Entity.GetHeading() + n.runbackHeadingDir2 = n.Entity.GetHeading() // In C++ these are separate values +} + +// Runback initiates runback movement +func (n *NPC) Runback(distance float32, stopFollowing bool) { + if n.runback == nil { + return + } + + if distance == 0.0 { + distance = n.GetRunbackDistance() + } + + n.mutex.Lock() + n.runningBack = true + n.mutex.Unlock() + + // TODO: Implement actual movement logic + // This would integrate with the movement system + + if stopFollowing && n.Entity != nil { + n.Entity.SetFollowing(false) + } +} + +// IsRunningBack returns whether the NPC is currently running back +func (n *NPC) IsRunningBack() bool { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.runningBack +} + +// Movement pause methods +func (n *NPC) PauseMovement(periodMS int32) bool { + if periodMS < 1 { + periodMS = 1 + } + + if periodMS > MaxPauseTime { + periodMS = MaxPauseTime + } + + // TODO: Integrate with movement system to stop movement + // For now, just start the pause timer + n.pauseTimer.Start(periodMS, true) + + return true +} + +func (n *NPC) IsPauseMovementTimerActive() bool { + if n.pauseTimer.Check() { + n.pauseTimer.Disable() + n.callRunback = true + } + + return n.pauseTimer.Enabled() +} + +// Brain methods +func (n *NPC) GetBrain() Brain { + n.brainMutex.RLock() + defer n.brainMutex.RUnlock() + return n.brain +} + +func (n *NPC) SetBrain(brain Brain) { + n.brainMutex.Lock() + defer n.brainMutex.Unlock() + + // Validate brain matches this NPC + if brain != nil && brain.GetBody() != n { + // TODO: Log error + return + } + + n.brain = brain +} + +// Shard methods +func (n *NPC) GetShardID() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.shardID +} + +func (n *NPC) SetShardID(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.shardID = id +} + +func (n *NPC) GetShardCharID() int32 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.shardCharID +} + +func (n *NPC) SetShardCharID(id int32) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.shardCharID = id +} + +func (n *NPC) GetShardCreatedTimestamp() int64 { + n.mutex.RLock() + defer n.mutex.RUnlock() + return n.shardCreatedTimestamp +} + +func (n *NPC) SetShardCreatedTimestamp(timestamp int64) { + n.mutex.Lock() + defer n.mutex.Unlock() + n.shardCreatedTimestamp = timestamp +} + +// HandleUse processes entity command usage +func (n *NPC) HandleUse(client Client, commandType string) bool { + if client == nil || len(commandType) == 0 { + return false + } + + // Check if NPC shows command icons + if n.Entity == nil { + return false + } + + // TODO: Implement entity command processing + // This would integrate with the command system + + return false +} + +// InCombat handles combat state changes +func (n *NPC) InCombat(val bool) { + if n.Entity == nil { + return + } + + currentCombat := n.Entity.GetInCombat() + if currentCombat == val { + return + } + + n.Entity.SetInCombat(val) + + if val { + // Entering combat + if n.GetRunbackLocation() == nil { + n.StartRunback(true) + } + + // Set max speed for combat + if n.Entity.GetMaxSpeed() > 0 { + n.Entity.SetSpeed(n.Entity.GetMaxSpeed()) + } + + // TODO: Add combat icon, call spawn scripts, etc. + + } else { + // Leaving combat + // TODO: Remove combat icon, call combat reset scripts, etc. + + if n.Entity.GetHP() > 0 { + // TODO: Re-enable action states, stop heroic opportunities + } + } +} + +// ProcessCombat handles combat processing +func (n *NPC) ProcessCombat() { + // TODO: Implement combat processing logic + // This would handle spell casting, AI decisions, etc. +} + +// Copy helper methods +func (n *NPC) copySkills(oldNPC *NPC) { + if oldNPC == nil { + return + } + + oldNPC.mutex.RLock() + oldSkills := make(map[string]*Skill) + for name, skill := range oldNPC.skills { + if skill != nil { + oldSkills[name] = &Skill{ + SkillID: skill.SkillID, + Name: skill.Name, + CurrentVal: skill.CurrentVal, + MaxVal: skill.MaxVal, + } + } + } + oldNPC.mutex.RUnlock() + + n.SetSkills(oldSkills) +} + +func (n *NPC) copySpells(oldNPC *NPC) { + if oldNPC == nil { + return + } + + oldNPC.mutex.RLock() + oldSpells := make([]*NPCSpell, len(oldNPC.spells)) + for i, spell := range oldNPC.spells { + if spell != nil { + oldSpells[i] = spell.Copy() + } + } + + // Also copy cast-on spells + for castType, spells := range oldNPC.castOnSpells { + for _, spell := range spells { + if spell != nil { + oldSpells = append(oldSpells, spell.Copy()) + } + } + } + oldNPC.mutex.RUnlock() + + n.SetSpells(oldSpells) +} + +// randomizeAppearance applies appearance randomization +func (n *NPC) randomizeAppearance(flags int32) { + // TODO: Implement full appearance randomization + // This is a complex system that would integrate with the appearance system + + // For now, just implement basic randomization + if n.Entity == nil { + return + } + + // Random gender + if flags&RandomizeGender != 0 { + gender := int8(rand.Intn(2) + 1) // 1 or 2 + n.Entity.SetGender(gender) + } + + // Random race (simplified) + if flags&RandomizeRace != 0 { + // TODO: Implement race randomization based on alignment + race := int16(rand.Intn(21)) // 0-20 for basic races + n.Entity.SetRace(race) + } + + // Color randomization + if flags&RandomizeSkinColor != 0 { + // TODO: Implement skin color randomization + } + + // More randomization options would be implemented here +} + +// Validation methods +func (n *NPC) IsValid() bool { + if n.Entity == nil { + return false + } + + // Basic validation + if n.Entity.GetLevel() < MinNPCLevel || n.Entity.GetLevel() > MaxNPCLevel { + return false + } + + if n.appearanceID < MinAppearanceID || n.appearanceID > MaxAppearanceID { + return false + } + + return true +} + +// String returns a string representation of the NPC +func (n *NPC) String() string { + if n.Entity == nil { + return fmt.Sprintf("NPC{ID: %d, AppearanceID: %d, Entity: nil}", n.npcID, n.appearanceID) + } + + return fmt.Sprintf("NPC{ID: %d, Name: %s, Level: %d, AppearanceID: %d}", + n.npcID, n.Entity.GetName(), n.Entity.GetLevel(), n.appearanceID) +} \ No newline at end of file diff --git a/internal/npc/types.go b/internal/npc/types.go new file mode 100644 index 0000000..e6f9b19 --- /dev/null +++ b/internal/npc/types.go @@ -0,0 +1,440 @@ +package npc + +import ( + "sync" + "time" + + "eq2emu/internal/common" + "eq2emu/internal/entity" + "eq2emu/internal/spawn" +) + +// NPCSpell represents a spell configuration for NPCs +type NPCSpell struct { + ListID int32 // Spell list identifier + SpellID int32 // Spell ID from master spell list + Tier int8 // Spell tier + CastOnSpawn bool // Cast when NPC spawns + CastOnInitialAggro bool // Cast when first entering combat + RequiredHPRatio int8 // HP ratio requirement for casting (-100 to 100) + mutex sync.RWMutex +} + +// NewNPCSpell creates a new NPCSpell +func NewNPCSpell() *NPCSpell { + return &NPCSpell{ + ListID: 0, + SpellID: 0, + Tier: 1, + CastOnSpawn: false, + CastOnInitialAggro: false, + RequiredHPRatio: 0, + } +} + +// Copy creates a deep copy of the NPCSpell +func (ns *NPCSpell) Copy() *NPCSpell { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + + return &NPCSpell{ + ListID: ns.ListID, + SpellID: ns.SpellID, + Tier: ns.Tier, + CastOnSpawn: ns.CastOnSpawn, + CastOnInitialAggro: ns.CastOnInitialAggro, + RequiredHPRatio: ns.RequiredHPRatio, + } +} + +// Getters +func (ns *NPCSpell) GetListID() int32 { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + return ns.ListID +} + +func (ns *NPCSpell) GetSpellID() int32 { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + return ns.SpellID +} + +func (ns *NPCSpell) GetTier() int8 { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + return ns.Tier +} + +func (ns *NPCSpell) GetCastOnSpawn() bool { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + return ns.CastOnSpawn +} + +func (ns *NPCSpell) GetCastOnInitialAggro() bool { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + return ns.CastOnInitialAggro +} + +func (ns *NPCSpell) GetRequiredHPRatio() int8 { + ns.mutex.RLock() + defer ns.mutex.RUnlock() + return ns.RequiredHPRatio +} + +// Setters +func (ns *NPCSpell) SetListID(id int32) { + ns.mutex.Lock() + defer ns.mutex.Unlock() + ns.ListID = id +} + +func (ns *NPCSpell) SetSpellID(id int32) { + ns.mutex.Lock() + defer ns.mutex.Unlock() + ns.SpellID = id +} + +func (ns *NPCSpell) SetTier(tier int8) { + ns.mutex.Lock() + defer ns.mutex.Unlock() + ns.Tier = tier +} + +func (ns *NPCSpell) SetCastOnSpawn(cast bool) { + ns.mutex.Lock() + defer ns.mutex.Unlock() + ns.CastOnSpawn = cast +} + +func (ns *NPCSpell) SetCastOnInitialAggro(cast bool) { + ns.mutex.Lock() + defer ns.mutex.Unlock() + ns.CastOnInitialAggro = cast +} + +func (ns *NPCSpell) SetRequiredHPRatio(ratio int8) { + ns.mutex.Lock() + defer ns.mutex.Unlock() + ns.RequiredHPRatio = ratio +} + +// SkillBonus represents a skill bonus from spells +type SkillBonus struct { + SpellID int32 // Spell providing the bonus + Skills map[int32]*SkillBonusValue // Map of skill ID to bonus value + mutex sync.RWMutex +} + +// SkillBonusValue represents the actual bonus value for a skill +type SkillBonusValue struct { + SkillID int32 // Skill receiving the bonus + Value float32 // Bonus amount +} + +// NewSkillBonus creates a new SkillBonus +func NewSkillBonus(spellID int32) *SkillBonus { + return &SkillBonus{ + SpellID: spellID, + Skills: make(map[int32]*SkillBonusValue), + } +} + +// AddSkill adds a skill bonus +func (sb *SkillBonus) AddSkill(skillID int32, value float32) { + sb.mutex.Lock() + defer sb.mutex.Unlock() + + sb.Skills[skillID] = &SkillBonusValue{ + SkillID: skillID, + Value: value, + } +} + +// RemoveSkill removes a skill bonus +func (sb *SkillBonus) RemoveSkill(skillID int32) bool { + sb.mutex.Lock() + defer sb.mutex.Unlock() + + if _, exists := sb.Skills[skillID]; exists { + delete(sb.Skills, skillID) + return true + } + return false +} + +// GetSkills returns a copy of all skill bonuses +func (sb *SkillBonus) GetSkills() map[int32]*SkillBonusValue { + sb.mutex.RLock() + defer sb.mutex.RUnlock() + + result := make(map[int32]*SkillBonusValue) + for id, bonus := range sb.Skills { + result[id] = &SkillBonusValue{ + SkillID: bonus.SkillID, + Value: bonus.Value, + } + } + return result +} + +// MovementLocation represents a movement destination for runback +type MovementLocation struct { + X float32 // X coordinate + Y float32 // Y coordinate + Z float32 // Z coordinate + GridID int32 // Grid location ID + Stage int32 // Movement stage + ResetHPOnRunback bool // Whether to reset HP when reaching location + UseNavPath bool // Whether to use navigation pathfinding + Mapped bool // Whether location is mapped +} + +// NewMovementLocation creates a new MovementLocation +func NewMovementLocation(x, y, z float32, gridID int32) *MovementLocation { + return &MovementLocation{ + X: x, + Y: y, + Z: z, + GridID: gridID, + Stage: 0, + ResetHPOnRunback: false, + UseNavPath: false, + Mapped: false, + } +} + +// Copy creates a deep copy of the MovementLocation +func (ml *MovementLocation) Copy() *MovementLocation { + return &MovementLocation{ + X: ml.X, + Y: ml.Y, + Z: ml.Z, + GridID: ml.GridID, + Stage: ml.Stage, + ResetHPOnRunback: ml.ResetHPOnRunback, + UseNavPath: ml.UseNavPath, + Mapped: ml.Mapped, + } +} + +// NPC represents a non-player character extending Entity +type NPC struct { + *entity.Entity // Embedded entity for combat capabilities + + // Core NPC properties + appearanceID int32 // Appearance ID for client display + npcID int32 // NPC database ID + aiStrategy int8 // AI strategy (balanced/offensive/defensive) + attackType int8 // Attack type preference + castPercentage int8 // Percentage chance to cast spells + maxPetLevel int8 // Maximum pet level + + // Combat and movement + aggroRadius float32 // Aggro detection radius + baseAggroRadius float32 // Base aggro radius (for resets) + runback *MovementLocation // Runback location when leaving combat + runningBack bool // Currently running back to spawn point + runbackHeadingDir1 int16 // Original heading direction 1 + runbackHeadingDir2 int16 // Original heading direction 2 + pauseTimer *Timer // Movement pause timer + + // Spell and skill management + primarySpellList int32 // Primary spell list ID + secondarySpellList int32 // Secondary spell list ID + primarySkillList int32 // Primary skill list ID + secondarySkillList int32 // Secondary skill list ID + equipmentListID int32 // Equipment list ID + skills map[string]*Skill // NPC skills by name + spells []*NPCSpell // Available spells + castOnSpells map[int8][]*NPCSpell // Spells to cast by trigger type + skillBonuses map[int32]*SkillBonus // Skill bonuses from spells + hasSpells bool // Whether NPC has any spells + castOnAggroCompleted bool // Whether cast-on-aggro spells are done + + // Brain/AI system (placeholder for now) + brain Brain // AI brain for decision making + + // Shard system (for cross-server functionality) + shardID int32 // Shard identifier + shardCharID int32 // Character ID on shard + shardCreatedTimestamp int64 // Timestamp when created on shard + + // Thread safety + mutex sync.RWMutex // Main NPC mutex + brainMutex sync.RWMutex // Brain-specific mutex + + // Atomic flags for thread-safe state management + callRunback bool // Flag to trigger runback +} + +// Timer represents a simple timer for NPC operations +type Timer struct { + duration time.Duration + startTime time.Time + enabled bool + mutex sync.RWMutex +} + +// NewTimer creates a new timer +func NewTimer() *Timer { + return &Timer{ + enabled: false, + } +} + +// Start starts the timer with the given duration +func (t *Timer) Start(durationMS int32, reset bool) { + t.mutex.Lock() + defer t.mutex.Unlock() + + if reset || !t.enabled { + t.duration = time.Duration(durationMS) * time.Millisecond + t.startTime = time.Now() + t.enabled = true + } +} + +// Check checks if the timer has expired +func (t *Timer) Check() bool { + t.mutex.RLock() + defer t.mutex.RUnlock() + + if !t.enabled { + return false + } + + return time.Since(t.startTime) >= t.duration +} + +// Enabled returns whether the timer is currently enabled +func (t *Timer) Enabled() bool { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.enabled +} + +// Disable disables the timer +func (t *Timer) Disable() { + t.mutex.Lock() + defer t.mutex.Unlock() + t.enabled = false +} + +// Skill represents an NPC skill (simplified from C++ version) +type Skill struct { + SkillID int32 // Skill identifier + Name string // Skill name + CurrentVal int16 // Current skill value + MaxVal int16 // Maximum skill value + mutex sync.RWMutex +} + +// NewSkill creates a new skill +func NewSkill(id int32, name string, current, max int16) *Skill { + return &Skill{ + SkillID: id, + Name: name, + CurrentVal: current, + MaxVal: max, + } +} + +// GetCurrentVal returns the current skill value +func (s *Skill) GetCurrentVal() int16 { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.CurrentVal +} + +// SetCurrentVal sets the current skill value +func (s *Skill) SetCurrentVal(val int16) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.CurrentVal = val +} + +// IncreaseSkill increases the skill value (with random chance) +func (s *Skill) IncreaseSkill() bool { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.CurrentVal < s.MaxVal { + s.CurrentVal++ + return true + } + return false +} + +// Brain interface represents the AI brain system (placeholder) +type Brain interface { + Think() error + GetBody() *NPC + SetBody(*NPC) + IsActive() bool + SetActive(bool) +} + +// DefaultBrain provides a simple brain implementation +type DefaultBrain struct { + npc *NPC + active bool + mutex sync.RWMutex +} + +// NewDefaultBrain creates a new default brain +func NewDefaultBrain(npc *NPC) *DefaultBrain { + return &DefaultBrain{ + npc: npc, + active: true, + } +} + +// Think processes AI logic (placeholder implementation) +func (b *DefaultBrain) Think() error { + // TODO: Implement AI thinking logic + return nil +} + +// GetBody returns the NPC this brain controls +func (b *DefaultBrain) GetBody() *NPC { + b.mutex.RLock() + defer b.mutex.RUnlock() + return b.npc +} + +// SetBody sets the NPC this brain controls +func (b *DefaultBrain) SetBody(npc *NPC) { + b.mutex.Lock() + defer b.mutex.Unlock() + b.npc = npc +} + +// IsActive returns whether the brain is active +func (b *DefaultBrain) IsActive() bool { + b.mutex.RLock() + defer b.mutex.RUnlock() + return b.active +} + +// SetActive sets the brain's active state +func (b *DefaultBrain) SetActive(active bool) { + b.mutex.Lock() + defer b.mutex.Unlock() + b.active = active +} + +// NPCStatistics contains NPC system statistics +type NPCStatistics struct { + TotalNPCs int `json:"total_npcs"` + NPCsInCombat int `json:"npcs_in_combat"` + NPCsWithSpells int `json:"npcs_with_spells"` + NPCsWithSkills int `json:"npcs_with_skills"` + AIStrategyCounts map[string]int `json:"ai_strategy_counts"` + SpellCastCount int64 `json:"spell_cast_count"` + SkillUsageCount int64 `json:"skill_usage_count"` + RunbackCount int64 `json:"runback_count"` + AverageAggroRadius float32 `json:"average_aggro_radius"` +} \ No newline at end of file diff --git a/internal/quests/actions.go b/internal/quests/actions.go new file mode 100644 index 0000000..d3fe75b --- /dev/null +++ b/internal/quests/actions.go @@ -0,0 +1,426 @@ +package quests + +// Action management methods for Quest + +// AddCompleteAction adds a completion action for a step +func (q *Quest) AddCompleteAction(stepID int32, action string) { + q.completeActionsMutex.Lock() + defer q.completeActionsMutex.Unlock() + q.CompleteActions[stepID] = action + q.SetSaveNeeded(true) +} + +// AddProgressAction adds a progress action for a step +func (q *Quest) AddProgressAction(stepID int32, action string) { + q.progressActionsMutex.Lock() + defer q.progressActionsMutex.Unlock() + q.ProgressActions[stepID] = action + q.SetSaveNeeded(true) +} + +// AddFailedAction adds a failure action for a step +func (q *Quest) AddFailedAction(stepID int32, action string) { + q.failedActionsMutex.Lock() + defer q.failedActionsMutex.Unlock() + q.FailedActions[stepID] = action + q.SetSaveNeeded(true) +} + +// RemoveCompleteAction removes a completion action for a step +func (q *Quest) RemoveCompleteAction(stepID int32) bool { + q.completeActionsMutex.Lock() + defer q.completeActionsMutex.Unlock() + + if _, exists := q.CompleteActions[stepID]; exists { + delete(q.CompleteActions, stepID) + q.SetSaveNeeded(true) + return true + } + return false +} + +// RemoveProgressAction removes a progress action for a step +func (q *Quest) RemoveProgressAction(stepID int32) bool { + q.progressActionsMutex.Lock() + defer q.progressActionsMutex.Unlock() + + if _, exists := q.ProgressActions[stepID]; exists { + delete(q.ProgressActions, stepID) + q.SetSaveNeeded(true) + return true + } + return false +} + +// RemoveFailedAction removes a failure action for a step +func (q *Quest) RemoveFailedAction(stepID int32) bool { + q.failedActionsMutex.Lock() + defer q.failedActionsMutex.Unlock() + + if _, exists := q.FailedActions[stepID]; exists { + delete(q.FailedActions, stepID) + q.SetSaveNeeded(true) + return true + } + return false +} + +// GetCompleteAction returns the completion action for a step +func (q *Quest) GetCompleteAction(stepID int32) (string, bool) { + q.completeActionsMutex.RLock() + defer q.completeActionsMutex.RUnlock() + + action, exists := q.CompleteActions[stepID] + return action, exists +} + +// GetProgressAction returns the progress action for a step +func (q *Quest) GetProgressAction(stepID int32) (string, bool) { + q.progressActionsMutex.RLock() + defer q.progressActionsMutex.RUnlock() + + action, exists := q.ProgressActions[stepID] + return action, exists +} + +// GetFailedAction returns the failure action for a step +func (q *Quest) GetFailedAction(stepID int32) (string, bool) { + q.failedActionsMutex.RLock() + defer q.failedActionsMutex.RUnlock() + + action, exists := q.FailedActions[stepID] + return action, exists +} + +// HasCompleteAction checks if a step has a completion action +func (q *Quest) HasCompleteAction(stepID int32) bool { + q.completeActionsMutex.RLock() + defer q.completeActionsMutex.RUnlock() + + _, exists := q.CompleteActions[stepID] + return exists +} + +// HasProgressAction checks if a step has a progress action +func (q *Quest) HasProgressAction(stepID int32) bool { + q.progressActionsMutex.RLock() + defer q.progressActionsMutex.RUnlock() + + _, exists := q.ProgressActions[stepID] + return exists +} + +// HasFailedAction checks if a step has a failure action +func (q *Quest) HasFailedAction(stepID int32) bool { + q.failedActionsMutex.RLock() + defer q.failedActionsMutex.RUnlock() + + _, exists := q.FailedActions[stepID] + return exists +} + +// SetCompleteAction sets the general quest completion action +func (q *Quest) SetCompleteAction(action string) { + q.CompleteAction = action + q.SetSaveNeeded(true) +} + +// GetQuestCompleteAction returns the general quest completion action +func (q *Quest) GetQuestCompleteAction() string { + return q.CompleteAction +} + +// ClearAllActions removes all actions for all steps +func (q *Quest) ClearAllActions() { + q.completeActionsMutex.Lock() + q.CompleteActions = make(map[int32]string) + q.completeActionsMutex.Unlock() + + q.progressActionsMutex.Lock() + q.ProgressActions = make(map[int32]string) + q.progressActionsMutex.Unlock() + + q.failedActionsMutex.Lock() + q.FailedActions = make(map[int32]string) + q.failedActionsMutex.Unlock() + + q.CompleteAction = "" + q.SetSaveNeeded(true) +} + +// ClearStepActions removes all actions for a specific step +func (q *Quest) ClearStepActions(stepID int32) { + q.RemoveCompleteAction(stepID) + q.RemoveProgressAction(stepID) + q.RemoveFailedAction(stepID) +} + +// GetAllCompleteActions returns a copy of all completion actions +func (q *Quest) GetAllCompleteActions() map[int32]string { + q.completeActionsMutex.RLock() + defer q.completeActionsMutex.RUnlock() + + actions := make(map[int32]string) + for stepID, action := range q.CompleteActions { + actions[stepID] = action + } + return actions +} + +// GetAllProgressActions returns a copy of all progress actions +func (q *Quest) GetAllProgressActions() map[int32]string { + q.progressActionsMutex.RLock() + defer q.progressActionsMutex.RUnlock() + + actions := make(map[int32]string) + for stepID, action := range q.ProgressActions { + actions[stepID] = action + } + return actions +} + +// GetAllFailedActions returns a copy of all failure actions +func (q *Quest) GetAllFailedActions() map[int32]string { + q.failedActionsMutex.RLock() + defer q.failedActionsMutex.RUnlock() + + actions := make(map[int32]string) + for stepID, action := range q.FailedActions { + actions[stepID] = action + } + return actions +} + +// ExecuteCompleteAction executes a completion action for a step +func (q *Quest) ExecuteCompleteAction(stepID int32) error { + action, exists := q.GetCompleteAction(stepID) + if !exists || action == "" { + return fmt.Errorf("no completion action for step %d", stepID) + } + + // TODO: Execute Lua script with action + // This would integrate with the Lua interface when available + return q.executeLuaAction("complete", stepID, action) +} + +// ExecuteProgressAction executes a progress action for a step +func (q *Quest) ExecuteProgressAction(stepID int32, progressAmount int32) error { + action, exists := q.GetProgressAction(stepID) + if !exists || action == "" { + return fmt.Errorf("no progress action for step %d", stepID) + } + + // TODO: Execute Lua script with action and progress amount + return q.executeLuaActionWithProgress("progress", stepID, action, progressAmount) +} + +// ExecuteFailedAction executes a failure action for a step +func (q *Quest) ExecuteFailedAction(stepID int32) error { + action, exists := q.GetFailedAction(stepID) + if !exists || action == "" { + return fmt.Errorf("no failure action for step %d", stepID) + } + + // TODO: Execute Lua script with action + return q.executeLuaAction("failed", stepID, action) +} + +// ExecuteQuestCompleteAction executes the general quest completion action +func (q *Quest) ExecuteQuestCompleteAction() error { + if q.CompleteAction == "" { + return nil // No action to execute + } + + // TODO: Execute Lua script with quest completion action + return q.executeLuaAction("quest_complete", 0, q.CompleteAction) +} + +// Placeholder methods for Lua integration (to be implemented when Lua interface is available) + +// executeLuaAction executes a Lua action script +func (q *Quest) executeLuaAction(actionType string, stepID int32, action string) error { + // TODO: Implement Lua script execution + // This is a placeholder that would integrate with the Lua interface + // when it becomes available in the Go codebase + + // For now, just log the action that would be executed + fmt.Printf("Quest %d: Would execute %s action for step %d: %s\n", q.ID, actionType, stepID, action) + return nil +} + +// executeLuaActionWithProgress executes a Lua action script with progress information +func (q *Quest) executeLuaActionWithProgress(actionType string, stepID int32, action string, progress int32) error { + // TODO: Implement Lua script execution with progress parameter + // This is a placeholder that would integrate with the Lua interface + + // For now, just log the action that would be executed + fmt.Printf("Quest %d: Would execute %s action for step %d with progress %d: %s\n", q.ID, actionType, stepID, progress, action) + return nil +} + +// Action validation methods + +// ValidateActions validates all quest actions +func (q *Quest) ValidateActions() error { + // Validate completion actions + q.completeActionsMutex.RLock() + for stepID, action := range q.CompleteActions { + if err := q.validateAction(stepID, action, "completion"); err != nil { + q.completeActionsMutex.RUnlock() + return err + } + } + q.completeActionsMutex.RUnlock() + + // Validate progress actions + q.progressActionsMutex.RLock() + for stepID, action := range q.ProgressActions { + if err := q.validateAction(stepID, action, "progress"); err != nil { + q.progressActionsMutex.RUnlock() + return err + } + } + q.progressActionsMutex.RUnlock() + + // Validate failure actions + q.failedActionsMutex.RLock() + for stepID, action := range q.FailedActions { + if err := q.validateAction(stepID, action, "failure"); err != nil { + q.failedActionsMutex.RUnlock() + return err + } + } + q.failedActionsMutex.RUnlock() + + // Validate quest completion action + if q.CompleteAction != "" { + if err := q.validateAction(0, q.CompleteAction, "quest completion"); err != nil { + return err + } + } + + return nil +} + +// validateAction validates a single action +func (q *Quest) validateAction(stepID int32, action, actionType string) error { + if action == "" { + return fmt.Errorf("empty %s action for step %d", actionType, stepID) + } + + if len(action) > MaxCompleteActionLength { + return fmt.Errorf("%s action too long for step %d (max %d)", actionType, stepID, MaxCompleteActionLength) + } + + // Validate that the step exists (except for quest completion action) + if stepID > 0 { + if step := q.GetQuestStep(stepID); step == nil { + return fmt.Errorf("%s action references non-existent step %d", actionType, stepID) + } + } + + // Basic Lua syntax validation (very basic check) + if err := q.validateLuaSyntax(action); err != nil { + return fmt.Errorf("invalid Lua syntax in %s action for step %d: %w", actionType, stepID, err) + } + + return nil +} + +// validateLuaSyntax performs basic Lua syntax validation +func (q *Quest) validateLuaSyntax(luaCode string) error { + // TODO: Implement proper Lua syntax validation + // This is a placeholder that does very basic checks + + // Check for balanced parentheses, brackets, and braces + if err := q.checkBalancedDelimiters(luaCode); err != nil { + return err + } + + // Check for obviously invalid syntax patterns + invalidPatterns := []string{ + "--[[", // Unfinished multi-line comments + "function(", // Function without closing + } + + for _, pattern := range invalidPatterns { + if strings.Contains(luaCode, pattern) { + // Do more thorough checking for these patterns + // This is just a basic check - real validation would be more sophisticated + } + } + + return nil +} + +// checkBalancedDelimiters checks if delimiters are balanced in Lua code +func (q *Quest) checkBalancedDelimiters(code string) error { + stack := make([]rune, 0) + pairs := map[rune]rune{ + ')': '(', + ']': '[', + '}': '{', + } + + inString := false + inComment := false + var stringChar rune + + for i, char := range code { + // Handle string literals + if (char == '"' || char == '\'') && !inComment { + if !inString { + inString = true + stringChar = char + } else if char == stringChar { + inString = false + } + continue + } + + // Skip everything inside strings + if inString { + continue + } + + // Handle line comments + if i > 0 && code[i-1] == '-' && char == '-' && !inComment { + inComment = true + continue + } + + // End line comment at newline + if inComment && char == '\n' { + inComment = false + continue + } + + // Skip everything in comments + if inComment { + continue + } + + // Check delimiters + switch char { + case '(', '[', '{': + stack = append(stack, char) + case ')', ']', '}': + if len(stack) == 0 { + return fmt.Errorf("unmatched closing delimiter '%c'", char) + } + + expected := pairs[char] + if stack[len(stack)-1] != expected { + return fmt.Errorf("mismatched delimiter: expected '%c', got '%c'", expected, char) + } + + stack = stack[:len(stack)-1] + } + } + + if len(stack) > 0 { + return fmt.Errorf("unclosed delimiter '%c'", stack[len(stack)-1]) + } + + return nil +} \ No newline at end of file diff --git a/internal/quests/constants.go b/internal/quests/constants.go new file mode 100644 index 0000000..25bdd9a --- /dev/null +++ b/internal/quests/constants.go @@ -0,0 +1,77 @@ +package quests + +// Quest step type constants +const ( + StepTypeKill int8 = 1 + StepTypeChat int8 = 2 + StepTypeObtainItem int8 = 3 + StepTypeLocation int8 = 4 + StepTypeSpell int8 = 5 + StepTypeNormal int8 = 6 + StepTypeCraft int8 = 7 + StepTypeHarvest int8 = 8 + StepTypeKillRaceReq int8 = 9 // kill using race type requirement instead of npc db id +) + +// Quest display status constants +const ( + DisplayStatusHidden int32 = 0 + DisplayStatusNoCheck int32 = 1 + DisplayStatusYellow int32 = 2 + DisplayStatusCompleted int32 = 4 + DisplayStatusRepeatable int32 = 8 + DisplayStatusCanShare int32 = 16 + DisplayStatusCompleteFlag int32 = 32 + DisplayStatusShow int32 = 64 + DisplayStatusCheck int32 = 128 +) + +// Quest shareable flags +const ( + ShareableNone int32 = 0 + ShareableActive int32 = 1 + ShareableDuring int32 = 2 + ShareableCompleted int32 = 4 +) + +// Default quest values +const ( + DefaultIcon int16 = 11 + DefaultPrereqLevel int8 = 1 + DefaultVisible int8 = 1 + DefaultTaskGroupNum int16 = 1 +) + +// Quest validation limits +const ( + MaxQuestNameLength int = 255 + MaxQuestDescriptionLength int = 2048 + MaxStepDescriptionLength int = 1024 + MaxTaskGroupNameLength int = 255 + MaxCompleteActionLength int = 512 +) + +// Location-based constants +const ( + MinLocationVariation float32 = 0.1 + MaxLocationVariation float32 = 1000.0 +) + +// Percentage constants +const ( + MinPercentage float32 = 0.0 + MaxPercentage float32 = 100.0 +) + +// Status constants +const ( + StatusInactive int32 = 0 + StatusActive int32 = 1 + StatusComplete int32 = 2 + StatusFailed int32 = 3 +) + +// Random constants +const ( + DefaultRandomRange float32 = 100.0 +) \ No newline at end of file diff --git a/internal/quests/interfaces.go b/internal/quests/interfaces.go new file mode 100644 index 0000000..0c863ce --- /dev/null +++ b/internal/quests/interfaces.go @@ -0,0 +1,476 @@ +package quests + +// Player interface defines the required player functionality for quest system +type Player interface { + // Basic player information + GetID() int32 + GetName() string + GetLevel() int8 + GetTSLevel() int8 + GetClass() int8 + GetTSClass() int8 + GetRace() int8 + + // Client interface for packet sending + GetClient() Client + + // Quest-related player methods + GetQuest(questID int32) *Quest + HasQuest(questID int32) bool + HasQuestBeenCompleted(questID int32) bool + AddQuest(quest *Quest) error + RemoveQuest(questID int32) bool + + // Position and zone information + GetX() float32 + GetY() float32 + GetZ() float32 + GetZoneID() int32 + + // Faction information + GetFactionValue(factionID int32) int32 + + // Experience and rewards + AddCoins(amount int64) + AddExp(amount int32) + AddTSExp(amount int32) + AddStatusPoints(amount int32) + + // Inventory management + GetPlayerItemList() ItemList + + // Arrow color calculation (difficulty indication) + GetArrowColor(level int8) int8 + GetTSArrowColor(level int8) int8 +} + +// Client interface defines client functionality needed for quest system +type Client interface { + // Basic client information + GetVersion() int16 + GetNameCRC() int32 + GetPlayer() Player + + // Packet sending + QueuePacket(packet Packet) + SimpleMessage(color int32, message string) + + // Quest-specific client methods + SendQuestJournalUpdate(quest *Quest, forceUpdate bool) + PopulateQuestRewardItems(items *[]Item, packet PacketStruct, arrayName ...string) +} + +// Spawn interface defines spawn functionality needed for quest system +type Spawn interface { + // Basic spawn information + GetID() int32 + GetDatabaseID() int32 + GetName() string + GetRace() int8 + GetModelType() int16 + + // Position information + GetX() float32 + GetY() float32 + GetZ() float32 + GetZoneID() int32 +} + +// Spell interface defines spell functionality needed for quest system +type Spell interface { + GetSpellID() int32 + GetName() string +} + +// Item interface defines item functionality needed for quest system +type Item interface { + GetItemID() int32 + GetName() string + GetIcon(version int16) int32 + GetDetails() ItemDetails + GetStackCount() int32 +} + +// ItemDetails interface defines item detail functionality +type ItemDetails interface { + GetItemID() int32 + GetUniqueID() int32 + GetCount() int32 +} + +// ItemList interface defines item list functionality +type ItemList interface { + GetItemFromID(itemID int32, count int32) Item +} + +// Packet interface defines packet functionality +type Packet interface { + // Packet data methods (placeholder) + Serialize() []byte +} + +// PacketStruct interface defines packet structure functionality +type PacketStruct interface { + // Packet building methods + SetDataByName(name string, value interface{}, index ...int) + SetArrayLengthByName(name string, length int) + SetArrayDataByName(name string, value interface{}, index int) + SetSubArrayLengthByName(name string, length int, index int) + SetSubArrayDataByName(name string, value interface{}, index1, index2 int) + SetSubstructArrayDataByName(substruct, name string, value interface{}, index int) + SetItemArrayDataByName(name string, item Item, player Player, index int, flag ...int) + Serialize() Packet + GetVersion() int16 +} + +// QuestOfferer interface defines NPCs that can offer quests +type QuestOfferer interface { + Spawn + GetOfferedQuests() []int32 + CanOfferQuest(questID int32, player Player) bool + OfferQuest(questID int32, player Player) error +} + +// QuestReceiver interface defines NPCs that can receive completed quests +type QuestReceiver interface { + Spawn + GetAcceptedQuests() []int32 + CanAcceptQuest(questID int32, player Player) bool + AcceptCompletedQuest(questID int32, player Player) error +} + +// QuestValidator interface defines quest validation functionality +type QuestValidator interface { + ValidatePrerequisites(quest *Quest, player Player) error + ValidateQuestCompletion(quest *Quest, player Player) error + ValidateQuestSharing(quest *Quest, sharer, receiver Player) error +} + +// QuestRewardProcessor interface defines quest reward processing +type QuestRewardProcessor interface { + ProcessQuestRewards(quest *Quest, player Player) error + CalculateRewards(quest *Quest, player Player) *QuestRewards + GiveRewards(rewards *QuestRewards, player Player) error +} + +// QuestRewards contains calculated quest rewards +type QuestRewards struct { + Coins int64 `json:"coins"` + Experience int32 `json:"experience"` + TSExperience int32 `json:"ts_experience"` + StatusPoints int32 `json:"status_points"` + Items []Item `json:"items"` + FactionRewards map[int32]int32 `json:"faction_rewards"` +} + +// QuestEventHandler interface defines quest event handling +type QuestEventHandler interface { + OnQuestStarted(quest *Quest, player Player) + OnQuestStepCompleted(quest *Quest, step *QuestStep, player Player) + OnQuestCompleted(quest *Quest, player Player) + OnQuestFailed(quest *Quest, step *QuestStep, player Player) + OnQuestAbandoned(quest *Quest, player Player) + OnQuestShared(quest *Quest, sharer, receiver Player) +} + +// Database interface defines database operations for quest system +type Database interface { + // Quest CRUD operations + LoadQuest(questID int32) (*Quest, error) + SaveQuest(quest *Quest) error + DeleteQuest(questID int32) error + LoadAllQuests() ([]*Quest, error) + + // Player quest operations + LoadPlayerQuests(playerID int32) ([]*Quest, error) + SavePlayerQuest(playerID int32, quest *Quest) error + DeletePlayerQuest(playerID, questID int32) error + MarkQuestCompleted(playerID, questID int32) error + IsQuestCompleted(playerID, questID int32) bool + + // Quest step operations + SaveQuestStepProgress(playerID, questID, stepID int32, progress int32) error + LoadQuestStepProgress(playerID, questID, stepID int32) (int32, error) +} + +// Logger interface defines logging functionality for quest system +type Logger interface { + LogInfo(message string, args ...interface{}) + LogError(message string, args ...interface{}) + LogDebug(message string, args ...interface{}) + LogWarning(message string, args ...interface{}) +} + +// Configuration interface defines quest system configuration +type Configuration interface { + GetMaxPlayerQuests() int + GetQuestSharingEnabled() bool + GetQuestFailureEnabled() bool + GetQuestTimersEnabled() bool + GetXPBonusMultiplier() float64 + GetCoinBonusMultiplier() float64 + GetStatusBonusMultiplier() float64 +} + +// QuestSystemAdapter provides integration with other game systems +type QuestSystemAdapter struct { + questManager *QuestManager + validator QuestValidator + rewardProcessor QuestRewardProcessor + eventHandler QuestEventHandler + database Database + logger Logger + config Configuration +} + +// NewQuestSystemAdapter creates a new quest system adapter +func NewQuestSystemAdapter(questManager *QuestManager, validator QuestValidator, rewardProcessor QuestRewardProcessor, eventHandler QuestEventHandler, database Database, logger Logger, config Configuration) *QuestSystemAdapter { + return &QuestSystemAdapter{ + questManager: questManager, + validator: validator, + rewardProcessor: rewardProcessor, + eventHandler: eventHandler, + database: database, + logger: logger, + config: config, + } +} + +// StartQuest handles starting a new quest for a player +func (qsa *QuestSystemAdapter) StartQuest(questID int32, player Player) error { + // Get quest from master list + quest := qsa.questManager.GetMasterList().GetQuest(questID, true) + if quest == nil { + return fmt.Errorf("quest %d not found", questID) + } + + // Validate prerequisites + if qsa.validator != nil { + if err := qsa.validator.ValidatePrerequisites(quest, player); err != nil { + return fmt.Errorf("quest prerequisites not met: %w", err) + } + } + + // Check if player can accept more quests + if qsa.config != nil { + maxQuests := qsa.config.GetMaxPlayerQuests() + if maxQuests > 0 && qsa.questManager.GetPlayerQuestCount(player.GetID()) >= maxQuests { + return fmt.Errorf("player has too many active quests") + } + } + + // Give quest to player + _, err := qsa.questManager.GiveQuestToPlayer(player.GetID(), questID) + if err != nil { + return err + } + + // Save to database + if qsa.database != nil { + if err := qsa.database.SavePlayerQuest(player.GetID(), quest); err != nil { + qsa.logger.LogError("Failed to save player quest to database: %v", err) + } + } + + // Trigger event + if qsa.eventHandler != nil { + qsa.eventHandler.OnQuestStarted(quest, player) + } + + // Log + if qsa.logger != nil { + qsa.logger.LogInfo("Player %s (%d) started quest %s (%d)", player.GetName(), player.GetID(), quest.Name, questID) + } + + return nil +} + +// CompleteQuest handles completing a quest for a player +func (qsa *QuestSystemAdapter) CompleteQuest(questID int32, player Player) error { + // Get player's quest + quest := qsa.questManager.GetPlayerQuest(player.GetID(), questID) + if quest == nil { + return fmt.Errorf("player does not have quest %d", questID) + } + + // Validate completion + if qsa.validator != nil { + if err := qsa.validator.ValidateQuestCompletion(quest, player); err != nil { + return fmt.Errorf("quest completion validation failed: %w", err) + } + } + + // Check if quest is actually complete + if !quest.GetCompleted() { + return fmt.Errorf("quest is not complete") + } + + // Process rewards + if qsa.rewardProcessor != nil { + if err := qsa.rewardProcessor.ProcessQuestRewards(quest, player); err != nil { + qsa.logger.LogError("Failed to process quest rewards: %v", err) + return fmt.Errorf("failed to process quest rewards: %w", err) + } + } + + // Mark as completed in database + if qsa.database != nil { + if err := qsa.database.MarkQuestCompleted(player.GetID(), questID); err != nil { + qsa.logger.LogError("Failed to mark quest as completed in database: %v", err) + } + } + + // Remove from active quests if not repeatable + if !quest.IsRepeatable() { + qsa.questManager.RemoveQuestFromPlayer(player.GetID(), questID) + } + + // Trigger event + if qsa.eventHandler != nil { + qsa.eventHandler.OnQuestCompleted(quest, player) + } + + // Log + if qsa.logger != nil { + qsa.logger.LogInfo("Player %s (%d) completed quest %s (%d)", player.GetName(), player.GetID(), quest.Name, questID) + } + + return nil +} + +// UpdateQuestProgress handles updating quest step progress +func (qsa *QuestSystemAdapter) UpdateQuestProgress(questID, stepID, progress int32, player Player) error { + // Get player's quest + quest := qsa.questManager.GetPlayerQuest(player.GetID(), questID) + if quest == nil { + return fmt.Errorf("player does not have quest %d", questID) + } + + // Update progress + if !quest.AddStepProgress(stepID, progress) { + return fmt.Errorf("failed to update quest step progress") + } + + // Check if step is now complete + step := quest.GetQuestStep(stepID) + if step != nil && step.Complete() { + // Execute complete action if exists + if quest.HasCompleteAction(stepID) { + if err := quest.ExecuteCompleteAction(stepID); err != nil { + qsa.logger.LogError("Failed to execute quest step complete action: %v", err) + } + } + + // Trigger event + if qsa.eventHandler != nil { + qsa.eventHandler.OnQuestStepCompleted(quest, step, player) + } + + // Log + if qsa.logger != nil { + qsa.logger.LogInfo("Player %s (%d) completed step %d of quest %s (%d)", player.GetName(), player.GetID(), stepID, quest.Name, questID) + } + } + + // Save progress to database + if qsa.database != nil { + if err := qsa.database.SaveQuestStepProgress(player.GetID(), questID, stepID, step.GetStepProgress()); err != nil { + qsa.logger.LogError("Failed to save quest step progress to database: %v", err) + } + } + + return nil +} + +// AbandonQuest handles abandoning a quest +func (qsa *QuestSystemAdapter) AbandonQuest(questID int32, player Player) error { + // Get player's quest + quest := qsa.questManager.GetPlayerQuest(player.GetID(), questID) + if quest == nil { + return fmt.Errorf("player does not have quest %d", questID) + } + + // Check if quest can be abandoned + if !quest.CanDeleteQuest { + return fmt.Errorf("quest cannot be abandoned") + } + + // Remove from player + if !qsa.questManager.RemoveQuestFromPlayer(player.GetID(), questID) { + return fmt.Errorf("failed to remove quest from player") + } + + // Remove from database + if qsa.database != nil { + if err := qsa.database.DeletePlayerQuest(player.GetID(), questID); err != nil { + qsa.logger.LogError("Failed to delete player quest from database: %v", err) + } + } + + // Trigger event + if qsa.eventHandler != nil { + qsa.eventHandler.OnQuestAbandoned(quest, player) + } + + // Log + if qsa.logger != nil { + qsa.logger.LogInfo("Player %s (%d) abandoned quest %s (%d)", player.GetName(), player.GetID(), quest.Name, questID) + } + + return nil +} + +// ShareQuest handles sharing a quest between players +func (qsa *QuestSystemAdapter) ShareQuest(questID int32, sharer, receiver Player) error { + if !qsa.config.GetQuestSharingEnabled() { + return fmt.Errorf("quest sharing is disabled") + } + + // Get sharer's quest + quest := qsa.questManager.GetPlayerQuest(sharer.GetID(), questID) + if quest == nil { + return fmt.Errorf("sharer does not have quest %d", questID) + } + + // Validate sharing + if qsa.validator != nil { + if err := qsa.validator.ValidateQuestSharing(quest, sharer, receiver); err != nil { + return fmt.Errorf("quest sharing validation failed: %w", err) + } + } + + // Check if receiver can accept more quests + if qsa.config != nil { + maxQuests := qsa.config.GetMaxPlayerQuests() + if maxQuests > 0 && qsa.questManager.GetPlayerQuestCount(receiver.GetID()) >= maxQuests { + return fmt.Errorf("receiver has too many active quests") + } + } + + // Give quest to receiver + _, err := qsa.questManager.GiveQuestToPlayer(receiver.GetID(), questID) + if err != nil { + return fmt.Errorf("failed to give quest to receiver: %w", err) + } + + // Save to database + if qsa.database != nil { + receiverQuest := qsa.questManager.GetPlayerQuest(receiver.GetID(), questID) + if err := qsa.database.SavePlayerQuest(receiver.GetID(), receiverQuest); err != nil { + qsa.logger.LogError("Failed to save shared quest to database: %v", err) + } + } + + // Trigger event + if qsa.eventHandler != nil { + qsa.eventHandler.OnQuestShared(quest, sharer, receiver) + } + + // Log + if qsa.logger != nil { + qsa.logger.LogInfo("Player %s (%d) shared quest %s (%d) with %s (%d)", + sharer.GetName(), sharer.GetID(), quest.Name, questID, receiver.GetName(), receiver.GetID()) + } + + return nil +} \ No newline at end of file diff --git a/internal/quests/manager.go b/internal/quests/manager.go new file mode 100644 index 0000000..6001886 --- /dev/null +++ b/internal/quests/manager.go @@ -0,0 +1,450 @@ +package quests + +import ( + "fmt" + "sync" +) + +// MasterQuestList manages all quests in the system +type MasterQuestList struct { + quests map[int32]*Quest + mutex sync.RWMutex +} + +// NewMasterQuestList creates a new master quest list +func NewMasterQuestList() *MasterQuestList { + return &MasterQuestList{ + quests: make(map[int32]*Quest), + } +} + +// AddQuest adds a quest to the master list +func (mql *MasterQuestList) AddQuest(questID int32, quest *Quest) error { + if quest == nil { + return fmt.Errorf("quest cannot be nil") + } + + if questID != quest.ID { + return fmt.Errorf("quest ID mismatch: provided %d, quest has %d", questID, quest.ID) + } + + mql.mutex.Lock() + defer mql.mutex.Unlock() + + if _, exists := mql.quests[questID]; exists { + return fmt.Errorf("quest %d already exists", questID) + } + + mql.quests[questID] = quest + return nil +} + +// GetQuest returns a quest by ID, optionally creating a copy +func (mql *MasterQuestList) GetQuest(questID int32, copyQuest bool) *Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + quest, exists := mql.quests[questID] + if !exists { + return nil + } + + if copyQuest { + return quest.Copy() + } + + return quest +} + +// RemoveQuest removes a quest from the master list +func (mql *MasterQuestList) RemoveQuest(questID int32) bool { + mql.mutex.Lock() + defer mql.mutex.Unlock() + + if _, exists := mql.quests[questID]; exists { + delete(mql.quests, questID) + return true + } + + return false +} + +// HasQuest checks if a quest exists +func (mql *MasterQuestList) HasQuest(questID int32) bool { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + _, exists := mql.quests[questID] + return exists +} + +// GetAllQuests returns a copy of all quests map +func (mql *MasterQuestList) GetAllQuests() map[int32]*Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + quests := make(map[int32]*Quest) + for id, quest := range mql.quests { + quests[id] = quest + } + + return quests +} + +// GetQuestsByLevel returns quests within a level range +func (mql *MasterQuestList) GetQuestsByLevel(minLevel, maxLevel int8) []*Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + var quests []*Quest + for _, quest := range mql.quests { + if quest.Level >= minLevel && quest.Level <= maxLevel { + quests = append(quests, quest) + } + } + + return quests +} + +// GetQuestsByType returns quests of a specific type +func (mql *MasterQuestList) GetQuestsByType(questType string) []*Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + var quests []*Quest + for _, quest := range mql.quests { + if quest.Type == questType { + quests = append(quests, quest) + } + } + + return quests +} + +// GetQuestsByZone returns quests in a specific zone +func (mql *MasterQuestList) GetQuestsByZone(zone string) []*Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + var quests []*Quest + for _, quest := range mql.quests { + if quest.Zone == zone { + quests = append(quests, quest) + } + } + + return quests +} + +// GetQuestsByGiver returns quests given by a specific NPC +func (mql *MasterQuestList) GetQuestsByGiver(giverID int32) []*Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + var quests []*Quest + for _, quest := range mql.quests { + if quest.QuestGiver == giverID { + quests = append(quests, quest) + } + } + + return quests +} + +// GetRepeatableQuests returns all repeatable quests +func (mql *MasterQuestList) GetRepeatableQuests() []*Quest { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + var quests []*Quest + for _, quest := range mql.quests { + if quest.Repeatable { + quests = append(quests, quest) + } + } + + return quests +} + +// GetQuestCount returns the total number of quests +func (mql *MasterQuestList) GetQuestCount() int { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + return len(mql.quests) +} + +// Clear removes all quests +func (mql *MasterQuestList) Clear() { + mql.mutex.Lock() + defer mql.mutex.Unlock() + + mql.quests = make(map[int32]*Quest) +} + +// Reload clears and reloads all quests +func (mql *MasterQuestList) Reload() { + mql.Clear() + + // TODO: Implement quest reloading from database or files + // This would typically involve: + // 1. Loading quest data from database + // 2. Creating Quest objects + // 3. Adding them to the master list + + // For now, this is a placeholder + fmt.Println("Quest reload requested - implementation pending") +} + +// ValidateAllQuests validates all quests in the master list +func (mql *MasterQuestList) ValidateAllQuests() []error { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + var errors []error + + for questID, quest := range mql.quests { + if err := quest.ValidateQuest(); err != nil { + errors = append(errors, fmt.Errorf("quest %d validation failed: %w", questID, err)) + } + } + + return errors +} + +// GetQuestStatistics returns basic statistics about the quest system +func (mql *MasterQuestList) GetQuestStatistics() *QuestStatistics { + mql.mutex.RLock() + defer mql.mutex.RUnlock() + + stats := &QuestStatistics{ + TotalQuests: len(mql.quests), + QuestsByType: make(map[string]int), + QuestsByLevel: make(map[int8]int), + RepeatableCount: 0, + HiddenCount: 0, + } + + for _, quest := range mql.quests { + // Count by type + stats.QuestsByType[quest.Type]++ + + // Count by level + stats.QuestsByLevel[quest.Level]++ + + // Count special flags + if quest.Repeatable { + stats.RepeatableCount++ + } + if quest.Hidden { + stats.HiddenCount++ + } + } + + return stats +} + +// QuestStatistics contains statistical information about quests +type QuestStatistics struct { + TotalQuests int `json:"total_quests"` + QuestsByType map[string]int `json:"quests_by_type"` + QuestsByLevel map[int8]int `json:"quests_by_level"` + RepeatableCount int `json:"repeatable_count"` + HiddenCount int `json:"hidden_count"` +} + +// QuestManager provides high-level quest management functionality +type QuestManager struct { + masterList *MasterQuestList + playerQuests map[int32]map[int32]*Quest // playerID -> questID -> quest + mutex sync.RWMutex +} + +// NewQuestManager creates a new quest manager +func NewQuestManager() *QuestManager { + return &QuestManager{ + masterList: NewMasterQuestList(), + playerQuests: make(map[int32]map[int32]*Quest), + } +} + +// GetMasterList returns the master quest list +func (qm *QuestManager) GetMasterList() *MasterQuestList { + return qm.masterList +} + +// GiveQuestToPlayer assigns a quest to a player +func (qm *QuestManager) GiveQuestToPlayer(playerID, questID int32) (*Quest, error) { + // Get quest from master list (copy it) + quest := qm.masterList.GetQuest(questID, true) + if quest == nil { + return nil, fmt.Errorf("quest %d not found", questID) + } + + qm.mutex.Lock() + defer qm.mutex.Unlock() + + // Initialize player quest map if needed + if qm.playerQuests[playerID] == nil { + qm.playerQuests[playerID] = make(map[int32]*Quest) + } + + // Check if player already has this quest + if _, exists := qm.playerQuests[playerID][questID]; exists { + return nil, fmt.Errorf("player %d already has quest %d", playerID, questID) + } + + // Assign quest to player + qm.playerQuests[playerID][questID] = quest + + return quest, nil +} + +// RemoveQuestFromPlayer removes a quest from a player +func (qm *QuestManager) RemoveQuestFromPlayer(playerID, questID int32) bool { + qm.mutex.Lock() + defer qm.mutex.Unlock() + + if playerQuests, exists := qm.playerQuests[playerID]; exists { + if _, questExists := playerQuests[questID]; questExists { + delete(playerQuests, questID) + + // Clean up empty player quest map + if len(playerQuests) == 0 { + delete(qm.playerQuests, playerID) + } + + return true + } + } + + return false +} + +// GetPlayerQuest returns a specific quest for a player +func (qm *QuestManager) GetPlayerQuest(playerID, questID int32) *Quest { + qm.mutex.RLock() + defer qm.mutex.RUnlock() + + if playerQuests, exists := qm.playerQuests[playerID]; exists { + return playerQuests[questID] + } + + return nil +} + +// GetPlayerQuests returns all quests for a player +func (qm *QuestManager) GetPlayerQuests(playerID int32) map[int32]*Quest { + qm.mutex.RLock() + defer qm.mutex.RUnlock() + + if playerQuests, exists := qm.playerQuests[playerID]; exists { + // Return a copy + quests := make(map[int32]*Quest) + for questID, quest := range playerQuests { + quests[questID] = quest + } + return quests + } + + return make(map[int32]*Quest) +} + +// PlayerHasQuest checks if a player has a specific quest +func (qm *QuestManager) PlayerHasQuest(playerID, questID int32) bool { + return qm.GetPlayerQuest(playerID, questID) != nil +} + +// GetPlayerQuestCount returns the number of quests a player has +func (qm *QuestManager) GetPlayerQuestCount(playerID int32) int { + qm.mutex.RLock() + defer qm.mutex.RUnlock() + + if playerQuests, exists := qm.playerQuests[playerID]; exists { + return len(playerQuests) + } + + return 0 +} + +// ClearPlayerQuests removes all quests from a player +func (qm *QuestManager) ClearPlayerQuests(playerID int32) { + qm.mutex.Lock() + defer qm.mutex.Unlock() + + delete(qm.playerQuests, playerID) +} + +// GetAllPlayerIDs returns all player IDs that have quests +func (qm *QuestManager) GetAllPlayerIDs() []int32 { + qm.mutex.RLock() + defer qm.mutex.RUnlock() + + var playerIDs []int32 + for playerID := range qm.playerQuests { + playerIDs = append(playerIDs, playerID) + } + + return playerIDs +} + +// UpdatePlayerQuestProgress updates progress for a player's quest step +func (qm *QuestManager) UpdatePlayerQuestProgress(playerID, questID, stepID, progress int32) bool { + quest := qm.GetPlayerQuest(playerID, questID) + if quest == nil { + return false + } + + return quest.AddStepProgress(stepID, progress) +} + +// CompletePlayerQuestStep marks a quest step as complete for a player +func (qm *QuestManager) CompletePlayerQuestStep(playerID, questID, stepID int32) bool { + quest := qm.GetPlayerQuest(playerID, questID) + if quest == nil { + return false + } + + return quest.SetStepComplete(stepID) +} + +// IsPlayerQuestComplete checks if a player's quest is complete +func (qm *QuestManager) IsPlayerQuestComplete(playerID, questID int32) bool { + quest := qm.GetPlayerQuest(playerID, questID) + if quest == nil { + return false + } + + return quest.GetCompleted() +} + +// GetPlayerQuestStatistics returns statistics for a player's quests +func (qm *QuestManager) GetPlayerQuestStatistics(playerID int32) *PlayerQuestStatistics { + quests := qm.GetPlayerQuests(playerID) + + stats := &PlayerQuestStatistics{ + TotalQuests: len(quests), + CompletedQuests: 0, + QuestsByType: make(map[string]int), + QuestsByLevel: make(map[int8]int), + } + + for _, quest := range quests { + if quest.GetCompleted() { + stats.CompletedQuests++ + } + + stats.QuestsByType[quest.Type]++ + stats.QuestsByLevel[quest.Level]++ + } + + return stats +} + +// PlayerQuestStatistics contains statistical information about a player's quests +type PlayerQuestStatistics struct { + TotalQuests int `json:"total_quests"` + CompletedQuests int `json:"completed_quests"` + QuestsByType map[string]int `json:"quests_by_type"` + QuestsByLevel map[int8]int `json:"quests_by_level"` +} \ No newline at end of file diff --git a/internal/quests/prerequisites.go b/internal/quests/prerequisites.go new file mode 100644 index 0000000..59a7e52 --- /dev/null +++ b/internal/quests/prerequisites.go @@ -0,0 +1,378 @@ +package quests + +// Prerequisite management methods for Quest + +// SetPrereqLevel sets the minimum level requirement +func (q *Quest) SetPrereqLevel(level int8) { + q.PrereqLevel = level + q.SetSaveNeeded(true) +} + +// SetPrereqTSLevel sets the minimum tradeskill level requirement +func (q *Quest) SetPrereqTSLevel(level int8) { + q.PrereqTSLevel = level + q.SetSaveNeeded(true) +} + +// SetPrereqMaxLevel sets the maximum level requirement +func (q *Quest) SetPrereqMaxLevel(level int8) { + q.PrereqMaxLevel = level + q.SetSaveNeeded(true) +} + +// SetPrereqMaxTSLevel sets the maximum tradeskill level requirement +func (q *Quest) SetPrereqMaxTSLevel(level int8) { + q.PrereqMaxTSLevel = level + q.SetSaveNeeded(true) +} + +// AddPrereqClass adds a class prerequisite +func (q *Quest) AddPrereqClass(classID int8) { + q.PrereqClasses = append(q.PrereqClasses, classID) + q.SetSaveNeeded(true) +} + +// AddPrereqTradeskillClass adds a tradeskill class prerequisite +func (q *Quest) AddPrereqTradeskillClass(classID int8) { + q.PrereqTSClasses = append(q.PrereqTSClasses, classID) + q.SetSaveNeeded(true) +} + +// AddPrereqModelType adds a model type prerequisite +func (q *Quest) AddPrereqModelType(modelType int16) { + q.PrereqModelTypes = append(q.PrereqModelTypes, modelType) + q.SetSaveNeeded(true) +} + +// AddPrereqRace adds a race prerequisite +func (q *Quest) AddPrereqRace(race int8) { + q.PrereqRaces = append(q.PrereqRaces, race) + q.SetSaveNeeded(true) +} + +// AddPrereqQuest adds a quest prerequisite +func (q *Quest) AddPrereqQuest(questID int32) { + q.PrereqQuests = append(q.PrereqQuests, questID) + q.SetSaveNeeded(true) +} + +// AddPrereqFaction adds a faction prerequisite +func (q *Quest) AddPrereqFaction(factionID int32, min, max int32) { + faction := NewQuestFactionPrereq(factionID, min, max) + q.PrereqFactions = append(q.PrereqFactions, faction) + q.SetSaveNeeded(true) +} + +// RemovePrereqClass removes a class prerequisite +func (q *Quest) RemovePrereqClass(classID int8) bool { + for i, class := range q.PrereqClasses { + if class == classID { + q.PrereqClasses = append(q.PrereqClasses[:i], q.PrereqClasses[i+1:]...) + q.SetSaveNeeded(true) + return true + } + } + return false +} + +// RemovePrereqTradeskillClass removes a tradeskill class prerequisite +func (q *Quest) RemovePrereqTradeskillClass(classID int8) bool { + for i, class := range q.PrereqTSClasses { + if class == classID { + q.PrereqTSClasses = append(q.PrereqTSClasses[:i], q.PrereqTSClasses[i+1:]...) + q.SetSaveNeeded(true) + return true + } + } + return false +} + +// RemovePrereqModelType removes a model type prerequisite +func (q *Quest) RemovePrereqModelType(modelType int16) bool { + for i, model := range q.PrereqModelTypes { + if model == modelType { + q.PrereqModelTypes = append(q.PrereqModelTypes[:i], q.PrereqModelTypes[i+1:]...) + q.SetSaveNeeded(true) + return true + } + } + return false +} + +// RemovePrereqRace removes a race prerequisite +func (q *Quest) RemovePrereqRace(race int8) bool { + for i, r := range q.PrereqRaces { + if r == race { + q.PrereqRaces = append(q.PrereqRaces[:i], q.PrereqRaces[i+1:]...) + q.SetSaveNeeded(true) + return true + } + } + return false +} + +// RemovePrereqQuest removes a quest prerequisite +func (q *Quest) RemovePrereqQuest(questID int32) bool { + for i, quest := range q.PrereqQuests { + if quest == questID { + q.PrereqQuests = append(q.PrereqQuests[:i], q.PrereqQuests[i+1:]...) + q.SetSaveNeeded(true) + return true + } + } + return false +} + +// RemovePrereqFaction removes a faction prerequisite +func (q *Quest) RemovePrereqFaction(factionID int32) bool { + for i, faction := range q.PrereqFactions { + if faction.FactionID == factionID { + q.PrereqFactions = append(q.PrereqFactions[:i], q.PrereqFactions[i+1:]...) + q.SetSaveNeeded(true) + return true + } + } + return false +} + +// HasPrereqClass checks if a class is a prerequisite +func (q *Quest) HasPrereqClass(classID int8) bool { + for _, class := range q.PrereqClasses { + if class == classID { + return true + } + } + return false +} + +// HasPrereqTradeskillClass checks if a tradeskill class is a prerequisite +func (q *Quest) HasPrereqTradeskillClass(classID int8) bool { + for _, class := range q.PrereqTSClasses { + if class == classID { + return true + } + } + return false +} + +// HasPrereqModelType checks if a model type is a prerequisite +func (q *Quest) HasPrereqModelType(modelType int16) bool { + for _, model := range q.PrereqModelTypes { + if model == modelType { + return true + } + } + return false +} + +// HasPrereqRace checks if a race is a prerequisite +func (q *Quest) HasPrereqRace(race int8) bool { + for _, r := range q.PrereqRaces { + if r == race { + return true + } + } + return false +} + +// HasPrereqQuest checks if a quest is a prerequisite +func (q *Quest) HasPrereqQuest(questID int32) bool { + for _, quest := range q.PrereqQuests { + if quest == questID { + return true + } + } + return false +} + +// HasPrereqFaction checks if a faction is a prerequisite +func (q *Quest) HasPrereqFaction(factionID int32) bool { + for _, faction := range q.PrereqFactions { + if faction.FactionID == factionID { + return true + } + } + return false +} + +// GetPrereqFaction returns the faction prerequisite for a given faction ID +func (q *Quest) GetPrereqFaction(factionID int32) *QuestFactionPrereq { + for _, faction := range q.PrereqFactions { + if faction.FactionID == factionID { + return faction + } + } + return nil +} + +// ClearAllPrerequisites removes all prerequisites +func (q *Quest) ClearAllPrerequisites() { + q.PrereqLevel = DefaultPrereqLevel + q.PrereqTSLevel = 0 + q.PrereqMaxLevel = 0 + q.PrereqMaxTSLevel = 0 + q.PrereqFactions = q.PrereqFactions[:0] + q.PrereqRaces = q.PrereqRaces[:0] + q.PrereqModelTypes = q.PrereqModelTypes[:0] + q.PrereqClasses = q.PrereqClasses[:0] + q.PrereqTSClasses = q.PrereqTSClasses[:0] + q.PrereqQuests = q.PrereqQuests[:0] + q.SetSaveNeeded(true) +} + +// Prerequisite validation methods + +// ValidatePrerequisites validates all quest prerequisites +func (q *Quest) ValidatePrerequisites() error { + // Validate level requirements + if q.PrereqLevel < 1 || q.PrereqLevel > 100 { + return fmt.Errorf("prerequisite level must be between 1 and 100") + } + + if q.PrereqTSLevel < 0 || q.PrereqTSLevel > 100 { + return fmt.Errorf("prerequisite tradeskill level must be between 0 and 100") + } + + if q.PrereqMaxLevel > 0 && q.PrereqMaxLevel < q.PrereqLevel { + return fmt.Errorf("prerequisite max level cannot be less than min level") + } + + if q.PrereqMaxTSLevel > 0 && q.PrereqMaxTSLevel < q.PrereqTSLevel { + return fmt.Errorf("prerequisite max tradeskill level cannot be less than min tradeskill level") + } + + // Validate class prerequisites + if err := q.validateClassPrerequisites(); err != nil { + return err + } + + // Validate race prerequisites + if err := q.validateRacePrerequisites(); err != nil { + return err + } + + // Validate faction prerequisites + if err := q.validateFactionPrerequisites(); err != nil { + return err + } + + return nil +} + +// validateClassPrerequisites validates class prerequisites +func (q *Quest) validateClassPrerequisites() error { + // Check for duplicate classes + classMap := make(map[int8]bool) + for _, classID := range q.PrereqClasses { + if classMap[classID] { + return fmt.Errorf("duplicate class prerequisite: %d", classID) + } + classMap[classID] = true + + // Validate class ID range (basic validation) + if classID < 1 || classID > 100 { + return fmt.Errorf("invalid class ID in prerequisites: %d", classID) + } + } + + // Check for duplicate tradeskill classes + tsClassMap := make(map[int8]bool) + for _, classID := range q.PrereqTSClasses { + if tsClassMap[classID] { + return fmt.Errorf("duplicate tradeskill class prerequisite: %d", classID) + } + tsClassMap[classID] = true + + // Validate tradeskill class ID range + if classID < 1 || classID > 100 { + return fmt.Errorf("invalid tradeskill class ID in prerequisites: %d", classID) + } + } + + return nil +} + +// validateRacePrerequisites validates race prerequisites +func (q *Quest) validateRacePrerequisites() error { + // Check for duplicate races + raceMap := make(map[int8]bool) + for _, race := range q.PrereqRaces { + if raceMap[race] { + return fmt.Errorf("duplicate race prerequisite: %d", race) + } + raceMap[race] = true + + // Validate race ID range + if race < 1 || race > 50 { + return fmt.Errorf("invalid race ID in prerequisites: %d", race) + } + } + + // Check for duplicate model types + modelMap := make(map[int16]bool) + for _, modelType := range q.PrereqModelTypes { + if modelMap[modelType] { + return fmt.Errorf("duplicate model type prerequisite: %d", modelType) + } + modelMap[modelType] = true + + // Validate model type range + if modelType < 1 { + return fmt.Errorf("invalid model type in prerequisites: %d", modelType) + } + } + + return nil +} + +// validateFactionPrerequisites validates faction prerequisites +func (q *Quest) validateFactionPrerequisites() error { + // Check for duplicate factions and validate ranges + factionMap := make(map[int32]bool) + for _, faction := range q.PrereqFactions { + if factionMap[faction.FactionID] { + return fmt.Errorf("duplicate faction prerequisite: %d", faction.FactionID) + } + factionMap[faction.FactionID] = true + + // Validate faction ID + if faction.FactionID <= 0 { + return fmt.Errorf("invalid faction ID in prerequisites: %d", faction.FactionID) + } + + // Validate faction value ranges + if faction.Min < -50000 || faction.Min > 50000 { + return fmt.Errorf("faction %d min value out of range: %d", faction.FactionID, faction.Min) + } + + if faction.Max < -50000 || faction.Max > 50000 { + return fmt.Errorf("faction %d max value out of range: %d", faction.FactionID, faction.Max) + } + + // Check min/max relationship + if faction.Max != 0 && faction.Max < faction.Min { + return fmt.Errorf("faction %d max value cannot be less than min value", faction.FactionID) + } + } + + // Check for duplicate quest prerequisites + questMap := make(map[int32]bool) + for _, questID := range q.PrereqQuests { + if questMap[questID] { + return fmt.Errorf("duplicate quest prerequisite: %d", questID) + } + questMap[questID] = true + + // Don't allow self-reference + if questID == q.ID { + return fmt.Errorf("quest cannot be a prerequisite of itself") + } + + // Validate quest ID + if questID <= 0 { + return fmt.Errorf("invalid quest ID in prerequisites: %d", questID) + } + } + + return nil +} \ No newline at end of file diff --git a/internal/quests/quest.go b/internal/quests/quest.go new file mode 100644 index 0000000..7b7d87b --- /dev/null +++ b/internal/quests/quest.go @@ -0,0 +1,737 @@ +package quests + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +// RegisterQuest sets the basic quest information +func (q *Quest) RegisterQuest(name, questType, zone string, level int8, description string) { + q.Name = name + q.Type = questType + q.Zone = zone + q.Level = level + q.Description = description + q.SetSaveNeeded(true) +} + +// AddQuestStep adds a step to the quest +func (q *Quest) AddQuestStep(step *QuestStep) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + // Check if step ID already exists + if _, exists := q.QuestStepMap[step.ID]; exists { + return false + } + + // Add to all tracking structures + q.QuestSteps = append(q.QuestSteps, step) + q.QuestStepMap[step.ID] = step + q.QuestStepReverseMap[step] = step.ID + + // Handle task groups + taskGroup := step.TaskGroup + if taskGroup == "" { + taskGroup = step.Description + } + + if taskGroup != "" { + // Add to task group order if new + if _, exists := q.getTaskGroupByName(taskGroup); !exists { + q.TaskGroupOrder[q.TaskGroupNum] = taskGroup + q.TaskGroupNum++ + } + + // Add step to task group + q.TaskGroup[taskGroup] = append(q.TaskGroup[taskGroup], step) + } + + q.SetSaveNeeded(true) + return true +} + +// CreateQuestStep creates and adds a new quest step +func (q *Quest) CreateQuestStep(id int32, stepType int8, description string, ids []int32, quantity int32, taskGroup string, locations []*Location, maxVariation, percentage float32, usableItemID int32) *QuestStep { + step := NewQuestStep(id, stepType, description, ids, quantity, taskGroup, locations, maxVariation, percentage, usableItemID) + if q.AddQuestStep(step) { + return step + } + return nil +} + +// RemoveQuestStep removes a step from the quest +func (q *Quest) RemoveQuestStep(stepID int32) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + step, exists := q.QuestStepMap[stepID] + if !exists { + return false + } + + // Remove from maps + delete(q.QuestStepMap, stepID) + delete(q.QuestStepReverseMap, step) + + // Remove from slice + for i, questStep := range q.QuestSteps { + if questStep == step { + q.QuestSteps = append(q.QuestSteps[:i], q.QuestSteps[i+1:]...) + break + } + } + + // Remove from task groups + taskGroup := step.TaskGroup + if taskGroup != "" { + if steps, exists := q.TaskGroup[taskGroup]; exists { + for i, taskStep := range steps { + if taskStep == step { + q.TaskGroup[taskGroup] = append(steps[:i], steps[i+1:]...) + break + } + } + + // If task group is now empty, remove it + if len(q.TaskGroup[taskGroup]) == 0 { + delete(q.TaskGroup, taskGroup) + // Find and remove from task group order + for orderNum, groupName := range q.TaskGroupOrder { + if groupName == taskGroup { + delete(q.TaskGroupOrder, orderNum) + q.TaskGroupNum-- + break + } + } + } + } + } + + // Remove from actions + q.completeActionsMutex.Lock() + delete(q.CompleteActions, stepID) + q.completeActionsMutex.Unlock() + + q.progressActionsMutex.Lock() + delete(q.ProgressActions, stepID) + q.progressActionsMutex.Unlock() + + q.failedActionsMutex.Lock() + delete(q.FailedActions, stepID) + q.failedActionsMutex.Unlock() + + q.SetSaveNeeded(true) + return true +} + +// GetQuestStep returns a quest step by ID +func (q *Quest) GetQuestStep(stepID int32) *QuestStep { + q.stepsMutex.RLock() + defer q.stepsMutex.RUnlock() + return q.QuestStepMap[stepID] +} + +// SetStepComplete marks a step as complete +func (q *Quest) SetStepComplete(stepID int32) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + step, exists := q.QuestStepMap[stepID] + if !exists || step.Complete() { + return false + } + + step.SetComplete() + q.StepUpdates = append(q.StepUpdates, step) + q.SetSaveNeeded(true) + return true +} + +// AddStepProgress adds progress to a step +func (q *Quest) AddStepProgress(stepID int32, progress int32) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + step, exists := q.QuestStepMap[stepID] + if !exists { + return false + } + + // Check percentage chance for success + if step.Percentage < MaxPercentage && step.Percentage > 0 { + if step.Percentage <= rand.Float32()*MaxPercentage { + q.StepFailures = append(q.StepFailures, step) + return false + } + } + + actualProgress := step.AddStepProgress(progress) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + q.SetSaveNeeded(true) + + // TODO: Call progress action if exists + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[stepID]; exists && action != "" { + // TODO: Execute Lua script with action + _ = action // Placeholder for Lua execution + } + q.progressActionsMutex.RUnlock() + + return true + } + + return false +} + +// GetStepProgress returns the current progress of a step +func (q *Quest) GetStepProgress(stepID int32) int32 { + step := q.GetQuestStep(stepID) + if step != nil { + return step.GetStepProgress() + } + return 0 +} + +// GetQuestStepCompleted checks if a step is completed +func (q *Quest) GetQuestStepCompleted(stepID int32) bool { + step := q.GetQuestStep(stepID) + return step != nil && step.Complete() +} + +// GetQuestStep returns the first incomplete step ID +func (q *Quest) GetCurrentQuestStep() int16 { + q.stepsMutex.RLock() + defer q.stepsMutex.RUnlock() + + for _, step := range q.QuestSteps { + if !step.Complete() { + return int16(step.ID) + } + } + return 0 +} + +// QuestStepIsActive checks if a step is active (not completed) +func (q *Quest) QuestStepIsActive(stepID int16) bool { + step := q.GetQuestStep(int32(stepID)) + return step != nil && !step.Complete() +} + +// GetTaskGroupStep returns the current task group step +func (q *Quest) GetTaskGroupStep() int16 { + q.stepsMutex.RLock() + defer q.stepsMutex.RUnlock() + + ret := int16(len(q.TaskGroupOrder)) + + for orderNum, taskGroupName := range q.TaskGroupOrder { + if steps, exists := q.TaskGroup[taskGroupName]; exists { + complete := true + for _, step := range steps { + if !step.Complete() { + complete = false + break + } + } + if !complete && orderNum < ret { + ret = orderNum + } + } + } + + return ret +} + +// CheckQuestReferencedSpawns checks if a spawn is referenced by any quest step +func (q *Quest) CheckQuestReferencedSpawns(spawnID int32) bool { + q.stepsMutex.RLock() + defer q.stepsMutex.RUnlock() + + for _, step := range q.QuestSteps { + if step.Complete() { + continue + } + + switch step.Type { + case StepTypeKill, StepTypeNormal: + if step.CheckStepReferencedID(spawnID) { + return true + } + case StepTypeKillRaceReq: + // TODO: Implement race requirement checking + // This would require spawn race information + } + } + + return false +} + +// CheckQuestKillUpdate checks and updates kill quest steps +func (q *Quest) CheckQuestKillUpdate(spawnID int32, update bool) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + hasUpdate := false + + for _, step := range q.QuestSteps { + if step.Complete() { + continue + } + + shouldUpdate := false + switch step.Type { + case StepTypeKill: + shouldUpdate = step.CheckStepReferencedID(spawnID) + case StepTypeKillRaceReq: + // TODO: Implement race requirement checking + // shouldUpdate = step.CheckStepKillRaceReqUpdate(spawn) + } + + if shouldUpdate { + if update { + // Check percentage chance + passed := true + if step.Percentage < MaxPercentage { + passed = step.Percentage > rand.Float32()*MaxPercentage + } + + if passed { + actualProgress := step.AddStepProgress(1) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + + // TODO: Call progress action + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[step.ID]; exists && action != "" { + // TODO: Execute Lua script + _ = action + } + q.progressActionsMutex.RUnlock() + + hasUpdate = true + } + } else { + q.StepFailures = append(q.StepFailures, step) + } + } else { + hasUpdate = true + } + } + } + + if hasUpdate && update { + q.SetSaveNeeded(true) + } + + return hasUpdate +} + +// CheckQuestChatUpdate checks and updates chat quest steps +func (q *Quest) CheckQuestChatUpdate(npcID int32, update bool) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + hasUpdate := false + + for _, step := range q.QuestSteps { + if step.Complete() || step.Type != StepTypeChat { + continue + } + + if step.CheckStepReferencedID(npcID) { + if update { + actualProgress := step.AddStepProgress(1) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + + // TODO: Call progress action + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[step.ID]; exists && action != "" { + // TODO: Execute Lua script + _ = action + } + q.progressActionsMutex.RUnlock() + } + } + hasUpdate = true + } + } + + if hasUpdate && update { + q.SetSaveNeeded(true) + } + + return hasUpdate +} + +// CheckQuestItemUpdate checks and updates item quest steps +func (q *Quest) CheckQuestItemUpdate(itemID int32, quantity int8) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + hasUpdate := false + + for _, step := range q.QuestSteps { + if step.Complete() || step.Type != StepTypeObtainItem { + continue + } + + if step.CheckStepReferencedID(itemID) { + // Check percentage chance + passed := true + if step.Percentage < MaxPercentage { + passed = step.Percentage > rand.Float32()*MaxPercentage + } + + if passed { + actualProgress := step.AddStepProgress(int32(quantity)) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + + // TODO: Call progress action + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[step.ID]; exists && action != "" { + // TODO: Execute Lua script + _ = action + } + q.progressActionsMutex.RUnlock() + } + hasUpdate = true + } else { + q.StepFailures = append(q.StepFailures, step) + } + } + } + + if hasUpdate { + q.SetSaveNeeded(true) + } + + return hasUpdate +} + +// CheckQuestLocationUpdate checks and updates location quest steps +func (q *Quest) CheckQuestLocationUpdate(charX, charY, charZ float32, zoneID int32) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + hasUpdate := false + + for _, step := range q.QuestSteps { + if step.Complete() || step.Type != StepTypeLocation { + continue + } + + if step.CheckStepLocationUpdate(charX, charY, charZ, zoneID) { + actualProgress := step.AddStepProgress(1) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + + // TODO: Call progress action + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[step.ID]; exists && action != "" { + // TODO: Execute Lua script + _ = action + } + q.progressActionsMutex.RUnlock() + } + hasUpdate = true + } + } + + if hasUpdate { + q.SetSaveNeeded(true) + } + + return hasUpdate +} + +// CheckQuestSpellUpdate checks and updates spell quest steps +func (q *Quest) CheckQuestSpellUpdate(spellID int32) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + hasUpdate := false + + for _, step := range q.QuestSteps { + if step.Complete() || step.Type != StepTypeSpell { + continue + } + + if step.CheckStepReferencedID(spellID) { + // Check percentage chance + passed := true + if step.Percentage < MaxPercentage { + passed = step.Percentage > rand.Float32()*MaxPercentage + } + + if passed { + actualProgress := step.AddStepProgress(1) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + + // TODO: Call progress action + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[step.ID]; exists && action != "" { + // TODO: Execute Lua script + _ = action + } + q.progressActionsMutex.RUnlock() + } + hasUpdate = true + } else { + q.StepFailures = append(q.StepFailures, step) + } + } + } + + if hasUpdate { + q.SetSaveNeeded(true) + } + + return hasUpdate +} + +// CheckQuestRefIDUpdate checks and updates reference ID quest steps (craft/harvest) +func (q *Quest) CheckQuestRefIDUpdate(refID int32, quantity int32) bool { + q.stepsMutex.Lock() + defer q.stepsMutex.Unlock() + + hasUpdate := false + + for _, step := range q.QuestSteps { + if step.Complete() { + continue + } + + if step.Type == StepTypeHarvest || step.Type == StepTypeCraft { + if step.CheckStepReferencedID(refID) { + // Check percentage chance + passed := true + if step.Percentage < MaxPercentage { + passed = step.Percentage > rand.Float32()*MaxPercentage + } + + if passed { + actualProgress := step.AddStepProgress(quantity) + if actualProgress > 0 { + q.StepUpdates = append(q.StepUpdates, step) + + // TODO: Call progress action + q.progressActionsMutex.RLock() + if action, exists := q.ProgressActions[step.ID]; exists && action != "" { + // TODO: Execute Lua script + _ = action + } + q.progressActionsMutex.RUnlock() + } + hasUpdate = true + } else { + q.StepFailures = append(q.StepFailures, step) + } + } + } + } + + if hasUpdate { + q.SetSaveNeeded(true) + } + + return hasUpdate +} + +// GetCompleted checks if the quest is complete +func (q *Quest) GetCompleted() bool { + q.stepsMutex.RLock() + defer q.stepsMutex.RUnlock() + + for _, step := range q.QuestSteps { + if !step.Complete() { + return false + } + } + return true +} + +// CheckCategoryYellow checks if the quest category should be displayed in yellow +func (q *Quest) CheckCategoryYellow() bool { + category := q.Type + yellowCategories := []string{ + "Signature", "Heritage", "Hallmark", "Deity", "Miscellaneous", + "Language", "Lore and Legend", "World Event", "Tradeskill", + } + + for _, yellowCat := range yellowCategories { + if strings.EqualFold(category, yellowCat) { + return true + } + } + return false +} + +// SetStepTimer sets a timer for a quest step +func (q *Quest) SetStepTimer(duration int32) { + if duration == 0 { + q.Timestamp = 0 + } else { + q.Timestamp = int32(time.Now().Unix()) + duration + } + q.SetSaveNeeded(true) +} + +// StepFailed handles when a step fails +func (q *Quest) StepFailed(stepID int32) { + q.failedActionsMutex.RLock() + action, exists := q.FailedActions[stepID] + q.failedActionsMutex.RUnlock() + + if exists && action != "" { + // TODO: Execute Lua script for failed action + _ = action + } +} + +// Helper methods + +// getTaskGroupByName finds a task group by name +func (q *Quest) getTaskGroupByName(name string) ([]*QuestStep, bool) { + steps, exists := q.TaskGroup[name] + return steps, exists +} + +// SetSaveNeeded sets the save needed flag +func (q *Quest) SetSaveNeeded(needed bool) { + q.NeedsSave = needed +} + +// SetQuestTemporaryState sets the quest temporary state +func (q *Quest) SetQuestTemporaryState(tempState bool, customDescription string) { + if !tempState { + q.TmpRewardCoins = 0 + q.TmpRewardStatus = 0 + // TODO: Clear temporary reward items + } + + q.QuestStateTemporary = tempState + q.QuestTempDescription = customDescription + q.SetSaveNeeded(true) +} + +// CanShareQuestCriteria checks if the quest meets sharing criteria +func (q *Quest) CanShareQuestCriteria(hasQuest, hasCompleted bool, currentStep int16) bool { + shareableFlag := q.QuestShareableFlag + + // Check if quest can be shared at all + if shareableFlag == ShareableNone { + return false + } + + // Check completed sharing + if (shareableFlag&ShareableCompleted) == 0 && hasCompleted { + return false + } + + // Check if can only share when completed + if shareableFlag == ShareableCompleted && !hasCompleted { + return false + } + + // Check during quest sharing + if (shareableFlag&ShareableDuring) == 0 && hasQuest && currentStep > 1 { + return false + } + + // Check active quest sharing + if (shareableFlag&ShareableActive) == 0 && hasQuest { + return false + } + + // Check if has quest for sharing + if (shareableFlag&ShareableCompleted) == 0 && !hasQuest { + return false + } + + return true +} + +// Validation methods + +// ValidateQuest performs basic quest validation +func (q *Quest) ValidateQuest() error { + if q.ID <= 0 { + return fmt.Errorf("quest ID must be positive") + } + + if q.Name == "" { + return fmt.Errorf("quest name cannot be empty") + } + + if len(q.Name) > MaxQuestNameLength { + return fmt.Errorf("quest name too long (max %d)", MaxQuestNameLength) + } + + if len(q.Description) > MaxQuestDescriptionLength { + return fmt.Errorf("quest description too long (max %d)", MaxQuestDescriptionLength) + } + + if q.Level < 1 || q.Level > 100 { + return fmt.Errorf("quest level must be between 1 and 100") + } + + if len(q.QuestSteps) == 0 { + return fmt.Errorf("quest must have at least one step") + } + + // Validate steps + for _, step := range q.QuestSteps { + if err := q.validateStep(step); err != nil { + return fmt.Errorf("step %d validation failed: %w", step.ID, err) + } + } + + return nil +} + +// validateStep validates a single quest step +func (q *Quest) validateStep(step *QuestStep) error { + if step.ID <= 0 { + return fmt.Errorf("step ID must be positive") + } + + if step.Type < StepTypeKill || step.Type > StepTypeKillRaceReq { + return fmt.Errorf("invalid step type: %d", step.Type) + } + + if len(step.Description) > MaxStepDescriptionLength { + return fmt.Errorf("step description too long (max %d)", MaxStepDescriptionLength) + } + + if step.Quantity <= 0 { + return fmt.Errorf("step quantity must be positive") + } + + if step.Percentage < MinPercentage || step.Percentage > MaxPercentage { + return fmt.Errorf("step percentage must be between %.1f and %.1f", MinPercentage, MaxPercentage) + } + + // Type-specific validation + switch step.Type { + case StepTypeLocation: + if len(step.Locations) == 0 { + return fmt.Errorf("location step must have at least one location") + } + if step.MaxVariation < MinLocationVariation || step.MaxVariation > MaxLocationVariation { + return fmt.Errorf("location max variation must be between %.1f and %.1f", MinLocationVariation, MaxLocationVariation) + } + default: + if len(step.IDs) == 0 { + return fmt.Errorf("non-location step must have at least one referenced ID") + } + } + + return nil +} \ No newline at end of file diff --git a/internal/quests/rewards.go b/internal/quests/rewards.go new file mode 100644 index 0000000..8b596d5 --- /dev/null +++ b/internal/quests/rewards.go @@ -0,0 +1,427 @@ +package quests + +// Reward management methods for Quest + +// AddRewardCoins adds coin rewards +func (q *Quest) AddRewardCoins(copper, silver, gold, platinum int32) { + q.RewardCoins = int64(copper) + int64(silver)*100 + int64(gold)*10000 + int64(platinum)*1000000 + q.SetSaveNeeded(true) +} + +// AddRewardCoinsMax sets the maximum coin reward +func (q *Quest) AddRewardCoinsMax(coins int64) { + q.RewardCoinsMax = coins + q.SetSaveNeeded(true) +} + +// AddRewardFaction adds a faction reward +func (q *Quest) AddRewardFaction(factionID int32, amount int32) { + q.RewardFactions[factionID] = amount + q.SetSaveNeeded(true) +} + +// RemoveRewardFaction removes a faction reward +func (q *Quest) RemoveRewardFaction(factionID int32) bool { + if _, exists := q.RewardFactions[factionID]; exists { + delete(q.RewardFactions, factionID) + q.SetSaveNeeded(true) + return true + } + return false +} + +// SetRewardStatus sets the status point reward +func (q *Quest) SetRewardStatus(amount int32) { + q.RewardStatus = amount + q.SetSaveNeeded(true) +} + +// SetRewardComment sets the reward comment text +func (q *Quest) SetRewardComment(comment string) { + q.RewardComment = comment + q.SetSaveNeeded(true) +} + +// SetRewardXP sets the experience point reward +func (q *Quest) SetRewardXP(xp int32) { + q.RewardExp = xp + q.SetSaveNeeded(true) +} + +// SetRewardTSXP sets the tradeskill experience point reward +func (q *Quest) SetRewardTSXP(xp int32) { + q.RewardTSExp = xp + q.SetSaveNeeded(true) +} + +// SetGeneratedCoin sets the generated coin amount +func (q *Quest) SetGeneratedCoin(coin int64) { + q.GeneratedCoin = coin + q.SetSaveNeeded(true) +} + +// GetCoinsReward returns the base coin reward +func (q *Quest) GetCoinsReward() int64 { + return q.RewardCoins +} + +// GetCoinsRewardMax returns the maximum coin reward +func (q *Quest) GetCoinsRewardMax() int64 { + return q.RewardCoinsMax +} + +// GetGeneratedCoin returns the generated coin amount +func (q *Quest) GetGeneratedCoin() int64 { + return q.GeneratedCoin +} + +// GetExpReward returns the experience point reward +func (q *Quest) GetExpReward() int32 { + return q.RewardExp +} + +// GetTSExpReward returns the tradeskill experience point reward +func (q *Quest) GetTSExpReward() int32 { + return q.RewardTSExp +} + +// GetStatusPoints returns the status point reward +func (q *Quest) GetStatusPoints() int32 { + return q.RewardStatus +} + +// GetRewardFactions returns the faction rewards map +func (q *Quest) GetRewardFactions() map[int32]int32 { + return q.RewardFactions +} + +// GetRewardFaction returns the faction reward amount for a specific faction +func (q *Quest) GetRewardFaction(factionID int32) (int32, bool) { + amount, exists := q.RewardFactions[factionID] + return amount, exists +} + +// HasRewardFaction checks if a faction has a reward +func (q *Quest) HasRewardFaction(factionID int32) bool { + _, exists := q.RewardFactions[factionID] + return exists +} + +// ClearAllRewards removes all rewards +func (q *Quest) ClearAllRewards() { + q.RewardCoins = 0 + q.RewardCoinsMax = 0 + q.RewardStatus = 0 + q.RewardComment = "" + q.RewardExp = 0 + q.RewardTSExp = 0 + q.GeneratedCoin = 0 + q.RewardFactions = make(map[int32]int32) + q.SetSaveNeeded(true) +} + +// Temporary reward methods + +// SetStatusTmpReward sets the temporary status reward +func (q *Quest) SetStatusTmpReward(status int32) { + q.TmpRewardStatus = status + q.SetSaveNeeded(true) +} + +// GetStatusTmpReward returns the temporary status reward +func (q *Quest) GetStatusTmpReward() int32 { + return q.TmpRewardStatus +} + +// SetCoinTmpReward sets the temporary coin reward +func (q *Quest) SetCoinTmpReward(coins int64) { + q.TmpRewardCoins = coins + q.SetSaveNeeded(true) +} + +// GetCoinTmpReward returns the temporary coin reward +func (q *Quest) GetCoinTmpReward() int64 { + return q.TmpRewardCoins +} + +// ClearTmpRewards clears temporary rewards +func (q *Quest) ClearTmpRewards() { + q.TmpRewardStatus = 0 + q.TmpRewardCoins = 0 + q.SetSaveNeeded(true) +} + +// Status earning methods + +// SetStatusToEarnMin sets the minimum status to earn +func (q *Quest) SetStatusToEarnMin(value int32) { + q.StatusToEarnMin = value + q.SetSaveNeeded(true) +} + +// GetStatusToEarnMin returns the minimum status to earn +func (q *Quest) GetStatusToEarnMin() int32 { + return q.StatusToEarnMin +} + +// SetStatusToEarnMax sets the maximum status to earn +func (q *Quest) SetStatusToEarnMax(value int32) { + q.StatusToEarnMax = value + q.SetSaveNeeded(true) +} + +// GetStatusToEarnMax returns the maximum status to earn +func (q *Quest) GetStatusToEarnMax() int32 { + return q.StatusToEarnMax +} + +// SetStatusEarned sets the quest status earned +func (q *Quest) SetStatusEarned(status int32) { + q.Status = status + q.SetSaveNeeded(true) +} + +// GetStatusEarned returns the quest status earned +func (q *Quest) GetStatusEarned() int32 { + return q.Status +} + +// Reward calculation methods + +// CalculateCoinsReward calculates the actual coin reward based on level and other factors +func (q *Quest) CalculateCoinsReward(playerLevel int8) int64 { + baseReward := q.RewardCoins + if baseReward <= 0 { + return 0 + } + + // Use generated coin if set + if q.GeneratedCoin > 0 { + return q.GeneratedCoin + } + + // Level-based coin scaling (simplified version) + levelMultiplier := float64(playerLevel) / float64(q.Level) + if levelMultiplier > 1.5 { + levelMultiplier = 1.5 // Cap the multiplier + } else if levelMultiplier < 0.5 { + levelMultiplier = 0.5 // Floor the multiplier + } + + calculatedReward := int64(float64(baseReward) * levelMultiplier) + + // Apply max reward cap if set + if q.RewardCoinsMax > 0 && calculatedReward > q.RewardCoinsMax { + calculatedReward = q.RewardCoinsMax + } + + return calculatedReward +} + +// CalculateExpReward calculates the actual experience reward based on level and other factors +func (q *Quest) CalculateExpReward(playerLevel int8) int32 { + baseReward := q.RewardExp + if baseReward <= 0 { + return 0 + } + + // Level-based experience scaling + if playerLevel > q.Level { + // Reduced XP for overleveled players + levelDiff := playerLevel - q.Level + reduction := float64(levelDiff) * 0.1 + if reduction > 0.8 { + reduction = 0.8 // Max 80% reduction + } + return int32(float64(baseReward) * (1.0 - reduction)) + } else if playerLevel < q.Level { + // Bonus XP for underleveled players (small bonus) + levelDiff := q.Level - playerLevel + bonus := float64(levelDiff) * 0.05 + if bonus > 0.25 { + bonus = 0.25 // Max 25% bonus + } + return int32(float64(baseReward) * (1.0 + bonus)) + } + + return baseReward +} + +// CalculateTSExpReward calculates the actual tradeskill experience reward +func (q *Quest) CalculateTSExpReward(playerTSLevel int8) int32 { + baseReward := q.RewardTSExp + if baseReward <= 0 { + return 0 + } + + // Similar scaling as regular XP but for tradeskill level + if playerTSLevel > q.Level { + levelDiff := playerTSLevel - q.Level + reduction := float64(levelDiff) * 0.1 + if reduction > 0.8 { + reduction = 0.8 + } + return int32(float64(baseReward) * (1.0 - reduction)) + } else if playerTSLevel < q.Level { + levelDiff := q.Level - playerTSLevel + bonus := float64(levelDiff) * 0.05 + if bonus > 0.25 { + bonus = 0.25 + } + return int32(float64(baseReward) * (1.0 + bonus)) + } + + return baseReward +} + +// CalculateStatusReward calculates the actual status reward +func (q *Quest) CalculateStatusReward() int32 { + // Use status earned if set, otherwise use base reward status + if q.Status > 0 { + return q.Status + } + return q.RewardStatus +} + +// Reward validation methods + +// ValidateRewards validates all quest rewards +func (q *Quest) ValidateRewards() error { + // Validate coin rewards + if q.RewardCoins < 0 { + return fmt.Errorf("reward coins cannot be negative") + } + + if q.RewardCoinsMax < 0 { + return fmt.Errorf("reward coins max cannot be negative") + } + + if q.RewardCoinsMax > 0 && q.RewardCoinsMax < q.RewardCoins { + return fmt.Errorf("reward coins max cannot be less than base reward coins") + } + + // Validate experience rewards + if q.RewardExp < 0 { + return fmt.Errorf("reward experience cannot be negative") + } + + if q.RewardTSExp < 0 { + return fmt.Errorf("reward tradeskill experience cannot be negative") + } + + // Validate status rewards + if q.RewardStatus < 0 { + return fmt.Errorf("reward status cannot be negative") + } + + if q.Status < 0 { + return fmt.Errorf("status earned cannot be negative") + } + + // Validate temporary rewards + if q.TmpRewardCoins < 0 { + return fmt.Errorf("temporary reward coins cannot be negative") + } + + if q.TmpRewardStatus < 0 { + return fmt.Errorf("temporary reward status cannot be negative") + } + + // Validate status to earn ranges + if q.StatusToEarnMin < 0 { + return fmt.Errorf("status to earn min cannot be negative") + } + + if q.StatusToEarnMax < 0 { + return fmt.Errorf("status to earn max cannot be negative") + } + + if q.StatusToEarnMax > 0 && q.StatusToEarnMax < q.StatusToEarnMin { + return fmt.Errorf("status to earn max cannot be less than min") + } + + // Validate faction rewards + for factionID, amount := range q.RewardFactions { + if factionID <= 0 { + return fmt.Errorf("invalid faction ID in rewards: %d", factionID) + } + + // Faction amounts can be negative (reputation loss) + if amount < -50000 || amount > 50000 { + return fmt.Errorf("faction reward amount out of range for faction %d: %d", factionID, amount) + } + } + + // Validate reward comment length + if len(q.RewardComment) > MaxCompleteActionLength { + return fmt.Errorf("reward comment too long (max %d)", MaxCompleteActionLength) + } + + return nil +} + +// Helper methods for coin conversion + +// ConvertCoinsToComponents breaks down coins into copper, silver, gold, platinum +func ConvertCoinsToComponents(totalCoins int64) (copper, silver, gold, platinum int32) { + platinum = int32(totalCoins / 1000000) + remaining := totalCoins % 1000000 + + gold = int32(remaining / 10000) + remaining = remaining % 10000 + + silver = int32(remaining / 100) + copper = int32(remaining % 100) + + return copper, silver, gold, platinum +} + +// ConvertComponentsToCoins converts individual coin components to total coins +func ConvertComponentsToCoins(copper, silver, gold, platinum int32) int64 { + return int64(copper) + int64(silver)*100 + int64(gold)*10000 + int64(platinum)*1000000 +} + +// FormatCoinsString formats coins as a readable string +func FormatCoinsString(totalCoins int64) string { + if totalCoins == 0 { + return "0 copper" + } + + copper, silver, gold, platinum := ConvertCoinsToComponents(totalCoins) + + var parts []string + if platinum > 0 { + parts = append(parts, fmt.Sprintf("%d platinum", platinum)) + } + if gold > 0 { + parts = append(parts, fmt.Sprintf("%d gold", gold)) + } + if silver > 0 { + parts = append(parts, fmt.Sprintf("%d silver", silver)) + } + if copper > 0 { + parts = append(parts, fmt.Sprintf("%d copper", copper)) + } + + if len(parts) == 0 { + return "0 copper" + } + + if len(parts) == 1 { + return parts[0] + } + + // Join with commas and "and" for the last item + result := "" + for i, part := range parts { + if i == 0 { + result = part + } else if i == len(parts)-1 { + result += " and " + part + } else { + result += ", " + part + } + } + + return result +} \ No newline at end of file diff --git a/internal/quests/types.go b/internal/quests/types.go new file mode 100644 index 0000000..950d20b --- /dev/null +++ b/internal/quests/types.go @@ -0,0 +1,602 @@ +package quests + +import ( + "sync" + "time" + "eq2emu/internal/common" +) + +// Location represents a 3D location in a zone for quest steps +type Location struct { + ID int32 `json:"id"` + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` + ZoneID int32 `json:"zone_id"` +} + +// NewLocation creates a new location +func NewLocation(id int32, x, y, z float32, zoneID int32) *Location { + return &Location{ + ID: id, + X: x, + Y: y, + Z: z, + ZoneID: zoneID, + } +} + +// QuestFactionPrereq represents faction requirements for a quest +type QuestFactionPrereq struct { + FactionID int32 `json:"faction_id"` + Min int32 `json:"min"` + Max int32 `json:"max"` +} + +// NewQuestFactionPrereq creates a new faction prerequisite +func NewQuestFactionPrereq(factionID, min, max int32) *QuestFactionPrereq { + return &QuestFactionPrereq{ + FactionID: factionID, + Min: min, + Max: max, + } +} + +// QuestStep represents a single step in a quest +type QuestStep struct { + // Basic step data + ID int32 `json:"step_id"` + Type int8 `json:"type"` + Description string `json:"description"` + TaskGroup string `json:"task_group"` + Quantity int32 `json:"quantity"` + StepProgress int32 `json:"step_progress"` + Icon int16 `json:"icon"` + MaxVariation float32 `json:"max_variation"` + Percentage float32 `json:"percentage"` + UsableItemID int32 `json:"usable_item_id"` + + // Tracking data + UpdateName string `json:"update_name"` + UpdateTargetName string `json:"update_target_name"` + Updated bool `json:"updated"` + + // Step data (one of these will be populated based on type) + IDs map[int32]bool `json:"ids,omitempty"` // For kill, chat, obtain item, etc. + Locations []*Location `json:"locations,omitempty"` // For location steps + + // Thread safety + mutex sync.RWMutex +} + +// NewQuestStep creates a new quest step +func NewQuestStep(id int32, stepType int8, description string, ids []int32, quantity int32, taskGroup string, locations []*Location, maxVariation, percentage float32, usableItemID int32) *QuestStep { + step := &QuestStep{ + ID: id, + Type: stepType, + Description: description, + TaskGroup: taskGroup, + Quantity: quantity, + StepProgress: 0, + Icon: DefaultIcon, + MaxVariation: maxVariation, + Percentage: percentage, + UsableItemID: usableItemID, + Updated: false, + } + + // Initialize IDs map for non-location steps + if stepType != StepTypeLocation && len(ids) > 0 { + step.IDs = make(map[int32]bool) + for _, id := range ids { + step.IDs[id] = true + } + } + + // Initialize locations for location steps + if stepType == StepTypeLocation && len(locations) > 0 { + step.Locations = make([]*Location, len(locations)) + copy(step.Locations, locations) + } + + return step +} + +// Copy creates a copy of a quest step +func (qs *QuestStep) Copy() *QuestStep { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + + newStep := &QuestStep{ + ID: qs.ID, + Type: qs.Type, + Description: qs.Description, + TaskGroup: qs.TaskGroup, + Quantity: qs.Quantity, + StepProgress: 0, // Reset progress for new quest copy + Icon: qs.Icon, + MaxVariation: qs.MaxVariation, + Percentage: qs.Percentage, + UsableItemID: qs.UsableItemID, + UpdateName: qs.UpdateName, + UpdateTargetName: qs.UpdateTargetName, + Updated: false, + } + + // Copy IDs map + if qs.IDs != nil { + newStep.IDs = make(map[int32]bool) + for id, value := range qs.IDs { + newStep.IDs[id] = value + } + } + + // Copy locations + if qs.Locations != nil { + newStep.Locations = make([]*Location, len(qs.Locations)) + for i, loc := range qs.Locations { + newStep.Locations[i] = &Location{ + ID: loc.ID, + X: loc.X, + Y: loc.Y, + Z: loc.Z, + ZoneID: loc.ZoneID, + } + } + } + + return newStep +} + +// Complete checks if the step is complete +func (qs *QuestStep) Complete() bool { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + return qs.StepProgress >= qs.Quantity +} + +// SetComplete marks the step as complete +func (qs *QuestStep) SetComplete() { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.StepProgress = qs.Quantity + qs.Updated = true +} + +// AddStepProgress adds progress to the step and returns the actual amount added +func (qs *QuestStep) AddStepProgress(val int32) int32 { + qs.mutex.Lock() + defer qs.mutex.Unlock() + + qs.Updated = true + remaining := qs.Quantity - qs.StepProgress + if val > remaining { + qs.StepProgress = qs.Quantity + return remaining + } + + qs.StepProgress += val + return val +} + +// SetStepProgress sets the progress directly +func (qs *QuestStep) SetStepProgress(val int32) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.StepProgress = val +} + +// GetStepProgress returns current progress +func (qs *QuestStep) GetStepProgress() int32 { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + return qs.StepProgress +} + +// CheckStepReferencedID checks if an ID is referenced by this step +func (qs *QuestStep) CheckStepReferencedID(id int32) bool { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + + if qs.IDs != nil { + _, exists := qs.IDs[id] + return exists + } + return false +} + +// CheckStepLocationUpdate checks if character location matches step requirements +func (qs *QuestStep) CheckStepLocationUpdate(charX, charY, charZ float32, zoneID int32) bool { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + + if qs.Locations == nil { + return false + } + + for _, loc := range qs.Locations { + if loc.ZoneID > 0 && loc.ZoneID != zoneID { + continue + } + + // Calculate distance within max variation + diffX := loc.X - charX + if diffX < 0 { + diffX = -diffX + } + if diffX <= qs.MaxVariation { + diffZ := loc.Z - charZ + if diffZ < 0 { + diffZ = -diffZ + } + if diffZ <= qs.MaxVariation { + totalDiff := diffX + diffZ + if totalDiff <= qs.MaxVariation { + diffY := loc.Y - charY + if diffY < 0 { + diffY = -diffY + } + if diffY <= qs.MaxVariation { + totalDiff += diffY + if totalDiff <= qs.MaxVariation { + return true + } + } + } + } + } + } + + return false +} + +// WasUpdated returns if step was updated +func (qs *QuestStep) WasUpdated() bool { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + return qs.Updated +} + +// SetWasUpdated sets the updated flag +func (qs *QuestStep) SetWasUpdated(val bool) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.Updated = val +} + +// GetCurrentQuantity returns current progress as int16 +func (qs *QuestStep) GetCurrentQuantity() int16 { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + return int16(qs.StepProgress) +} + +// GetNeededQuantity returns required quantity as int16 +func (qs *QuestStep) GetNeededQuantity() int16 { + qs.mutex.RLock() + defer qs.mutex.RUnlock() + return int16(qs.Quantity) +} + +// ResetTaskGroup clears the task group +func (qs *QuestStep) ResetTaskGroup() { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.TaskGroup = "" +} + +// SetTaskGroup sets the task group +func (qs *QuestStep) SetTaskGroup(taskGroup string) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.TaskGroup = taskGroup +} + +// SetDescription sets the step description +func (qs *QuestStep) SetDescription(description string) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.Description = description +} + +// SetUpdateName sets the update name +func (qs *QuestStep) SetUpdateName(name string) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.UpdateName = name +} + +// SetUpdateTargetName sets the update target name +func (qs *QuestStep) SetUpdateTargetName(name string) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.UpdateTargetName = name +} + +// SetIcon sets the step icon +func (qs *QuestStep) SetIcon(icon int16) { + qs.mutex.Lock() + defer qs.mutex.Unlock() + qs.Icon = icon +} + +// Quest represents a complete quest with all its steps and requirements +type Quest struct { + // Basic quest information + ID int32 `json:"quest_id"` + Name string `json:"name"` + Type string `json:"type"` + Zone string `json:"zone"` + Level int8 `json:"level"` + EncounterLevel int8 `json:"encounter_level"` + Description string `json:"description"` + CompletedDesc string `json:"completed_description"` + + // Quest giver and return NPC + QuestGiver int32 `json:"quest_giver"` + ReturnID int32 `json:"return_id"` + + // Prerequisites + PrereqLevel int8 `json:"prereq_level"` + PrereqTSLevel int8 `json:"prereq_ts_level"` + PrereqMaxLevel int8 `json:"prereq_max_level"` + PrereqMaxTSLevel int8 `json:"prereq_max_ts_level"` + PrereqFactions []*QuestFactionPrereq `json:"prereq_factions"` + PrereqRaces []int8 `json:"prereq_races"` + PrereqModelTypes []int16 `json:"prereq_model_types"` + PrereqClasses []int8 `json:"prereq_classes"` + PrereqTSClasses []int8 `json:"prereq_ts_classes"` + PrereqQuests []int32 `json:"prereq_quests"` + + // Rewards + RewardCoins int64 `json:"reward_coins"` + RewardCoinsMax int64 `json:"reward_coins_max"` + RewardFactions map[int32]int32 `json:"reward_factions"` + RewardStatus int32 `json:"reward_status"` + RewardComment string `json:"reward_comment"` + RewardExp int32 `json:"reward_exp"` + RewardTSExp int32 `json:"reward_ts_exp"` + GeneratedCoin int64 `json:"generated_coin"` + + // Temporary rewards + TmpRewardStatus int32 `json:"tmp_reward_status"` + TmpRewardCoins int64 `json:"tmp_reward_coins"` + + // Steps and task groups + QuestSteps []*QuestStep `json:"quest_steps"` + QuestStepMap map[int32]*QuestStep `json:"-"` // For quick lookup + QuestStepReverseMap map[*QuestStep]int32 `json:"-"` // Reverse lookup + StepUpdates []*QuestStep `json:"-"` // Steps that were updated + StepFailures []*QuestStep `json:"-"` // Steps that failed + TaskGroupOrder map[int16]string `json:"task_group_order"` + TaskGroup map[string][]*QuestStep `json:"-"` // Grouped steps + TaskGroupNum int16 `json:"task_group_num"` + + // Actions + CompleteActions map[int32]string `json:"complete_actions"` + ProgressActions map[int32]string `json:"progress_actions"` + FailedActions map[int32]string `json:"failed_actions"` + CompleteAction string `json:"complete_action"` + + // State tracking + Deleted bool `json:"deleted"` + TurnedIn bool `json:"turned_in"` + UpdateNeeded bool `json:"update_needed"` + HasSentLastUpdate bool `json:"has_sent_last_update"` + NeedsSave bool `json:"needs_save"` + Visible int8 `json:"visible"` + + // Date tracking + Day int8 `json:"day"` + Month int8 `json:"month"` + Year int8 `json:"year"` + + // Quest flags and settings + FeatherColor int8 `json:"feather_color"` + Repeatable bool `json:"repeatable"` + Tracked bool `json:"tracked"` + CompletedFlag bool `json:"completed_flag"` + YellowName bool `json:"yellow_name"` + QuestFlags int32 `json:"quest_flags"` + Hidden bool `json:"hidden"` + Status int32 `json:"status"` + + // Timer and completion tracking + Timestamp int32 `json:"timestamp"` + TimerStep int32 `json:"timer_step"` + CompleteCount int16 `json:"complete_count"` + + // Temporary state + QuestStateTemporary bool `json:"quest_state_temporary"` + QuestTempDescription string `json:"quest_temp_description"` + QuestShareableFlag int32 `json:"quest_shareable_flag"` + CanDeleteQuest bool `json:"can_delete_quest"` + StatusToEarnMin int32 `json:"status_to_earn_min"` + StatusToEarnMax int32 `json:"status_to_earn_max"` + HideReward bool `json:"hide_reward"` + + // Thread safety + stepsMutex sync.RWMutex + completeActionsMutex sync.RWMutex + progressActionsMutex sync.RWMutex + failedActionsMutex sync.RWMutex +} + +// NewQuest creates a new quest with the given ID +func NewQuest(id int32) *Quest { + now := time.Now() + + quest := &Quest{ + ID: id, + PrereqLevel: DefaultPrereqLevel, + PrereqTSLevel: 0, + PrereqMaxLevel: 0, + PrereqMaxTSLevel: 0, + RewardCoins: 0, + RewardCoinsMax: 0, + CompletedFlag: false, + HasSentLastUpdate: false, + EncounterLevel: 0, + RewardExp: 0, + RewardTSExp: 0, + FeatherColor: 0, + Repeatable: false, + YellowName: false, + Hidden: false, + GeneratedCoin: 0, + QuestFlags: 0, + Timestamp: 0, + CompleteCount: 0, + QuestStateTemporary: false, + TmpRewardStatus: 0, + TmpRewardCoins: 0, + CompletedDesc: "", + QuestTempDescription: "", + QuestShareableFlag: 0, + CanDeleteQuest: false, + Status: 0, + StatusToEarnMin: 0, + StatusToEarnMax: 0, + HideReward: false, + Deleted: false, + TurnedIn: false, + UpdateNeeded: true, + NeedsSave: false, + TaskGroupNum: DefaultTaskGroupNum, + Visible: DefaultVisible, + Day: int8(now.Day()), + Month: int8(now.Month()), + Year: int8(now.Year() - 2000), // EQ2 uses 2-digit years + + // Initialize maps and slices + QuestStepMap: make(map[int32]*QuestStep), + QuestStepReverseMap: make(map[*QuestStep]int32), + TaskGroupOrder: make(map[int16]string), + TaskGroup: make(map[string][]*QuestStep), + CompleteActions: make(map[int32]string), + ProgressActions: make(map[int32]string), + FailedActions: make(map[int32]string), + RewardFactions: make(map[int32]int32), + } + + return quest +} + +// Copy creates a complete copy of a quest (used when giving quest to player) +func (q *Quest) Copy() *Quest { + q.stepsMutex.RLock() + defer q.stepsMutex.RUnlock() + + newQuest := NewQuest(q.ID) + + // Copy basic information + newQuest.Name = q.Name + newQuest.Type = q.Type + newQuest.Zone = q.Zone + newQuest.Level = q.Level + newQuest.EncounterLevel = q.EncounterLevel + newQuest.Description = q.Description + newQuest.CompletedDesc = q.CompletedDesc + newQuest.QuestGiver = q.QuestGiver + newQuest.ReturnID = q.ReturnID + + // Copy prerequisites + newQuest.PrereqLevel = q.PrereqLevel + newQuest.PrereqTSLevel = q.PrereqTSLevel + newQuest.PrereqMaxLevel = q.PrereqMaxLevel + newQuest.PrereqMaxTSLevel = q.PrereqMaxTSLevel + + // Copy prerequisite slices - create new slices + newQuest.PrereqRaces = make([]int8, len(q.PrereqRaces)) + copy(newQuest.PrereqRaces, q.PrereqRaces) + + newQuest.PrereqModelTypes = make([]int16, len(q.PrereqModelTypes)) + copy(newQuest.PrereqModelTypes, q.PrereqModelTypes) + + newQuest.PrereqClasses = make([]int8, len(q.PrereqClasses)) + copy(newQuest.PrereqClasses, q.PrereqClasses) + + newQuest.PrereqTSClasses = make([]int8, len(q.PrereqTSClasses)) + copy(newQuest.PrereqTSClasses, q.PrereqTSClasses) + + newQuest.PrereqQuests = make([]int32, len(q.PrereqQuests)) + copy(newQuest.PrereqQuests, q.PrereqQuests) + + // Copy faction prerequisites + newQuest.PrereqFactions = make([]*QuestFactionPrereq, len(q.PrereqFactions)) + for i, faction := range q.PrereqFactions { + newQuest.PrereqFactions[i] = &QuestFactionPrereq{ + FactionID: faction.FactionID, + Min: faction.Min, + Max: faction.Max, + } + } + + // Copy rewards + newQuest.RewardCoins = q.RewardCoins + newQuest.RewardCoinsMax = q.RewardCoinsMax + newQuest.RewardStatus = q.RewardStatus + newQuest.RewardComment = q.RewardComment + newQuest.RewardExp = q.RewardExp + newQuest.RewardTSExp = q.RewardTSExp + newQuest.GeneratedCoin = q.GeneratedCoin + + // Copy reward factions map + for factionID, amount := range q.RewardFactions { + newQuest.RewardFactions[factionID] = amount + } + + // Copy quest steps + for _, step := range q.QuestSteps { + newQuest.AddQuestStep(step.Copy()) + } + + // Copy actions maps + q.completeActionsMutex.RLock() + for stepID, action := range q.CompleteActions { + newQuest.CompleteActions[stepID] = action + } + q.completeActionsMutex.RUnlock() + + q.progressActionsMutex.RLock() + for stepID, action := range q.ProgressActions { + newQuest.ProgressActions[stepID] = action + } + q.progressActionsMutex.RUnlock() + + q.failedActionsMutex.RLock() + for stepID, action := range q.FailedActions { + newQuest.FailedActions[stepID] = action + } + q.failedActionsMutex.RUnlock() + + // Copy other properties + newQuest.CompleteAction = q.CompleteAction + newQuest.FeatherColor = q.FeatherColor + newQuest.Repeatable = q.Repeatable + newQuest.Hidden = q.Hidden + newQuest.QuestFlags = q.QuestFlags + newQuest.Status = q.Status + newQuest.CompleteCount = q.CompleteCount + newQuest.QuestShareableFlag = q.QuestShareableFlag + newQuest.CanDeleteQuest = q.CanDeleteQuest + newQuest.StatusToEarnMin = q.StatusToEarnMin + newQuest.StatusToEarnMax = q.StatusToEarnMax + newQuest.HideReward = q.HideReward + newQuest.CompletedFlag = q.CompletedFlag + newQuest.HasSentLastUpdate = q.HasSentLastUpdate + newQuest.YellowName = q.YellowName + + // Reset state for new quest copy + newQuest.StepUpdates = make([]*QuestStep, 0) + newQuest.StepFailures = make([]*QuestStep, 0) + newQuest.Deleted = false + newQuest.UpdateNeeded = true + newQuest.TurnedIn = false + newQuest.QuestStateTemporary = false + newQuest.TmpRewardStatus = 0 + newQuest.TmpRewardCoins = 0 + newQuest.QuestTempDescription = "" + + return newQuest +} \ No newline at end of file