diff --git a/common/emu_oplist.h b/common/emu_oplist.h index e695c94a9..29a20cb14 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -408,6 +408,7 @@ N(OP_ReloadUI), N(OP_RemoveAllDoors), N(OP_RemoveBlockedBuffs), N(OP_RemoveNimbusEffect), +N(OP_RemoveTrap), N(OP_Report), N(OP_ReqClientSpawn), N(OP_ReqNewZone), @@ -523,6 +524,7 @@ N(OP_TributeToggle), N(OP_TributeUpdate), N(OP_Untargetable), N(OP_UpdateAA), +N(OP_UpdateAura), N(OP_UpdateLeadershipAA), N(OP_VetClaimReply), N(OP_VetClaimRequest), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index c234e303a..021b18557 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -5332,6 +5332,24 @@ struct fling_struct { /* 28 */ }; +// used when action == 0 +struct AuraCreate_Struct { +/* 00 */ uint32 action; // 0 = add, 1 = delete, 2 = reset +/* 04 */ uint32 type; // unsure -- normal auras show 1 clicky (ex. Circle of Power) show 0 +/* 08 */ char aura_name[64]; +/* 72 */ uint32 entity_id; +/* 76 */ uint32 icon; +/* 80 */ +}; + +// used when action == 1 +struct AuraDestory_Struct { +/* 00 */ uint32 action; // 0 = add, 1 = delete, 2 = reset +/* 04 */ uint32 entity_id; +/* 08 */ +}; +// I think we can assume it's just action for 2, client doesn't seem to do anything with the rest of the data in that case + // Restore structure packing to default #pragma pack() diff --git a/common/shareddb.cpp b/common/shareddb.cpp index 2efb06913..331826ef2 100644 --- a/common/shareddb.cpp +++ b/common/shareddb.cpp @@ -1674,6 +1674,7 @@ void SharedDatabase::LoadSpells(void *data, int max_spells) { for (y = 0; y < 16; y++) sp[tempid].deities[y]=atoi(row[126+y]); + sp[tempid].new_icon=atoi(row[144]); sp[tempid].uninterruptable=atoi(row[146]) != 0; sp[tempid].ResistDiff=atoi(row[147]); sp[tempid].dot_stacking_exempt = atoi(row[148]) != 0; diff --git a/common/spdat.h b/common/spdat.h index bd1dacdf8..605e9a6cf 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -562,9 +562,9 @@ typedef enum { #define SE_LimitManaMin 348 // implemented #define SE_ShieldEquipDmgMod 349 // implemented[AA] Increase melee base damage (indirectly increasing hate) when wearing a shield. #define SE_ManaBurn 350 // implemented - Drains mana for damage/heal at a defined ratio up to a defined maximum amount of mana. -//#define SE_PersistentEffect 351 // *not implemented. creates a trap/totem that casts a spell (spell id + base1?) when anything comes near it. can probably make a beacon for this -//#define SE_IncreaseTrapCount 352 // *not implemented - looks to be some type of invulnerability? Test ITC (8755) -//#define SE_AdditionalAura 353 // *not implemented - allows use of more than 1 aura, aa effect +#define SE_PersistentEffect 351 // *not implemented. creates a trap/totem that casts a spell (spell id + base1?) when anything comes near it. can probably make a beacon for this +#define SE_IncreaseTrapCount 352 // *not implemented - looks to be some type of invulnerability? Test ITC (8755) +#define SE_AdditionalAura 353 // *not implemented - allows use of more than 1 aura, aa effect //#define SE_DeactivateAllTraps 354 // *not implemented - looks to be some type of invulnerability? Test DAT (8757) //#define SE_LearnTrap 355 // *not implemented - looks to be some type of invulnerability? Test LT (8758) //#define SE_ChangeTriggerType 356 // not used @@ -757,7 +757,7 @@ struct SPDat_Spell_Struct // -- DIETY_BERTOXXULOUS ... DIETY_VEESHAN /* 142 */ //int8 npc_no_cast; // 142: between 0 & 100 -- NPC_NO_CAST /* 143 */ //int ai_pt_bonus; // 143: always set to 0, client doesn't save this -- AI_PT_BONUS -/* 144 */ //int16 new_icon // Spell icon used by the client in uifiles/default/spells??.tga, both for spell gems & buff window. Looks to depreciate icon & memicon -- NEW_ICON +/* 144 */ int16 new_icon; // Spell icon used by the client in uifiles/default/spells??.tga, both for spell gems & buff window. Looks to depreciate icon & memicon -- NEW_ICON /* 145 */ //int16 spellanim; // Doesn't look like it's the same as #doanim, so not sure what this is, particles I think -- SPELL_EFFECT_INDEX /* 146 */ bool uninterruptable; // Looks like anything != 0 is uninterruptable. Values are mostly -1, 0, & 1 (Fetid Breath = 90?) -- NO_INTERRUPT /* 147 */ int16 ResistDiff; // -- RESIST_MOD diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index ee9b0e646..e63e5e9e9 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -675,3 +675,7 @@ OP_RAWOutOfSession=0x0000 # we need to document the differences between these packets to make identifying them easier OP_Some3ByteHPUpdate=0x0000 # initial HP update for mobs OP_InitialHPUpdate=0x0000 + +#aura related +OP_UpdateAura=0x1456 +OP_RemoveTrap=0x71da diff --git a/utils/sql/git/required/2017_07_xx_aura.sql b/utils/sql/git/required/2017_07_xx_aura.sql new file mode 100644 index 000000000..ed777d2e5 --- /dev/null +++ b/utils/sql/git/required/2017_07_xx_aura.sql @@ -0,0 +1,13 @@ +CREATE TABLE `auras` ( + `type` INT(10) NOT NULL, + `npc_type` INT(10) NOT NULL, + `name` VARCHAR(64) NOT NULL, + `spell_id` INT(10) NOT NULL, + `distance` INT(10) NOT NULL DEFAULT 60, + `aura_type` INT(10) NOT NULL DEFAULT 1, + `spawn_type` INT(10) NOT NULL DEFAULT 0, + `movement` INT(10) NOT NULL DEFAULT 0, + `duration` INT(10) NOT NULL DEFAULT 5400, + `icon` INT(10) NOT NULL DEFAULT -1, + PRIMARY KEY(`type`) +) diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 0c2c9bd63..0171c8eb9 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -5,6 +5,7 @@ SET(zone_sources aa_ability.cpp aggro.cpp aggromanager.cpp + aura.cpp attack.cpp beacon.cpp bonuses.cpp @@ -136,6 +137,7 @@ SET(zone_headers aa.h aa_ability.h aggromanager.h + aura.h basic_functions.h beacon.h bot.h diff --git a/zone/aura.cpp b/zone/aura.cpp new file mode 100644 index 000000000..6b4e06323 --- /dev/null +++ b/zone/aura.cpp @@ -0,0 +1,435 @@ +#include "../common/string_util.h" + +#include "aura.h" +#include "client.h" +#include "string_ids.h" +#include "raids.h" + +Aura::Aura(NPCType *type_data, Mob *owner, AuraRecord &record) + : NPC(type_data, 0, owner->GetPosition(), FlyMode3), spell_id(record.spell_id), distance(record.distance), + remove_timer(record.duration), movement_timer(100), process_timer(100) +{ + GiveNPCTypeData(type_data); // we will delete this later on + m_owner = owner->GetID(); + + if (record.aura_type < static_cast(AuraType::Max)) + type = static_cast(record.aura_type); + else + type = AuraType::OnAllGroupMembers; + + if (record.spawn_type < static_cast(AuraSpawns::Max)) + spawn_type = static_cast(record.spawn_type); + else + spawn_type = AuraSpawns::GroupMembers; + + if (record.movement < static_cast(AuraMovement::Max)) + movement_type = static_cast(record.movement); + else + movement_type = AuraMovement::Follow; + + switch (type) { + case AuraType::OnAllFriendlies: + process_func = &Aura::ProcessOnAllFriendlies; + break; + case AuraType::OnAllGroupMembers: + process_func = &Aura::ProcessOnAllGroupMembers; + break; + case AuraType::OnGroupMembersPets: + process_func = &Aura::ProcessOnGroupMembersPets; + break; + case AuraType::Totem: + process_func = &Aura::ProcessTotem; + break; + case AuraType::EnterTrap: + process_func = &Aura::ProcessEnterTrap; + break; + case AuraType::ExitTrap: + process_func = &Aura::ProcessExitTrap; + break; + default: + process_func = nullptr; + } +} + +void Aura::ProcessOnAllFriendlies(Mob *owner) +{ + Shout("Stub 1"); +} + +void Aura::ProcessOnAllGroupMembers(Mob *owner) +{ + if (!process_timer.Check()) + return; + auto &mob_list = entity_list.GetMobList(); // read only reference so we can do it all inline + std::set delayed_remove; + if (owner->IsRaidGrouped() && owner->IsClient()) { // currently raids are just client, but safety check + auto raid = owner->GetRaid(); + if (raid == nullptr) { // well shit + Depop(); + return; + } + auto group_id = raid->GetGroup(owner->CastToClient()); + + // some lambdas so the for loop is less horrible ... + auto verify_raid_client = [&raid, &group_id, this](Client *c) { + auto idx = raid->GetPlayerIndex(c); + if (c->GetID() == m_owner) { + return DistanceSquared(GetPosition(), c->GetPosition()) <= distance; + } else if (idx == 0xFFFFFFFF || raid->members[idx].GroupNumber != group_id || raid->members[idx].GroupNumber == 0xFFFFFFFF) { + return false; + } else if (DistanceSquared(GetPosition(), c->GetPosition()) > distance) { + return false; + } + return true; + }; + + auto verify_raid_client_pet = [&raid, &group_id, this](Mob *m) { + auto idx = raid->GetPlayerIndex(m->GetOwner()->CastToClient()); + if (m->GetOwner()->GetID() == m_owner) { + return DistanceSquared(GetPosition(), m->GetPosition()) <= distance; + } else if (idx == 0xFFFFFFFF || raid->members[idx].GroupNumber != group_id || raid->members[idx].GroupNumber == 0xFFFFFFFF) { + return false; + } else if (DistanceSquared(GetPosition(), m->GetPosition()) > distance) { + return false; + } + return true; + }; + + auto verify_raid_client_swarm = [&raid, &group_id, this](NPC *n) { + auto owner = entity_list.GetMob(n->GetSwarmOwner()); + if (owner == nullptr) + return false; + auto idx = raid->GetPlayerIndex(owner->CastToClient()); + if (owner->GetID() == m_owner) { + return DistanceSquared(GetPosition(), n->GetPosition()) <= distance; + } else if (idx == 0xFFFFFFFF || raid->members[idx].GroupNumber != group_id || raid->members[idx].GroupNumber == 0xFFFFFFFF) { + return false; + } else if (DistanceSquared(GetPosition(), n->GetPosition()) > distance) { + return false; + } + return true; + }; + + for (auto &e : mob_list) { + auto mob = e.second; + // step 1: check if we're already managing this NPC's buff + auto it = casted_on.find(mob->GetID()); + if (it != casted_on.end()) { + // verify still good! + if (mob->IsClient()) { + if (!verify_raid_client(mob->CastToClient())) + delayed_remove.insert(mob->GetID()); + } else if (mob->IsPet() && mob->IsPetOwnerClient() && mob->GetOwner()) { + if (!verify_raid_client_pet(mob)) + delayed_remove.insert(mob->GetID()); + } else if (mob->IsNPC() && mob->IsPetOwnerClient()) { + auto npc = mob->CastToNPC(); + if (!verify_raid_client_swarm(npc)) + delayed_remove.insert(mob->GetID()); + } + } else { // we're not on it! + if (mob->IsClient() && verify_raid_client(mob->CastToClient())) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } else if (mob->IsPet() && mob->IsPetOwnerClient() && mob->GetOwner() && verify_raid_client_pet(mob)) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } else if (mob->IsNPC() && mob->IsPetOwnerClient()) { + auto npc = mob->CastToNPC(); + if (verify_raid_client_swarm(npc)) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } + } + } + } + } else if (owner->IsGrouped()) { + auto group = owner->GetGroup(); + if (group == nullptr) { // uh oh + Depop(); + return; + } + + // lambdas to make for loop less ugly + auto verify_group_pet = [&group, this](Mob *m) { + auto owner = m->GetOwner(); + if (owner != nullptr && group->IsGroupMember(owner) && DistanceSquared(GetPosition(), m->GetPosition()) <= distance) + return true; + return false; + }; + + auto verify_group_swarm = [&group, this](NPC *n) { + auto owner = entity_list.GetMob(n->GetSwarmOwner()); + if (owner != nullptr && group->IsGroupMember(owner) && DistanceSquared(GetPosition(), n->GetPosition()) <= distance) + return true; + return false; + }; + + for (auto &e : mob_list) { + auto mob = e.second; + auto it = casted_on.find(mob->GetID()); + + if (it != casted_on.end()) { // make sure we're still valid + if (mob->IsPet()) { + if (!verify_group_pet(mob)) + delayed_remove.insert(mob->GetID()); + } else if (mob->IsNPC() && mob->CastToNPC()->GetSwarmInfo()) { + if (!verify_group_swarm(mob->CastToNPC())) + delayed_remove.insert(mob->GetID()); + } else if (!group->IsGroupMember(mob) || DistanceSquared(GetPosition(), mob->GetPosition()) > distance) { + delayed_remove.insert(mob->GetID()); + } + } else { // not on, check if we should be! + if (mob->IsPet() && verify_group_pet(mob)) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } else if (mob->IsNPC() && mob->CastToNPC()->GetSwarmInfo() && verify_group_swarm(mob->CastToNPC())) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } else if (group->IsGroupMember(mob) && DistanceSquared(GetPosition(), mob->GetPosition()) <= distance) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } + } + } + } else { + auto verify_solo = [&owner, this](Mob *m) { + if (m->IsPet() && m->GetOwnerID() == owner->GetID()) + return true; + else if (m->IsNPC() && m->CastToNPC()->GetSwarmOwner() == owner->GetID()) + return true; + else if (m->GetID() == owner->GetID()) + return true; + else + return false; + }; + for (auto &e : mob_list) { + auto mob = e.second; + auto it = casted_on.find(mob->GetID()); + bool good = verify_solo(mob); + + if (it != casted_on.end()) { // make sure still valid + if (!good || DistanceSquared(GetPosition(), mob->GetPosition()) > distance) { + delayed_remove.insert(mob->GetID()); + } + } else if (good && DistanceSquared(GetPosition(), mob->GetPosition()) <= distance) { + casted_on.insert(mob->GetID()); + SpellFinished(spell_id, mob); + } + } + } + + bool is_buff = IsBuffSpell(spell_id); + + for (auto &e : delayed_remove) { + auto mob = entity_list.GetMob(e); + if (mob != nullptr && is_buff) // some auras cast instant spells so no need to remove + mob->BuffFadeBySpellIDAndCaster(spell_id, GetID()); + casted_on.erase(e); + } + + if (cast_timer.Enabled() || !cast_timer.Check()) + return; + + // TODO: some auras have to recast (DRU for example, non-buff too) + /* for (auto &e : casted_on) { + auto mob = entity_list.GetMob(e); + if (mob != nullptr && (!is_buff || !mob->IsAffectedByBuff(spell_id))) + + }*/ +} + +void Aura::ProcessOnGroupMembersPets(Mob *owner) +{ + Shout("Stub 3"); +} + +void Aura::ProcessTotem(Mob *owner) +{ + Shout("Stub 4"); +} + +void Aura::ProcessEnterTrap(Mob *owner) +{ + Shout("Stub 5"); +} + +void Aura::ProcessExitTrap(Mob *owner) +{ + Shout("Stub 6"); +} + +bool Aura::Process() +{ + // Aura::Depop clears buffs + if (p_depop) + return false; + + auto owner = entity_list.GetMob(m_owner); + if (owner == nullptr) { + Depop(); + return true; + } + + if (movement_type == AuraMovement::Follow && GetPosition() != owner->GetPosition() && movement_timer.Check()) { + m_Position = owner->GetPosition(); + SendPosUpdate(); + } + // TODO: waypoints? + + if (process_func) + process_func(*this, owner); + + // TODO: quest calls + return true; +} + +void Aura::Depop(bool unused) +{ + // TODO: clean up buffs + p_depop = true; +} + +void Mob::MakeAura(uint16 spell_id) +{ + // TODO: verify room in AuraMgr + if (!IsValidSpell(spell_id)) + return; + + AuraRecord record; + if (!database.GetAuraEntry(spell_id, record)) { + Message(13, "Unable to find data for aura %s", spells[spell_id].name); + Log(Logs::General, Logs::Error, "Unable to find data for aura %d, check auras table.", spell_id); + return; + } + + if (!IsValidSpell(record.spell_id)) { + Message(13, "Casted spell (%d) is not valid for aura %s", record.spell_id, spells[spell_id].name); + Log(Logs::General, Logs::Error, "Casted spell (%d) is not valid for aura %d, check auras table.", + record.spell_id, spell_id); + return; + } + + if (record.aura_type > static_cast(AuraType::Max)) { + return; // TODO: log + } + + bool trap = false; + + switch (static_cast(record.aura_type)) { + case AuraType::ExitTrap: + case AuraType::EnterTrap: + case AuraType::Totem: + trap = true; + break; + default: + trap = false; + break; + } + + if (!CanSpawnAura(trap)) + return; + + const auto base = database.LoadNPCTypesData(record.npc_type); + if (base == nullptr) { + Message(13, "Unable to load NPC data for aura %s", spells[spell_id].teleport_zone); + Log(Logs::General, Logs::Error, + "Unable to load NPC data for aura %s (NPC ID %d), check auras and npc_types tables.", + spells[spell_id].teleport_zone, record.npc_type); + return; + } + + auto npc_type = new NPCType; + memcpy(npc_type, base, sizeof(NPCType)); + + strn0cpy(npc_type->name, record.name, 64); + + auto npc = new Aura(npc_type, this, record); + entity_list.AddNPC(npc, true, true); + + if (trap) + AddTrap(npc, record); + else + AddAura(npc, record); +} + +bool ZoneDatabase::GetAuraEntry(uint16 spell_id, AuraRecord &record) +{ + auto query = StringFormat("SELECT npc_type, name, spell_id, distance, aura_type, spawn_type, movement, " + "duration, icon FROM auras WHERE type='%d'", + spell_id); + + auto results = QueryDatabase(query); + if (!results.Success()) + return false; + + if (results.RowCount() != 1) + return false; + + auto row = results.begin(); + + record.npc_type = atoi(row[0]); + strn0cpy(record.name, row[1], 64); + record.spell_id = atoi(row[2]); + record.distance = atoi(row[3]); + record.distance *= record.distance; // so we can avoid sqrt + record.aura_type = atoi(row[4]); + record.spawn_type = atoi(row[5]); + record.movement = atoi(row[6]); + record.duration = atoi(row[7]) * 1000; // DB is in seconds + record.icon = atoi(row[8]); + + return true; +} + +void Mob::AddAura(Aura *aura, AuraRecord &record) +{ + // this is called only when it's safe + assert(aura != nullptr); + strn0cpy(aura_mgr.auras[aura_mgr.count].name, aura->GetCleanName(), 64); + aura_mgr.auras[aura_mgr.count].spawn_id = aura->GetID(); + if (record.icon == -1) + aura_mgr.auras[aura_mgr.count].icon = spells[record.spell_id].new_icon; + else + aura_mgr.auras[aura_mgr.count].icon = record.icon; + if (IsClient()) { + auto outapp = new EQApplicationPacket(OP_UpdateAura, sizeof(AuraCreate_Struct)); + auto aura_create = (AuraCreate_Struct *)outapp->pBuffer; + aura_create->action = 0; + aura_create->type = 1; // this can be 0 sometimes too + strn0cpy(aura_create->aura_name, aura_mgr.auras[aura_mgr.count].name, 64); + aura_create->entity_id = aura_mgr.auras[aura_mgr.count].spawn_id; + aura_create->icon = aura_mgr.auras[aura_mgr.count].icon; + CastToClient()->FastQueuePacket(&outapp); + } + // we can increment this now + aura_mgr.count++; +} + +void Mob::AddTrap(Aura *aura, AuraRecord &record) +{ + // this is called only when it's safe + assert(aura != nullptr); + strn0cpy(trap_mgr.auras[trap_mgr.count].name, aura->GetCleanName(), 64); + trap_mgr.auras[trap_mgr.count].spawn_id = aura->GetID(); + if (record.icon == -1) + trap_mgr.auras[trap_mgr.count].icon = spells[record.spell_id].new_icon; + else + trap_mgr.auras[trap_mgr.count].icon = record.icon; + // doesn't send to client + trap_mgr.count++; +} + +bool Mob::CanSpawnAura(bool trap) +{ + if (trap && !HasFreeTrapSlots()) { + Message_StringID(MT_SpellFailure, NO_MORE_TRAPS); + return false; + } else if (!trap && !HasFreeAuraSlots()) { + Message_StringID(MT_SpellFailure, NO_MORE_AURAS); + return false; + } + + return true; +} + diff --git a/zone/aura.h b/zone/aura.h new file mode 100644 index 000000000..cdc01be55 --- /dev/null +++ b/zone/aura.h @@ -0,0 +1,76 @@ +#ifndef AURA_H +#define AURA_H + +#include +#include + +#include "mob.h" +#include "npc.h" +#include "../common/types.h" +#include "../common/timer.h" + +class Group; +class Raid; +class Mob; +struct NPCType; + +enum class AuraType { + OnAllFriendlies, // AE PC/Pet basically (ex. Circle of Power) + OnAllGroupMembers, // Normal buffing aura (ex. Champion's Aura) + OnGroupMembersPets, // Hits just pets (ex. Rathe's Strength) + Totem, // Starts pulsing on a timer when an enemy enters (ex. Idol of Malos) + EnterTrap, // Casts once when an enemy enters (ex. Fire Rune) + ExitTrap, // Casts when they start to flee (ex. Poison Spikes Trap) + FullyScripted, // We just call script function not a predefined + Max +}; + +enum class AuraSpawns { + GroupMembers, // most auras use this + Everyone, // this is like traps and clickies who cast on everyone + Noone, // custom! + Max +}; + +enum class AuraMovement { + Follow, // follows caster + Stationary, + Pathing, // some sorted pathing TODO: implement + Max +}; + +class Aura : public NPC +{ +public: + Aura(NPCType *type_data, Mob *owner, AuraRecord &record); + ~Aura() { }; + + bool IsAura() const { return true; } + bool Process(); + void Depop(bool unused = false); + + void ProcessOnAllFriendlies(Mob *owner); + void ProcessOnAllGroupMembers(Mob *owner); + void ProcessOnGroupMembersPets(Mob *owner); + void ProcessTotem(Mob *owner); + void ProcessEnterTrap(Mob *owner); + void ProcessExitTrap(Mob *owner); + +private: + int m_owner; + int spell_id; // spell we cast + int distance; // distance we remove + Timer remove_timer; // when we depop + Timer process_timer; // rate limit process calls + Timer cast_timer; // some auras pulse + Timer movement_timer; // rate limit movement updates + AuraType type; + AuraSpawns spawn_type; + AuraMovement movement_type; + + std::function process_func; + std::set casted_on; // we keep track of the other entities we've casted on +}; + +#endif /* !AURA_H */ + diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 22a323a81..4406508fe 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -1460,6 +1460,14 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) newbon->FeignedMinionChance = base1; break; + case SE_AdditionalAura: + newbon->aura_slots += base1; + break; + + case SE_IncreaseTrapCount: + newbon->trap_slots += base1; + break; + // to do case SE_PetDiscipline: break; @@ -3201,6 +3209,16 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne if (new_bonus->FeignedCastOnChance < effect_value) new_bonus->FeignedCastOnChance = effect_value; break; + + case SE_AdditionalAura: + if (new_bonus->aura_slots < effect_value) + new_bonus->aura_slots = effect_value; + break; + + case SE_IncreaseTrapCount: + if (new_bonus->trap_slots < effect_value) + new_bonus->trap_slots = effect_value; + break; //Special custom cases for loading effects on to NPC from 'npc_spels_effects' table if (IsAISpellEffect) { diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 5e1bcc8ca..eb17f4db8 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -383,6 +383,7 @@ void MapOpcodes() ConnectedOpcodes[OP_TributeUpdate] = &Client::Handle_OP_TributeUpdate; ConnectedOpcodes[OP_VetClaimRequest] = &Client::Handle_OP_VetClaimRequest; ConnectedOpcodes[OP_VoiceMacroIn] = &Client::Handle_OP_VoiceMacroIn; + ConnectedOpcodes[OP_UpdateAura] = &Client::Handle_OP_UpdateAura;; ConnectedOpcodes[OP_WearChange] = &Client::Handle_OP_WearChange; ConnectedOpcodes[OP_WhoAllRequest] = &Client::Handle_OP_WhoAllRequest; ConnectedOpcodes[OP_WorldUnknown001] = &Client::Handle_OP_Ignore; @@ -14360,6 +14361,29 @@ void Client::Handle_OP_VoiceMacroIn(const EQApplicationPacket *app) } +void Client::Handle_OP_UpdateAura(const EQApplicationPacket *app) +{ + if (app->size < 4) { + Log(Logs::General, Logs::None, "Size mismatch in OP_UpdateAura"); + return; + } + auto action = *(uint32_t *)app->pBuffer; // action tells us the size + switch (action) { + case 2: + RemoveAllAuras(); + break; + case 1: { + auto ads = (AuraDestory_Struct *)app->pBuffer; + RemoveAura(ads->entity_id); + break; + } + case 0: // client doesn't send this + break; + } + + return; +} + void Client::Handle_OP_WearChange(const EQApplicationPacket *app) { if (app->size != sizeof(WearChange_Struct)) { diff --git a/zone/client_packet.h b/zone/client_packet.h index 76a26e04e..e84f89aaa 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -288,6 +288,7 @@ void Handle_OP_TributeNPC(const EQApplicationPacket *app); void Handle_OP_TributeToggle(const EQApplicationPacket *app); void Handle_OP_TributeUpdate(const EQApplicationPacket *app); + void Handle_OP_UpdateAura(const EQApplicationPacket *app); void Handle_OP_VetClaimRequest(const EQApplicationPacket *app); void Handle_OP_VoiceMacroIn(const EQApplicationPacket *app); void Handle_OP_WearChange(const EQApplicationPacket *app); diff --git a/zone/common.h b/zone/common.h index 7294c87dc..246600c8f 100644 --- a/zone/common.h +++ b/zone/common.h @@ -105,6 +105,8 @@ #define PET_BUTTON_FOCUS 8 #define PET_BUTTON_SPELLHOLD 9 +#define AURA_HARDCAP 2 + typedef enum { //focus types focusSpellHaste = 1, focusSpellDuration, @@ -536,6 +538,8 @@ struct StatBonuses { int16 FeignedCastOnChance; // Percent Value bool PetCommands[PET_MAXCOMMANDS]; // SPA 267 int FeignedMinionChance; // SPA 281 base1 = chance, just like normal FD + int aura_slots; + int trap_slots; }; typedef struct diff --git a/zone/entity.h b/zone/entity.h index 036e0c419..29075d9fe 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -80,6 +80,7 @@ public: virtual bool IsBeacon() const { return false; } virtual bool IsEncounter() const { return false; } virtual bool IsBot() const { return false; } + virtual bool IsAura() const { return false; } virtual bool Process() { return false; } virtual bool Save() { return true; } diff --git a/zone/mob.h b/zone/mob.h index 5292aff00..f95322d36 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -45,6 +45,8 @@ class EQApplicationPacket; class Group; class NPC; class Raid; +class Aura; +struct AuraRecord; struct NewSpawn_Struct; struct PlayerPositionUpdateServer_Struct; @@ -85,6 +87,22 @@ public: int params[MAX_SPECIAL_ATTACK_PARAMS]; }; + struct AuraInfo { + char name[64]; + int spawn_id; + int icon; + AuraInfo() : spawn_id(0), icon(0) + { + memset(name, 0, 64); + } + }; + + struct AuraMgr { + int count; // active auras + AuraInfo auras[AURA_HARDCAP]; + AuraMgr() : count(0) { } + }; + Mob(const char* in_name, const char* in_lastname, int32 in_cur_hp, @@ -308,6 +326,7 @@ public: void BuffProcess(); virtual void DoBuffTic(const Buffs_Struct &buff, int slot, Mob* caster = nullptr); void BuffFadeBySpellID(uint16 spell_id); + void BuffFadeBySpellIDAndCaster(uint16 spell_id, uint16 caster_id); void BuffFadeByEffect(int effectid, int skipslot = -1); void BuffFadeAll(); void BuffFadeNonPersistDeath(); @@ -315,6 +334,7 @@ public: void BuffFadeBySlot(int slot, bool iRecalcBonuses = true); void BuffFadeDetrimentalByCaster(Mob *caster); void BuffFadeBySitModifier(); + bool IsAffectedByBuff(uint16 spell_id); void BuffModifyDurationBySpellID(uint16 spell_id, int32 newDuration); int AddBuff(Mob *caster, const uint16 spell_id, int duration = 0, int32 level_override = -1); int CanBuffStack(uint16 spellid, uint8 caster_level, bool iFailIfOverwrite = false); @@ -604,6 +624,17 @@ public: bool PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, float &z_dest, bool lookForAftArc = true); + void MakeAura(uint16 spell_id); + inline int GetAuraSlots() { return 1 + aabonuses.aura_slots + itembonuses.aura_slots + spellbonuses.aura_slots; } + inline int GetTrapSlots() { return 1 + aabonuses.trap_slots + itembonuses.trap_slots + spellbonuses.trap_slots; } + inline bool HasFreeAuraSlots() { return aura_mgr.count < GetAuraSlots(); } + inline bool HasFreeTrapSlots() { return trap_mgr.count < GetTrapSlots(); } + void AddAura(Aura *aura, AuraRecord &record); + void AddTrap(Aura *aura, AuraRecord &record); + bool CanSpawnAura(bool trap); + void RemoveAura(int spawn_id) {} + void RemoveAllAuras() {} + //Procs void TriggerDefensiveProcs(Mob *on, uint16 hand = EQEmu::inventory::slotPrimary, bool FromSkillProc = false, int damage = 0); bool AddRangedProc(uint16 spell_id, uint16 iChance = 3, uint16 base_spell_id = SPELL_UNKNOWN); @@ -1465,6 +1496,9 @@ protected: bool IsHorse; + AuraMgr aura_mgr; + AuraMgr trap_mgr; + private: void _StopSong(); //this is not what you think it is Mob* target; diff --git a/zone/npc.h b/zone/npc.h index 30e54da55..c973eb79d 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -90,6 +90,7 @@ class Client; class Group; class Raid; class Spawn2; +class Aura; namespace EQEmu { @@ -425,6 +426,7 @@ protected: NPCType* NPCTypedata_ours; //special case for npcs with uniquely created data. friend class EntityList; + friend class Aura; std::list faction_list; uint32 copper; uint32 silver; diff --git a/zone/raids.cpp b/zone/raids.cpp index baf2d71b0..26845910c 100644 --- a/zone/raids.cpp +++ b/zone/raids.cpp @@ -491,6 +491,14 @@ uint32 Raid::GetPlayerIndex(const char *name){ return 0; //should never get to here if we do everything else right, set it to 0 so we never crash things that rely on it. } +uint32 Raid::GetPlayerIndex(Client *c) +{ + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) + if (c == members[i].member) + return i; + return 0xFFFFFFFF; // return sentinel value, make sure you check it unlike the above function +} + Client *Raid::GetClientByIndex(uint16 index) { if(index > MAX_RAID_MEMBERS) diff --git a/zone/raids.h b/zone/raids.h index 232a0057f..df43d7755 100644 --- a/zone/raids.h +++ b/zone/raids.h @@ -142,6 +142,7 @@ public: //keeps me from having to keep iterating through the list //when I want lots of data from the same entry uint32 GetPlayerIndex(const char *name); + uint32 GetPlayerIndex(Client *c); //for perl interface Client *GetClientByIndex(uint16 index); const char *GetClientNameByIndex(uint8 index); diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 7cddf0057..4d3f1ee1d 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -2791,6 +2791,10 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove break; } + case SE_PersistentEffect: + MakeAura(spell_id); + break; + // Handled Elsewhere case SE_ImmuneFleeing: case SE_NegateSpellEffect: diff --git a/zone/spells.cpp b/zone/spells.cpp index 58121a760..d95d328fe 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -4189,6 +4189,21 @@ void Mob::BuffFadeBySpellID(uint16 spell_id) CalcBonuses(); } +void Mob::BuffFadeBySpellIDAndCaster(uint16 spell_id, uint16 caster_id) +{ + bool recalc_bonus = false; + auto buff_count = GetMaxTotalSlots(); + for (int i = 0; i < buff_count; ++i) { + if (buffs[i].spellid == spell_id && buffs[i].casterid == caster_id) { + BuffFadeBySlot(i, false); + recalc_bonus = true; + } + } + + if (recalc_bonus) + CalcBonuses(); +} + // removes buffs containing effectid, skipping skipslot void Mob::BuffFadeByEffect(int effectid, int skipslot) { @@ -4207,6 +4222,16 @@ void Mob::BuffFadeByEffect(int effectid, int skipslot) CalcBonuses(); } +bool Mob::IsAffectedByBuff(uint16 spell_id) +{ + int buff_count = GetMaxTotalSlots(); + for (int i = 0; i < buff_count; ++i) + if (buffs[i].spellid == spell_id) + return true; + + return false; +} + // checks if 'this' can be affected by spell_id from caster // returns true if the spell should fail, false otherwise bool Mob::IsImmuneToSpell(uint16 spell_id, Mob *caster) diff --git a/zone/string_ids.h b/zone/string_ids.h index 65810d576..663d2067a 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -361,6 +361,7 @@ #define GAIN_GROUP_LEADERSHIP_EXP 8788 // #define GAIN_RAID_LEADERSHIP_EXP 8789 // #define BUFF_MINUTES_REMAINING 8799 //%1 (%2 minutes remaining) +#define NO_MORE_TRAPS 9002 //You have already placed your maximum number of traps. #define FEAR_TOO_HIGH 9035 //Your target is too high of a level for your fear spell. #define SLOW_MOSTLY_SUCCESSFUL 9029 //Your spell was mostly successful. #define SLOW_PARTIALLY_SUCCESSFUL 9030 // Your spell was partially successful. @@ -375,6 +376,7 @@ #define SHAKE_OFF_STUN 9077 //You shake off the stun effect! #define STRIKETHROUGH_STRING 9078 //You strike through your opponent's defenses! #define SPELL_REFLECT 9082 //%1's spell has been reflected by %2. +#define NO_MORE_AURAS 9160 //You do not have sufficient focus to maintain that ability. #define NEW_SPELLS_AVAIL 9149 //You have new spells available to you. Check the merchants near your guild master. #define FD_CAST_ON_NO_BREAK 9174 //The strength of your will allows you to resume feigning death. #define SNEAK_RESTRICT 9240 //You can not use this ability because you have not been hidden for long enough. diff --git a/zone/zonedb.h b/zone/zonedb.h index b592be480..50c924b02 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -123,6 +123,18 @@ struct PetRecord { uint32 equipmentset; // default equipment for the pet }; +struct AuraRecord { + uint32 npc_type; + char name[64]; // name shown in UI if shown and spawn name + int spell_id; + int distance; + int aura_type; + int spawn_type; + int movement; + int duration; // seconds some live for 90 mins (normal) others for 2 mins (traps) + int icon; // -1 will use the buffs NEW_ICON +}; + // Actual pet info for a client. struct PetInfo { uint16 SpellID; @@ -404,6 +416,7 @@ public: void AddLootDropToNPC(NPC* npc, uint32 lootdrop_id, ItemList* itemlist, uint8 droplimit, uint8 mindrop); uint32 GetMaxNPCSpellsID(); uint32 GetMaxNPCSpellsEffectsID(); + bool GetAuraEntry(uint16 spell_id, AuraRecord &record); DBnpcspells_Struct* GetNPCSpells(uint32 iDBSpellsID); DBnpcspellseffects_Struct* GetNPCSpellsEffects(uint32 iDBSpellsEffectsID);