diff --git a/changelog.txt b/changelog.txt index f1af1009a..7647ea8ec 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,34 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 08/14/2016 == +mackal: Implement Linked Spell Reuse Timers + - For whatever reason this is a bit unfriendly, but that's how it is on live. + - Titanium is especially unfriendly with large differences in reuse times (ex higher canni and the first 4) + - Unsure when this went live for spells, but canni was at least linked at OoW launch + +== 08/13/2016 == +Kinglykrab: Implemented optional avoidance cap rules. + - Serves to eliminate God-like characters on custom servers with high item stats + - Rule Names: + - Character:EnableAvoidanceCap (default is false) + - Character:AvoidanceCap (default is 750, beyond 1,000 seems to make characters dodge all attacks) + +== 08/02/2016 == +Uleat: Changed 'SendZoneSpawnsBulk' behavior to use near/far criteria (live-like) when sending packets. + - Zone-to-Zone client loading will see a small decrease in time (less than 10~15%) + - World-to-Zone client loading appears to greatly benefit from this (tested 'devastation' - pre-change: ~22 seconds, post-change: 12~15 seconds) + - This change does not affect the final spawning of mobs in the client + +== 07/31/2016 == +mackal: Implement more spell gems! + - There are a few things still left to due like make dealing with losing gems nice (reset AAs, going to an older client etc) + - Sadly SoF disc release doesn't support gem 10 like one might expect :( + - So changed clients: + - SoD = 10 + - UF = 12 + - RoF/RoF2 = 12. I know the UI supports 16, but the client does not and can cause client crashes + - The quest APIs assume you pass a valid spell gem ... + == 07/28/2016 == Uleat: Implemented zone memory-mapped file usage - Zone map files are converted to pre-loaded binary files, bypassing the (sometimes) time-consuming raw data transform process diff --git a/common/emu_constants.h b/common/emu_constants.h index 799161aa8..bf8189acc 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -44,6 +44,26 @@ namespace EQEmu const size_t SayLinkBodySize = RoF2::constants::SayLinkBodySize; } /*constants*/ + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Gem10 = 9, + Gem11 = 10, + Gem12 = 11, + MaxGems = 12, + Ability = 20, // HT/LoH for Tit + PotionBelt = 21, // Tit uses a different slot for PB + Item = 22, + Discipline = 23, + AltAbility = 0xFF + }; } /*EQEmu*/ diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 8b913095b..6b7c58841 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -289,6 +289,7 @@ N(OP_LFGuild), N(OP_LFPCommand), N(OP_LFPGetMatchesRequest), N(OP_LFPGetMatchesResponse), +N(OP_LinkedReuse), N(OP_LoadSpellSet), N(OP_LocInfo), N(OP_LockoutTimerInfo), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 3252d07cc..a1b614b97 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -30,6 +30,7 @@ static const uint32 BUFF_COUNT = 25; +static const uint32 PET_BUFF_COUNT = 30; static const uint32 MAX_MERC = 100; static const uint32 MAX_MERC_GRADES = 10; static const uint32 MAX_MERC_STANCES = 10; @@ -386,7 +387,19 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -uint32 unknown12; +uint32 reduction; // lower reuse +}; + +/* +** Linked Spell Reuse Timer +** Length: 12 +** Comes before the OP_Memorize +** Live (maybe TDS steam) has an extra DWORD after timer_id +*/ +struct LinkedSpellReuseTimer_Struct { + uint32 timer_id; // Timer ID of the spell + uint32 end_time; // timestamp of when it will be ready + uint32 start_time; // timestamp of when it started }; /* @@ -419,10 +432,11 @@ struct DeleteSpell_Struct struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like }; struct SwapSpell_Struct @@ -522,8 +536,8 @@ struct BuffRemoveRequest_Struct struct PetBuff_Struct { /*000*/ uint32 petid; -/*004*/ uint32 spellid[BUFF_COUNT+5]; -/*124*/ int32 ticsremaining[BUFF_COUNT+5]; +/*004*/ uint32 spellid[PET_BUFF_COUNT]; +/*124*/ int32 ticsremaining[PET_BUFF_COUNT]; /*244*/ uint32 buffcount; }; @@ -834,7 +848,7 @@ struct SuspendedMinion_Struct */ static const uint32 MAX_PP_LANGUAGE = 28; static const uint32 MAX_PP_SPELLBOOK = 480; // Set for all functions -static const uint32 MAX_PP_MEMSPELL = 9; // Set to latest client so functions can work right +static const uint32 MAX_PP_MEMSPELL = static_cast(EQEmu::CastingSlot::MaxGems); // Set to latest client so functions can work right -- 12 static const uint32 MAX_PP_REF_SPELLBOOK = 480; // Set for Player Profile size retain static const uint32 MAX_PP_REF_MEMSPELL = 9; // Set for Player Profile size retain @@ -915,7 +929,7 @@ struct PlayerProfile_Struct /*0245*/ uint8 guildbanker; /*0246*/ uint8 unknown0246[6]; // /*0252*/ uint32 intoxication; -/*0256*/ uint32 spellSlotRefresh[MAX_PP_REF_MEMSPELL]; //in ms +/*0256*/ uint32 spellSlotRefresh[MAX_PP_MEMSPELL]; //in ms /*0292*/ uint32 abilitySlotRefresh; /*0296*/ uint8 haircolor; // Player hair color /*0297*/ uint8 beardcolor; // Player beard color @@ -956,7 +970,7 @@ struct PlayerProfile_Struct /*2580*/ uint8 unknown2616[4]; /*2584*/ uint32 spell_book[MAX_PP_REF_SPELLBOOK]; /*4504*/ uint8 unknown4540[128]; // Was [428] all 0xff -/*4632*/ uint32 mem_spells[MAX_PP_REF_MEMSPELL]; +/*4632*/ uint32 mem_spells[MAX_PP_MEMSPELL]; /*4668*/ uint8 unknown4704[32]; // /*4700*/ float y; // Player y position /*4704*/ float x; // Player x position diff --git a/common/item.cpp b/common/item.cpp index 41e3d86c3..61e7f926a 100644 --- a/common/item.cpp +++ b/common/item.cpp @@ -1863,6 +1863,9 @@ uint8 ItemInst::FirstOpenSlot() const uint8 ItemInst::GetTotalItemCount() const { + if (!m_item) + return 0; + uint8 item_count = 1; if (m_item && !m_item->IsClassBag()) { return item_count; } diff --git a/common/patches/rof.cpp b/common/patches/rof.cpp index 4bf8ed63d..939ad03fa 100644 --- a/common/patches/rof.cpp +++ b/common/patches/rof.cpp @@ -63,6 +63,9 @@ namespace RoF // client to server text link converter static inline void RoFToServerTextLink(std::string& serverTextLink, const std::string& rofTextLink); + static inline CastingSlot ServerToRoFCastingSlot(EQEmu::CastingSlot slot); + static inline EQEmu::CastingSlot RoFToServerCastingSlot(CastingSlot slot); + void Register(EQStreamIdentifier &into) { //create our opcode manager if we havent already @@ -507,10 +510,7 @@ namespace RoF ENCODE_LENGTH_EXACT(CastSpell_Struct); SETUP_DIRECT_ENCODE(CastSpell_Struct, structs::CastSpell_Struct); - if (emu->slot == 10) - eq->slot = 13; - else - OUT(slot); + eq->slot = static_cast(ServerToRoFCastingSlot(static_cast(emu->slot))); OUT(spell_id); eq->inventory_slot = ServerToRoFSlot(emu->inventoryslot); @@ -1623,7 +1623,8 @@ namespace RoF OUT(new_mana); OUT(stamina); OUT(spell_id); - eq->unknown16 = -1; // Self Interrupt/Success = -1, Fizzle = 1, Other Interrupt = 2? + OUT(keepcasting); + eq->slot = -1; // this is spell gem slot. It's -1 in normal operation FINISH_ENCODE(); } @@ -2012,18 +2013,18 @@ namespace RoF __packet->WriteUInt8(1); // 1 indicates all buffs on the pet (0 to add or remove a single buff) __packet->WriteUInt16(emu->buffcount); - for (uint16 i = 0; i < BUFF_COUNT; ++i) + for (uint16 i = 0; i < PET_BUFF_COUNT; ++i) { if (emu->spellid[i]) { __packet->WriteUInt32(i); __packet->WriteUInt32(emu->spellid[i]); __packet->WriteUInt32(emu->ticsremaining[i]); - __packet->WriteUInt32(0); // Unknown + __packet->WriteUInt32(0); // numhits __packet->WriteString(""); } } - __packet->WriteUInt8(0); // Unknown + __packet->WriteUInt8(0); // some sort of type FINISH_ENCODE(); } @@ -2259,22 +2260,23 @@ namespace RoF outapp->WriteUInt32(structs::MAX_PP_MEMSPELL); // Memorised spell slots - for (uint32 r = 0; r < MAX_PP_MEMSPELL; r++) + for (uint32 r = 0; r < MAX_PP_MEMSPELL; r++) // first 12 { outapp->WriteUInt32(emu->mem_spells[r]); } - // zeroes for the rest of the slots + // zeroes for the rest of the slots -- the other 4 which don't work at all! for (uint32 r = 0; r < structs::MAX_PP_MEMSPELL - MAX_PP_MEMSPELL; r++) { outapp->WriteUInt32(0xFFFFFFFFU); } - outapp->WriteUInt32(13); // Unknown count + outapp->WriteUInt32(13); // gem refresh count - for (uint32 r = 0; r < 13; r++) + for (uint32 r = 0; r < MAX_PP_MEMSPELL; r++) { - outapp->WriteUInt32(0); // Unknown + outapp->WriteUInt32(emu->spellSlotRefresh[r]); // spell gem refresh } + outapp->WriteUInt32(0); // also refresh -- historically HT/LoH :P outapp->WriteUInt8(0); // Unknown @@ -4334,10 +4336,7 @@ namespace RoF DECODE_LENGTH_EXACT(structs::CastSpell_Struct); SETUP_DIRECT_DECODE(CastSpell_Struct, structs::CastSpell_Struct); - if (eq->slot == 13) - emu->slot = 10; - else - IN(slot); + emu->slot = static_cast(RoFToServerCastingSlot(static_cast(eq->slot))); IN(spell_id); emu->inventoryslot = RoFToServerSlot(eq->inventory_slot); @@ -6009,4 +6008,80 @@ namespace RoF } } + static inline CastingSlot ServerToRoFCastingSlot(EQEmu::CastingSlot slot) + { + switch (slot) { + case EQEmu::CastingSlot::Gem1: + return CastingSlot::Gem1; + case EQEmu::CastingSlot::Gem2: + return CastingSlot::Gem2; + case EQEmu::CastingSlot::Gem3: + return CastingSlot::Gem3; + case EQEmu::CastingSlot::Gem4: + return CastingSlot::Gem4; + case EQEmu::CastingSlot::Gem5: + return CastingSlot::Gem5; + case EQEmu::CastingSlot::Gem6: + return CastingSlot::Gem6; + case EQEmu::CastingSlot::Gem7: + return CastingSlot::Gem7; + case EQEmu::CastingSlot::Gem8: + return CastingSlot::Gem8; + case EQEmu::CastingSlot::Gem9: + return CastingSlot::Gem9; + case EQEmu::CastingSlot::Gem10: + return CastingSlot::Gem10; + case EQEmu::CastingSlot::Gem11: + return CastingSlot::Gem11; + case EQEmu::CastingSlot::Gem12: + return CastingSlot::Gem12; + case EQEmu::CastingSlot::Item: + case EQEmu::CastingSlot::PotionBelt: + return CastingSlot::Item; + case EQEmu::CastingSlot::Discipline: + return CastingSlot::Discipline; + case EQEmu::CastingSlot::AltAbility: + return CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return CastingSlot::Discipline; + } + } + + static inline EQEmu::CastingSlot RoFToServerCastingSlot(CastingSlot slot) + { + switch (slot) { + case CastingSlot::Gem1: + return EQEmu::CastingSlot::Gem1; + case CastingSlot::Gem2: + return EQEmu::CastingSlot::Gem2; + case CastingSlot::Gem3: + return EQEmu::CastingSlot::Gem3; + case CastingSlot::Gem4: + return EQEmu::CastingSlot::Gem4; + case CastingSlot::Gem5: + return EQEmu::CastingSlot::Gem5; + case CastingSlot::Gem6: + return EQEmu::CastingSlot::Gem6; + case CastingSlot::Gem7: + return EQEmu::CastingSlot::Gem7; + case CastingSlot::Gem8: + return EQEmu::CastingSlot::Gem8; + case CastingSlot::Gem9: + return EQEmu::CastingSlot::Gem9; + case CastingSlot::Gem10: + return EQEmu::CastingSlot::Gem10; + case CastingSlot::Gem11: + return EQEmu::CastingSlot::Gem11; + case CastingSlot::Gem12: + return EQEmu::CastingSlot::Gem12; + case CastingSlot::Discipline: + return EQEmu::CastingSlot::Discipline; + case CastingSlot::Item: + return EQEmu::CastingSlot::Item; + case CastingSlot::AltAbility: + return EQEmu::CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return EQEmu::CastingSlot::Discipline; + } + } } /*RoF*/ diff --git a/common/patches/rof.h b/common/patches/rof.h index 205998b32..146b34f1f 100644 --- a/common/patches/rof.h +++ b/common/patches/rof.h @@ -50,6 +50,24 @@ namespace RoF #include "rof_ops.h" }; + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Gem10 = 9, + Gem11 = 10, + Gem12 = 11, + Item = 12, + Discipline = 13, + AltAbility = 0xFF + }; + }; /*RoF*/ #endif /*COMMON_ROF_H*/ diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 41d34a50a..118419f48 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -63,6 +63,9 @@ namespace RoF2 // client to server text link converter static inline void RoF2ToServerTextLink(std::string& serverTextLink, const std::string& rof2TextLink); + static inline CastingSlot ServerToRoF2CastingSlot(EQEmu::CastingSlot slot); + static inline EQEmu::CastingSlot RoF2ToServerCastingSlot(CastingSlot slot); + void Register(EQStreamIdentifier &into) { //create our opcode manager if we havent already @@ -584,10 +587,7 @@ namespace RoF2 ENCODE_LENGTH_EXACT(CastSpell_Struct); SETUP_DIRECT_ENCODE(CastSpell_Struct, structs::CastSpell_Struct); - if (emu->slot == 10) - eq->slot = 13; - else - OUT(slot); + eq->slot = static_cast(ServerToRoF2CastingSlot(static_cast(emu->slot))); OUT(spell_id); eq->inventory_slot = ServerToRoF2Slot(emu->inventoryslot); @@ -1701,7 +1701,8 @@ namespace RoF2 OUT(new_mana); OUT(stamina); OUT(spell_id); - eq->unknown16 = -1; // Self Interrupt/Success = -1, Fizzle = 1, Other Interrupt = 2? + OUT(keepcasting); + eq->slot = -1; // this is spell gem slot. It's -1 in normal operation FINISH_ENCODE(); } @@ -2099,18 +2100,18 @@ namespace RoF2 __packet->WriteUInt8(1); // 1 indicates all buffs on the pet (0 to add or remove a single buff) __packet->WriteUInt16(emu->buffcount); - for (uint16 i = 0; i < BUFF_COUNT; ++i) + for (uint16 i = 0; i < PET_BUFF_COUNT; ++i) { if (emu->spellid[i]) { __packet->WriteUInt32(i); __packet->WriteUInt32(emu->spellid[i]); __packet->WriteUInt32(emu->ticsremaining[i]); - __packet->WriteUInt32(0); // Unknown + __packet->WriteUInt32(0); // num hits __packet->WriteString(""); } } - __packet->WriteUInt8(0); // Unknown + __packet->WriteUInt8(0); // some sort of type FINISH_ENCODE(); } @@ -2346,22 +2347,23 @@ namespace RoF2 outapp->WriteUInt32(structs::MAX_PP_MEMSPELL); // Memorised spell slots - for (uint32 r = 0; r < MAX_PP_MEMSPELL; r++) + for (uint32 r = 0; r < MAX_PP_MEMSPELL; r++) // write first 12 { outapp->WriteUInt32(emu->mem_spells[r]); } - // zeroes for the rest of the slots + // zeroes for the rest of the slots the other 4, which actually don't work on the client at all :D for (uint32 r = 0; r < structs::MAX_PP_MEMSPELL - MAX_PP_MEMSPELL; r++) { outapp->WriteUInt32(0xFFFFFFFFU); } - outapp->WriteUInt32(13); // Unknown count + outapp->WriteUInt32(13); // gem refresh counts - for (uint32 r = 0; r < 13; r++) + for (uint32 r = 0; r < MAX_PP_MEMSPELL; r++) { - outapp->WriteUInt32(0); // Unknown + outapp->WriteUInt32(emu->spellSlotRefresh[r]); // spell gem refresh } + outapp->WriteUInt32(0); // also refresh -- historically HT/LoH :P outapp->WriteUInt8(0); // Unknown @@ -4574,10 +4576,7 @@ namespace RoF2 DECODE_LENGTH_EXACT(structs::CastSpell_Struct); SETUP_DIRECT_DECODE(CastSpell_Struct, structs::CastSpell_Struct); - if (eq->slot == 13) - emu->slot = 10; - else - IN(slot); + emu->slot = static_cast(RoF2ToServerCastingSlot(static_cast(eq->slot))); IN(spell_id); emu->inventoryslot = RoF2ToServerSlot(eq->inventory_slot); @@ -6314,4 +6313,80 @@ namespace RoF2 } } + static inline CastingSlot ServerToRoF2CastingSlot(EQEmu::CastingSlot slot) + { + switch (slot) { + case EQEmu::CastingSlot::Gem1: + return CastingSlot::Gem1; + case EQEmu::CastingSlot::Gem2: + return CastingSlot::Gem2; + case EQEmu::CastingSlot::Gem3: + return CastingSlot::Gem3; + case EQEmu::CastingSlot::Gem4: + return CastingSlot::Gem4; + case EQEmu::CastingSlot::Gem5: + return CastingSlot::Gem5; + case EQEmu::CastingSlot::Gem6: + return CastingSlot::Gem6; + case EQEmu::CastingSlot::Gem7: + return CastingSlot::Gem7; + case EQEmu::CastingSlot::Gem8: + return CastingSlot::Gem8; + case EQEmu::CastingSlot::Gem9: + return CastingSlot::Gem9; + case EQEmu::CastingSlot::Gem10: + return CastingSlot::Gem10; + case EQEmu::CastingSlot::Gem11: + return CastingSlot::Gem11; + case EQEmu::CastingSlot::Gem12: + return CastingSlot::Gem12; + case EQEmu::CastingSlot::Item: + case EQEmu::CastingSlot::PotionBelt: + return CastingSlot::Item; + case EQEmu::CastingSlot::Discipline: + return CastingSlot::Discipline; + case EQEmu::CastingSlot::AltAbility: + return CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return CastingSlot::Discipline; + } + } + + static inline EQEmu::CastingSlot RoF2ToServerCastingSlot(CastingSlot slot) + { + switch (slot) { + case CastingSlot::Gem1: + return EQEmu::CastingSlot::Gem1; + case CastingSlot::Gem2: + return EQEmu::CastingSlot::Gem2; + case CastingSlot::Gem3: + return EQEmu::CastingSlot::Gem3; + case CastingSlot::Gem4: + return EQEmu::CastingSlot::Gem4; + case CastingSlot::Gem5: + return EQEmu::CastingSlot::Gem5; + case CastingSlot::Gem6: + return EQEmu::CastingSlot::Gem6; + case CastingSlot::Gem7: + return EQEmu::CastingSlot::Gem7; + case CastingSlot::Gem8: + return EQEmu::CastingSlot::Gem8; + case CastingSlot::Gem9: + return EQEmu::CastingSlot::Gem9; + case CastingSlot::Gem10: + return EQEmu::CastingSlot::Gem10; + case CastingSlot::Gem11: + return EQEmu::CastingSlot::Gem11; + case CastingSlot::Gem12: + return EQEmu::CastingSlot::Gem12; + case CastingSlot::Discipline: + return EQEmu::CastingSlot::Discipline; + case CastingSlot::Item: + return EQEmu::CastingSlot::Item; + case CastingSlot::AltAbility: + return EQEmu::CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return EQEmu::CastingSlot::Discipline; + } + } } /*RoF2*/ diff --git a/common/patches/rof2.h b/common/patches/rof2.h index 9eef8e596..4fb94a1d4 100644 --- a/common/patches/rof2.h +++ b/common/patches/rof2.h @@ -50,6 +50,24 @@ namespace RoF2 #include "rof2_ops.h" }; + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Gem10 = 9, + Gem11 = 10, + Gem12 = 11, + Item = 12, + Discipline = 13, + AltAbility = 0xFF + }; + }; /*RoF2*/ #endif /*COMMON_ROF2_H*/ diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 61cbb0d0b..ae4d2c799 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -635,7 +635,7 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -uint32 unknown12; +uint32 reduction; // lowers reuse }; /* @@ -668,11 +668,12 @@ struct DeleteSpell_Struct struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; - uint32 unknown16; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like +/*16*/ int32 slot; // -1 for normal usage slot for when we want silent interrupt? I think it does timer stuff or something. Linked Spell Reuse interrupt uses it }; struct SwapSpell_Struct diff --git a/common/patches/rof_structs.h b/common/patches/rof_structs.h index 9c5aa8ab8..74b47c09e 100644 --- a/common/patches/rof_structs.h +++ b/common/patches/rof_structs.h @@ -624,7 +624,7 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -uint32 unknown12; +uint32 reduction; // lowers reuse }; /* @@ -657,11 +657,12 @@ struct DeleteSpell_Struct struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; - uint32 unknown16; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like +/*16*/ int32 slot; // -1 for normal usage slot for when we want silent interrupt? I think it does timer stuff or something. Linked Spell Reuse interrupt uses it }; struct SwapSpell_Struct diff --git a/common/patches/sod.cpp b/common/patches/sod.cpp index c148a02be..e23c1ca78 100644 --- a/common/patches/sod.cpp +++ b/common/patches/sod.cpp @@ -59,6 +59,9 @@ namespace SoD // client to server text link converter static inline void SoDToServerTextLink(std::string& serverTextLink, const std::string& sodTextLink); + static inline CastingSlot ServerToSoDCastingSlot(EQEmu::CastingSlot slot); + static inline EQEmu::CastingSlot SoDToServerCastingSlot(CastingSlot slot); + void Register(EQStreamIdentifier &into) { //create our opcode manager if we havent already @@ -1136,7 +1139,8 @@ namespace SoD OUT(new_mana); OUT(stamina); OUT(spell_id); - eq->unknown16 = -1; // Self Interrupt/Success = -1, Fizzle = 1, Other Interrupt = 2? + OUT(keepcasting); + eq->slot = -1; // this is spell gem slot. It's -1 in normal operation FINISH_ENCODE(); } @@ -1499,7 +1503,7 @@ namespace SoD VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->petid); VARSTRUCT_ENCODE_TYPE(uint16, Buffer, emu->buffcount); - for (unsigned int i = 0; i < BUFF_COUNT; ++i) + for (unsigned int i = 0; i < PET_BUFF_COUNT; ++i) { if (emu->spellid[i]) { @@ -1510,7 +1514,7 @@ namespace SoD } } - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->buffcount); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->buffcount); // I think this is actually some sort of type delete[] __emu_buffer; dest->FastQueuePacket(&in, ack_req); @@ -2948,6 +2952,7 @@ namespace SoD DECODE_LENGTH_EXACT(structs::CastSpell_Struct); SETUP_DIRECT_DECODE(CastSpell_Struct, structs::CastSpell_Struct); + emu->slot = static_cast(SoDToServerCastingSlot(static_cast(eq->slot))); IN(slot); IN(spell_id); emu->inventoryslot = SoDToServerSlot(eq->inventoryslot); @@ -4013,4 +4018,72 @@ namespace SoD } } + static inline CastingSlot ServerToSoDCastingSlot(EQEmu::CastingSlot slot) + { + switch (slot) { + case EQEmu::CastingSlot::Gem1: + return CastingSlot::Gem1; + case EQEmu::CastingSlot::Gem2: + return CastingSlot::Gem2; + case EQEmu::CastingSlot::Gem3: + return CastingSlot::Gem3; + case EQEmu::CastingSlot::Gem4: + return CastingSlot::Gem4; + case EQEmu::CastingSlot::Gem5: + return CastingSlot::Gem5; + case EQEmu::CastingSlot::Gem6: + return CastingSlot::Gem6; + case EQEmu::CastingSlot::Gem7: + return CastingSlot::Gem7; + case EQEmu::CastingSlot::Gem8: + return CastingSlot::Gem8; + case EQEmu::CastingSlot::Gem9: + return CastingSlot::Gem9; + case EQEmu::CastingSlot::Gem10: + return CastingSlot::Gem10; + case EQEmu::CastingSlot::Item: + case EQEmu::CastingSlot::PotionBelt: + return CastingSlot::Item; + case EQEmu::CastingSlot::Discipline: + return CastingSlot::Discipline; + case EQEmu::CastingSlot::AltAbility: + return CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return CastingSlot::Discipline; + } + } + + static inline EQEmu::CastingSlot SoDToServerCastingSlot(CastingSlot slot) + { + switch (slot) { + case CastingSlot::Gem1: + return EQEmu::CastingSlot::Gem1; + case CastingSlot::Gem2: + return EQEmu::CastingSlot::Gem2; + case CastingSlot::Gem3: + return EQEmu::CastingSlot::Gem3; + case CastingSlot::Gem4: + return EQEmu::CastingSlot::Gem4; + case CastingSlot::Gem5: + return EQEmu::CastingSlot::Gem5; + case CastingSlot::Gem6: + return EQEmu::CastingSlot::Gem6; + case CastingSlot::Gem7: + return EQEmu::CastingSlot::Gem7; + case CastingSlot::Gem8: + return EQEmu::CastingSlot::Gem8; + case CastingSlot::Gem9: + return EQEmu::CastingSlot::Gem9; + case CastingSlot::Gem10: + return EQEmu::CastingSlot::Gem10; + case CastingSlot::Discipline: + return EQEmu::CastingSlot::Discipline; + case CastingSlot::Item: + return EQEmu::CastingSlot::Item; + case CastingSlot::AltAbility: + return EQEmu::CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return EQEmu::CastingSlot::Discipline; + } + } } /*SoD*/ diff --git a/common/patches/sod.h b/common/patches/sod.h index 977645231..5c29e36c0 100644 --- a/common/patches/sod.h +++ b/common/patches/sod.h @@ -50,6 +50,22 @@ namespace SoD #include "sod_ops.h" }; + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Gem10 = 9, + Item = 10, + Discipline = 11, + AltAbility = 0xFF + }; + }; /*SoD*/ #endif /*COMMON_SOD_H*/ diff --git a/common/patches/sod_structs.h b/common/patches/sod_structs.h index 191d048e9..6a73b7283 100644 --- a/common/patches/sod_structs.h +++ b/common/patches/sod_structs.h @@ -479,7 +479,7 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -//uint32 unknown12; +uint32 reduction; // lowers reuse }; /* @@ -512,11 +512,12 @@ struct DeleteSpell_Struct struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; - uint32 unknown16; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like +/*16*/ int32 slot; // -1 for normal usage slot for when we want silent interrupt? I think it does timer stuff or something. Linked Spell Reuse interrupt uses it }; struct SwapSpell_Struct diff --git a/common/patches/sof.cpp b/common/patches/sof.cpp index 7984debd9..3ab4a7631 100644 --- a/common/patches/sof.cpp +++ b/common/patches/sof.cpp @@ -59,6 +59,9 @@ namespace SoF // client to server text link converter static inline void SoFToServerTextLink(std::string& serverTextLink, const std::string& sofTextLink); + static inline CastingSlot ServerToSoFCastingSlot(EQEmu::CastingSlot slot); + static inline EQEmu::CastingSlot SoFToServerCastingSlot(CastingSlot slot, uint32 itemlocation); + void Register(EQStreamIdentifier &into) { //create our opcode manager if we havent already @@ -933,7 +936,24 @@ namespace SoF OUT(new_mana); OUT(stamina); OUT(spell_id); - eq->unknown16 = -1; // Self Interrupt/Success = -1, Fizzle = 1, Other Interrupt = 2? + OUT(keepcasting); + eq->slot = -1; // this is spell gem slot. It's -1 in normal operation + + FINISH_ENCODE(); + } + + ENCODE(OP_MemorizeSpell) + { + ENCODE_LENGTH_EXACT(MemorizeSpell_Struct); + SETUP_DIRECT_ENCODE(MemorizeSpell_Struct, structs::MemorizeSpell_Struct); + + // Since HT/LoH are translated up, we need to translate down only for memSpellSpellbar case + if (emu->scribing == 3) + eq->slot = static_cast(ServerToSoFCastingSlot(static_cast(emu->slot))); + else + OUT(slot); + OUT(spell_id); + OUT(scribing); FINISH_ENCODE(); } @@ -1158,9 +1178,9 @@ namespace SoF OUT(petid); OUT(buffcount); - int EQBuffSlot = 0; + int EQBuffSlot = 0; // do we really want to shuffle them around like this? - for (uint32 EmuBuffSlot = 0; EmuBuffSlot < BUFF_COUNT; ++EmuBuffSlot) + for (uint32 EmuBuffSlot = 0; EmuBuffSlot < PET_BUFF_COUNT; ++EmuBuffSlot) { if (emu->spellid[EmuBuffSlot]) { @@ -2366,7 +2386,7 @@ namespace SoF DECODE_LENGTH_EXACT(structs::CastSpell_Struct); SETUP_DIRECT_DECODE(CastSpell_Struct, structs::CastSpell_Struct); - IN(slot); + emu->slot = static_cast(SoFToServerCastingSlot(static_cast(eq->slot), eq->inventoryslot)); IN(spell_id); emu->inventoryslot = SoFToServerSlot(eq->inventoryslot); IN(target_id); @@ -3355,4 +3375,75 @@ namespace SoF } } + static inline CastingSlot ServerToSoFCastingSlot(EQEmu::CastingSlot slot) + { + switch (slot) { + case EQEmu::CastingSlot::Gem1: + return CastingSlot::Gem1; + case EQEmu::CastingSlot::Gem2: + return CastingSlot::Gem2; + case EQEmu::CastingSlot::Gem3: + return CastingSlot::Gem3; + case EQEmu::CastingSlot::Gem4: + return CastingSlot::Gem4; + case EQEmu::CastingSlot::Gem5: + return CastingSlot::Gem5; + case EQEmu::CastingSlot::Gem6: + return CastingSlot::Gem6; + case EQEmu::CastingSlot::Gem7: + return CastingSlot::Gem7; + case EQEmu::CastingSlot::Gem8: + return CastingSlot::Gem8; + case EQEmu::CastingSlot::Gem9: + return CastingSlot::Gem9; + case EQEmu::CastingSlot::Item: + return CastingSlot::Item; + case EQEmu::CastingSlot::PotionBelt: + return CastingSlot::PotionBelt; + case EQEmu::CastingSlot::Discipline: + return CastingSlot::Discipline; + case EQEmu::CastingSlot::AltAbility: + return CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return CastingSlot::Discipline; + } + } + + static inline EQEmu::CastingSlot SoFToServerCastingSlot(CastingSlot slot, uint32 itemlocation) + { + switch (slot) { + case CastingSlot::Gem1: + return EQEmu::CastingSlot::Gem1; + case CastingSlot::Gem2: + return EQEmu::CastingSlot::Gem2; + case CastingSlot::Gem3: + return EQEmu::CastingSlot::Gem3; + case CastingSlot::Gem4: + return EQEmu::CastingSlot::Gem4; + case CastingSlot::Gem5: + return EQEmu::CastingSlot::Gem5; + case CastingSlot::Gem6: + return EQEmu::CastingSlot::Gem6; + case CastingSlot::Gem7: + return EQEmu::CastingSlot::Gem7; + case CastingSlot::Gem8: + return EQEmu::CastingSlot::Gem8; + case CastingSlot::Gem9: + return EQEmu::CastingSlot::Gem9; + case CastingSlot::Ability: + return EQEmu::CastingSlot::Ability; + // Tit uses 10 for item and discipline casting, but items have a valid location + case CastingSlot::Item: + if (itemlocation == INVALID_INDEX) + return EQEmu::CastingSlot::Discipline; + else + return EQEmu::CastingSlot::Item; + case CastingSlot::PotionBelt: + return EQEmu::CastingSlot::PotionBelt; + case CastingSlot::AltAbility: + return EQEmu::CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return EQEmu::CastingSlot::Discipline; + } + } } /*SoF*/ diff --git a/common/patches/sof.h b/common/patches/sof.h index dde55f9a3..47c023471 100644 --- a/common/patches/sof.h +++ b/common/patches/sof.h @@ -50,6 +50,23 @@ namespace SoF #include "sof_ops.h" }; + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Ability = 9, + Item = 10, + Discipline = 10, + PotionBelt = 11, + AltAbility = 0xFF + }; + }; /*SoF*/ #endif /*COMMON_SOF_H*/ diff --git a/common/patches/sof_ops.h b/common/patches/sof_ops.h index 01caf8674..68ff402f1 100644 --- a/common/patches/sof_ops.h +++ b/common/patches/sof_ops.h @@ -57,6 +57,7 @@ E(OP_LeadershipExpUpdate) E(OP_LogServer) E(OP_LootItem) E(OP_ManaChange) +E(OP_MemorizeSpell) E(OP_MoveItem) E(OP_NewSpawn) E(OP_NewZone) diff --git a/common/patches/sof_structs.h b/common/patches/sof_structs.h index 86dc91e14..0256132bd 100644 --- a/common/patches/sof_structs.h +++ b/common/patches/sof_structs.h @@ -458,7 +458,7 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -//uint32 unknown12; +uint32 reduction; // lowers reuse }; /* @@ -491,11 +491,12 @@ struct DeleteSpell_Struct struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; - uint32 unknown16; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like +/*16*/ int32 slot; // -1 for normal usage slot for when we want silent interrupt? I think it does timer stuff or something. Linked Spell Reuse interrupt uses it }; struct SwapSpell_Struct diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp index 98ece403c..cc10f6487 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -58,6 +58,9 @@ namespace Titanium // client to server text link converter static inline void TitaniumToServerTextLink(std::string& serverTextLink, const std::string& titaniumTextLink); + static inline CastingSlot ServerToTitaniumCastingSlot(EQEmu::CastingSlot slot); + static inline EQEmu::CastingSlot TitaniumToServerCastingSlot(CastingSlot slot, uint32 itemlocation); + void Register(EQStreamIdentifier &into) { auto Config = EQEmuConfig::get(); @@ -833,6 +836,22 @@ namespace Titanium FINISH_ENCODE(); } + ENCODE(OP_MemorizeSpell) + { + ENCODE_LENGTH_EXACT(MemorizeSpell_Struct); + SETUP_DIRECT_ENCODE(MemorizeSpell_Struct, structs::MemorizeSpell_Struct); + + // Since HT/LoH are translated up, we need to translate down only for memSpellSpellbar case + if (emu->scribing == 3) + eq->slot = static_cast(ServerToTitaniumCastingSlot(static_cast(emu->slot))); + else + OUT(slot); + OUT(spell_id); + OUT(scribing); + + FINISH_ENCODE(); + } + ENCODE(OP_MoveItem) { ENCODE_LENGTH_EXACT(MoveItem_Struct); @@ -872,9 +891,9 @@ namespace Titanium OUT(petid); OUT(buffcount); - int EQBuffSlot = 0; + int EQBuffSlot = 0; // do we really want to shuffle them around like this? - for (uint32 EmuBuffSlot = 0; EmuBuffSlot < BUFF_COUNT; ++EmuBuffSlot) + for (uint32 EmuBuffSlot = 0; EmuBuffSlot < PET_BUFF_COUNT; ++EmuBuffSlot) { if (emu->spellid[EmuBuffSlot]) { @@ -1731,7 +1750,7 @@ namespace Titanium DECODE_LENGTH_EXACT(structs::CastSpell_Struct); SETUP_DIRECT_DECODE(CastSpell_Struct, structs::CastSpell_Struct); - IN(slot); + emu->slot = static_cast(TitaniumToServerCastingSlot(static_cast(eq->slot), eq->inventoryslot)); IN(spell_id); emu->inventoryslot = TitaniumToServerSlot(eq->inventoryslot); IN(target_id); @@ -2487,4 +2506,75 @@ namespace Titanium } } + static inline CastingSlot ServerToTitaniumCastingSlot(EQEmu::CastingSlot slot) + { + switch (slot) { + case EQEmu::CastingSlot::Gem1: + return CastingSlot::Gem1; + case EQEmu::CastingSlot::Gem2: + return CastingSlot::Gem2; + case EQEmu::CastingSlot::Gem3: + return CastingSlot::Gem3; + case EQEmu::CastingSlot::Gem4: + return CastingSlot::Gem4; + case EQEmu::CastingSlot::Gem5: + return CastingSlot::Gem5; + case EQEmu::CastingSlot::Gem6: + return CastingSlot::Gem6; + case EQEmu::CastingSlot::Gem7: + return CastingSlot::Gem7; + case EQEmu::CastingSlot::Gem8: + return CastingSlot::Gem8; + case EQEmu::CastingSlot::Gem9: + return CastingSlot::Gem9; + case EQEmu::CastingSlot::Item: + return CastingSlot::Item; + case EQEmu::CastingSlot::PotionBelt: + return CastingSlot::PotionBelt; + case EQEmu::CastingSlot::Discipline: + return CastingSlot::Discipline; + case EQEmu::CastingSlot::AltAbility: + return CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return CastingSlot::Discipline; + } + } + + static inline EQEmu::CastingSlot TitaniumToServerCastingSlot(CastingSlot slot, uint32 itemlocation) + { + switch (slot) { + case CastingSlot::Gem1: + return EQEmu::CastingSlot::Gem1; + case CastingSlot::Gem2: + return EQEmu::CastingSlot::Gem2; + case CastingSlot::Gem3: + return EQEmu::CastingSlot::Gem3; + case CastingSlot::Gem4: + return EQEmu::CastingSlot::Gem4; + case CastingSlot::Gem5: + return EQEmu::CastingSlot::Gem5; + case CastingSlot::Gem6: + return EQEmu::CastingSlot::Gem6; + case CastingSlot::Gem7: + return EQEmu::CastingSlot::Gem7; + case CastingSlot::Gem8: + return EQEmu::CastingSlot::Gem8; + case CastingSlot::Gem9: + return EQEmu::CastingSlot::Gem9; + case CastingSlot::Ability: + return EQEmu::CastingSlot::Ability; + // Tit uses 10 for item and discipline casting, but items have a valid location + case CastingSlot::Item: + if (itemlocation == INVALID_INDEX) + return EQEmu::CastingSlot::Discipline; + else + return EQEmu::CastingSlot::Item; + case CastingSlot::PotionBelt: + return EQEmu::CastingSlot::PotionBelt; + case CastingSlot::AltAbility: + return EQEmu::CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return EQEmu::CastingSlot::Discipline; + } + } } /*Titanium*/ diff --git a/common/patches/titanium.h b/common/patches/titanium.h index 2f063c9ad..ea68dd59a 100644 --- a/common/patches/titanium.h +++ b/common/patches/titanium.h @@ -50,6 +50,23 @@ namespace Titanium #include "titanium_ops.h" }; + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Ability = 9, + Item = 10, + Discipline = 10, + PotionBelt = 11, + AltAbility = 0xFF + }; + }; /*Titanium*/ #endif /*COMMON_TITANIUM_H*/ diff --git a/common/patches/titanium_ops.h b/common/patches/titanium_ops.h index f878fbd76..9ca0ba770 100644 --- a/common/patches/titanium_ops.h +++ b/common/patches/titanium_ops.h @@ -50,6 +50,7 @@ E(OP_ItemPacket) E(OP_LeadershipExpUpdate) E(OP_LFGuild) E(OP_LootItem) +E(OP_MemorizeSpell) E(OP_MoveItem) E(OP_OnLevelMessage) E(OP_PetBuffWindow) diff --git a/common/patches/titanium_structs.h b/common/patches/titanium_structs.h index caa798da0..8d258871f 100644 --- a/common/patches/titanium_structs.h +++ b/common/patches/titanium_structs.h @@ -390,7 +390,7 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -uint32 unknown12; +uint32 reduction; // lowers reuse }; /* @@ -420,13 +420,13 @@ struct DeleteSpell_Struct /*005*/uint8 unknowndss006[3]; /*008*/ }; - struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like }; struct SwapSpell_Struct diff --git a/common/patches/uf.cpp b/common/patches/uf.cpp index 80a266ebe..6a1a03a09 100644 --- a/common/patches/uf.cpp +++ b/common/patches/uf.cpp @@ -59,6 +59,9 @@ namespace UF // client to server text link converter static inline void UFToServerTextLink(std::string& serverTextLink, const std::string& ufTextLink); + static inline CastingSlot ServerToUFCastingSlot(EQEmu::CastingSlot slot); + static inline EQEmu::CastingSlot UFToServerCastingSlot(CastingSlot slot); + void Register(EQStreamIdentifier &into) { //create our opcode manager if we havent already @@ -1383,7 +1386,8 @@ namespace UF OUT(new_mana); OUT(stamina); OUT(spell_id); - eq->unknown16 = -1; // Self Interrupt/Success = -1, Fizzle = 1, Other Interrupt = 2? + OUT(keepcasting); + eq->slot = -1; // this is spell gem slot. It's -1 in normal operation FINISH_ENCODE(); } @@ -1761,18 +1765,18 @@ namespace UF VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 1); VARSTRUCT_ENCODE_TYPE(uint16, Buffer, emu->buffcount); - for (unsigned int i = 0; i < BUFF_COUNT; ++i) + for (unsigned int i = 0; i < PET_BUFF_COUNT; ++i) { if (emu->spellid[i]) { VARSTRUCT_ENCODE_TYPE(uint32, Buffer, i); VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->spellid[i]); VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->ticsremaining[i]); - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // numhits VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // This is a string. Name of the caster of the buff. } } - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->buffcount); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->buffcount); /// I think this is actually some sort of type delete[] __emu_buffer; dest->FastQueuePacket(&in, ack_req); @@ -1867,7 +1871,7 @@ namespace UF OUT(thirst_level); OUT(hunger_level); //PS this needs to be figured out more; but it was 'good enough' - for (r = 0; r < structs::BUFF_COUNT; r++) + for (r = 0; r < BUFF_COUNT; r++) { if (emu->buffs[r].spellid != 0xFFFF && emu->buffs[r].spellid != 0) { @@ -3259,10 +3263,7 @@ namespace UF DECODE_LENGTH_EXACT(structs::CastSpell_Struct); SETUP_DIRECT_DECODE(CastSpell_Struct, structs::CastSpell_Struct); - if (eq->slot == 13) - emu->slot = 10; - else - IN(slot); + emu->slot = static_cast(UFToServerCastingSlot(static_cast(eq->slot))); IN(spell_id); emu->inventoryslot = UFToServerSlot(eq->inventoryslot); @@ -4378,4 +4379,80 @@ namespace UF } } + static inline CastingSlot ServerToUFCastingSlot(EQEmu::CastingSlot slot) + { + switch (slot) { + case EQEmu::CastingSlot::Gem1: + return CastingSlot::Gem1; + case EQEmu::CastingSlot::Gem2: + return CastingSlot::Gem2; + case EQEmu::CastingSlot::Gem3: + return CastingSlot::Gem3; + case EQEmu::CastingSlot::Gem4: + return CastingSlot::Gem4; + case EQEmu::CastingSlot::Gem5: + return CastingSlot::Gem5; + case EQEmu::CastingSlot::Gem6: + return CastingSlot::Gem6; + case EQEmu::CastingSlot::Gem7: + return CastingSlot::Gem7; + case EQEmu::CastingSlot::Gem8: + return CastingSlot::Gem8; + case EQEmu::CastingSlot::Gem9: + return CastingSlot::Gem9; + case EQEmu::CastingSlot::Gem10: + return CastingSlot::Gem10; + case EQEmu::CastingSlot::Gem11: + return CastingSlot::Gem11; + case EQEmu::CastingSlot::Gem12: + return CastingSlot::Gem12; + case EQEmu::CastingSlot::Item: + case EQEmu::CastingSlot::PotionBelt: + return CastingSlot::Item; + case EQEmu::CastingSlot::Discipline: + return CastingSlot::Discipline; + case EQEmu::CastingSlot::AltAbility: + return CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return CastingSlot::Discipline; + } + } + + static inline EQEmu::CastingSlot UFToServerCastingSlot(CastingSlot slot) + { + switch (slot) { + case CastingSlot::Gem1: + return EQEmu::CastingSlot::Gem1; + case CastingSlot::Gem2: + return EQEmu::CastingSlot::Gem2; + case CastingSlot::Gem3: + return EQEmu::CastingSlot::Gem3; + case CastingSlot::Gem4: + return EQEmu::CastingSlot::Gem4; + case CastingSlot::Gem5: + return EQEmu::CastingSlot::Gem5; + case CastingSlot::Gem6: + return EQEmu::CastingSlot::Gem6; + case CastingSlot::Gem7: + return EQEmu::CastingSlot::Gem7; + case CastingSlot::Gem8: + return EQEmu::CastingSlot::Gem8; + case CastingSlot::Gem9: + return EQEmu::CastingSlot::Gem9; + case CastingSlot::Gem10: + return EQEmu::CastingSlot::Gem10; + case CastingSlot::Gem11: + return EQEmu::CastingSlot::Gem11; + case CastingSlot::Gem12: + return EQEmu::CastingSlot::Gem12; + case CastingSlot::Discipline: + return EQEmu::CastingSlot::Discipline; + case CastingSlot::Item: + return EQEmu::CastingSlot::Item; + case CastingSlot::AltAbility: + return EQEmu::CastingSlot::AltAbility; + default: // we shouldn't have any issues with other slots ... just return something + return EQEmu::CastingSlot::Discipline; + } + } } /*UF*/ diff --git a/common/patches/uf.h b/common/patches/uf.h index c36d9e851..1dc5046c3 100644 --- a/common/patches/uf.h +++ b/common/patches/uf.h @@ -50,6 +50,24 @@ namespace UF #include "uf_ops.h" }; + enum class CastingSlot : uint32 { + Gem1 = 0, + Gem2 = 1, + Gem3 = 2, + Gem4 = 3, + Gem5 = 4, + Gem6 = 5, + Gem7 = 6, + Gem8 = 7, + Gem9 = 8, + Gem10 = 9, + Gem11 = 10, + Gem12 = 11, + Item = 12, + Discipline = 13, + AltAbility = 0xFF + }; + }; /*UF*/ #endif /*COMMON_UF_H*/ diff --git a/common/patches/uf_structs.h b/common/patches/uf_structs.h index 932213827..46e5b21cb 100644 --- a/common/patches/uf_structs.h +++ b/common/patches/uf_structs.h @@ -26,7 +26,7 @@ namespace UF namespace structs { -static const uint32 BUFF_COUNT = 25; +static const uint32 BUFF_COUNT = 30; /* ** Compiler override to ensure @@ -479,7 +479,7 @@ struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot uint32 spell_id; // Spell id (200 or c8 is minor healing, etc) uint32 scribing; // 1 if memorizing a spell, set to 0 if scribing to book, 2 if un-memming -//uint32 unknown12; +uint32 reduction; // lowers reuse }; /* @@ -512,11 +512,12 @@ struct DeleteSpell_Struct struct ManaChange_Struct { - uint32 new_mana; // New Mana AMount - uint32 stamina; - uint32 spell_id; - uint32 unknown12; - uint32 unknown16; +/*00*/ uint32 new_mana; // New Mana AMount +/*04*/ uint32 stamina; +/*08*/ uint32 spell_id; +/*12*/ uint8 keepcasting; // won't stop the cast. Change mana while casting? +/*13*/ uint8 padding[3]; // client doesn't read it, garbage data seems like +/*16*/ int32 slot; // -1 for normal usage slot for when we want silent interrupt? I think it does timer stuff or something. Linked Spell Reuse interrupt uses it }; struct SwapSpell_Struct @@ -958,8 +959,7 @@ struct PlayerProfile_Struct /*07880*/ uint32 toxicity; // Potion Toxicity (15=too toxic, each potion adds 3) /*07884*/ uint32 thirst_level; // Drink (ticks till next drink) /*07888*/ uint32 hunger_level; // Food (ticks till next eat) -/*07892*/ SpellBuff_Struct buffs[BUFF_COUNT]; // [1900] Buffs currently on the player (30 Max) - (Each Size 76) -/*09792*/ uint8 unknown09792[380]; // BUFF_COUNT has been left at 25. These are the extra 5 buffs in Underfoot +/*07892*/ SpellBuff_Struct buffs[BUFF_COUNT]; // [2280] Buffs currently on the player (30 Max) - (Each Size 76) /*10172*/ Disciplines_Struct disciplines; // [400] Known disciplines /*10972*/ uint32 recastTimers[MAX_RECAST_TYPES]; // Timers (UNIX Time of last use) /*11052*/ uint8 unknown11052[160]; // Some type of Timers @@ -2167,9 +2167,7 @@ struct GroupFollow_Struct { // Underfoot Follow Struct struct InspectBuffs_Struct { /*000*/ uint32 spell_id[BUFF_COUNT]; -/*100*/ uint32 filler100[5]; // BUFF_COUNT is really 30... /*120*/ int32 tics_remaining[BUFF_COUNT]; -/*220*/ uint32 filler220[5]; // BUFF_COUNT is really 30... }; diff --git a/common/ptimer.h b/common/ptimer.h index 7194ab49b..d7398bea3 100644 --- a/common/ptimer.h +++ b/common/ptimer.h @@ -37,10 +37,12 @@ enum { //values for pTimerType pTimerSenseTraps = 12, pTimerDisarmTraps = 13, pTimerDisciplineReuseStart = 14, - pTimerDisciplineReuseEnd = 24, + pTimerDisciplineReuseEnd = 24, // client actually has 20 ids, but still no disc go that high even on live pTimerCombatAbility = 25, pTimerCombatAbility2 = 26, // RoF2+ Tiger Claw is unlinked from other monk skills, generic in case other classes ever need it pTimerBeggingPickPocket = 27, + pTimerLinkedSpellReuseStart = 28, + pTimerLinkedSpellReuseEnd = 48, pTimerLayHands = 87, //these IDs are used by client too pTimerHarmTouch = 89, //so dont change them diff --git a/common/races.h b/common/races.h index 050b2fd78..d34d9b754 100644 --- a/common/races.h +++ b/common/races.h @@ -45,6 +45,7 @@ #define TIGER 63 #define ELEMENTAL 75 #define ALLIGATOR 91 +#define OGGOK_CITIZEN 93 #define EYE_OF_ZOMM 108 #define WOLF_ELEMENTAL 120 #define INVISIBLE_MAN 127 diff --git a/common/ruletypes.h b/common/ruletypes.h index 41dbeacd2..c19ebee45 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -142,6 +142,8 @@ RULE_INT(Character, InvSnapshotMinRetryM, 30) // Time (in minutes) to re-attempt RULE_INT(Character, InvSnapshotHistoryD, 30) // Time (in days) to keep snapshot entries RULE_BOOL(Character, RestrictSpellScribing, false) // Restricts spell scribing to allowable races/classes of spell scroll, if true RULE_BOOL(Character, UseStackablePickPocketing, true) // Allows stackable pickpocketed items to stack instead of only being allowed in empty inventory slots +RULE_BOOL(Character, EnableAvoidanceCap, false) +RULE_INT(Character, AvoidanceCap, 750) // 750 Is a pretty good value, seen people dodge all attacks beyond 1,000 Avoidance RULE_CATEGORY_END() RULE_CATEGORY(Mercs) @@ -329,7 +331,7 @@ RULE_INT(Spells, MaxBuffSlotsNPC, 25) RULE_INT(Spells, MaxSongSlotsNPC, 10) RULE_INT(Spells, MaxDiscSlotsNPC, 1) RULE_INT(Spells, MaxTotalSlotsNPC, 36) -RULE_INT(Spells, MaxTotalSlotsPET, 25) // do not set this higher than 25 until the player profile is removed from the blob +RULE_INT(Spells, MaxTotalSlotsPET, 30) // do not set this higher than 25 until the player profile is removed from the blob RULE_BOOL (Spells, EnableBlockedBuffs, true) RULE_INT(Spells, ReflectType, 1) //0 = disabled, 1 = single target player spells only, 2 = all player spells, 3 = all single target spells, 4 = all spells RULE_INT(Spells, VirusSpreadDistance, 30) // The distance a viral spell will jump to its next victim @@ -379,6 +381,7 @@ RULE_BOOL(Spells, UseAdditiveFocusFromWornSlot, false) // Allows an additive foc RULE_BOOL(Spells, AlwaysSendTargetsBuffs, false) // ignore LAA level if true RULE_BOOL(Spells, FlatItemExtraSpellAmt, false) // allow SpellDmg stat to affect all spells, regardless of cast time/cooldown/etc RULE_BOOL(Spells, IgnoreSpellDmgLvlRestriction, false) // ignore the 5 level spread on applying SpellDmg +RULE_BOOL(Spells, AllowItemTGB, false) // TGB doesn't work with items on live, custom servers want it though RULE_CATEGORY_END() RULE_CATEGORY(Combat) @@ -503,6 +506,7 @@ RULE_BOOL(NPC, EnableMeritBasedFaction, false) // If set to true, faction will g RULE_INT(NPC, NPCToNPCAggroTimerMin, 500) RULE_INT(NPC, NPCToNPCAggroTimerMax, 6000) RULE_BOOL(NPC, UseClassAsLastName, true) // Uses class archetype as LastName for npcs with none +RULE_BOOL(NPC, NewLevelScaling, true) // Better level scaling, use old if new formulas would break your server RULE_CATEGORY_END() RULE_CATEGORY(Aggro) @@ -518,6 +522,7 @@ RULE_REAL(Aggro, TunnelVisionAggroMod, 0.75) //people not currently the top hate RULE_INT(Aggro, MaxScalingProcAggro, 400) // Set to -1 for no limit. Maxmimum amount of aggro that HP scaling SPA effect in a proc will add. RULE_INT(Aggro, IntAggroThreshold, 75) // Int <= this will aggro regardless of level difference. RULE_BOOL(Aggro, AllowTickPulling, false) // tick pulling is an exploit in an NPC's call for help fixed sometime in 2006 on live +RULE_BOOL(Aggro, UseLevelAggro, true) // Level 18+ and Undead will aggro regardless of level difference. (this will disabled Rule:IntAggroThreshold if set to true) RULE_CATEGORY_END() RULE_CATEGORY(TaskSystem) diff --git a/common/shareddb.cpp b/common/shareddb.cpp index c9213f3a2..fd89831c0 100644 --- a/common/shareddb.cpp +++ b/common/shareddb.cpp @@ -455,7 +455,7 @@ bool SharedDatabase::GetSharedBank(uint32 id, Inventory *inv, bool is_charid) } } - if (row[9]) { + if (inst && row[9]) { std::string data_str(row[9]); std::string idAsString; std::string value; @@ -480,6 +480,7 @@ bool SharedDatabase::GetSharedBank(uint32 id, Inventory *inv, bool is_charid) } } + // theoretically inst can be nullptr ... this would be very bad ... put_slot_id = inv->PutItem(slot_id, *inst); safe_delete(inst); diff --git a/common/skills.cpp b/common/skills.cpp index 9be28781f..a23d77a7b 100644 --- a/common/skills.cpp +++ b/common/skills.cpp @@ -110,6 +110,20 @@ bool EQEmu::skills::IsBardInstrumentSkill(SkillType skill) } } +bool EQEmu::skills::IsCastingSkill(SkillType skill) +{ + switch (skill) { + case SkillAbjuration: + case SkillAlteration: + case SkillConjuration: + case SkillDivination: + case SkillEvocation: + return true; + default: + return false; + } +} + const std::map& EQEmu::skills::GetSkillTypeMap() { /* VS2013 code diff --git a/common/skills.h b/common/skills.h index cd7603667..19a996729 100644 --- a/common/skills.h +++ b/common/skills.h @@ -161,10 +161,11 @@ namespace EQEmu // server profile does not reflect this yet..so, prefixed with 'PACKET_' #define PACKET_SKILL_ARRAY_SIZE 100 - extern bool IsTradeskill(SkillType skill); - extern bool IsSpecializedSkill(SkillType skill); - extern float GetSkillMeleePushForce(SkillType skill); - extern bool IsBardInstrumentSkill(SkillType skill); + bool IsTradeskill(SkillType skill); + bool IsSpecializedSkill(SkillType skill); + float GetSkillMeleePushForce(SkillType skill); + bool IsBardInstrumentSkill(SkillType skill); + bool IsCastingSkill(SkillType skill); extern const std::map& GetSkillTypeMap(); diff --git a/common/spdat.cpp b/common/spdat.cpp index ab9150122..f22a82000 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -235,8 +235,11 @@ bool IsBeneficialSpell(uint16 spell_id) // If the resisttype is magic and SpellAffectIndex is Calm/memblur/dispell sight // it's not beneficial if (spells[spell_id].resisttype == RESIST_MAGIC) { - if (sai == SAI_Calm || sai == SAI_Dispell_Sight || - sai == SAI_Memory_Blur || sai == SAI_Calm_Song) + // checking these SAI cause issues with the rng defensive proc line + // So I guess instead of fixing it for real, just a quick hack :P + if (spells[spell_id].effectid[0] != SE_DefensiveProc && + (sai == SAI_Calm || sai == SAI_Dispell_Sight || sai == SAI_Memory_Blur || + sai == SAI_Calm_Song)) return false; } else { // If the resisttype is not magic and spell is Bind Sight or Cast Sight @@ -669,9 +672,7 @@ bool IsDisciplineBuff(uint16 spell_id) if (!IsValidSpell(spell_id)) return false; - if (spells[spell_id].mana == 0 && spells[spell_id].short_buff_box == 0 && - (spells[spell_id].EndurCost || spells[spell_id].EndurUpkeep) && - spells[spell_id].targettype == ST_Self) + if (spells[spell_id].IsDisciplineBuff && spells[spell_id].targettype == ST_Self) return true; return false; diff --git a/common/spdat.h b/common/spdat.h index cb6e49fc9..2734f6078 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -446,7 +446,7 @@ typedef enum { #define SE_IncreaseRunSpeedCap 290 // implemented[AA] - increases run speed over the hard cap #define SE_Purify 291 // implemented - Removes determental effects #define SE_StrikeThrough2 292 // implemented[AA] - increasing chance of bypassing an opponent's special defenses, such as dodge, block, parry, and riposte. -#define SE_FrontalStunResist 293 // implemented[AA] - Reduce chance to be stunned from front. +#define SE_FrontalStunResist 293 // implemented[AA] - Reduce chance to be stunned from front. -- live descriptions sounds like this isn't limited to frontal anymore #define SE_CriticalSpellChance 294 // implemented - increase chance to critical hit and critical damage modifier. //#define SE_ReduceTimerSpecial 295 // not used #define SE_FcSpellVulnerability 296 // implemented - increase in incoming spell damage diff --git a/ucs/clientlist.cpp b/ucs/clientlist.cpp index 47e0eb726..f49e02986 100644 --- a/ucs/clientlist.cpp +++ b/ucs/clientlist.cpp @@ -1985,7 +1985,7 @@ void Client::ChannelGrantVoice(std::string CommandString) { return; } - if(RequiredChannel->IsOwner(RequiredClient->GetName()) || RequiredChannel->IsModerator(RequiredClient->GetName())) { + if(RequiredClient && (RequiredChannel->IsOwner(RequiredClient->GetName()) || RequiredChannel->IsModerator(RequiredClient->GetName()))) { GeneralChannelMessage("The channel owner and moderators automatically have voice."); return; diff --git a/utils/patches/patch_RoF.conf b/utils/patches/patch_RoF.conf index 00682d57f..aefcd8dbc 100644 --- a/utils/patches/patch_RoF.conf +++ b/utils/patches/patch_RoF.conf @@ -172,6 +172,7 @@ OP_BeginCast=0x17ff OP_ColoredText=0x41cb OP_ConsentResponse=0x183d OP_MemorizeSpell=0x2fac +OP_LinkedReuse=0x3ac0 OP_SwapSpell=0x4736 OP_CastSpell=0x1cb5 OP_Consider=0x4d8d diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 9237a0238..19ef8d842 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -171,6 +171,7 @@ OP_BeginCast=0x318f OP_ColoredText=0x43af OP_ConsentResponse=0x384a OP_MemorizeSpell=0x217c +OP_LinkedReuse=0x1619 OP_SwapSpell=0x0efa OP_CastSpell=0x1287 OP_Consider=0x742b diff --git a/utils/patches/patch_SoD.conf b/utils/patches/patch_SoD.conf index 4944b0ef3..08e072d33 100644 --- a/utils/patches/patch_SoD.conf +++ b/utils/patches/patch_SoD.conf @@ -171,6 +171,7 @@ OP_BeginCast=0x0d5a # C OP_ColoredText=0x569a # C OP_ConsentResponse=0x6e47 # C OP_MemorizeSpell=0x8543 # C +OP_LinkedReuse=0x6ef9 OP_SwapSpell=0x3fd2 # C OP_CastSpell=0x3582 # C OP_Consider=0x6024 # C diff --git a/utils/patches/patch_SoF.conf b/utils/patches/patch_SoF.conf index 113038e49..d758daea9 100644 --- a/utils/patches/patch_SoF.conf +++ b/utils/patches/patch_SoF.conf @@ -169,6 +169,7 @@ OP_BeginCast=0x5A50 #SEQ 12/04/08 OP_ColoredText=0x3BC7 #SEQ 12/04/08 OP_ConsentResponse=0x4D30 #SEQ 12/04/08 OP_MemorizeSpell=0x6A93 #SEQ 12/04/08 +OP_LinkedReuse=0x2c26 OP_SwapSpell=0x1418 #SEQ 12/04/08 OP_CastSpell=0x7F5D #SEQ 12/04/08 OP_Consider=0x32E1 #SEQ 12/04/08 diff --git a/utils/patches/patch_Titanium.conf b/utils/patches/patch_Titanium.conf index a48de92b5..23f07109f 100644 --- a/utils/patches/patch_Titanium.conf +++ b/utils/patches/patch_Titanium.conf @@ -170,6 +170,7 @@ OP_Save=0x736b # ShowEQ 10/27/05 OP_Camp=0x78c1 # ShowEQ 10/27/05 OP_EndLootRequest=0x2316 # ShowEQ 10/27/05 OP_MemorizeSpell=0x308e # ShowEQ 10/27/05 +OP_LinkedReuse=0x6a00 OP_SwapSpell=0x2126 # ShowEQ 10/27/05 OP_CastSpell=0x304b # ShowEQ 10/27/05 OP_DeleteSpell=0x4f37 diff --git a/utils/patches/patch_UF.conf b/utils/patches/patch_UF.conf index 4b650176f..f41b62740 100644 --- a/utils/patches/patch_UF.conf +++ b/utils/patches/patch_UF.conf @@ -173,6 +173,7 @@ OP_BeginCast=0x0d5a # C OP_ColoredText=0x71bf # C OP_ConsentResponse=0x0e87 # C OP_MemorizeSpell=0x3887 # C +OP_LinkedReuse=0x1b26 OP_SwapSpell=0x5805 # C OP_CastSpell=0x50c2 # C OP_Consider=0x3c2d # C diff --git a/utils/scripts/db_dumper.pl b/utils/scripts/db_dumper.pl index 5b4bb229d..47f14feef 100644 --- a/utils/scripts/db_dumper.pl +++ b/utils/scripts/db_dumper.pl @@ -33,6 +33,7 @@ if(!$ARGV[0]){ print " tables=\"table1,table2,table3\" - Manually specify tables, default is to dump all tables from database\n"; print " compress - Compress Database with 7-ZIP, will fallback to WinRAR depending on what is installed (Must be installed to default program dir)...\n"; print " nolock - Does not lock tables, meant for backuping while the server is running..\n"; + print " backup_name=\"name\" - Sets database backup prefix name\n"; print ' Example: perl DB_Dumper.pl Loc="E:\Backups"' . "\n\n"; print "######################################################\n"; exit; @@ -72,6 +73,11 @@ while($ARGV[$n]){ print "Database is " . $DB_NAME[1] . "\n"; $db = $DB_NAME[1]; } + if($ARGV[$n]=~/backup_name=/i){ + @data = split('=', $ARGV[$n]); + print "Backup Name is " . $data[1] . "\n"; + $backup_name = $data[1]; + } if($ARGV[$n]=~/loc=/i){ @B_LOC = split('=', $ARGV[$n]); print "Backup Directory: " . $B_LOC[1] . "\n"; @@ -108,8 +114,14 @@ else { } if($t_tables ne ""){ - $tables_f_l = substr($t_tables_l, 0, 20) . '...'; - $target_file = '' . $tables_f_l . '_' . $date . ''; + $tables_f_l = substr($t_tables_l, 0, 20) . '-'; + if($backup_name){ + $target_file = $backup_name . '_' . $date . ''; + } + else { + $target_file = '' . $tables_f_l . '_' . $date . ''; + } + print "Performing table based backup...\n"; #::: Backup Database... print "Backing up Database " . $db . "... \n\n"; @@ -121,7 +133,14 @@ if($t_tables ne ""){ system($cmd); } else{ #::: Entire DB Backup - $target_file = '' . $db . '_' . $date . ''; + + if($backup_name){ + $target_file = $backup_name . '_' . $db . '_' . $date . ''; + } + else { + $target_file = '' . $db . '_' . $date . ''; + } + #::: Backup Database... print "Backing up Database " . $db . "... \n\n"; if($no_lock == 1){ @@ -195,6 +214,9 @@ if($Compress == 1){ $final_file = $target_file . ".tar.gz"; } } +else { + $final_file = $target_file . ".sql"; +} #::: Get Final File Location for display if($B_LOC[1] ne ""){ $final_loc = $B_LOC[1] . '' . $file_app . ""; } diff --git a/utils/scripts/eqemu_server.pl b/utils/scripts/eqemu_server.pl new file mode 100644 index 000000000..b575bb1cf --- /dev/null +++ b/utils/scripts/eqemu_server.pl @@ -0,0 +1,1552 @@ +#!/usr/bin/perl + +########################################################### +#::: Automatic (Database) Upgrade Script +#::: Author: Akkadius +#::: Purpose: To upgrade databases with ease and maintain versioning +########################################################### + +$menu_displayed = 0; + +use Config; +use File::Copy qw(copy); +use POSIX qw(strftime); +use File::Path; +use File::Find; +use URI::Escape; +use Time::HiRes qw(usleep); + +$time_stamp = strftime('%m-%d-%Y', gmtime()); + +$console_output .= " Operating System is: $Config{osname}\n"; +if($Config{osname}=~/freebsd|linux/i){ $OS = "Linux"; } +if($Config{osname}=~/Win|MS/i){ $OS = "Windows"; } + +#::: If current version is less than what world is reporting, then download a new one... +$current_version = 14; + +if($ARGV[0] eq "V"){ + if($ARGV[1] > $current_version){ + print "eqemu_update.pl Automatic Database Upgrade Needs updating...\n"; + print " Current version: " . $current_version . "\n"; + print " New version: " . $ARGV[1] . "\n"; + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/scripts/eqemu_update.pl", "eqemu_update.pl"); + exit; + } + else{ + print "[Upgrade Script] No script update necessary \n"; + } + exit; +} + +#::: Sets database run stage check +$db_run_stage = 0; + +$perl_version = $^V; +$perl_version =~s/v//g; +print "Perl Version is " . $perl_version . "\n"; +if($perl_version > 5.12){ no warnings 'uninitialized'; } +no warnings; + +($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(); + +my $confile = "eqemu_config.xml"; #default +open(F, "<$confile"); +my $indb = 0; +while() { + s/\r//g; + if(//i) { $indb = 1; } + next unless($indb == 1); + if(/<\/database>/i) { $indb = 0; last; } + if(/(.*)<\/host>/i) { $host = $1; } + elsif(/(.*)<\/username>/i) { $user = $1; } + elsif(/(.*)<\/password>/i) { $pass = $1; } + elsif(/(.*)<\/db>/i) { $db = $1; } +} + +$console_output = +"============================================================ + EQEmu: Automatic Upgrade Check +============================================================ +"; + +if($OS eq "Windows"){ + $has_mysql_path = `echo %PATH%`; + if($has_mysql_path=~/MySQL|MariaDB/i){ + @mysql = split(';', $has_mysql_path); + foreach my $v (@mysql){ + if($v=~/MySQL|MariaDB/i){ + $v =~s/\n//g; + $path = trim($v) . "/mysql"; + last; + } + } + $console_output .= " (Windows) MySQL is in system path \n"; + $console_output .= " Path = " . $path . "\n"; + $console_output .= "============================================================\n"; + } +} + +#::: Linux Check +if($OS eq "Linux"){ + $path = `which mysql`; + if ($path eq "") { + $path = `which mariadb`; + } + $path =~s/\n//g; + + $console_output .= " (Linux) MySQL is in system path \n"; + $console_output .= " Path = " . $path . "\n"; + $console_output .= "============================================================\n"; +} + +#::: Path not found, error and exit +if($path eq ""){ + print "MySQL path not found, please add the path for automatic database upgrading to continue... \n\n"; + print "script_exiting...\n"; + exit; +} + +if($ARGV[0] eq "install_peq_db"){ + + $db_name = "peq"; + if($ARGV[1]){ + $db_name = $ARGV[1]; + } + + $db = $db_name; + + #::: Database Routines + print "MariaDB :: Creating Database '" . $db_name . "'\n"; + print `"$path" --host $host --user $user --password="$pass" -N -B -e "DROP DATABASE IF EXISTS $db_name;"`; + print `"$path" --host $host --user $user --password="$pass" -N -B -e "CREATE DATABASE $db_name"`; + if($OS eq "Windows"){ @db_version = split(': ', `world db_version`); } + if($OS eq "Linux"){ @db_version = split(': ', `./world db_version`); } + $bin_db_ver = trim($db_version[1]); + check_db_version_table(); + $local_db_ver = trim(get_mysql_result("SELECT version FROM db_version LIMIT 1")); + fetch_peq_db_full(); + print "\nFetching Latest Database Updates...\n"; + main_db_management(); + print "\nApplying Latest Database Updates...\n"; + main_db_management(); + + print get_mysql_result("UPDATE `launcher` SET `dynamics` = 30 WHERE `name` = 'zone'"); +} + +if($ARGV[0] eq "remove_duplicate_rules"){ + remove_duplicate_rule_values(); + exit; +} + +if($ARGV[0] eq "installer"){ + print "Running EQEmu Server installer routines...\n"; + mkdir('logs'); + mkdir('updates_staged'); + mkdir('shared'); + fetch_latest_windows_binaries(); + map_files_fetch_bulk(); + opcodes_fetch(); + plugins_fetch(); + quest_files_fetch(); + lua_modules_fetch(); + + #::: Binary dll's + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/lua51.dll", "lua51.dll", 1); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/zlib1.dll", "zlib1.dll", 1); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/libmysql.dll", "libmysql.dll", 1); + + #::: Server scripts + fetch_utility_scripts(); + + #::: Database Routines + print "MariaDB :: Creating Database 'peq'\n"; + print `"$path" --host $host --user $user --password="$pass" -N -B -e "DROP DATABASE IF EXISTS peq;"`; + print `"$path" --host $host --user $user --password="$pass" -N -B -e "CREATE DATABASE peq"`; + if($OS eq "Windows"){ @db_version = split(': ', `world db_version`); } + if($OS eq "Linux"){ @db_version = split(': ', `./world db_version`); } + $bin_db_ver = trim($db_version[1]); + check_db_version_table(); + $local_db_ver = trim(get_mysql_result("SELECT version FROM db_version LIMIT 1")); + fetch_peq_db_full(); + print "\nFetching Latest Database Updates...\n"; + main_db_management(); + print "\nApplying Latest Database Updates...\n"; + main_db_management(); + + print get_mysql_result("UPDATE `launcher` SET `dynamics` = 30 WHERE `name` = 'zone'"); + + if($OS eq "Windows"){ + check_windows_firewall_rules(); + do_windows_login_server_setup(); + } + exit; +} + +if($ARGV[0] eq "db_dump_compress"){ database_dump_compress(); exit; } +if($ARGV[0] eq "login_server_setup"){ + do_windows_login_server_setup(); + exit; +} + +#::: Create db_update working directory if not created +mkdir('db_update'); + +#::: Check if db_version table exists... +if(trim(get_mysql_result("SHOW COLUMNS FROM db_version LIKE 'Revision'")) ne "" && $db){ + print get_mysql_result("DROP TABLE db_version"); + print "Old db_version table present, dropping...\n\n"; +} + +sub check_db_version_table{ + if(get_mysql_result("SHOW TABLES LIKE 'db_version'") eq "" && $db){ + print get_mysql_result(" + CREATE TABLE db_version ( + version int(11) DEFAULT '0' + ) ENGINE=InnoDB DEFAULT CHARSET=latin1; + INSERT INTO db_version (version) VALUES ('1000');"); + print "Table 'db_version' does not exists.... Creating...\n\n"; + } +} + +check_db_version_table(); + +if($OS eq "Windows"){ @db_version = split(': ', `world db_version`); } +if($OS eq "Linux"){ @db_version = split(': ', `./world db_version`); } + +$bin_db_ver = trim($db_version[1]); +$local_db_ver = trim(get_mysql_result("SELECT version FROM db_version LIMIT 1")); + +#::: If ran from Linux startup script, supress output +if($bin_db_ver == $local_db_ver && $ARGV[0] eq "ran_from_start"){ + print "Database up to date...\n"; + exit; +} +else{ + print $console_output if $db; +} + +if($db){ + print " Binary Revision / Local: (" . $bin_db_ver . " / " . $local_db_ver . ")\n"; + + #::: Bots + #::: Make sure we're running a bots binary to begin with + if(trim($db_version[2]) > 0){ + $bots_local_db_version = get_bots_db_version(); + if($bots_local_db_version > 0){ + print " (Bots) Binary Revision / Local: (" . trim($db_version[2]) . " / " . $bots_local_db_version . ")\n"; + } + } + + #::: If World ran this script, and our version is up to date, continue... + if($bin_db_ver <= $local_db_ver && $ARGV[0] eq "ran_from_world"){ + print " Database up to Date: Continuing World Bootup...\n"; + print "============================================================\n"; + exit; + } + +} + +if($local_db_ver < $bin_db_ver && $ARGV[0] eq "ran_from_world"){ + print "You have missing database updates, type 1 or 2 to backup your database before running them as recommended...\n\n"; + #::: Display Menu + show_menu_prompt(); +} +else{ + #::: Most likely ran standalone + print "\n"; + show_menu_prompt(); +} + +sub do_update_self{ + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/scripts/eqemu_update.pl", "eqemu_update.pl"); + die "Rerun eqemu_update.pl"; +} + +sub fetch_utility_scripts { + if($OS eq "Windows"){ + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/t_database_backup.bat", "t_database_backup.bat"); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/t_start_server.bat", "t_start_server.bat"); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/t_start_server_with_login_server.bat", "t_start_server_with_login_server.bat"); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/t_stop_server.bat", "t_stop_server.bat"); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/t_server_crash_report.pl", "t_server_crash_report.pl"); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/win_server_launcher.pl", "win_server_launcher.pl"); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/t_start_server_with_login_server.bat", "t_start_server_with_login_server.bat"); + } + else { + print "No scripts found for OS: " . $OS . "...\n"; + } +} + +sub show_menu_prompt { + my %dispatch = ( + 1 => \&database_dump, + 2 => \&database_dump_compress, + 3 => \&main_db_management, + 4 => \&bots_db_management, + 5 => \&opcodes_fetch, + 6 => \&map_files_fetch, + 7 => \&plugins_fetch, + 8 => \&quest_files_fetch, + 9 => \&lua_modules_fetch, + 10 => \&aa_fetch, + 11 => \&fetch_latest_windows_binaries, + 12 => \&fetch_server_dlls, + 13 => \&do_windows_login_server_setup, + 14 => \&remove_duplicate_rule_values, + 15 => \&fetch_utility_scripts, + 18 => \&fetch_latest_windows_binaries_bots, + 19 => \&do_bots_db_schema_drop, + 20 => \&do_update_self, + 21 => \&database_dump_player_tables, + 0 => \&script_exit, + ); + + while (1) { + { + local $| = 1; + if(!$menu_show && ($ARGV[0] eq "ran_from_world" || $ARGV[0] eq "ran_from_start")){ + $menu_show++; + next; + } + print menu_options(), '> '; + $menu_displayed++; + if($menu_displayed > 50){ + print "Safety: Menu looping too many times, exiting...\n"; + exit; + } + } + + my $choice = <>; + + $choice =~ s/\A\s+//; + $choice =~ s/\s+\z//; + + if (defined(my $handler = $dispatch{$choice})) { + my $result = $handler->(); + unless (defined $result) { + exit 0; + } + } + else { + if($ARGV[0] ne "ran_from_world"){ + # warn "\n\nInvalid selection\n\n"; + } + } + } +} + +sub menu_options { + if(@total_updates){ + if($bots_db_management == 1){ + $option[3] = "Check and stage pending REQUIRED Database updates"; + $bots_management = "Run pending REQUIRED updates... (" . scalar (@total_updates) . ")"; + } + else{ + $option[3] = "Run pending REQUIRED updates... (" . scalar (@total_updates) . ")"; + if(get_mysql_result("SHOW TABLES LIKE 'bots'") eq ""){ + $bots_management = "Install bots database pre-requisites (Requires bots server binaries)"; + } + else{ + $bots_management = "Check for Bot pending REQUIRED database updates... (Must have bots enabled)"; + } + } + } + else{ + $option[3] = "Check and stage pending REQUIRED Database updates"; + $bots_management = "Check for Bot REQUIRED database updates... (Must have bots enabled)"; + } + +return <){ + chomp; + $o = $_; + $tables .= $o . ","; + } + $tables = substr($tables, 0, -1); + + print `perl db_dumper.pl database="$db" loc="backups" tables="$tables" backup_name="player_tables_export" nolock`; + + print "\nPress any key to continue...\n"; + + <>; #Read from STDIN + +} + +sub database_dump_compress { + check_for_database_dump_script(); + print "Performing database backup....\n"; + print `perl db_dumper.pl database="$db" loc="backups" compress`; +} + +sub script_exit{ + #::: Cleanup staged folder... + rmtree("updates_staged/"); + exit; +} + +#::: Returns Tab Delimited MySQL Result from Command Line +sub get_mysql_result{ + my $run_query = $_[0]; + if(!$db){ return; } + if($OS eq "Windows"){ return `"$path" --host $host --user $user --password="$pass" $db -N -B -e "$run_query"`; } + if($OS eq "Linux"){ + $run_query =~s/`//g; + return `$path --user="$user" --host $host --password="$pass" $db -N -B -e "$run_query"`; + } +} + +sub get_mysql_result_from_file{ + my $update_file = $_[0]; + if(!$db){ return; } + if($OS eq "Windows"){ return `"$path" --host $host --user $user --password="$pass" --force $db < $update_file`; } + if($OS eq "Linux"){ return `"$path" --host $host --user $user --password="$pass" --force $db < $update_file`; } +} + +#::: Gets Remote File based on URL (1st Arg), and saves to destination file (2nd Arg) +#::: Example: get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/db_update_manifest.txt", "db_update/db_update_manifest.txt"); +sub get_remote_file{ + my $URL = $_[0]; + my $Dest_File = $_[1]; + my $content_type = $_[2]; + + #::: Build file path of the destination file so that we may check for the folder's existence and make it if necessary + if($Dest_File=~/\//i){ + my @dir_path = split('/', $Dest_File); + $build_path = ""; + $di = 0; + while($dir_path[$di]){ + $build_path .= $dir_path[$di] . "/"; + #::: If path does not exist, create the directory... + if (!-d $build_path) { + mkdir($build_path); + } + if(!$dir_path[$di + 2] && $dir_path[$di + 1]){ + # print $actual_path . "\n"; + $actual_path = $build_path; + last; + } + $di++; + } + } + + if($OS eq "Windows"){ + #::: For non-text type requests... + if($content_type == 1){ + $break = 0; + while($break == 0) { + use LWP::Simple qw(getstore); + if(!getstore($URL, $Dest_File)){ + # print "Error, no connection or failed request...\n\n"; + } + # sleep(1); + #::: Make sure the file exists before continuing... + if(-e $Dest_File) { + $break = 1; + print " [URL] :: " . $URL . "\n"; + print " [Saved] :: " . $Dest_File . "\n"; + } else { $break = 0; } + usleep(500); + } + } + else{ + $break = 0; + while($break == 0) { + require LWP::UserAgent; + my $ua = LWP::UserAgent->new; + $ua->timeout(10); + $ua->env_proxy; + my $response = $ua->get($URL); + if ($response->is_success){ + open (FILE, '> ' . $Dest_File . ''); + print FILE $response->decoded_content; + close (FILE); + } + else { + # print "Error, no connection or failed request...\n\n"; + } + if(-e $Dest_File) { + $break = 1; + print " [URL] :: " . $URL . "\n"; + print " [Saved] :: " . $Dest_File . "\n"; + } else { $break = 0; } + usleep(500); + } + } + } + if($OS eq "Linux"){ + #::: wget -O db_update/db_update_manifest.txt https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/db_update_manifest.txt + $wget = `wget --no-check-certificate --quiet -O $Dest_File $URL`; + print " o URL: (" . $URL . ")\n"; + print " o Saved: (" . $Dest_File . ") \n"; + if($wget=~/unable to resolve/i){ + print "Error, no connection or failed request...\n\n"; + #die; + } + } +} + +#::: Trim Whitespaces +sub trim { + my $string = $_[0]; + $string =~ s/^\s+//; + $string =~ s/\s+$//; + return $string; +} + +#::: Fetch Latest PEQ AA's +sub aa_fetch{ + if(!$db){ + print "No database present, check your eqemu_config.xml for proper MySQL/MariaDB configuration...\n"; + return; + } + + print "Pulling down PEQ AA Tables...\n"; + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/peq_aa_tables_post_rework.sql", "db_update/peq_aa_tables_post_rework.sql"); + print "\n\nInstalling AA Tables...\n"; + print get_mysql_result_from_file("db_update/peq_aa_tables_post_rework.sql"); + print "\nDone...\n\n"; +} + +#::: Fetch Latest Opcodes +sub opcodes_fetch{ + print "Pulling down latest opcodes...\n"; + %opcodes = ( + 1 => ["opcodes", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/opcodes.conf"], + 2 => ["mail_opcodes", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/mail_opcodes.conf"], + 3 => ["Titanium", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/patch_Titanium.conf"], + 4 => ["Secrets of Faydwer", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/patch_SoF.conf"], + 5 => ["Seeds of Destruction", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/patch_SoD.conf"], + 6 => ["Underfoot", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/patch_UF.conf"], + 7 => ["Rain of Fear", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/patch_RoF.conf"], + 8 => ["Rain of Fear 2", "https://raw.githubusercontent.com/EQEmu/Server/master/utils/patches/patch_RoF2.conf"], + ); + $loop = 1; + while($opcodes{$loop}[0]){ + #::: Split the URL by the patches folder to get the file name from URL + @real_file = split("patches/", $opcodes{$loop}[1]); + $find = 0; + while($real_file[$find]){ + $file_name = $real_file[$find]; + $find++; + } + + print "\nDownloading (" . $opcodes{$loop}[0] . ") File: '" . $file_name . "'...\n\n"; + get_remote_file($opcodes{$loop}[1], $file_name); + $loop++; + } + print "\nDone...\n\n"; +} + +sub remove_duplicate_rule_values{ + $ruleset_id = trim(get_mysql_result("SELECT `ruleset_id` FROM `rule_sets` WHERE `name` = 'default'")); + print "Default Ruleset ID: " . $ruleset_id . "\n"; + + $total_removed = 0; + #::: Store Default values... + $mysql_result = get_mysql_result("SELECT * FROM `rule_values` WHERE `ruleset_id` = " . $ruleset_id); + my @lines = split("\n", $mysql_result); + foreach my $val (@lines){ + my @values = split("\t", $val); + $rule_set_values{$values[1]}[0] = $values[2]; + } + #::: Compare default values against other rulesets to check for duplicates... + $mysql_result = get_mysql_result("SELECT * FROM `rule_values` WHERE `ruleset_id` != " . $ruleset_id); + my @lines = split("\n", $mysql_result); + foreach my $val (@lines){ + my @values = split("\t", $val); + if($values[2] == $rule_set_values{$values[1]}[0]){ + print "DUPLICATE : " . $values[1] . " (Ruleset (" . $values[0] . ")) matches default value of : " . $values[2] . ", removing...\n"; + get_mysql_result("DELETE FROM `rule_values` WHERE `ruleset_id` = " . $values[0] . " AND `rule_name` = '" . $values[1] . "'"); + $total_removed++; + } + } + + print "Total duplicate rules removed... " . $total_removed . "\n"; +} + +sub copy_file{ + $l_source_file = $_[0]; + $l_dest_file = $_[1]; + if($l_dest_file=~/\//i){ + my @dir_path = split('/', $l_dest_file); + $build_path = ""; + $di = 0; + while($dir_path[$di]){ + $build_path .= $dir_path[$di] . "/"; + #::: If path does not exist, create the directory... + if (!-d $build_path) { + mkdir($build_path); + } + if(!$dir_path[$di + 2] && $dir_path[$di + 1]){ + # print $actual_path . "\n"; + $actual_path = $build_path; + last; + } + $di++; + } + } + copy $l_source_file, $l_dest_file; +} + +sub fetch_latest_windows_binaries{ + print "\n --- Fetching Latest Windows Binaries... --- \n"; + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/master_windows_build.zip", "updates_staged/master_windows_build.zip", 1); + print "\n --- Fetched Latest Windows Binaries... --- \n"; + print "\n --- Extracting... --- \n"; + unzip('updates_staged/master_windows_build.zip', 'updates_staged/binaries/'); + my @files; + my $start_dir = "updates_staged/binaries"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + $dest_file = $file; + $dest_file =~s/updates_staged\/binaries\///g; + print "Installing :: " . $dest_file . "\n"; + copy_file($file, $dest_file); + } + print "\n --- Done... --- \n"; + + rmtree('updates_staged'); +} + +sub fetch_latest_windows_binaries_bots{ + print "\n --- Fetching Latest Windows Binaries with Bots... --- \n"; + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/master_windows_build_bots.zip", "updates_staged/master_windows_build_bots.zip", 1); + print "\n --- Fetched Latest Windows Binaries with Bots... --- \n"; + print "\n --- Extracting... --- \n"; + unzip('updates_staged/master_windows_build_bots.zip', 'updates_staged/binaries/'); + my @files; + my $start_dir = "updates_staged/binaries"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + $dest_file = $file; + $dest_file =~s/updates_staged\/binaries\///g; + print "Installing :: " . $dest_file . "\n"; + copy_file($file, $dest_file); + } + print "\n --- Done... --- \n"; + + rmtree('updates_staged'); +} + +sub do_windows_login_server_setup{ + print "\n --- Fetching Loginserver... --- \n"; + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/login_server.zip", "updates_staged/login_server.zip", 1); + print "\n --- Extracting... --- \n"; + unzip('updates_staged/login_server.zip', 'updates_staged/login_server/'); + my @files; + my $start_dir = "updates_staged/login_server"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + $dest_file = $file; + $dest_file =~s/updates_staged\/login_server\///g; + print "Installing :: " . $dest_file . "\n"; + copy_file($file, $dest_file); + } + print "\n Done... \n"; + + print "Pulling down Loginserver database tables...\n"; + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/login_server_tables.sql", "db_update/login_server_tables.sql"); + print "\n\nInstalling Loginserver tables...\n"; + print get_mysql_result_from_file("db_update/login_server_tables.sql"); + print "\nDone...\n\n"; + + add_login_server_firewall_rules(); + + rmtree('updates_staged'); + rmtree('db_update'); + + print "\nPress any key to continue...\n"; + + <>; #Read from STDIN + +} + +sub add_login_server_firewall_rules{ + #::: Check Loginserver Firewall install for Windows + if($OS eq "Windows"){ + $output = `netsh advfirewall firewall show rule name=all`; + @output_buffer = split("\n", $output); + $has_loginserver_rules_titanium = 0; + $has_loginserver_rules_sod = 0; + foreach my $val (@output_buffer){ + if($val=~/Rule Name/i){ + $val=~s/Rule Name://g; + if($val=~/EQEmu Loginserver/i && $val=~/Titanium/i){ + $has_loginserver_rules_titanium = 1; + print "Found existing rule :: " . trim($val) . "\n"; + } + if($val=~/EQEmu Loginserver/i && $val=~/SOD/i){ + $has_loginserver_rules_sod = 1; + print "Found existing rule :: " . trim($val) . "\n"; + } + } + } + + if($has_loginserver_rules_titanium == 0){ + print "Attempting to add EQEmu Loginserver Firewall Rules (Titanium) (TCP) port 5998 \n"; + print `netsh advfirewall firewall add rule name="EQEmu Loginserver (Titanium) (5998) TCP" dir=in action=allow protocol=TCP localport=5998`; + print "Attempting to add EQEmu Loginserver Firewall Rules (Titanium) (UDP) port 5998 \n"; + print `netsh advfirewall firewall add rule name="EQEmu Loginserver (Titanium) (5998) UDP" dir=in action=allow protocol=UDP localport=5998`; + } + if($has_loginserver_rules_sod == 0){ + print "Attempting to add EQEmu Loginserver Firewall Rules (SOD+) (TCP) port 5999 \n"; + print `netsh advfirewall firewall add rule name="EQEmu Loginserver (SOD+) (5999) TCP" dir=in action=allow protocol=TCP localport=5999`; + print "Attempting to add EQEmu Loginserver Firewall Rules (SOD+) (UDP) port 5999 \n"; + print `netsh advfirewall firewall add rule name="EQEmu Loginserver (SOD+) (5999) UDP" dir=in action=allow protocol=UDP localport=5999`; + } + + print "If firewall rules don't add you must run this script (eqemu_update.pl) as administrator\n"; + print "\n"; + print "#::: Instructions \n"; + print "In order to connect your server to the loginserver you must point your eqemu_config.xml to your local server similar to the following:\n"; + print " + + login.eqemulator.net + 5998 + + + + + 127.0.0.1 + 5998 + + + + "; + print "\nWhen done, make sure your EverQuest client points to your loginserver's IP (In this case it would be 127.0.0.1) in the eqhosts.txt file\n"; + } +} + +sub check_windows_firewall_rules{ + $output = `netsh advfirewall firewall show rule name=all`; + @output_buffer = split("\n", $output); + $has_world_rules = 0; + $has_zone_rules = 0; + foreach my $val (@output_buffer){ + if($val=~/Rule Name/i){ + $val=~s/Rule Name://g; + if($val=~/EQEmu World/i){ + $has_world_rules = 1; + print "Found existing rule :: " . trim($val) . "\n"; + } + if($val=~/EQEmu Zone/i){ + $has_zone_rules = 1; + print "Found existing rule :: " . trim($val) . "\n"; + } + } + } + + if($has_world_rules == 0){ + print "Attempting to add EQEmu World Firewall Rules (TCP) port 9000 \n"; + print `netsh advfirewall firewall add rule name="EQEmu World (9000) TCP" dir=in action=allow protocol=TCP localport=9000`; + print "Attempting to add EQEmu World Firewall Rules (UDP) port 9000 \n"; + print `netsh advfirewall firewall add rule name="EQEmu World (9000) UDP" dir=in action=allow protocol=UDP localport=9000`; + } + if($has_zone_rules == 0){ + print "Attempting to add EQEmu Zones (7000-7500) TCP \n"; + print `netsh advfirewall firewall add rule name="EQEmu Zones (7000-7500) TCP" dir=in action=allow protocol=TCP localport=7000-7500`; + print "Attempting to add EQEmu Zones (7000-7500) UDP \n"; + print `netsh advfirewall firewall add rule name="EQEmu Zones (7000-7500) UDP" dir=in action=allow protocol=UDP localport=7000-7500`; + } +} + +sub fetch_server_dlls{ + print "Fetching lua51.dll, zlib1.dll, libmysql.dll...\n"; + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/lua51.dll", "lua51.dll", 1); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/zlib1.dll", "zlib1.dll", 1); + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/libmysql.dll", "libmysql.dll", 1); +} + +sub fetch_peq_db_full{ + print "Downloading latest PEQ Database... Please wait...\n"; + get_remote_file("http://edit.peqtgc.com/weekly/peq_beta.zip", "updates_staged/peq_beta.zip", 1); + print "Downloaded latest PEQ Database... Extracting...\n"; + unzip('updates_staged/peq_beta.zip', 'updates_staged/peq_db/'); + my $start_dir = "updates_staged\\peq_db"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + $dest_file = $file; + $dest_file =~s/updates_staged\\peq_db\///g; + if($file=~/peqbeta|player_tables/i){ + print "MariaDB :: Installing :: " . $dest_file . "\n"; + get_mysql_result_from_file($file); + } + if($file=~/eqtime/i){ + print "Installing eqtime.cfg\n"; + copy_file($file, "eqtime.cfg"); + } + } +} + +sub map_files_fetch_bulk{ + print "\n --- Fetching Latest Maps... (This could take a few minutes...) --- \n"; + get_remote_file("http://github.com/Akkadius/EQEmuMaps/archive/master.zip", "maps/maps.zip", 1); + unzip('maps/maps.zip', 'maps/'); + my @files; + my $start_dir = "maps\\EQEmuMaps-master\\maps"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + $dest_file = $file; + $dest_file =~s/maps\\EQEmuMaps-master\\maps\///g; + print "Installing :: " . $dest_file . "\n"; + copy_file($file, "maps/" . $new_file); + } + print "\n --- Fetched Latest Maps... --- \n"; + + rmtree('maps/EQEmuMaps-master'); + unlink('maps/maps.zip'); +} + +sub map_files_fetch{ + print "\n --- Fetching Latest Maps --- \n"; + + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuMaps/master/!eqemu_maps_manifest.txt", "updates_staged/eqemu_maps_manifest.txt"); + + #::: Get Data from manifest + open (FILE, "updates_staged/eqemu_maps_manifest.txt"); + $i = 0; + while (){ + chomp; + $o = $_; + @manifest_map_data = split(',', $o); + if($manifest_map_data[0] ne ""){ + $maps_manifest[$i] = [$manifest_map_data[0], $manifest_map_data[1]]; + $i++; + } + } + + #::: Download + $fc = 0; + for($m = 0; $m <= $i; $m++){ + my $file_existing = $maps_manifest[$m][0]; + my $file_existing_size = (stat $file_existing)[7]; + if($file_existing_size != $maps_manifest[$m][1]){ + print "Updating: '" . $maps_manifest[$m][0] . "'\n"; + get_remote_file("https://raw.githubusercontent.com/Akkadius/EQEmuMaps/master/" . $maps_manifest[$m][0], $maps_manifest[$m][0], 1); + $fc++; + } + } + + if($fc == 0){ + print "\nNo Map Updates found... \n\n"; + } +} + +sub quest_files_fetch{ + if (!-e "updates_staged/Quests-Plugins-master/quests/") { + print "\n --- Fetching Latest Quests --- \n"; + get_remote_file("https://github.com/EQEmu/Quests-Plugins/archive/master.zip", "updates_staged/Quests-Plugins-master.zip", 1); + print "\nFetched latest quests...\n"; + mkdir('updates_staged'); + unzip('updates_staged/Quests-Plugins-master.zip', 'updates_staged/'); + } + + $fc = 0; + use File::Find; + use File::Compare; + + my @files; + my $start_dir = "updates_staged/Quests-Plugins-master/quests/"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + if($file=~/\.pl|\.lua|\.ext/i){ + $staged_file = $file; + $dest_file = $file; + $dest_file =~s/updates_staged\/Quests-Plugins-master\///g; + + if (!-e $dest_file) { + copy_file($staged_file, $dest_file); + print "Installing :: '" . $dest_file . "'\n"; + $fc++; + } + else{ + $diff = do_file_diff($dest_file, $staged_file); + if($diff ne ""){ + $backup_dest = "updates_backups/" . $time_stamp . "/" . $dest_file; + + print $diff . "\n"; + print "\nFile Different :: '" . $dest_file . "'\n"; + print "\nDo you wish to update this Quest? '" . $dest_file . "' [Yes (Enter) - No (N)] \nA backup will be found in '" . $backup_dest . "'\n"; + my $input = ; + if($input=~/N/i){} + else{ + #::: Make a backup + copy_file($dest_file, $backup_dest); + #::: Copy staged to running + copy($staged_file, $dest_file); + print "Installing :: '" . $dest_file . "'\n\n"; + } + $fc++; + } + } + } + } + + rmtree('updates_staged'); + + if($fc == 0){ + print "\nNo Quest Updates found... \n\n"; + } +} + +sub lua_modules_fetch{ + if (!-e "updates_staged/Quests-Plugins-master/quests/lua_modules/") { + print "\n --- Fetching Latest LUA Modules --- \n"; + get_remote_file("https://github.com/EQEmu/Quests-Plugins/archive/master.zip", "updates_staged/Quests-Plugins-master.zip", 1); + print "\nFetched latest LUA Modules...\n"; + unzip('updates_staged/Quests-Plugins-master.zip', 'updates_staged/'); + } + + $fc = 0; + use File::Find; + use File::Compare; + + my @files; + my $start_dir = "updates_staged/Quests-Plugins-master/quests/lua_modules/"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + if($file=~/\.pl|\.lua|\.ext/i){ + $staged_file = $file; + $dest_file = $file; + $dest_file =~s/updates_staged\/Quests-Plugins-master\/quests\///g; + + if (!-e $dest_file) { + copy_file($staged_file, $dest_file); + print "Installing :: '" . $dest_file . "'\n"; + $fc++; + } + else{ + $diff = do_file_diff($dest_file, $staged_file); + if($diff ne ""){ + $backup_dest = "updates_backups/" . $time_stamp . "/" . $dest_file; + print $diff . "\n"; + print "\nFile Different :: '" . $dest_file . "'\n"; + print "\nDo you wish to update this LUA Module? '" . $dest_file . "' [Yes (Enter) - No (N)] \nA backup will be found in '" . $backup_dest . "'\n"; + my $input = ; + if($input=~/N/i){} + else{ + #::: Make a backup + copy_file($dest_file, $backup_dest); + #::: Copy staged to running + copy($staged_file, $dest_file); + print "Installing :: '" . $dest_file . "'\n\n"; + } + $fc++; + } + } + } + } + + if($fc == 0){ + print "\nNo LUA Modules Updates found... \n\n"; + } +} + +sub plugins_fetch{ + if (!-e "updates_staged/Quests-Plugins-master/plugins/") { + print "\n --- Fetching Latest Plugins --- \n"; + get_remote_file("https://github.com/EQEmu/Quests-Plugins/archive/master.zip", "updates_staged/Quests-Plugins-master.zip", 1); + print "\nFetched latest plugins...\n"; + unzip('updates_staged/Quests-Plugins-master.zip', 'updates_staged/'); + } + + $fc = 0; + use File::Find; + use File::Compare; + + my @files; + my $start_dir = "updates_staged/Quests-Plugins-master/plugins/"; + find( + sub { push @files, $File::Find::name unless -d; }, + $start_dir + ); + for my $file (@files) { + if($file=~/\.pl|\.lua|\.ext/i){ + $staged_file = $file; + $dest_file = $file; + $dest_file =~s/updates_staged\/Quests-Plugins-master\///g; + + if (!-e $dest_file) { + copy_file($staged_file, $dest_file); + print "Installing :: '" . $dest_file . "'\n"; + $fc++; + } + else{ + $diff = do_file_diff($dest_file, $staged_file); + if($diff ne ""){ + $backup_dest = "updates_backups/" . $time_stamp . "/" . $dest_file; + print $diff . "\n"; + print "\nFile Different :: '" . $dest_file . "'\n"; + print "\nDo you wish to update this Plugin? '" . $dest_file . "' [Yes (Enter) - No (N)] \nA backup will be found in '" . $backup_dest . "'\n"; + my $input = ; + if($input=~/N/i){} + else{ + #::: Make a backup + copy_file($dest_file, $backup_dest); + #::: Copy staged to running + copy($staged_file, $dest_file); + print "Installing :: '" . $dest_file . "'\n\n"; + } + $fc++; + } + } + } + } + + if($fc == 0){ + print "\nNo Plugin Updates found... \n\n"; + } +} + +sub do_file_diff{ + $file_1 = $_[0]; + $file_2 = $_[1]; + if($OS eq "Windows"){ + eval "use Text::Diff"; + $diff = diff($file_1, $file_2, { STYLE => "Unified" }); + return $diff; + } + if($OS eq "Linux"){ + # print 'diff -u "$file_1" "$file_2"' . "\n"; + return `diff -u "$file_1" "$file_2"`; + } +} + +sub unzip{ + $archive_to_unzip = $_[0]; + $dest_folder = $_[1]; + + if($OS eq "Windows"){ + eval "use Archive::Zip qw( :ERROR_CODES :CONSTANTS )"; + my $zip = Archive::Zip->new(); + unless ( $zip->read($archive_to_unzip) == AZ_OK ) { + die 'read error'; + } + print "Extracting...\n"; + $zip->extractTree('', $dest_folder); + } + if($OS eq "Linux"){ + print `unzip -o "$archive_to_unzip" -d "$dest_folder"`; + } +} + +sub are_file_sizes_different{ + $file_1 = $_[0]; + $file_2 = $_[1]; + my $file_1 = (stat $file_1)[7]; + my $file_2 = (stat $file_2)[7]; + # print $file_1 . " :: " . $file_2 . "\n"; + if($file_1 != $file_2){ + return 1; + } + return; +} + +sub do_bots_db_schema_drop{ + #"drop_bots.sql" is run before reverting database back to 'normal' + print "Fetching drop_bots.sql...\n"; + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/git/bots/drop_bots.sql", "db_update/drop_bots.sql"); + print get_mysql_result_from_file("db_update/drop_bots.sql"); + + print "Restoring normality...\n"; + print get_mysql_result("DELETE FROM `rule_values` WHERE `rule_name` LIKE 'Bots:%';"); + + if(get_mysql_result("SHOW TABLES LIKE 'commands'") ne "" && $db){ + print get_mysql_result("DELETE FROM `commands` WHERE `command` LIKE 'bot';"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'command_settings'") ne "" && $db){ + print get_mysql_result("DELETE FROM `command_settings` WHERE `command` LIKE 'bot';"); + } + + if(get_mysql_result("SHOW KEYS FROM `group_id` WHERE `Key_name` LIKE 'PRIMARY'") ne "" && $db){ + print get_mysql_result("ALTER TABLE `group_id` DROP PRIMARY KEY;"); + } + print get_mysql_result("ALTER TABLE `group_id` ADD PRIMARY KEY (`groupid`, `charid`, `ismerc`);"); + + if(get_mysql_result("SHOW KEYS FROM `guild_members` WHERE `Key_name` LIKE 'PRIMARY'") ne "" && $db){ + print get_mysql_result("ALTER TABLE `guild_members` DROP PRIMARY KEY;"); + } + print get_mysql_result("ALTER TABLE `guild_members` ADD PRIMARY KEY (`char_id`);"); + + print get_mysql_result("UPDATE `spawn2` SET `enabled` = 0 WHERE `id` IN (59297,59298);"); + + if(get_mysql_result("SHOW COLUMNS FROM `db_version` LIKE 'bots_version'") ne "" && $db){ + print get_mysql_result("UPDATE `db_version` SET `bots_version` = 0;"); + } +} + +sub modify_db_for_bots{ + #Called after the db bots schema (2015_09_30_bots.sql) has been loaded + print "Modifying database for bots...\n"; + print get_mysql_result("UPDATE `spawn2` SET `enabled` = 1 WHERE `id` IN (59297,59298);"); + + if(get_mysql_result("SHOW KEYS FROM `guild_members` WHERE `Key_name` LIKE 'PRIMARY'") ne "" && $db){ + print get_mysql_result("ALTER TABLE `guild_members` DROP PRIMARY KEY;"); + } + + if(get_mysql_result("SHOW KEYS FROM `group_id` WHERE `Key_name` LIKE 'PRIMARY'") ne "" && $db){ + print get_mysql_result("ALTER TABLE `group_id` DROP PRIMARY KEY;"); + } + print get_mysql_result("ALTER TABLE `group_id` ADD PRIMARY KEY USING BTREE(`groupid`, `charid`, `name`, `ismerc`);"); + + if(get_mysql_result("SHOW TABLES LIKE 'command_settings'") ne "" && get_mysql_result("SELECT `command` FROM `command_settings` WHERE `command` LIKE 'bot'") eq "" && $db){ + print get_mysql_result("INSERT INTO `command_settings` VALUES ('bot', '0', '');"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'commands'") ne "" && get_mysql_result("SELECT `command` FROM `commands` WHERE `command` LIKE 'bot'") eq "" && $db){ + print get_mysql_result("INSERT INTO `commands` VALUES ('bot', '0');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:BotAAExpansion'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:AAExpansion' WHERE `rule_name` LIKE 'Bots:BotAAExpansion';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:AAExpansion'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:AAExpansion', '8', 'The expansion through which bots will obtain AAs');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:CreateBotCount'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:CreationLimit' WHERE `rule_name` LIKE 'Bots:CreateBotCount';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:CreationLimit'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:CreationLimit', '150', 'Number of bots that each account can create');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:BotFinishBuffing'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:FinishBuffing' WHERE `rule_name` LIKE 'Bots:BotFinishBuffing';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:FinishBuffing'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:FinishBuffing', 'false', 'Allow for buffs to complete even if the bot caster is out of mana. Only affects buffing out of combat.');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:BotGroupBuffing'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:GroupBuffing' WHERE `rule_name` LIKE 'Bots:BotGroupBuffing';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:GroupBuffing'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:GroupBuffing', 'false', 'Bots will cast single target buffs as group buffs, default is false for single. Does not make single target buffs work for MGB.');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:BotManaRegen'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:ManaRegen' WHERE `rule_name` LIKE 'Bots:BotManaRegen';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:ManaRegen'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:ManaRegen', '3.0', 'Adjust mana regen for bots, 1 is fast and higher numbers slow it down 3 is about the same as players.');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:BotQuest'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:QuestableSpawnLimit' WHERE `rule_name` LIKE 'Bots:BotQuest';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:QuestableSpawnLimit'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:QuestableSpawnLimit', 'false', 'Optional quest method to manage bot spawn limits using the quest_globals name bot_spawn_limit, see: /bazaar/Aediles_Thrall.pl');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:BotSpellQuest'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:QuestableSpells' WHERE `rule_name` LIKE 'Bots:BotSpellQuest';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:QuestableSpells'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:QuestableSpells', 'false', 'Anita Thrall\\\'s (Anita_Thrall.pl) Bot Spell Scriber quests.');"); + } + + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:SpawnBotCount'") ne "" && $db){ + print get_mysql_result("UPDATE `rule_values` SET `rule_name` = 'Bots:SpawnLimit' WHERE `rule_name` LIKE 'Bots:SpawnBotCount';"); + } + if(get_mysql_result("SELECT `rule_name` FROM `rule_values` WHERE `rule_name` LIKE 'Bots:SpawnLimit'") eq "" && $db){ + print get_mysql_result("INSERT INTO `rule_values` VALUES ('1', 'Bots:SpawnLimit', '71', 'Number of bots a character can have spawned at one time, You + 71 bots is a 12 group raid');"); + } + + convert_existing_bot_data(); +} + +sub convert_existing_bot_data{ + if(get_mysql_result("SHOW TABLES LIKE 'bots'") ne "" && $db){ + print "Converting existing bot data...\n"; + print get_mysql_result("INSERT INTO `bot_data` (`bot_id`, `owner_id`, `spells_id`, `name`, `last_name`, `zone_id`, `gender`, `race`, `class`, `level`, `creation_day`, `last_spawn`, `time_spawned`, `size`, `face`, `hair_color`, `hair_style`, `beard`, `beard_color`, `eye_color_1`, `eye_color_2`, `drakkin_heritage`, `drakkin_tattoo`, `drakkin_details`, `ac`, `atk`, `hp`, `mana`, `str`, `sta`, `cha`, `dex`, `int`, `agi`, `wis`, `fire`, `cold`, `magic`, `poison`, `disease`, `corruption`) SELECT `BotID`, `BotOwnerCharacterID`, `BotSpellsID`, `Name`, `LastName`, `LastZoneId`, `Gender`, `Race`, `Class`, `BotLevel`, UNIX_TIMESTAMP(`BotCreateDate`), UNIX_TIMESTAMP(`LastSpawnDate`), `TotalPlayTime`, `Size`, `Face`, `LuclinHairColor`, `LuclinHairStyle`, `LuclinBeard`, `LuclinBeardColor`, `LuclinEyeColor`, `LuclinEyeColor2`, `DrakkinHeritage`, `DrakkinTattoo`, `DrakkinDetails`, `AC`, `ATK`, `HP`, `Mana`, `STR`, `STA`, `CHA`, `DEX`, `_INT`, `AGI`, `WIS`, `FR`, `CR`, `MR`, `PR`, `DR`, `Corrup` FROM `bots`;"); + + print get_mysql_result("INSERT INTO `bot_inspect_messages` (`bot_id`, `inspect_message`) SELECT `BotID`, `BotInspectMessage` FROM `bots`;"); + + print get_mysql_result("RENAME TABLE `bots` TO `bots_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botstances'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_stances` (`bot_id`, `stance_id`) SELECT bs.`BotID`, bs.`StanceID` FROM `botstances` bs INNER JOIN `bot_data` bd ON bs.`BotID` = bd.`bot_id`;"); + + print get_mysql_result("RENAME TABLE `botstances` TO `botstances_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'bottimers'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_timers` (`bot_id`, `timer_id`, `timer_value`) SELECT bt.`BotID`, bt.`TimerID`, bt.`Value` FROM `bottimers` bt INNER JOIN `bot_data` bd ON bt.`BotID` = bd.`bot_id`;"); + + print get_mysql_result("RENAME TABLE `bottimers` TO `bottimers_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botbuffs'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_buffs` (`buffs_index`, `bot_id`, `spell_id`, `caster_level`, `duration_formula`, `tics_remaining`, `poison_counters`, `disease_counters`, `curse_counters`, `corruption_counters`, `numhits`, `melee_rune`, `magic_rune`, `persistent`) SELECT bb.`BotBuffId`, bb.`BotId`, bb.`SpellId`, bb.`CasterLevel`, bb.`DurationFormula`, bb.`TicsRemaining`, bb.`PoisonCounters`, bb.`DiseaseCounters`, bb.`CurseCounters`, bb.`CorruptionCounters`, bb.`HitCount`, bb.`MeleeRune`, bb.`MagicRune`, bb.`Persistent` FROM `botbuffs` bb INNER JOIN `bot_data` bd ON bb.`BotId` = bd.`bot_id`;"); + + if(get_mysql_result("SHOW COLUMNS FROM `botbuffs` LIKE 'dot_rune'") ne "" && $db){ + print get_mysql_result("UPDATE `bot_buffs` bb INNER JOIN `botbuffs` bbo ON bb.`buffs_index` = bbo.`BotBuffId` SET bb.`dot_rune` = bbo.`dot_rune` WHERE bb.`bot_id` = bbo.`BotID`;"); + } + + if(get_mysql_result("SHOW COLUMNS FROM `botbuffs` LIKE 'caston_x'") ne "" && $db){ + print get_mysql_result("UPDATE `bot_buffs` bb INNER JOIN `botbuffs` bbo ON bb.`buffs_index` = bbo.`BotBuffId` SET bb.`caston_x` = bbo.`caston_x` WHERE bb.`bot_id` = bbo.`BotID`;"); + } + + if(get_mysql_result("SHOW COLUMNS FROM `botbuffs` LIKE 'caston_y'") ne "" && $db){ + print get_mysql_result("UPDATE `bot_buffs` bb INNER JOIN `botbuffs` bbo ON bb.`buffs_index` = bbo.`BotBuffId` SET bb.`caston_y` = bbo.`caston_y` WHERE bb.`bot_id` = bbo.`BotID`;"); + } + + if(get_mysql_result("SHOW COLUMNS FROM `botbuffs` LIKE 'caston_z'") ne "" && $db){ + print get_mysql_result("UPDATE `bot_buffs` bb INNER JOIN `botbuffs` bbo ON bb.`buffs_index` = bbo.`BotBuffId` SET bb.`caston_z` = bbo.`caston_z` WHERE bb.`bot_id` = bbo.`BotID`;"); + } + + if(get_mysql_result("SHOW COLUMNS FROM `botbuffs` LIKE 'ExtraDIChance'") ne "" && $db){ + print get_mysql_result("UPDATE `bot_buffs` bb INNER JOIN `botbuffs` bbo ON bb.`buffs_index` = bbo.`BotBuffId` SET bb.`extra_di_chance` = bbo.`ExtraDIChance` WHERE bb.`bot_id` = bbo.`BotID`;"); + } + + print get_mysql_result("RENAME TABLE `botbuffs` TO `botbuffs_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botinventory'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_inventories` (`inventories_index`, `bot_id`, `slot_id`, `item_id`, `inst_charges`, `inst_color`, `inst_no_drop`, `augment_1`, `augment_2`, `augment_3`, `augment_4`, `augment_5`) SELECT bi.`BotInventoryID`, bi.`BotID`, bi.`SlotID`, bi.`ItemID`, bi.`charges`, bi.`color`, bi.`instnodrop`, bi.`augslot1`, bi.`augslot2`, bi.`augslot3`, bi.`augslot4`, bi.`augslot5` FROM `botinventory` bi INNER JOIN `bot_data` bd ON bi.`BotID` = bd.`bot_id`;"); + + if(get_mysql_result("SHOW COLUMNS FROM `botinventory` LIKE 'augslot6'") ne "" && $db){ + print get_mysql_result("UPDATE `bot_inventories` bi INNER JOIN `botinventory` bio ON bi.`inventories_index` = bio.`BotInventoryID` SET bi.`augment_6` = bio.`augslot6` WHERE bi.`bot_id` = bio.`BotID`;"); + } + + print get_mysql_result("RENAME TABLE `botinventory` TO `botinventory_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botpets'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_pets` (`pets_index`, `pet_id`, `bot_id`, `name`, `mana`, `hp`) SELECT bp.`BotPetsId`, bp.`PetId`, bp.`BotId`, bp.`Name`, bp.`Mana`, bp.`HitPoints` FROM `botpets` bp INNER JOIN `bot_data` bd ON bp.`BotId` = bd.`bot_id`;"); + + print get_mysql_result("RENAME TABLE `botpets` TO `botpets_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botpetbuffs'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_pet_buffs` (`pet_buffs_index`, `pets_index`, `spell_id`, `caster_level`, `duration`) SELECT bpb.`BotPetBuffId`, bpb.`BotPetsId`, bpb.`SpellId`, bpb.`CasterLevel`, bpb.`Duration` FROM `botpetbuffs` bpb INNER JOIN `bot_pets` bp ON bpb.`BotPetsId` = bp.`pets_index`;"); + + print get_mysql_result("RENAME TABLE `botpetbuffs` TO `botpetbuffs_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botpetinventory'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_pet_inventories` (`pet_inventories_index`, `pets_index`, `item_id`) SELECT bpi.`BotPetInventoryId`, bpi.`BotPetsId`, bpi.`ItemId` FROM `botpetinventory` bpi INNER JOIN `bot_pets` bp ON bpi.`BotPetsId` = bp.`pets_index`;"); + + print get_mysql_result("RENAME TABLE `botpetinventory` TO `botpetinventory_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botgroup'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_groups` (`groups_index`, `group_leader_id`, `group_name`) SELECT bg.`BotGroupId`, bg.`BotGroupLeaderBotId`, bg.`BotGroupName` FROM `botgroup` bg INNER JOIN `bot_data` bd ON bg.`BotGroupLeaderBotId` = bd.`bot_id`;"); + + print get_mysql_result("RENAME TABLE `botgroup` TO `botgroup_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botgroupmembers'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_group_members` (`group_members_index`, `groups_index`, `bot_id`) SELECT bgm.`BotGroupMemberId`, bgm.`BotGroupId`, bgm.`BotId` FROM `botgroupmembers` bgm INNER JOIN `bot_groups` bg ON bgm.`BotGroupId` = bg.`groups_index` INNER JOIN `bot_data` bd ON bgm.`BotId` = bd.`bot_id`;"); + + print get_mysql_result("RENAME TABLE `botgroupmembers` TO `botgroupmembers_old`;"); + } + + if(get_mysql_result("SHOW TABLES LIKE 'botguildmembers'") ne "" && $db){ + print get_mysql_result("INSERT INTO `bot_guild_members` (`bot_id`, `guild_id`, `rank`, `tribute_enable`, `total_tribute`, `last_tribute`, `banker`, `public_note`, `alt`) SELECT bgm.`char_id`, bgm.`guild_id`, bgm.`rank`, bgm.`tribute_enable`, bgm.`total_tribute`, bgm.`last_tribute`, bgm.`banker`, bgm.`public_note`, bgm.`alt` FROM `botguildmembers` bgm INNER JOIN `guilds` g ON bgm.`guild_id` = g.`id` INNER JOIN `bot_data` bd ON bgm.`char_id` = bd.`bot_id`;"); + + print get_mysql_result("RENAME TABLE `botguildmembers` TO `botguildmembers_old`;"); + } +} + +sub get_bots_db_version{ + #::: Check if bots_version column exists... + if(get_mysql_result("SHOW COLUMNS FROM db_version LIKE 'bots_version'") eq "" && $db){ + print get_mysql_result("ALTER TABLE db_version ADD bots_version int(11) DEFAULT '0' AFTER version;"); + print "\nColumn 'bots_version' does not exists.... Adding to 'db_version' table...\n\n"; + } + $bots_local_db_version = trim(get_mysql_result("SELECT bots_version FROM db_version LIMIT 1")); + return $bots_local_db_version; +} + +sub bots_db_management{ + #::: Main Binary Database version + $bin_db_ver = trim($db_version[2]); + + #::: If we have stale data from main db run + if($db_run_stage > 0 && $bots_db_management == 0){ + clear_database_runs(); + } + + if($bin_db_ver == 0){ + print "Your server binaries (world/zone) are not compiled for bots...\n"; + return; + } + + #::: Set on flag for running bot updates... + $bots_db_management = 1; + + $bots_local_db_version = get_bots_db_version(); + + run_database_check(); +} + +sub main_db_management{ + #::: If we have stale data from bots db run + if($db_run_stage > 0 && $bots_db_management == 1){ + clear_database_runs(); + } + + #::: Main Binary Database version + $bin_db_ver = trim($db_version[1]); + + $bots_db_management = 0; + run_database_check(); +} + +sub clear_database_runs{ + # print "DEBUG :: clear_database_runs\n\n"; + #::: Clear manifest data... + %m_d = (); + #::: Clear updates... + @total_updates = (); + #::: Clear stage + $db_run_stage = 0; +} + +#::: Responsible for Database Upgrade Routines +sub run_database_check{ + + if(!$db){ + print "No database present, check your eqemu_config.xml for proper MySQL/MariaDB configuration...\n"; + return; + } + + if(!@total_updates){ + #::: Pull down bots database manifest + if($bots_db_management == 1){ + print "Retrieving latest bots database manifest...\n"; + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/git/bots/bots_db_update_manifest.txt", "db_update/db_update_manifest.txt"); + } + #::: Pull down mainstream database manifest + else{ + print "Retrieving latest database manifest...\n"; + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/db_update_manifest.txt", "db_update/db_update_manifest.txt"); + } + } + + #::: Run 2 - Running pending updates... + if(@total_updates){ + @total_updates = sort @total_updates; + foreach my $val (@total_updates){ + $file_name = trim($m_d{$val}[1]); + print "Running Update: " . $val . " - " . $file_name . "\n"; + print get_mysql_result_from_file("db_update/$file_name"); + print get_mysql_result("UPDATE db_version SET version = $val WHERE version < $val"); + + if($bots_db_management == 1 && $val == 9000){ + modify_db_for_bots(); + } + } + $db_run_stage = 2; + } + #::: Run 1 - Initial checking of needed updates... + else{ + print "Reading manifest...\n\n"; + use Data::Dumper; + open (FILE, "db_update/db_update_manifest.txt"); + while () { + chomp; + $o = $_; + if($o=~/#/i){ next; } + @manifest = split('\|', $o); + $m_d{$manifest[0]} = [@manifest]; + } + #::: Setting Manifest stage... + $db_run_stage = 1; + } + + @total_updates = (); + + #::: This is where we set checkpoints for where a database might be so we don't check so far back in the manifest... + $revision_check = 1000; + if(get_mysql_result("SHOW TABLES LIKE 'character_data'") ne ""){ + $revision_check = 9000; + } + + #::: Iterate through Manifest backwards from binary version down to local version... + for($i = $bin_db_ver; $i > $revision_check; $i--){ + if(!defined($m_d{$i}[0])){ next; } + + $file_name = trim($m_d{$i}[1]); + $query_check = trim($m_d{$i}[2]); + $match_type = trim($m_d{$i}[3]); + $match_text = trim($m_d{$i}[4]); + + #::: Match type update + if($match_type eq "contains"){ + if(trim(get_mysql_result($query_check))=~/$match_text/i){ + print "Missing DB Update " . $i . " '" . $file_name . "' \n"; + fetch_missing_db_update($i, $file_name); + push(@total_updates, $i); + } + else{ + print "DB up to date with: " . $i . " - '" . $file_name . "' \n"; + } + print_match_debug(); + print_break(); + } + if($match_type eq "missing"){ + if(get_mysql_result($query_check)=~/$match_text/i){ + print "DB up to date with: " . $i . " - '" . $file_name . "' \n"; + next; + } + else{ + print "Missing DB Update " . $i . " '" . $file_name . "' \n"; + fetch_missing_db_update($i, $file_name); + push(@total_updates, $i); + } + print_match_debug(); + print_break(); + } + if($match_type eq "empty"){ + if(get_mysql_result($query_check) eq ""){ + print "Missing DB Update " . $i . " '" . $file_name . "' \n"; + fetch_missing_db_update($i, $file_name); + push(@total_updates, $i); + } + else{ + print "DB up to date with: " . $i . " - '" . $file_name . "' \n"; + } + print_match_debug(); + print_break(); + } + if($match_type eq "not_empty"){ + if(get_mysql_result($query_check) ne ""){ + print "Missing DB Update " . $i . " '" . $file_name . "' \n"; + fetch_missing_db_update($i, $file_name); + push(@total_updates, $i); + } + else{ + print "DB up to date with: " . $i . " - '" . $file_name . "' \n"; + } + print_match_debug(); + print_break(); + } + } + print "\n"; + + if(scalar (@total_updates) == 0 && $db_run_stage == 2){ + print "No updates need to be run...\n"; + if($bots_db_management == 1){ + print "Setting Database to Bots Binary Version (" . $bin_db_ver . ") if not already...\n\n"; + get_mysql_result("UPDATE db_version SET bots_version = $bin_db_ver"); + } + else{ + print "Setting Database to Binary Version (" . $bin_db_ver . ") if not already...\n\n"; + get_mysql_result("UPDATE db_version SET version = $bin_db_ver"); + } + + clear_database_runs(); + } +} + +sub fetch_missing_db_update{ + $db_update = $_[0]; + $update_file = $_[1]; + if($db_update >= 9000){ + if($bots_db_management == 1){ + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/git/bots/required/" . $update_file, "db_update/" . $update_file . ""); + } + else{ + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/git/required/" . $update_file, "db_update/" . $update_file . ""); + } + } + elsif($db_update >= 5000 && $db_update <= 9000){ + get_remote_file("https://raw.githubusercontent.com/EQEmu/Server/master/utils/sql/svn/" . $update_file, "db_update/" . $update_file . ""); + } +} + +sub print_match_debug{ + if(!$debug){ return; } + print " Match Type: '" . $match_type . "'\n"; + print " Match Text: '" . $match_text . "'\n"; + print " Query Check: '" . $query_check . "'\n"; + print " Result: '" . trim(get_mysql_result($query_check)) . "'\n"; +} +sub print_break{ + if(!$debug){ return; } + print "\n==============================================\n"; +} \ No newline at end of file diff --git a/utils/scripts/eqemu_update.pl b/utils/scripts/eqemu_update.pl index 6d1e32c3a..a7da3c792 100644 --- a/utils/scripts/eqemu_update.pl +++ b/utils/scripts/eqemu_update.pl @@ -298,6 +298,7 @@ sub show_menu_prompt { 18 => \&fetch_latest_windows_binaries_bots, 19 => \&do_bots_db_schema_drop, 20 => \&do_update_self, + 21 => \&database_dump_player_tables, 0 => \&script_exit, ); @@ -378,6 +379,7 @@ return <){ + chomp; + $o = $_; + $tables .= $o . ","; + } + $tables = substr($tables, 0, -1); + + print `perl db_dumper.pl database="$db" loc="backups" tables="$tables" backup_name="player_tables_export" nolock`; + + print "\nPress any key to continue...\n"; + + <>; #Read from STDIN + +} + sub database_dump_compress { check_for_database_dump_script(); print "Performing database backup....\n"; diff --git a/utils/sql/character_table_list.txt b/utils/sql/character_table_list.txt new file mode 100644 index 000000000..2a902d009 --- /dev/null +++ b/utils/sql/character_table_list.txt @@ -0,0 +1,33 @@ +adventure_stats +char_recipe_list +character_activities +character_alt_currency +character_alternate_abilities +character_bandolier +character_bind +character_currency +character_data +character_disciplines +character_enabledtasks +character_inspect_messages +character_languages +character_leadership_abilities +character_material +character_memmed_spells +character_potionbelt +character_skills +character_spells +character_tribute +completed_tasks +faction_values +friends +guild_members +instance_list_player +inventory +keyring +mail +player_titlesets +quest_globals +timers +titles +zone_flags diff --git a/world/client.cpp b/world/client.cpp index 8265ab03a..d2ca3705a 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -142,7 +142,7 @@ void Client::SendLogServer() if(RuleB(World, IsGMPetitionWindowEnabled)) l->enable_petition_wnd = 1; - if(RuleI(World, FVNoDropFlag) == 1 || RuleI(World, FVNoDropFlag) == 2 && GetAdmin() > RuleI(Character, MinStatusForNoDropExemptions)) + if((RuleI(World, FVNoDropFlag) == 1 || RuleI(World, FVNoDropFlag) == 2) && GetAdmin() > RuleI(Character, MinStatusForNoDropExemptions)) l->enable_FV = 1; QueuePacket(outapp); @@ -1483,7 +1483,7 @@ bool Client::OPCharCreate(char *name, CharCreate_Struct *cc) for (i = 0; i < MAX_PP_REF_SPELLBOOK; i++) pp.spell_book[i] = 0xFFFFFFFF; - for(i = 0; i < MAX_PP_REF_MEMSPELL; i++) + for(i = 0; i < MAX_PP_MEMSPELL; i++) pp.mem_spells[i] = 0xFFFFFFFF; for(i = 0; i < BUFF_COUNT; i++) diff --git a/world/zonelist.cpp b/world/zonelist.cpp index abd5c1057..55600c367 100644 --- a/world/zonelist.cpp +++ b/world/zonelist.cpp @@ -88,7 +88,7 @@ void ZSList::Process() { Process(); CatchSignal(2); } - if(reminder && reminder->Check()){ + if(reminder && reminder->Check() && shutdowntimer){ SendEmoteMessage(0,0,0,15,":SYSTEM MSG:World coming down, everyone log out now. World will shut down in %i minutes...", ((shutdowntimer->GetRemainingTime()/1000) / 60)); } LinkedListIterator iterator(list); diff --git a/zone/aa.cpp b/zone/aa.cpp index d18936956..97b444311 100644 --- a/zone/aa.cpp +++ b/zone/aa.cpp @@ -867,7 +867,7 @@ void Client::SendAlternateAdvancementRank(int aa_id, int level) { aai->max_level = ability->GetMaxLevel(this); aai->prev_id = rank->prev_id; - if(rank->next && !CanUseAlternateAdvancementRank(rank->next) || ability->charges > 0) { + if((rank->next && !CanUseAlternateAdvancementRank(rank->next)) || ability->charges > 0) { aai->next_id = -1; } else { aai->next_id = rank->next_id; @@ -1183,12 +1183,12 @@ void Client::ActivateAlternateAdvancementAbility(int rank_id, int target_id) { CommonBreakInvisible(); // Bards can cast instant cast AAs while they are casting another song if(spells[rank->spell].cast_time == 0 && GetClass() == BARD && IsBardSong(casting_spell_id)) { - if(!SpellFinished(rank->spell, entity_list.GetMob(target_id), ALTERNATE_ABILITY_SPELL_SLOT, spells[rank->spell].mana, -1, spells[rank->spell].ResistDiff, false)) { + if(!SpellFinished(rank->spell, entity_list.GetMob(target_id), EQEmu::CastingSlot::AltAbility, spells[rank->spell].mana, -1, spells[rank->spell].ResistDiff, false)) { return; } ExpendAlternateAdvancementCharge(ability->id); } else { - if(!CastSpell(rank->spell, target_id, ALTERNATE_ABILITY_SPELL_SLOT, -1, -1, 0, -1, rank->spell_type + pTimerAAStart, cooldown, nullptr, rank->id)) { + if(!CastSpell(rank->spell, target_id, EQEmu::CastingSlot::AltAbility, -1, -1, 0, -1, rank->spell_type + pTimerAAStart, cooldown, nullptr, rank->id)) { return; } } @@ -1240,6 +1240,10 @@ void Mob::ExpendAlternateAdvancementCharge(uint32 aa_id) { CastToClient()->GetEPP().expended_aa += r->cost; } } + if (IsClient()) { + auto c = CastToClient(); + c->RemoveExpendedAA(ability->first_rank_id); + } aa_ranks.erase(iter.first); } diff --git a/zone/aggro.cpp b/zone/aggro.cpp index 847c51e76..d50c1d43e 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -156,10 +156,21 @@ void NPC::DescribeAggro(Client *towho, Mob *mob, bool verbose) { return; } - if(GetINT() > RuleI(Aggro, IntAggroThreshold) && mob->GetLevelCon(GetLevel()) == CON_GREEN ) { - towho->Message(0, "...%s is red to me (basically)", mob->GetName(), - dist2, iAggroRange2); - return; + if (RuleB(Aggro, UseLevelAggro)) + { + if (GetLevel() < 18 && mob->GetLevelCon(GetLevel()) == CON_GREEN && GetBodyType() != 3) + { + towho->Message(0, "...%s is red to me (basically)", mob->GetName(), dist2, iAggroRange2); + return; + } + } + else + { + if(GetINT() > RuleI(Aggro, IntAggroThreshold) && mob->GetLevelCon(GetLevel()) == CON_GREEN ) { + towho->Message(0, "...%s is red to me (basically)", mob->GetName(), + dist2, iAggroRange2); + return; + } } if(verbose) { @@ -321,11 +332,12 @@ bool Mob::CheckWillAggro(Mob *mob) { int heroicCHA_mod = mob->itembonuses.HeroicCHA/25; // 800 Heroic CHA cap if(heroicCHA_mod > THREATENLY_ARRGO_CHANCE) heroicCHA_mod = THREATENLY_ARRGO_CHANCE; - if + if (RuleB(Aggro, UseLevelAggro) && ( //old InZone check taken care of above by !mob->CastToClient()->Connected() ( - ( GetINT() <= RuleI(Aggro, IntAggroThreshold) ) + ( GetLevel() >= 18 ) + ||(GetBodyType() == 3) ||( mob->IsClient() && mob->CastToClient()->IsSitting() ) ||( mob->GetLevelCon(GetLevel()) != CON_GREEN ) @@ -344,6 +356,7 @@ bool Mob::CheckWillAggro(Mob *mob) { ) ) ) + ) { //FatherNiwtit: make sure we can see them. last since it is very expensive if(CheckLosFN(mob)) { @@ -351,6 +364,39 @@ bool Mob::CheckWillAggro(Mob *mob) { return( mod_will_aggro(mob, this) ); } } + else + { + if + ( + //old InZone check taken care of above by !mob->CastToClient()->Connected() + ( + ( GetINT() <= RuleI(Aggro, IntAggroThreshold) ) + ||( mob->IsClient() && mob->CastToClient()->IsSitting() ) + ||( mob->GetLevelCon(GetLevel()) != CON_GREEN ) + + ) + && + ( + ( + fv == FACTION_SCOWLS + || + (mob->GetPrimaryFaction() != GetPrimaryFaction() && mob->GetPrimaryFaction() == -4 && GetOwner() == nullptr) + || + ( + fv == FACTION_THREATENLY + && zone->random.Roll(THREATENLY_ARRGO_CHANCE - heroicCHA_mod) + ) + ) + ) + ) + { + //FatherNiwtit: make sure we can see them. last since it is very expensive + if(CheckLosFN(mob)) { + Log.Out(Logs::Detail, Logs::Aggro, "Check aggro for %s target %s.", GetName(), mob->GetName()); + return( mod_will_aggro(mob, this) ); + } + } + } Log.Out(Logs::Detail, Logs::Aggro, "Is In zone?:%d\n", mob->InZone()); Log.Out(Logs::Detail, Logs::Aggro, "Dist^2: %f\n", dist2); @@ -462,7 +508,7 @@ void EntityList::AIYellForHelp(Mob* sender, Mob* attacker) { { //if they are in range, make sure we are not green... //then jump in if they are our friend - if(attacker->GetLevelCon(mob->GetLevel()) != CON_GREEN) + if(mob->GetLevel() >= 50 || attacker->GetLevelCon(mob->GetLevel()) != CON_GREEN) { bool useprimfaction = false; if(mob->GetPrimaryFaction() == sender->CastToNPC()->GetPrimaryFaction()) diff --git a/zone/attack.cpp b/zone/attack.cpp index 1c7796f42..ed6a26daf 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -23,6 +23,7 @@ #include "../common/skills.h" #include "../common/spdat.h" #include "../common/string_util.h" +#include "../common/data_verification.h" #include "queryserv.h" #include "quest_parser_collection.h" #include "string_ids.h" @@ -369,18 +370,15 @@ bool Mob::AvoidDamage(Mob *other, int32 &damage, int hand) counter_dodge = attacker->GetSpecialAbilityParam(COUNTER_AVOID_DAMAGE, 4); } - ////////////////////////////////////////////////////////// - // make enrage same as riposte - ///////////////////////////////////////////////////////// - if (IsEnraged() && InFront) { - damage = -3; - Log.Out(Logs::Detail, Logs::Combat, "I am enraged, riposting frontal attack."); - } - // riposte -- it may seem crazy, but if the attacker has SPA 173 on them, they are immune to Ripo bool ImmuneRipo = attacker->aabonuses.RiposteChance || attacker->spellbonuses.RiposteChance || attacker->itembonuses.RiposteChance; // Need to check if we have something in MainHand to actually attack with (or fists) if (hand != EQEmu::legacy::SlotRange && CanThisClassRiposte() && InFront && !ImmuneRipo) { + if (IsEnraged()) { + damage = -3; + Log.Out(Logs::Detail, Logs::Combat, "I am enraged, riposting frontal attack."); + return true; + } if (IsClient()) CastToClient()->CheckIncreaseSkill(EQEmu::skills::SkillRiposte, other, -10); // check auto discs ... I guess aa/items too :P @@ -1205,7 +1203,7 @@ bool Client::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, b IsValidSpell(aabonuses.SkillAttackProc[2])) { float chance = aabonuses.SkillAttackProc[0] / 1000.0f; if (zone->random.Roll(chance)) - SpellFinished(aabonuses.SkillAttackProc[2], other, 10, 0, -1, + SpellFinished(aabonuses.SkillAttackProc[2], other, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SkillAttackProc[2]].ResistDiff); } other->Damage(this, damage, SPELL_UNKNOWN, skillinuse, true, -1, false, special); @@ -1802,7 +1800,7 @@ void NPC::Damage(Mob* other, int32 damage, uint16 spell_id, EQEmu::skills::Skill //handle EVENT_ATTACK. Resets after we have not been attacked for 12 seconds if(attacked_timer.Check()) { - Log.Out(Logs::Detail, Logs::Combat, "Triggering EVENT_ATTACK due to attack by %s", other->GetName()); + Log.Out(Logs::Detail, Logs::Combat, "Triggering EVENT_ATTACK due to attack by %s", other ? other->GetName() : "nullptr"); parse->EventNPC(EVENT_ATTACK, this, other, "", 0); } attacked_timer.Start(CombatEventTimer_expire); @@ -1821,7 +1819,7 @@ void NPC::Damage(Mob* other, int32 damage, uint16 spell_id, EQEmu::skills::Skill if(IsLDoNTrapped()) { Message_StringID(13, LDON_ACCIDENT_SETOFF2); - SpellFinished(GetLDoNTrapSpellID(), other, 10, 0, -1, spells[GetLDoNTrapSpellID()].ResistDiff, false); + SpellFinished(GetLDoNTrapSpellID(), other, EQEmu::CastingSlot::Item, 0, -1, spells[GetLDoNTrapSpellID()].ResistDiff, false); SetLDoNTrapSpellID(0); SetLDoNTrapped(false); SetLDoNTrapDetected(false); @@ -3169,68 +3167,63 @@ void Mob::CommonDamage(Mob* attacker, int32 &damage, const uint16 spell_id, cons BuffFadeByEffect(SE_Mez); } - //check stun chances if bashing - if (damage > 0 && ((skill_used == EQEmu::skills::SkillBash || skill_used == EQEmu::skills::SkillKick) && attacker)) { - // NPCs can stun with their bash/kick as soon as they receive it. - // Clients can stun mobs under level 56 with their kick when they get level 55 or greater. - // Clients have a chance to stun if the mob is 56+ - - // Calculate the chance to stun - int stun_chance = 0; - if (!GetSpecialAbility(UNSTUNABLE)) { - if (attacker->IsNPC()) { - stun_chance = RuleI(Combat, NPCBashKickStunChance); - } else if (attacker->IsClient()) { - // Less than base immunity - // Client vs. Client always uses the chance - if (!IsClient() && GetLevel() <= RuleI(Spells, BaseImmunityLevel)) { - if (skill_used == EQEmu::skills::SkillBash) // Bash always will - stun_chance = 100; - else if (attacker->GetLevel() >= RuleI(Combat, ClientStunLevel)) - stun_chance = 100; // only if you're over level 55 and using kick - } else { // higher than base immunity or Client vs. Client - // not sure on this number, use same as NPC for now - if (skill_used == EQEmu::skills::SkillKick && attacker->GetLevel() < RuleI(Combat, ClientStunLevel)) - stun_chance = RuleI(Combat, NPCBashKickStunChance); - else if (skill_used == EQEmu::skills::SkillBash) - stun_chance = RuleI(Combat, NPCBashKickStunChance) + - attacker->spellbonuses.StunBashChance + - attacker->itembonuses.StunBashChance + - attacker->aabonuses.StunBashChance; - } - } + // broken up for readability + // This is based on what the client is doing + // We had a bunch of stuff like BaseImmunityLevel checks, which I think is suppose to just be for spells + // This is missing some merc checks, but those mostly just skipped the spell bonuses I think ... + bool can_stun = false; + int stunbash_chance = 0; // bonus + if (attacker) { + if (skill_used == EQEmu::skills::SkillBash) { + can_stun = true; + if (attacker->IsClient()) + stunbash_chance = attacker->spellbonuses.StunBashChance + + attacker->itembonuses.StunBashChance + + attacker->aabonuses.StunBashChance; + } else if (skill_used == EQEmu::skills::SkillKick && + (attacker->GetLevel() > 55 || attacker->IsNPC()) && GetClass() == WARRIOR) { + can_stun = true; } - if (stun_chance && zone->random.Roll(stun_chance)) { - // Passed stun, try to resist now - int stun_resist = itembonuses.StunResist + spellbonuses.StunResist; - int frontal_stun_resist = itembonuses.FrontalStunResist + spellbonuses.FrontalStunResist; - - Log.Out(Logs::Detail, Logs::Combat, "Stun passed, checking resists. Was %d chance.", stun_chance); - if (IsClient()) { - stun_resist += aabonuses.StunResist; - frontal_stun_resist += aabonuses.FrontalStunResist; - } - - // frontal stun check for ogres/bonuses - if (((GetBaseRace() == OGRE && IsClient()) || - (frontal_stun_resist && zone->random.Roll(frontal_stun_resist))) && - !attacker->BehindMob(this, attacker->GetX(), attacker->GetY())) { - Log.Out(Logs::Detail, Logs::Combat, "Frontal stun resisted. %d chance.", frontal_stun_resist); - - } else { - // Normal stun resist check. - if (stun_resist && zone->random.Roll(stun_resist)) { + if ((GetBaseRace() == OGRE || GetBaseRace() == OGGOK_CITIZEN) && + !attacker->BehindMob(this, attacker->GetX(), attacker->GetY())) + can_stun = false; + if (GetSpecialAbility(UNSTUNABLE)) + can_stun = false; + } + if (can_stun) { + int bashsave_roll = zone->random.Int(0, 100); + if (bashsave_roll > 98 || bashsave_roll > (55 - stunbash_chance)) { + // did stun -- roll other resists + // SE_FrontalStunResist description says any angle now a days + int stun_resist2 = spellbonuses.FrontalStunResist + itembonuses.FrontalStunResist + + aabonuses.FrontalStunResist; + if (zone->random.Int(1, 100) > stun_resist2) { + // stun resist 2 failed + // time to check SE_StunResist and mod2 stun resist + int stun_resist = + spellbonuses.StunResist + itembonuses.StunResist + aabonuses.StunResist; + if (zone->random.Int(0, 100) >= stun_resist) { + // did stun + // nothing else to check! + Stun(2000); // straight 2 seconds every time + } else { + // stun resist passed! if (IsClient()) Message_StringID(MT_Stun, SHAKE_OFF_STUN); - Log.Out(Logs::Detail, Logs::Combat, "Stun Resisted. %d chance.", stun_resist); - } else { - Log.Out(Logs::Detail, Logs::Combat, "Stunned. %d resist chance.", stun_resist); - Stun(zone->random.Int(0, 2) * 1000); // 0-2 seconds } + } else { + // stun resist 2 passed! + if (IsClient()) + Message_StringID(MT_Stun, AVOID_STUNNING_BLOW); } } else { - Log.Out(Logs::Detail, Logs::Combat, "Stun failed. %d chance.", stun_chance); + // main stun failed -- extra interrupt roll + if (IsCasting() && + !EQEmu::ValueWithin(casting_spell_id, 859, 1023)) // these spells are excluded + // 90% chance >< -- stun immune won't reach this branch though :( + if (zone->random.Int(0, 9) > 1) + InterruptSpell(); } } @@ -3268,7 +3261,7 @@ void Mob::CommonDamage(Mob* attacker, int32 &damage, const uint16 spell_id, cons a->damage = damage; a->spellid = spell_id; a->special = special; - a->meleepush_xy = attacker->GetHeading() * 2.0f; + a->meleepush_xy = attacker ? attacker->GetHeading() * 2.0f : 0.0f; if (RuleB(Combat, MeleePush) && damage > 0 && !IsRooted() && (IsClient() || zone->random.Roll(RuleI(Combat, MeleePushChance)))) { a->force = EQEmu::skills::GetSkillMeleePushForce(skill_used); @@ -3805,7 +3798,7 @@ void Mob::TryPetCriticalHit(Mob *defender, uint16 skill, int32 &damage) void Mob::TryCriticalHit(Mob *defender, uint16 skill, int32 &damage, ExtraAttackOptions *opts) { - if(damage < 1) + if(damage < 1 || !defender) return; // decided to branch this into it's own function since it's going to be duplicating a lot of the @@ -3826,8 +3819,8 @@ void Mob::TryCriticalHit(Mob *defender, uint16 skill, int32 &damage, ExtraAttack bool IsBerskerSPA = false; //1: Try Slay Undead - if (defender && (defender->GetBodyType() == BT_Undead || - defender->GetBodyType() == BT_SummonedUndead || defender->GetBodyType() == BT_Vampire)) { + if (defender->GetBodyType() == BT_Undead || + defender->GetBodyType() == BT_SummonedUndead || defender->GetBodyType() == BT_Vampire) { int32 SlayRateBonus = aabonuses.SlayUndead[0] + itembonuses.SlayUndead[0] + spellbonuses.SlayUndead[0]; if (SlayRateBonus) { float slayChance = static_cast(SlayRateBonus) / 10000.0f; diff --git a/zone/bot.cpp b/zone/bot.cpp index 10f8c12f4..a3dd19b2d 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -1709,8 +1709,8 @@ bool Bot::LoadPet() NPC *pet_inst = GetPet()->CastToNPC(); - SpellBuff_Struct pet_buffs[BUFF_COUNT]; - memset(pet_buffs, 0, (sizeof(SpellBuff_Struct) * BUFF_COUNT)); + SpellBuff_Struct pet_buffs[PET_BUFF_COUNT]; + memset(pet_buffs, 0, (sizeof(SpellBuff_Struct) * PET_BUFF_COUNT)); if (!botdb.LoadPetBuffs(GetBotID(), pet_buffs)) bot_owner->Message(13, "%s for %s's pet", BotDatabase::fail::LoadPetBuffs(), GetCleanName()); @@ -1741,11 +1741,11 @@ bool Bot::SavePet() return false; char* pet_name = new char[64]; - SpellBuff_Struct pet_buffs[BUFF_COUNT]; + SpellBuff_Struct pet_buffs[PET_BUFF_COUNT]; uint32 pet_items[EQEmu::legacy::EQUIPMENT_SIZE]; memset(pet_name, 0, 64); - memset(pet_buffs, 0, (sizeof(SpellBuff_Struct) * BUFF_COUNT)); + memset(pet_buffs, 0, (sizeof(SpellBuff_Struct) * PET_BUFF_COUNT)); memset(pet_items, 0, (sizeof(uint32) * EQEmu::legacy::EQUIPMENT_SIZE)); pet_inst->GetPetState(pet_buffs, pet_items, pet_name); @@ -2085,7 +2085,7 @@ void Bot::DoMeleeSkillAttackDmg(Mob* other, uint16 weapon_damage, EQEmu::skills: CheckNumHitsRemaining(NumHit::OutgoingHitSuccess); if ((skillinuse == EQEmu::skills::SkillDragonPunch) && GetAA(aaDragonPunch) && zone->random.Int(0, 99) < 25){ - SpellFinished(904, other, 10, 0, -1, spells[904].ResistDiff); + SpellFinished(904, other, EQEmu::CastingSlot::Item, 0, -1, spells[904].ResistDiff); other->Stun(100); } @@ -5900,7 +5900,7 @@ void Bot::DoBuffTic(const Buffs_Struct &buff, int slot, Mob* caster) { Mob::DoBuffTic(buff, slot, caster); } -bool Bot::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, int32 cast_time, int32 mana_cost, +bool Bot::CastSpell(uint16 spell_id, uint16 target_id, EQEmu::CastingSlot slot, int32 cast_time, int32 mana_cost, uint32* oSpellWillFinish, uint32 item_slot, int16 *resist_adjust, uint32 aa_id) { bool Result = false; if(zone && !zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) { @@ -5920,7 +5920,7 @@ bool Bot::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, int32 cast_t Message_StringID(13, MELEE_SILENCE); if(casting_spell_id) - AI_Event_SpellCastFinished(false, casting_spell_slot); + AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); return false; } @@ -5929,7 +5929,7 @@ bool Bot::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, int32 cast_t if(IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()){ Message_StringID(13, SPELL_WOULDNT_HOLD); if(casting_spell_id) - AI_Event_SpellCastFinished(false, casting_spell_slot); + AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); return false; } @@ -5940,7 +5940,7 @@ bool Bot::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, int32 cast_t return false; } - if(slot < MAX_PP_MEMSPELL && !CheckFizzle(spell_id)) { + if(slot < EQEmu::CastingSlot::MaxGems && !CheckFizzle(spell_id)) { int fizzle_msg = IsBardSong(spell_id) ? MISS_NOTE : SPELL_FIZZLE; InterruptSpell(fizzle_msg, 0x121, spell_id); @@ -5954,7 +5954,7 @@ bool Bot::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, int32 cast_t Log.Out(Logs::Detail, Logs::Spells, "Casting a new spell/song while singing a song. Killing old song %d.", bardsong); bardsong = 0; bardsong_target_id = 0; - bardsong_slot = 0; + bardsong_slot = EQEmu::CastingSlot::Gem1; bardsong_timer.Disable(); } @@ -6084,7 +6084,7 @@ bool Bot::IsImmuneToSpell(uint16 spell_id, Mob *caster) { return Result; } -bool Bot::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, uint16 slot) { +bool Bot::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, EQEmu::CastingSlot slot) { bool Result = false; SpellTargetType targetType = spells[spell_id].targettype; if(targetType == ST_GroupClientAndPet) { @@ -6097,7 +6097,7 @@ bool Bot::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce return Result; } -bool Bot::DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot, int32 cast_time, int32 mana_cost, uint32* oSpellWillFinish, uint32 item_slot, uint32 aa_id) { +bool Bot::DoCastSpell(uint16 spell_id, uint16 target_id, EQEmu::CastingSlot slot, int32 cast_time, int32 mana_cost, uint32* oSpellWillFinish, uint32 item_slot, uint32 aa_id) { bool Result = false; if(GetClass() == BARD) cast_time = 0; @@ -6201,7 +6201,7 @@ void Bot::GenerateSpecialAttacks() { SetSpecialAbility(SPECATK_TRIPLE, 1); } -bool Bot::DoFinishedSpellAETarget(uint16 spell_id, Mob* spellTarget, uint16 slot, bool& stopLogic) { +bool Bot::DoFinishedSpellAETarget(uint16 spell_id, Mob* spellTarget, EQEmu::CastingSlot slot, bool& stopLogic) { if(GetClass() == BARD) { if(!ApplyNextBardPulse(bardsong, this, bardsong_slot)) InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); @@ -6211,7 +6211,7 @@ bool Bot::DoFinishedSpellAETarget(uint16 spell_id, Mob* spellTarget, uint16 slot return true; } -bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, uint16 slot, bool& stopLogic) { +bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQEmu::CastingSlot slot, bool& stopLogic) { if(spellTarget) { if(IsGrouped() && (spellTarget->IsBot() || spellTarget->IsClient()) && RuleB(Bots, GroupBuffing)) { bool noGroupSpell = false; @@ -6223,7 +6223,7 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, uint16 bool spelltypeequal = ((spelltype == 2) || (spelltype == 16) || (spelltype == 32)); bool spelltypetargetequal = ((spelltype == 8) && (spells[thespell].targettype == ST_Self)); bool spelltypeclassequal = ((spelltype == 1024) && (GetClass() == SHAMAN)); - bool slotequal = (slot == USE_ITEM_SPELL_SLOT); + bool slotequal = (slot == EQEmu::CastingSlot::Item); if(spellequal || slotequal) { if((spelltypeequal || spelltypetargetequal) || spelltypeclassequal || slotequal) { if(((spells[thespell].effectid[0] == 0) && (spells[thespell].base[0] < 0)) && @@ -6262,7 +6262,7 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, uint16 return true; } -bool Bot::DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, uint16 slot, bool& stopLogic) { +bool Bot::DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, EQEmu::CastingSlot slot, bool& stopLogic) { bool isMainGroupMGB = false; if(isMainGroupMGB && (GetClass() != BARD)) { BotGroupSay(this, "MGB %s", spells[spell_id].name); @@ -8159,7 +8159,25 @@ bool Bot::GetNeedsCured(Mob *tar) { int buffsWithCounters = 0; needCured = true; for (unsigned int j = 0; j < buff_count; j++) { - if(tar->GetBuffs()[j].spellid != SPELL_UNKNOWN) { + // this should prevent crashes until the cause can be found + if (!tar->GetBuffs()) { + std::string mob_type = "Unknown"; + if (tar->IsClient()) + mob_type = "Client"; + else if (tar->IsBot()) + mob_type = "Bot"; + else if (tar->IsMerc()) + mob_type = "Merc"; + else if (tar->IsPet()) + mob_type = "Pet"; + else if (tar->IsNPC()) + mob_type = "NPC"; + + Log.Out(Logs::General, Logs::Error, "Bot::GetNeedsCured() processed mob type '%s' with a null buffs pointer (mob: '%s')", mob_type.c_str(), tar->GetName()); + + continue; + } + else if(tar->GetBuffs()[j].spellid != SPELL_UNKNOWN) { if(CalculateCounters(tar->GetBuffs()[j].spellid) > 0) { buffsWithCounters++; if(buffsWithCounters == 1 && (tar->GetBuffs()[j].ticsremaining < 2 || (int32)((tar->GetBuffs()[j].ticsremaining * 6) / tar->GetBuffs()[j].counters) < 2)) { @@ -8251,7 +8269,7 @@ bool Bot::UseDiscipline(uint32 spell_id, uint32 target) { if(IsCasting()) InterruptSpell(); - CastSpell(spell_id, target, DISCIPLINE_SPELL_SLOT); + CastSpell(spell_id, target, EQEmu::CastingSlot::Discipline); return true; } diff --git a/zone/bot.h b/zone/bot.h index b294fa108..3d45475a3 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -274,9 +274,9 @@ public: virtual void SetAttackTimer(); uint32 GetClassHPFactor(); virtual int32 CalcMaxHP(); - bool DoFinishedSpellAETarget(uint16 spell_id, Mob* spellTarget, uint16 slot, bool &stopLogic); - bool DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, uint16 slot, bool &stopLogic); - bool DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, uint16 slot, bool &stopLogic); + bool DoFinishedSpellAETarget(uint16 spell_id, Mob* spellTarget, EQEmu::CastingSlot slot, bool &stopLogic); + bool DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQEmu::CastingSlot slot, bool &stopLogic); + bool DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, EQEmu::CastingSlot slot, bool &stopLogic); void SendBotArcheryWearChange(uint8 material_slot, uint32 material, uint32 color); void Camp(bool databaseSave = true); virtual void AddToHateList(Mob* other, uint32 hate = 0, int32 damage = 0, bool iYellForHelp = true, bool bFrenzy = false, bool iBuffTic = false); @@ -374,12 +374,12 @@ public: virtual float GetAOERange(uint16 spell_id); virtual bool SpellEffect(Mob* caster, uint16 spell_id, float partial = 100); virtual void DoBuffTic(const Buffs_Struct &buff, int slot, Mob* caster = nullptr); - virtual bool CastSpell(uint16 spell_id, uint16 target_id, uint16 slot = USE_ITEM_SPELL_SLOT, int32 casttime = -1, int32 mana_cost = -1, uint32* oSpellWillFinish = 0, + virtual bool CastSpell(uint16 spell_id, uint16 target_id, EQEmu::CastingSlot slot = EQEmu::CastingSlot::Item, int32 casttime = -1, int32 mana_cost = -1, uint32* oSpellWillFinish = 0, uint32 item_slot = 0xFFFFFFFF, int16 *resist_adjust = nullptr, uint32 aa_id = 0); virtual bool SpellOnTarget(uint16 spell_id, Mob* spelltar); virtual bool IsImmuneToSpell(uint16 spell_id, Mob *caster); - virtual bool DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, uint16 slot); - virtual bool DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot = USE_ITEM_SPELL_SLOT, int32 casttime = -1, int32 mana_cost = -1, uint32* oSpellWillFinish = 0, uint32 item_slot = 0xFFFFFFFF, uint32 aa_id = 0); + virtual bool DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, EQEmu::CastingSlot slot); + virtual bool DoCastSpell(uint16 spell_id, uint16 target_id, EQEmu::CastingSlot slot = EQEmu::CastingSlot::Item, int32 casttime = -1, int32 mana_cost = -1, uint32* oSpellWillFinish = 0, uint32 item_slot = 0xFFFFFFFF, uint32 aa_id = 0); // Bot Equipment & Inventory Class Methods void BotTradeSwapItem(Client* client, int16 lootSlot, const ItemInst* inst, const ItemInst* inst_swap, uint32 equipableSlots, std::string* errorMessage, bool swap = true); diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index e3b16219a..6b07bbdda 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -7613,7 +7613,7 @@ bool helper_cast_standard_spell(Bot* casting_bot, Mob* target_mob, int spell_id, if (annouce_cast) Bot::BotGroupSay(casting_bot, "Attempting to cast '%s' on %s", spells[spell_id].name, target_mob->GetCleanName()); - return casting_bot->CastSpell(spell_id, target_mob->GetID(), 1, -1, -1, dont_root_before); + return casting_bot->CastSpell(spell_id, target_mob->GetID(), EQEmu::CastingSlot::Gem2, -1, -1, dont_root_before); } bool helper_command_alias_fail(Client *bot_owner, const char* command_handler, const char *alias, const char *command) diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index 300f4c8b4..db9827a8a 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -1472,7 +1472,7 @@ bool BotDatabase::LoadPetBuffs(const uint32 bot_id, SpellBuff_Struct* pet_buffs) return true; int buff_index = 0; - for (auto row = results.begin(); row != results.end() && buff_index < BUFF_COUNT; ++row) { + for (auto row = results.begin(); row != results.end() && buff_index < PET_BUFF_COUNT; ++row) { pet_buffs[buff_index].spellid = atoi(row[0]); pet_buffs[buff_index].level = atoi(row[1]); pet_buffs[buff_index].duration = atoi(row[2]); @@ -1509,7 +1509,7 @@ bool BotDatabase::SavePetBuffs(const uint32 bot_id, const SpellBuff_Struct* pet_ if (!saved_pet_index) return true; - for (int buff_index = 0; buff_index < BUFF_COUNT; ++buff_index) { + for (int buff_index = 0; buff_index < PET_BUFF_COUNT; ++buff_index) { if (!pet_buffs[buff_index].spellid || pet_buffs[buff_index].spellid == SPELL_UNKNOWN) continue; diff --git a/zone/client.cpp b/zone/client.cpp index 732485a60..59998bfdb 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -44,6 +44,7 @@ extern volatile bool RunLoops; #include "zonedb.h" #include "petitions.h" #include "command.h" +#include "water_map.h" #ifdef BOTS #include "bot_command.h" #endif @@ -155,7 +156,8 @@ Client::Client(EQStreamInterface* ieqs) m_Proximity(FLT_MAX, FLT_MAX, FLT_MAX), //arbitrary large number m_ZoneSummonLocation(-2.0f,-2.0f,-2.0f), m_AutoAttackPosition(0.0f, 0.0f, 0.0f, 0.0f), - m_AutoAttackTargetLocation(0.0f, 0.0f, 0.0f) + m_AutoAttackTargetLocation(0.0f, 0.0f, 0.0f), + last_region_type(RegionTypeUnsupported) { for(int cf=0; cf < _FilterCount; cf++) ClientFilters[cf] = FilterShow; @@ -565,6 +567,11 @@ bool Client::SaveAA() { return true; } +void Client::RemoveExpendedAA(int aa_id) +{ + database.QueryDatabase(StringFormat("DELETE from `character_alternate_abilities` WHERE `id` = %d and `aa_id` = %d", character_id, aa_id)); +} + bool Client::Save(uint8 iCommitNow) { if(!ClientDataLoaded()) return false; @@ -1787,7 +1794,7 @@ const int32& Client::SetMana(int32 amount) { } void Client::SendManaUpdatePacket() { - if (!Connected() || IsCasting()) + if (!Connected()) return; if (ClientVersion() >= EQEmu::versions::ClientVersion::SoD) { @@ -1801,7 +1808,8 @@ void Client::SendManaUpdatePacket() { ManaChange_Struct* manachange = (ManaChange_Struct*)outapp->pBuffer; manachange->new_mana = cur_mana; manachange->stamina = cur_end; - manachange->spell_id = casting_spell_id; //always going to be 0... since we check IsCasting() + manachange->spell_id = casting_spell_id; + manachange->keepcasting = 1; outapp->priority = 6; QueuePacket(outapp); safe_delete(outapp); @@ -2476,13 +2484,15 @@ uint16 Client::GetMaxSkillAfterSpecializationRules(EQEmu::skills::SkillType skil return Result; } -void Client::SetPVP(bool toggle) { +void Client::SetPVP(bool toggle, bool message) { m_pp.pvp = toggle ? 1 : 0; - if(GetPVP()) - this->Message_StringID(MT_Shout,PVP_ON); - else - Message(13, "You no longer follow the ways of discord."); + if (message) { + if(GetPVP()) + this->Message_StringID(MT_Shout,PVP_ON); + else + Message(13, "You no longer follow the ways of discord."); + } SendAppearancePacket(AT_PVP, GetPVP()); Save(); @@ -3674,58 +3684,58 @@ void Client::SacrificeConfirm(Client *caster) //Essentially a special case death function void Client::Sacrifice(Client *caster) { - if(GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)){ - int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000); - if(exploss < GetEXP()){ - SetEXP(GetEXP()-exploss, GetAAXP()); - SendLogoutPackets(); + if (GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)) { + int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000); + if (exploss < GetEXP()) { + SetEXP(GetEXP() - exploss, GetAAXP()); + SendLogoutPackets(); - //make our become corpse packet, and queue to ourself before OP_Death. - EQApplicationPacket app2(OP_BecomeCorpse, sizeof(BecomeCorpse_Struct)); - BecomeCorpse_Struct* bc = (BecomeCorpse_Struct*)app2.pBuffer; - bc->spawn_id = GetID(); - bc->x = GetX(); - bc->y = GetY(); - bc->z = GetZ(); - QueuePacket(&app2); + // make our become corpse packet, and queue to ourself before OP_Death. + EQApplicationPacket app2(OP_BecomeCorpse, sizeof(BecomeCorpse_Struct)); + BecomeCorpse_Struct *bc = (BecomeCorpse_Struct *)app2.pBuffer; + bc->spawn_id = GetID(); + bc->x = GetX(); + bc->y = GetY(); + bc->z = GetZ(); + QueuePacket(&app2); - // make death packet - EQApplicationPacket app(OP_Death, sizeof(Death_Struct)); - Death_Struct* d = (Death_Struct*)app.pBuffer; - d->spawn_id = GetID(); - d->killer_id = caster ? caster->GetID() : 0; - d->bindzoneid = GetPP().binds[0].zoneId; - d->spell_id = SPELL_UNKNOWN; - d->attack_skill = 0xe7; - d->damage = 0; - app.priority = 6; - entity_list.QueueClients(this, &app); + // make death packet + EQApplicationPacket app(OP_Death, sizeof(Death_Struct)); + Death_Struct *d = (Death_Struct *)app.pBuffer; + d->spawn_id = GetID(); + d->killer_id = caster ? caster->GetID() : 0; + d->bindzoneid = GetPP().binds[0].zoneId; + d->spell_id = SPELL_UNKNOWN; + d->attack_skill = 0xe7; + d->damage = 0; + app.priority = 6; + entity_list.QueueClients(this, &app); - BuffFadeAll(); - UnmemSpellAll(); - Group *g = GetGroup(); - if(g){ - g->MemberZoned(this); - } - Raid *r = entity_list.GetRaidByClient(this); - if(r){ - r->MemberZoned(this); - } - ClearAllProximities(); - if(RuleB(Character, LeaveCorpses)){ - auto new_corpse = new Corpse(this, 0); - entity_list.AddCorpse(new_corpse, GetID()); - SetID(0); - entity_list.QueueClients(this, &app2, true); - } - Save(); - GoToDeath(); - caster->SummonItem(RuleI(Spells, SacrificeItemID)); - } - } - else{ - caster->Message_StringID(13, SAC_TOO_LOW); //This being is not a worthy sacrifice. - } + BuffFadeAll(); + UnmemSpellAll(); + Group *g = GetGroup(); + if (g) { + g->MemberZoned(this); + } + Raid *r = entity_list.GetRaidByClient(this); + if (r) { + r->MemberZoned(this); + } + ClearAllProximities(); + if (RuleB(Character, LeaveCorpses)) { + auto new_corpse = new Corpse(this, 0); + entity_list.AddCorpse(new_corpse, GetID()); + SetID(0); + entity_list.QueueClients(this, &app2, true); + } + Save(); + GoToDeath(); + if (caster) // I guess it's possible? + caster->SummonItem(RuleI(Spells, SacrificeItemID)); + } + } else { + caster->Message_StringID(13, SAC_TOO_LOW); // This being is not a worthy sacrifice. + } } void Client::SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID) { @@ -4604,7 +4614,7 @@ void Client::HandleLDoNOpen(NPC *target) if(target->GetLDoNTrapSpellID() != 0) { Message_StringID(13, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, 10, 0, -1, spells[target->GetLDoNTrapSpellID()].ResistDiff); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQEmu::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].ResistDiff); target->SetLDoNTrapSpellID(0); target->SetLDoNTrapped(false); target->SetLDoNTrapDetected(false); @@ -4726,7 +4736,7 @@ void Client::HandleLDoNDisarm(NPC *target, uint16 skill, uint8 type) break; case -1: Message_StringID(13, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, 10, 0, -1, spells[target->GetLDoNTrapSpellID()].ResistDiff); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQEmu::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].ResistDiff); target->SetLDoNTrapSpellID(0); target->SetLDoNTrapped(false); target->SetLDoNTrapDetected(false); @@ -4745,7 +4755,7 @@ void Client::HandleLDoNPickLock(NPC *target, uint16 skill, uint8 type) if(target->IsLDoNTrapped()) { Message_StringID(13, LDON_ACCIDENT_SETOFF2); - target->SpellFinished(target->GetLDoNTrapSpellID(), this, 10, 0, -1, spells[target->GetLDoNTrapSpellID()].ResistDiff); + target->SpellFinished(target->GetLDoNTrapSpellID(), this, EQEmu::CastingSlot::Item, 0, -1, spells[target->GetLDoNTrapSpellID()].ResistDiff); target->SetLDoNTrapSpellID(0); target->SetLDoNTrapped(false); target->SetLDoNTrapDetected(false); @@ -8396,7 +8406,7 @@ void Client::SendColoredText(uint32 color, std::string message) void Client::QuestReward(Mob* target, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, uint32 itemid, uint32 exp, bool faction) { auto outapp = new EQApplicationPacket(OP_Sound, sizeof(QuestReward_Struct)); - memset(outapp->pBuffer, 0, sizeof(outapp->pBuffer)); + memset(outapp->pBuffer, 0, sizeof(QuestReward_Struct)); QuestReward_Struct* qr = (QuestReward_Struct*)outapp->pBuffer; qr->mob_id = target->GetID(); // Entity ID for the from mob name @@ -8527,3 +8537,27 @@ uint32 Client::GetMoney(uint8 type, uint8 subtype) { int Client::GetAccountAge() { return (time(nullptr) - GetAccountCreation()); } + +void Client::CheckRegionTypeChanges() +{ + if (!zone->HasWaterMap()) + return; + + auto new_region = zone->watermap->ReturnRegionType(glm::vec3(m_Position)); + + // still same region, do nothing + if (last_region_type == new_region) + return; + + // region type changed + last_region_type = new_region; + + // PVP is the only state we need to keep track of, so we can just return now for PVP servers + if (RuleI(World, PVPSettings) > 0) + return; + + if (last_region_type == RegionTypePVP) + SetPVP(true, false); + else if (GetPVP()) + SetPVP(false, false); +} diff --git a/zone/client.h b/zone/client.h index 1d30b97ce..df7c1fce3 100644 --- a/zone/client.h +++ b/zone/client.h @@ -27,6 +27,7 @@ class Object; class Raid; class Seperator; class ServerPacket; +enum WaterRegionType : int; namespace EQEmu { @@ -105,6 +106,7 @@ enum { //Type arguments to the Message* routines. #define SPELLBAR_UNLOCK 0x2bc enum { //scribing argument to MemorizeSpell + memSpellUnknown = -1, // this modifies some state data memSpellScribing = 0, memSpellMemorize = 1, memSpellForget = 2, @@ -326,6 +328,7 @@ public: /* New PP Save Functions */ bool SaveCurrency(){ return database.SaveCharacterCurrency(this->CharacterID(), &m_pp); } bool SaveAA(); + void RemoveExpendedAA(int aa_id); inline bool ClientDataLoaded() const { return client_data_loaded; } inline bool Connected() const { return (client_state == CLIENT_CONNECTED); } @@ -359,9 +362,9 @@ public: int32 LevelRegen(); void HPTick(); void SetGM(bool toggle); - void SetPVP(bool toggle); + void SetPVP(bool toggle, bool message = true); - inline bool GetPVP() const { return zone->GetZoneID() == 77 ? true : (m_pp.pvp != 0); } + inline bool GetPVP() const { return m_pp.pvp != 0; } inline bool GetGM() const { return m_pp.gm != 0; } inline void SetBaseClass(uint32 i) { m_pp.class_=i; } @@ -510,6 +513,8 @@ public: virtual int GetMaxSongSlots() const { return 12; } virtual int GetMaxDiscSlots() const { return 1; } virtual int GetMaxTotalSlots() const { return 38; } + virtual uint32 GetFirstBuffSlot(bool disc, bool song); + virtual uint32 GetLastBuffSlot(bool disc, bool song); virtual void InitializeBuffSlots(); virtual void UninitializeBuffSlots(); @@ -889,6 +894,9 @@ public: void SendDisciplineTimer(uint32 timer_id, uint32 duration); bool UseDiscipline(uint32 spell_id, uint32 target); + void SetLinkedSpellReuseTimer(uint32 timer_id, uint32 duration); + bool IsLinkedSpellReuseTimerReady(uint32 timer_id); + bool CheckTitle(int titleset); void EnableTitle(int titleset); void RemoveTitle(int titleset); @@ -1230,6 +1238,8 @@ public: void SendHPUpdateMarquee(); + void CheckRegionTypeChanges(); + protected: friend class Mob; void CalcItemBonuses(StatBonuses* newbon); @@ -1414,6 +1424,7 @@ private: uint8 zonesummon_ignorerestrictions; ZoneMode zone_mode; + WaterRegionType last_region_type; Timer position_timer; uint8 position_timer_counter; diff --git a/zone/client_mods.cpp b/zone/client_mods.cpp index 804e8df62..c57fc340f 100644 --- a/zone/client_mods.cpp +++ b/zone/client_mods.cpp @@ -1036,6 +1036,13 @@ int32 Client::CalcAC() if (avoidance < 0) { avoidance = 0; } + + if (RuleB(Character, EnableAvoidanceCap)) { + if (avoidance > RuleI(Character, AvoidanceCap)) { + avoidance = RuleI(Character, AvoidanceCap); + } + } + int mitigation = 0; if (m_pp.class_ == WIZARD || m_pp.class_ == MAGICIAN || m_pp.class_ == NECROMANCER || m_pp.class_ == ENCHANTER) { //something is wrong with this, naked casters have the wrong natural AC @@ -1113,6 +1120,13 @@ int32 Client::GetACAvoid() if (avoidance < 0) { avoidance = 0; } + + if (RuleB(Character, EnableAvoidanceCap)) { + if ((avoidance * 1000 / 847) > RuleI(Character, AvoidanceCap)) { + return RuleI(Character, AvoidanceCap); + } + } + return (avoidance * 1000 / 847); } diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index f0de676f9..37108697c 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -502,6 +502,27 @@ void Client::CompleteConnect() SetDuelTarget(0); SetDueling(false); + database.LoadPetInfo(this); + /* + This was moved before the spawn packets are sent + in hopes that it adds more consistency... + Remake pet + */ + if (m_petinfo.SpellID > 1 && !GetPet() && m_petinfo.SpellID <= SPDAT_RECORDS) { + MakePoweredPet(m_petinfo.SpellID, spells[m_petinfo.SpellID].teleport_zone, m_petinfo.petpower, m_petinfo.Name, m_petinfo.size); + if (GetPet() && GetPet()->IsNPC()) { + NPC *pet = GetPet()->CastToNPC(); + pet->SetPetState(m_petinfo.Buffs, m_petinfo.Items); + pet->CalcBonuses(); + pet->SetHP(m_petinfo.HP); + pet->SetMana(m_petinfo.Mana); + } + m_petinfo.SpellID = 0; + } + /* Moved here so it's after where we load the pet data. */ + if (!GetAA(aaPersistentMinion)) + memset(&m_suspendedminion, 0, sizeof(PetInfo)); + EnteringMessages(this); LoadZoneFlags(); @@ -1628,27 +1649,6 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) if (m_pp.RestTimer) rest_timer.Start(m_pp.RestTimer * 1000); - database.LoadPetInfo(this); - /* - This was moved before the spawn packets are sent - in hopes that it adds more consistency... - Remake pet - */ - if (m_petinfo.SpellID > 1 && !GetPet() && m_petinfo.SpellID <= SPDAT_RECORDS) { - MakePoweredPet(m_petinfo.SpellID, spells[m_petinfo.SpellID].teleport_zone, m_petinfo.petpower, m_petinfo.Name, m_petinfo.size); - if (GetPet() && GetPet()->IsNPC()) { - NPC *pet = GetPet()->CastToNPC(); - pet->SetPetState(m_petinfo.Buffs, m_petinfo.Items); - pet->CalcBonuses(); - pet->SetHP(m_petinfo.HP); - pet->SetMana(m_petinfo.Mana); - } - m_petinfo.SpellID = 0; - } - /* Moved here so it's after where we load the pet data. */ - if (!GetAA(aaPersistentMinion)) - memset(&m_suspendedminion, 0, sizeof(PetInfo)); - /* Server Zone Entry Packet */ outapp = new EQApplicationPacket(OP_ZoneEntry, sizeof(ServerZoneEntry_Struct)); ServerZoneEntry_Struct* sze = (ServerZoneEntry_Struct*)outapp->pBuffer; @@ -3966,6 +3966,7 @@ void Client::Handle_OP_CancelTrade(const EQApplicationPacket *app) void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) { + using EQEmu::CastingSlot; if (app->size != sizeof(CastSpell_Struct)) { std::cout << "Wrong size: OP_CastSpell, size=" << app->size << ", expected " << sizeof(CastSpell_Struct) << std::endl; return; @@ -3978,13 +3979,13 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) CastSpell_Struct* castspell = (CastSpell_Struct*)app->pBuffer; - m_TargetRing = glm::vec3(castspell->x_pos, castspell->y_pos, castspell->z_pos); + m_TargetRing = glm::vec3(castspell->x_pos, castspell->y_pos, castspell->z_pos); Log.Out(Logs::General, Logs::Spells, "OP CastSpell: slot=%d, spell=%d, target=%d, inv=%lx", castspell->slot, castspell->spell_id, castspell->target_id, (unsigned long)castspell->inventoryslot); + CastingSlot slot = static_cast(castspell->slot); /* Memorized Spell */ - if (m_pp.mem_spells[castspell->slot] && m_pp.mem_spells[castspell->slot] == castspell->spell_id){ - + if (m_pp.mem_spells[castspell->slot] && m_pp.mem_spells[castspell->slot] == castspell->spell_id) { uint16 spell_to_cast = 0; if (castspell->slot < MAX_PP_MEMSPELL) { spell_to_cast = m_pp.mem_spells[castspell->slot]; @@ -3998,20 +3999,12 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) return; } - CastSpell(spell_to_cast, castspell->target_id, castspell->slot); + CastSpell(spell_to_cast, castspell->target_id, slot); } /* Spell Slot or Potion Belt Slot */ - else if ((castspell->slot == USE_ITEM_SPELL_SLOT) || (castspell->slot == POTION_BELT_SPELL_SLOT)|| (castspell->slot == TARGET_RING_SPELL_SLOT)) // ITEM or POTION cast + else if (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt) // ITEM or POTION cast { - //discipline, using the item spell slot - if (castspell->inventoryslot == INVALID_INDEX) { - if (!UseDiscipline(castspell->spell_id, castspell->target_id)) { - Log.Out(Logs::General, Logs::Spells, "Unknown ability being used by %s, spell being cast is: %i\n", GetName(), castspell->spell_id); - InterruptSpell(castspell->spell_id); - } - return; - } - else if (m_inv.SupportsClickCasting(castspell->inventoryslot) || (castspell->slot == POTION_BELT_SPELL_SLOT) || (castspell->slot == TARGET_RING_SPELL_SLOT)) // sanity check + if (m_inv.SupportsClickCasting(castspell->inventoryslot) || slot == CastingSlot::PotionBelt) // sanity check { // packet field types will be reviewed as packet transistions occur const ItemInst* inst = m_inv[castspell->inventoryslot]; //slot values are int16, need to check packet on this field @@ -4036,7 +4029,7 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) int i = parse->EventItem(EVENT_ITEM_CLICK_CAST, this, p_inst, nullptr, "", castspell->inventoryslot); if (i == 0) { - CastSpell(item->Click.Effect, castspell->target_id, castspell->slot, item->CastTime, 0, 0, castspell->inventoryslot); + CastSpell(item->Click.Effect, castspell->target_id, slot, item->CastTime, 0, 0, castspell->inventoryslot); } else { InterruptSpell(castspell->spell_id); @@ -4056,7 +4049,7 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) int i = parse->EventItem(EVENT_ITEM_CLICK_CAST, this, p_inst, nullptr, "", castspell->inventoryslot); if (i == 0) { - CastSpell(item->Click.Effect, castspell->target_id, castspell->slot, item->CastTime, 0, 0, castspell->inventoryslot); + CastSpell(item->Click.Effect, castspell->target_id, slot, item->CastTime, 0, 0, castspell->inventoryslot); } else { InterruptSpell(castspell->spell_id); @@ -4081,8 +4074,8 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) InterruptSpell(castspell->spell_id); } } - /* Discipline */ - else if (castspell->slot == DISCIPLINE_SPELL_SLOT) { + /* Discipline -- older clients use the same slot as items, but we translate to it's own */ + else if (slot == CastingSlot::Discipline) { if (!UseDiscipline(castspell->spell_id, castspell->target_id)) { Log.Out(Logs::General, Logs::Spells, "Unknown ability being used by %s, spell being cast is: %i\n", GetName(), castspell->spell_id); InterruptSpell(castspell->spell_id); @@ -4090,7 +4083,7 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) } } /* ABILITY cast (LoH and Harm Touch) */ - else if (castspell->slot == ABILITY_SPELL_SLOT) { + else if (slot == CastingSlot::Ability) { uint16 spell_to_cast = 0; if (castspell->spell_id == SPELL_LAY_ON_HANDS && GetClass() == PALADIN) { @@ -4120,7 +4113,7 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) } if (spell_to_cast > 0) // if we've matched LoH or HT, cast now - CastSpell(spell_to_cast, castspell->target_id, castspell->slot); + CastSpell(spell_to_cast, castspell->target_id, slot); } return; } @@ -4583,8 +4576,11 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) safe_delete(outapp); } - if(zone->watermap && zone->watermap->InLiquid(glm::vec3(m_Position))) - CheckIncreaseSkill(EQEmu::skills::SkillSwimming, nullptr, -17); + if (zone->watermap) { + if (zone->watermap->InLiquid(glm::vec3(m_Position))) + CheckIncreaseSkill(EQEmu::skills::SkillSwimming, nullptr, -17); + CheckRegionTypeChanges(); + } return; } @@ -8410,6 +8406,7 @@ void Client::Handle_OP_ItemPreview(const EQApplicationPacket *app) void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) { + using EQEmu::CastingSlot; if (app->size != sizeof(ItemVerifyRequest_Struct)) { Log.Out(Logs::General, Logs::Error, "OP size error: OP_ItemVerifyRequest expected:%i got:%i", sizeof(ItemVerifyRequest_Struct), app->size); @@ -8554,7 +8551,7 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) if (i == 0) { if (!IsCastWhileInvis(item->Click.Effect)) CommonBreakInvisible(); // client can't do this for us :( - CastSpell(item->Click.Effect, target_id, USE_ITEM_SPELL_SLOT, item->CastTime, 0, 0, slot_id); + CastSpell(item->Click.Effect, target_id, CastingSlot::Item, item->CastTime, 0, 0, slot_id); } } else @@ -8583,7 +8580,7 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) if (i == 0) { if (!IsCastWhileInvis(augitem->Click.Effect)) CommonBreakInvisible(); // client can't do this for us :( - CastSpell(augitem->Click.Effect, target_id, USE_ITEM_SPELL_SLOT, augitem->CastTime, 0, 0, slot_id); + CastSpell(augitem->Click.Effect, target_id, CastingSlot::Item, augitem->CastTime, 0, 0, slot_id); } } else diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 354d5b6aa..e1a769743 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -624,7 +624,7 @@ bool Client::Process() { { //client logged out or errored out //ResetTrade(); - if (client_state != CLIENT_KICKED) { + if (client_state != CLIENT_KICKED && !zoning && !instalog) { Save(); } diff --git a/zone/command.cpp b/zone/command.cpp index 68049cc43..4c60ad65b 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -2199,14 +2199,14 @@ void command_castspell(Client *c, const Seperator *sep) else if (c->GetTarget() == 0) if(c->Admin() >= commandInstacast) - c->SpellFinished(spellid, 0, USE_ITEM_SPELL_SLOT, 0, -1, spells[spellid].ResistDiff); + c->SpellFinished(spellid, 0, EQEmu::CastingSlot::Item, 0, -1, spells[spellid].ResistDiff); else - c->CastSpell(spellid, 0, USE_ITEM_SPELL_SLOT, 0); + c->CastSpell(spellid, 0, EQEmu::CastingSlot::Item, 0); else if(c->Admin() >= commandInstacast) - c->SpellFinished(spellid, c->GetTarget(), 10, 0, -1, spells[spellid].ResistDiff); + c->SpellFinished(spellid, c->GetTarget(), EQEmu::CastingSlot::Item, 0, -1, spells[spellid].ResistDiff); else - c->CastSpell(spellid, c->GetTarget()->GetID(), USE_ITEM_SPELL_SLOT, 0); + c->CastSpell(spellid, c->GetTarget()->GetID(), EQEmu::CastingSlot::Item, 0); } } @@ -4339,11 +4339,15 @@ void command_goto(Client *c, const Seperator *sep) void command_iteminfo(Client *c, const Seperator *sep) { auto inst = c->GetInv()[EQEmu::legacy::SlotCursor]; - if (!inst) { c->Message(13, "Error: You need an item on your cursor for this command"); } + if (!inst) { + c->Message(13, "Error: You need an item on your cursor for this command"); + return; + } auto item = inst->GetItem(); if (!item) { Log.Out(Logs::General, Logs::Inventory, "(%s) Command #iteminfo processed an item with no data pointer"); c->Message(13, "Error: This item has no data reference"); + return; } EQEmu::SayLinkEngine linker; diff --git a/zone/common.h b/zone/common.h index 97bb1a50b..9a5768c56 100644 --- a/zone/common.h +++ b/zone/common.h @@ -17,13 +17,6 @@ #define _NPCPET(x) (x && x->IsNPC() && x->CastToMob()->GetOwner() && x->CastToMob()->GetOwner()->IsNPC()) #define _BECOMENPCPET(x) (x && x->CastToMob()->GetOwner() && x->CastToMob()->GetOwner()->IsClient() && x->CastToMob()->GetOwner()->CastToClient()->IsBecomeNPC()) -#define USE_ITEM_SPELL_SLOT 10 -#define POTION_BELT_SPELL_SLOT 11 -#define TARGET_RING_SPELL_SLOT 12 -#define DISCIPLINE_SPELL_SLOT 10 -#define ABILITY_SPELL_SLOT 9 -#define ALTERNATE_ABILITY_SPELL_SLOT 0xFF - //LOS Parameters: #define HEAD_POSITION 0.9f //ratio of GetSize() where NPCs see from #define SEE_POSITION 0.5f //ratio of GetSize() where NPCs try to see for LOS diff --git a/zone/corpse.cpp b/zone/corpse.cpp index ff5e4806b..ca32fc528 100644 --- a/zone/corpse.cpp +++ b/zone/corpse.cpp @@ -1163,9 +1163,9 @@ void Corpse::LootItem(Client* client, const EQApplicationPacket* app) { } char buf[88]; - char corpse_name[64]; - strcpy(corpse_name, corpse_name); - snprintf(buf, 87, "%d %d %s", inst->GetItem()->ID, inst->GetCharges(), EntityList::RemoveNumbers(corpse_name)); + char q_corpse_name[64]; + strcpy(q_corpse_name, corpse_name); + snprintf(buf, 87, "%d %d %s", inst->GetItem()->ID, inst->GetCharges(), EntityList::RemoveNumbers(q_corpse_name)); buf[87] = '\0'; std::vector args; args.push_back(inst); diff --git a/zone/effects.cpp b/zone/effects.cpp index 7d0b9af02..5b022f97d 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -684,9 +684,9 @@ bool Client::UseDiscipline(uint32 spell_id, uint32 target) { } if (reduced_recast > 0) - CastSpell(spell_id, target, DISCIPLINE_SPELL_SLOT, -1, -1, 0, -1, (uint32)DiscTimer, reduced_recast); + CastSpell(spell_id, target, EQEmu::CastingSlot::Discipline, -1, -1, 0, -1, (uint32)DiscTimer, reduced_recast); else{ - CastSpell(spell_id, target, DISCIPLINE_SPELL_SLOT); + CastSpell(spell_id, target, EQEmu::CastingSlot::Discipline); return true; } @@ -694,7 +694,7 @@ bool Client::UseDiscipline(uint32 spell_id, uint32 target) { } else { - CastSpell(spell_id, target, DISCIPLINE_SPELL_SLOT); + CastSpell(spell_id, target, EQEmu::CastingSlot::Discipline); } return(true); } diff --git a/zone/entity.cpp b/zone/entity.cpp index 975b793e9..08e1534c8 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -927,12 +927,18 @@ bool EntityList::MakeDoorSpawnPacket(EQApplicationPacket *app, Client *client) Entity *EntityList::GetEntityMob(uint16 id) { - return mob_list.count(id) ? mob_list.at(id) : nullptr; + auto it = mob_list.find(id); + if (it != mob_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityMerc(uint16 id) { - return merc_list.count(id) ? merc_list.at(id) : nullptr; + auto it = merc_list.find(id); + if (it != merc_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityMob(const char *name) @@ -952,12 +958,18 @@ Entity *EntityList::GetEntityMob(const char *name) Entity *EntityList::GetEntityDoor(uint16 id) { - return door_list.count(id) ? door_list.at(id) : nullptr; + auto it = door_list.find(id); + if (it != door_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityCorpse(uint16 id) { - return corpse_list.count(id) ? corpse_list.at(id) : nullptr; + auto it = corpse_list.find(id); + if (it != corpse_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityCorpse(const char *name) @@ -977,22 +989,34 @@ Entity *EntityList::GetEntityCorpse(const char *name) Entity *EntityList::GetEntityTrap(uint16 id) { - return trap_list.count(id) ? trap_list.at(id) : nullptr; + auto it = trap_list.find(id); + if (it != trap_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityObject(uint16 id) { - return object_list.count(id) ? object_list.at(id) : nullptr; + auto it = object_list.find(id); + if (it != object_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityBeacon(uint16 id) { - return beacon_list.count(id) ? beacon_list.at(id) : nullptr; + auto it = beacon_list.find(id); + if (it != beacon_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetEntityEncounter(uint16 id) { - return encounter_list.count(id) ? encounter_list.at(id) : nullptr; + auto it = encounter_list.find(id); + if (it != encounter_list.end()) + return it->second; + return nullptr; } Entity *EntityList::GetID(uint16 get_id) @@ -1188,6 +1212,8 @@ void EntityList::ChannelMessage(Mob *from, uint8 chan_num, uint8 language, void EntityList::ChannelMessageSend(Mob *to, uint8 chan_num, uint8 language, const char *message, ...) { + if (!to->IsClient()) + return; va_list argptr; char buffer[4096]; @@ -1195,8 +1221,7 @@ void EntityList::ChannelMessageSend(Mob *to, uint8 chan_num, uint8 language, con vsnprintf(buffer, 4096, message, argptr); va_end(argptr); - if (client_list.count(to->GetID())) - client_list.at(to->GetID())->ChannelMessageSend(0, 0, chan_num, language, buffer); + to->CastToClient()->ChannelMessageSend(0, 0, chan_num, language, buffer); } void EntityList::SendZoneSpawns(Client *client) @@ -1232,7 +1257,9 @@ void EntityList::SendZoneSpawnsBulk(Client *client) maxspawns = mob_list.size(); auto bzsp = new BulkZoneSpawnPacket(client, maxspawns); - int32 race=-1; + bool delaypkt = false; + const glm::vec4& cpos = client->GetPosition(); + const float dmax = 600.0 * 600.0; for (auto it = mob_list.begin(); it != mob_list.end(); ++it) { spawn = it->second; if (spawn && spawn->GetID() > 0 && spawn->Spawned()) { @@ -1240,8 +1267,30 @@ void EntityList::SendZoneSpawnsBulk(Client *client) spawn->CastToClient()->IsHoveringForRespawn())) continue; - race = spawn->GetRace(); +#if 1 + const glm::vec4& spos = spawn->GetPosition(); + + delaypkt = false; + if (DistanceSquared(cpos, spos) > dmax || (spawn->IsClient() && (spawn->GetRace() == MINOR_ILL_OBJ || spawn->GetRace() == TREE))) + delaypkt = true; + + if (delaypkt) { + app = new EQApplicationPacket; + spawn->CreateSpawnPacket(app); + client->QueuePacket(app, true, Client::CLIENT_CONNECTED); + safe_delete(app); + } + else { + memset(&ns, 0, sizeof(NewSpawn_Struct)); + spawn->FillSpawnStruct(&ns, client); + bzsp->AddSpawn(&ns); + } + spawn->SendArmorAppearance(client); +#else + /* original code kept for spawn packet research */ + int32 race = spawn->GetRace(); + // Illusion races on PCs don't work as a mass spawn // But they will work as an add_spawn AFTER CLIENT_CONNECTED. if (spawn->IsClient() && (race == MINOR_ILL_OBJ || race == TREE)) { @@ -1259,6 +1308,7 @@ void EntityList::SendZoneSpawnsBulk(Client *client) // Despite being sent in the OP_ZoneSpawns packet, the client // does not display worn armor correctly so display it. spawn->SendArmorAppearance(client); +#endif } } safe_delete(bzsp); diff --git a/zone/entity.h b/zone/entity.h index 90a372af8..8abb34ca2 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -148,14 +148,29 @@ public: bool IsMobSpawnedByNpcTypeID(uint32 get_id); Mob *GetTargetForVirus(Mob* spreader, int range); inline NPC *GetNPCByID(uint16 id) - { return npc_list.count(id) ? npc_list.at(id) : nullptr; } + { + auto it = npc_list.find(id); + if (it != npc_list.end()) + return it->second; + return nullptr; + } NPC *GetNPCByNPCTypeID(uint32 npc_id); inline Merc *GetMercByID(uint16 id) - { return merc_list.count(id) ? merc_list.at(id) : nullptr; } + { + auto it = merc_list.find(id); + if (it != merc_list.end()) + return it->second; + return nullptr; + } Client *GetClientByName(const char *name); Client *GetClientByAccID(uint32 accid); inline Client *GetClientByID(uint16 id) - { return client_list.count(id) ? client_list.at(id) : nullptr; } + { + auto it = client_list.find(id); + if (it != client_list.end()) + return it->second; + return nullptr; + } Client *GetClientByCharID(uint32 iCharID); Client *GetClientByWID(uint32 iWID); Client *GetClient(uint32 ip, uint16 port); @@ -172,7 +187,12 @@ public: Corpse *GetCorpseByOwner(Client* client); Corpse *GetCorpseByOwnerWithinRange(Client* client, Mob* center, int range); inline Corpse *GetCorpseByID(uint16 id) - { return corpse_list.count(id) ? corpse_list.at(id) : nullptr; } + { + auto it = corpse_list.find(id); + if (it != corpse_list.end()) + return it->second; + return nullptr; + } Corpse *GetCorpseByDBID(uint32 dbid); Corpse *GetCorpseByName(const char* name); @@ -181,10 +201,20 @@ public: Client* FindCorpseDragger(uint16 CorpseID); inline Object *GetObjectByID(uint16 id) - { return object_list.count(id) ? object_list.at(id) : nullptr; } + { + auto it = object_list.find(id); + if (it != object_list.end()) + return it->second; + return nullptr; + } Object *GetObjectByDBID(uint32 id); inline Doors *GetDoorsByID(uint16 id) - { return door_list.count(id) ? door_list.at(id) : nullptr; } + { + auto it = door_list.find(id); + if (it != door_list.end()) + return it->second; + return nullptr; + } Doors *GetDoorsByDoorID(uint32 id); Doors *GetDoorsByDBID(uint32 id); void RemoveAllCorpsesByCharID(uint32 charid); diff --git a/zone/lua_mob.cpp b/zone/lua_mob.cpp index 124726b3f..f5b8c8382 100644 --- a/zone/lua_mob.cpp +++ b/zone/lua_mob.cpp @@ -759,28 +759,28 @@ bool Lua_Mob::CastSpell(int spell_id, int target_id) { bool Lua_Mob::CastSpell(int spell_id, int target_id, int slot) { Lua_Safe_Call_Bool(); - return self->CastSpell(spell_id, target_id, slot); + return self->CastSpell(spell_id, target_id, static_cast(slot)); } bool Lua_Mob::CastSpell(int spell_id, int target_id, int slot, int cast_time) { Lua_Safe_Call_Bool(); - return self->CastSpell(spell_id, target_id, slot, cast_time); + return self->CastSpell(spell_id, target_id, static_cast(slot), cast_time); } bool Lua_Mob::CastSpell(int spell_id, int target_id, int slot, int cast_time, int mana_cost) { Lua_Safe_Call_Bool(); - return self->CastSpell(spell_id, target_id, slot, cast_time, mana_cost); + return self->CastSpell(spell_id, target_id, static_cast(slot), cast_time, mana_cost); } bool Lua_Mob::CastSpell(int spell_id, int target_id, int slot, int cast_time, int mana_cost, int item_slot) { Lua_Safe_Call_Bool(); - return self->CastSpell(spell_id, target_id, slot, cast_time, mana_cost, nullptr, static_cast(item_slot)); + return self->CastSpell(spell_id, target_id, static_cast(slot), cast_time, mana_cost, nullptr, static_cast(item_slot)); } bool Lua_Mob::CastSpell(int spell_id, int target_id, int slot, int cast_time, int mana_cost, int item_slot, int timer, int timer_duration) { Lua_Safe_Call_Bool(); - return self->CastSpell(spell_id, target_id, slot, cast_time, mana_cost, nullptr, static_cast(item_slot), + return self->CastSpell(spell_id, target_id, static_cast(slot), cast_time, mana_cost, nullptr, static_cast(item_slot), static_cast(timer), static_cast(timer_duration)); } @@ -789,7 +789,7 @@ bool Lua_Mob::CastSpell(int spell_id, int target_id, int slot, int cast_time, in Lua_Safe_Call_Bool(); int16 res = resist_adjust; - return self->CastSpell(spell_id, target_id, slot, cast_time, mana_cost, nullptr, static_cast(item_slot), + return self->CastSpell(spell_id, target_id, static_cast(slot), cast_time, mana_cost, nullptr, static_cast(item_slot), static_cast(timer), static_cast(timer_duration), &res); } @@ -800,27 +800,27 @@ bool Lua_Mob::SpellFinished(int spell_id, Lua_Mob target) { bool Lua_Mob::SpellFinished(int spell_id, Lua_Mob target, int slot) { Lua_Safe_Call_Bool(); - return self->SpellFinished(spell_id, target, slot); + return self->SpellFinished(spell_id, target, static_cast(slot)); } bool Lua_Mob::SpellFinished(int spell_id, Lua_Mob target, int slot, int mana_used) { Lua_Safe_Call_Bool(); - return self->SpellFinished(spell_id, target, slot, mana_used); + return self->SpellFinished(spell_id, target, static_cast(slot), mana_used); } bool Lua_Mob::SpellFinished(int spell_id, Lua_Mob target, int slot, int mana_used, uint32 inventory_slot) { Lua_Safe_Call_Bool(); - return self->SpellFinished(spell_id, target, slot, mana_used, inventory_slot); + return self->SpellFinished(spell_id, target, static_cast(slot), mana_used, inventory_slot); } bool Lua_Mob::SpellFinished(int spell_id, Lua_Mob target, int slot, int mana_used, uint32 inventory_slot, int resist_adjust) { Lua_Safe_Call_Bool(); - return self->SpellFinished(spell_id, target, slot, mana_used, inventory_slot, resist_adjust); + return self->SpellFinished(spell_id, target, static_cast(slot), mana_used, inventory_slot, resist_adjust); } bool Lua_Mob::SpellFinished(int spell_id, Lua_Mob target, int slot, int mana_used, uint32 inventory_slot, int resist_adjust, bool proc) { Lua_Safe_Call_Bool(); - return self->SpellFinished(spell_id, target, slot, mana_used, inventory_slot, resist_adjust, proc); + return self->SpellFinished(spell_id, target, static_cast(slot), mana_used, inventory_slot, resist_adjust, proc); } void Lua_Mob::SpellEffect(Lua_Mob caster, int spell_id, double partial) { diff --git a/zone/map.cpp b/zone/map.cpp index 630e277c2..29a386331 100644 --- a/zone/map.cpp +++ b/zone/map.cpp @@ -163,7 +163,7 @@ float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result) const { to.z = -BEST_Z_INVALID; hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance); if (hit) { - if (abs(from.z - result->z) < abs(ClosestZ - from.z)) + if (std::abs(from.z - result->z) < std::abs(ClosestZ - from.z)) return result->z; } diff --git a/zone/merc.cpp b/zone/merc.cpp index 4ff8e8d90..c787d00c8 100644 --- a/zone/merc.cpp +++ b/zone/merc.cpp @@ -1963,7 +1963,7 @@ bool Merc::AIDoSpellCast(uint16 spellid, Mob* tar, int32 mana_cost, uint32* oDon SendPosition(); SetMoving(false); - result = CastSpell(spellid, tar->GetID(), 1, -1, mana_cost, oDontDoAgainBefore, -1, -1, 0, 0); + result = CastSpell(spellid, tar->GetID(), EQEmu::CastingSlot::Gem2, -1, mana_cost, oDontDoAgainBefore, -1, -1, 0, 0); if(IsCasting() && IsSitting()) Stand(); @@ -4015,7 +4015,7 @@ bool Merc::UseDiscipline(int32 spell_id, int32 target) { if(IsCasting()) InterruptSpell(); - CastSpell(spell_id, target, DISCIPLINE_SPELL_SLOT); + CastSpell(spell_id, target, EQEmu::CastingSlot::Discipline); return(true); } diff --git a/zone/mob.cpp b/zone/mob.cpp index 580e154e7..accd36bb0 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -2357,7 +2357,6 @@ void Mob::SetZone(uint32 zone_id, uint32 instance_id) { CastToClient()->GetPP().zone_id = zone_id; CastToClient()->GetPP().zoneInstance = instance_id; - CastToClient()->Save(); } Save(); } @@ -3224,13 +3223,13 @@ void Mob::ExecWeaponProc(const ItemInst *inst, uint16 spell_id, Mob *on, int lev if(twinproc_chance && zone->random.Roll(twinproc_chance)) twinproc = true; - if (IsBeneficialSpell(spell_id)) { - SpellFinished(spell_id, this, 10, 0, -1, spells[spell_id].ResistDiff, true, level_override); + if (IsBeneficialSpell(spell_id) && (!IsNPC() || (IsNPC() && CastToNPC()->GetInnateProcSpellID() != spell_id))) { // NPC innate procs don't take this path ever + SpellFinished(spell_id, this, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff, true, level_override); if(twinproc) SpellOnTarget(spell_id, this, false, false, 0, true, level_override); } else if(!(on->IsClient() && on->CastToClient()->dead)) { //dont proc on dead clients - SpellFinished(spell_id, on, 10, 0, -1, spells[spell_id].ResistDiff, true, level_override); + SpellFinished(spell_id, on, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff, true, level_override); if(twinproc) SpellOnTarget(spell_id, on, false, false, 0, true, level_override); } @@ -3518,10 +3517,9 @@ void Mob::TryTriggerOnCast(uint32 spell_id, bool aa_trigger) } } - void Mob::TriggerOnCast(uint32 focus_spell, uint32 spell_id, bool aa_trigger) { - if(!IsValidSpell(focus_spell) || !IsValidSpell(spell_id)) + if (!IsValidSpell(focus_spell) || !IsValidSpell(spell_id)) return; uint32 trigger_spell_id = 0; @@ -3532,15 +3530,17 @@ void Mob::TriggerOnCast(uint32 focus_spell, uint32 spell_id, bool aa_trigger) if (rank) trigger_spell_id = CastToClient()->CalcAAFocus(focusTriggerOnCast, *rank, spell_id); - if(IsValidSpell(trigger_spell_id) && GetTarget()) - SpellFinished(trigger_spell_id, GetTarget(), 10, 0, -1, spells[trigger_spell_id].ResistDiff); + if (IsValidSpell(trigger_spell_id) && GetTarget()) + SpellFinished(trigger_spell_id, GetTarget(), EQEmu::CastingSlot::Item, 0, -1, + spells[trigger_spell_id].ResistDiff); } - else{ + else { trigger_spell_id = CalcFocusEffect(focusTriggerOnCast, focus_spell, spell_id); - if(IsValidSpell(trigger_spell_id) && GetTarget()){ - SpellFinished(trigger_spell_id, GetTarget(),10, 0, -1, spells[trigger_spell_id].ResistDiff); + if (IsValidSpell(trigger_spell_id) && GetTarget()) { + SpellFinished(trigger_spell_id, GetTarget(), EQEmu::CastingSlot::Item, 0, -1, + spells[trigger_spell_id].ResistDiff); CheckNumHitsRemaining(NumHit::MatchingSpells, -1, focus_spell); } } @@ -3570,7 +3570,7 @@ bool Mob::TrySpellTrigger(Mob *target, uint32 spell_id, int effect) { // If we trigger an effect then its over. if (IsValidSpell(spells[spell_id].base2[i])){ - SpellFinished(spells[spell_id].base2[i], target, 10, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); + SpellFinished(spells[spell_id].base2[i], target, EQEmu::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); return true; } } @@ -3589,7 +3589,7 @@ bool Mob::TrySpellTrigger(Mob *target, uint32 spell_id, int effect) if(zone->random.Int(0, 100) <= spells[spell_id].base[effect]) { if (IsValidSpell(spells[spell_id].base2[effect])){ - SpellFinished(spells[spell_id].base2[effect], target, 10, 0, -1, spells[spells[spell_id].base2[effect]].ResistDiff); + SpellFinished(spells[spell_id].base2[effect], target, EQEmu::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[effect]].ResistDiff); return true; //Only trigger once of these per spell effect. } } @@ -3666,7 +3666,7 @@ void Mob::TryTriggerOnValueAmount(bool IsHP, bool IsMana, bool IsEndur, bool IsP } if (use_spell){ - SpellFinished(spells[spell_id].base[i], this, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spells[spell_id].base[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); if(!TryFadeEffect(e)) BuffFadeBySlot(e); @@ -3694,7 +3694,7 @@ void Mob::TryTwincast(Mob *caster, Mob *target, uint32 spell_id) if(zone->random.Roll(focus)) { Message(MT_Spells,"You twincast %s!",spells[spell_id].name); - SpellFinished(spell_id, target, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spell_id, target, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } } } @@ -3712,7 +3712,7 @@ void Mob::TryTwincast(Mob *caster, Mob *target, uint32 spell_id) { if(zone->random.Roll(focus)) { - SpellFinished(spell_id, target, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spell_id, target, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } } } @@ -3831,10 +3831,10 @@ bool Mob::TryFadeEffect(int slot) if(IsValidSpell(spell_id)) { if (IsBeneficialSpell(spell_id)) { - SpellFinished(spell_id, this, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spell_id, this, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } else if(!(IsClient() && CastToClient()->dead)) { - SpellFinished(spell_id, this, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spell_id, this, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } return true; } @@ -3868,7 +3868,7 @@ void Mob::TrySympatheticProc(Mob *target, uint32 spell_id) SpellFinished(focus_trigger, target); else - SpellFinished(focus_trigger, this, 10, 0, -1, spells[focus_trigger].ResistDiff); + SpellFinished(focus_trigger, this, EQEmu::CastingSlot::Item, 0, -1, spells[focus_trigger].ResistDiff); } // For detrimental spells, if the triggered spell is beneficial, then it will land on the caster // if the triggered spell is also detrimental, then it will land on the target @@ -3878,7 +3878,7 @@ void Mob::TrySympatheticProc(Mob *target, uint32 spell_id) SpellFinished(focus_trigger, this); else - SpellFinished(focus_trigger, target, 10, 0, -1, spells[focus_trigger].ResistDiff); + SpellFinished(focus_trigger, target, EQEmu::CastingSlot::Item, 0, -1, spells[focus_trigger].ResistDiff); } CheckNumHitsRemaining(NumHit::MatchingSpells, -1, focus_spell); @@ -4529,7 +4529,7 @@ void Mob::TrySpellOnKill(uint8 level, uint16 spell_id) if (IsValidSpell(spells[spell_id].base2[i]) && spells[spell_id].max[i] <= level) { if(zone->random.Roll(spells[spell_id].base[i])) - SpellFinished(spells[spell_id].base2[i], this, 10, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); + SpellFinished(spells[spell_id].base2[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); } } } @@ -4544,17 +4544,17 @@ void Mob::TrySpellOnKill(uint8 level, uint16 spell_id) if(aabonuses.SpellOnKill[i] && IsValidSpell(aabonuses.SpellOnKill[i]) && (level >= aabonuses.SpellOnKill[i + 2])) { if(zone->random.Roll(static_cast(aabonuses.SpellOnKill[i + 1]))) - SpellFinished(aabonuses.SpellOnKill[i], this, 10, 0, -1, spells[aabonuses.SpellOnKill[i]].ResistDiff); + SpellFinished(aabonuses.SpellOnKill[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SpellOnKill[i]].ResistDiff); } if(itembonuses.SpellOnKill[i] && IsValidSpell(itembonuses.SpellOnKill[i]) && (level >= itembonuses.SpellOnKill[i + 2])){ if(zone->random.Roll(static_cast(itembonuses.SpellOnKill[i + 1]))) - SpellFinished(itembonuses.SpellOnKill[i], this, 10, 0, -1, spells[aabonuses.SpellOnKill[i]].ResistDiff); + SpellFinished(itembonuses.SpellOnKill[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SpellOnKill[i]].ResistDiff); } if(spellbonuses.SpellOnKill[i] && IsValidSpell(spellbonuses.SpellOnKill[i]) && (level >= spellbonuses.SpellOnKill[i + 2])) { if(zone->random.Roll(static_cast(spellbonuses.SpellOnKill[i + 1]))) - SpellFinished(spellbonuses.SpellOnKill[i], this, 10, 0, -1, spells[aabonuses.SpellOnKill[i]].ResistDiff); + SpellFinished(spellbonuses.SpellOnKill[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SpellOnKill[i]].ResistDiff); } } @@ -4571,19 +4571,19 @@ bool Mob::TrySpellOnDeath() for(int i = 0; i < MAX_SPELL_TRIGGER*2; i+=2) { if(IsClient() && aabonuses.SpellOnDeath[i] && IsValidSpell(aabonuses.SpellOnDeath[i])) { if(zone->random.Roll(static_cast(aabonuses.SpellOnDeath[i + 1]))) { - SpellFinished(aabonuses.SpellOnDeath[i], this, 10, 0, -1, spells[aabonuses.SpellOnDeath[i]].ResistDiff); + SpellFinished(aabonuses.SpellOnDeath[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SpellOnDeath[i]].ResistDiff); } } if(itembonuses.SpellOnDeath[i] && IsValidSpell(itembonuses.SpellOnDeath[i])) { if(zone->random.Roll(static_cast(itembonuses.SpellOnDeath[i + 1]))) { - SpellFinished(itembonuses.SpellOnDeath[i], this, 10, 0, -1, spells[itembonuses.SpellOnDeath[i]].ResistDiff); + SpellFinished(itembonuses.SpellOnDeath[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[itembonuses.SpellOnDeath[i]].ResistDiff); } } if(spellbonuses.SpellOnDeath[i] && IsValidSpell(spellbonuses.SpellOnDeath[i])) { if(zone->random.Roll(static_cast(spellbonuses.SpellOnDeath[i + 1]))) { - SpellFinished(spellbonuses.SpellOnDeath[i], this, 10, 0, -1, spells[spellbonuses.SpellOnDeath[i]].ResistDiff); + SpellFinished(spellbonuses.SpellOnDeath[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[spellbonuses.SpellOnDeath[i]].ResistDiff); } } } diff --git a/zone/mob.h b/zone/mob.h index ff4879edd..22e4bf357 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -26,6 +26,7 @@ #include "aa_ability.h" #include "aa.h" #include "../common/light_source.h" +#include "../common/emu_constants.h" #include #include #include @@ -218,7 +219,7 @@ public: //Song bool UseBardSpellLogic(uint16 spell_id = 0xffff, int slot = -1); - bool ApplyNextBardPulse(uint16 spell_id, Mob *spell_target, uint16 slot); + bool ApplyNextBardPulse(uint16 spell_id, Mob *spell_target, EQEmu::CastingSlot slot); void BardPulse(uint16 spell_id, Mob *caster); //Spell @@ -248,29 +249,30 @@ public: void SendSpellBarEnable(uint16 spellid); void ZeroCastingVars(); virtual void SpellProcess(); - virtual bool CastSpell(uint16 spell_id, uint16 target_id, uint16 slot = USE_ITEM_SPELL_SLOT, int32 casttime = -1, + virtual bool CastSpell(uint16 spell_id, uint16 target_id, EQEmu::CastingSlot slot = EQEmu::CastingSlot::Item, int32 casttime = -1, int32 mana_cost = -1, uint32* oSpellWillFinish = 0, uint32 item_slot = 0xFFFFFFFF, uint32 timer = 0xFFFFFFFF, uint32 timer_duration = 0, int16 *resist_adjust = nullptr, uint32 aa_id = 0); - virtual bool DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot = 10, int32 casttime = -1, + virtual bool DoCastSpell(uint16 spell_id, uint16 target_id, EQEmu::CastingSlot slot = EQEmu::CastingSlot::Item, int32 casttime = -1, int32 mana_cost = -1, uint32* oSpellWillFinish = 0, uint32 item_slot = 0xFFFFFFFF, uint32 timer = 0xFFFFFFFF, uint32 timer_duration = 0, int16 resist_adjust = 0, uint32 aa_id = 0); - void CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, uint16 mana_used, + void CastedSpellFinished(uint16 spell_id, uint32 target_id, EQEmu::CastingSlot slot, uint16 mana_used, uint32 inventory_slot = 0xFFFFFFFF, int16 resist_adjust = 0); - bool SpellFinished(uint16 spell_id, Mob *target, uint16 slot = 10, uint16 mana_used = 0, + bool SpellFinished(uint16 spell_id, Mob *target, EQEmu::CastingSlot slot = EQEmu::CastingSlot::Item, uint16 mana_used = 0, uint32 inventory_slot = 0xFFFFFFFF, int16 resist_adjust = 0, bool isproc = false, int level_override = -1); virtual bool SpellOnTarget(uint16 spell_id, Mob* spelltar, bool reflect = false, bool use_resist_adjust = false, int16 resist_adjust = 0, bool isproc = false, int level_override = -1); virtual bool SpellEffect(Mob* caster, uint16 spell_id, float partial = 100, int level_override = -1); virtual bool DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, - CastAction_type &CastAction, uint16 slot); + CastAction_type &CastAction, EQEmu::CastingSlot slot, bool isproc = false); virtual bool CheckFizzle(uint16 spell_id); virtual bool CheckSpellLevelRestriction(uint16 spell_id); virtual bool IsImmuneToSpell(uint16 spell_id, Mob *caster); virtual float GetAOERange(uint16 spell_id); void InterruptSpell(uint16 spellid = SPELL_UNKNOWN); void InterruptSpell(uint16, uint16, uint16 spellid = SPELL_UNKNOWN); + void StopCasting(); inline bool IsCasting() const { return((casting_spell_id != 0)); } uint16 CastingSpellID() const { return casting_spell_id; } bool DoCastingChecks(); @@ -306,6 +308,8 @@ public: virtual int GetMaxSongSlots() const { return 0; } virtual int GetMaxDiscSlots() const { return 0; } virtual int GetMaxTotalSlots() const { return 0; } + virtual uint32 GetFirstBuffSlot(bool disc, bool song); + virtual uint32 GetLastBuffSlot(bool disc, bool song); virtual void InitializeBuffSlots() { buffs = nullptr; current_buff_count = 0; } virtual void UninitializeBuffSlots() { } EQApplicationPacket *MakeBuffsPacket(bool for_target = true); @@ -1216,7 +1220,7 @@ protected: int attacked_count; bool delaytimer; uint16 casting_spell_targetid; - uint16 casting_spell_slot; + EQEmu::CastingSlot casting_spell_slot; uint16 casting_spell_mana; uint32 casting_spell_inventory_slot; uint32 casting_spell_timer; @@ -1226,7 +1230,7 @@ protected: uint32 casting_spell_aa_id; bool casting_spell_checks; uint16 bardsong; - uint8 bardsong_slot; + EQEmu::CastingSlot bardsong_slot; uint32 bardsong_target_id; bool ActiveProjectileATK; diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index ef7d222dd..eb14865ce 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -341,7 +341,7 @@ bool NPC::AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain SetCurrentSpeed(0); } - return CastSpell(AIspells[i].spellid, tar->GetID(), 1, AIspells[i].manacost == -2 ? 0 : -1, mana_cost, oDontDoAgainBefore, -1, -1, 0, &(AIspells[i].resist_adjust)); + return CastSpell(AIspells[i].spellid, tar->GetID(), EQEmu::CastingSlot::Gem2, AIspells[i].manacost == -2 ? 0 : -1, mana_cost, oDontDoAgainBefore, -1, -1, 0, &(AIspells[i].resist_adjust)); } bool EntityList::AICheckCloseBeneficialSpells(NPC* caster, uint8 iChance, float iRange, uint16 iSpellTypes) { @@ -673,11 +673,11 @@ void Client::AI_SpellCast() } uint32 spell_to_cast = 0xFFFFFFFF; - uint32 slot_to_use = 10; + EQEmu::CastingSlot slot_to_use = EQEmu::CastingSlot::Item; if(valid_spells.size() == 1) { spell_to_cast = valid_spells[0]; - slot_to_use = slots[0]; + slot_to_use = static_cast(slots[0]); } else if(valid_spells.empty()) { @@ -687,7 +687,7 @@ void Client::AI_SpellCast() { uint32 idx = zone->random.Int(0, (valid_spells.size()-1)); spell_to_cast = valid_spells[idx]; - slot_to_use = slots[idx]; + slot_to_use = static_cast(slots[idx]); } if(IsMezSpell(spell_to_cast) || IsFearSpell(spell_to_cast)) @@ -2362,8 +2362,10 @@ bool NPC::AI_AddNPCSpells(uint32 iDBSpellsID) { return a.priority > b.priority; }); - if (IsValidSpell(attack_proc_spell)) + if (IsValidSpell(attack_proc_spell)) { AddProcToWeapon(attack_proc_spell, true, proc_chance); + innate_proc_spell_id = attack_proc_spell; + } if (IsValidSpell(range_proc_spell)) AddRangedProc(range_proc_spell, (rproc_chance + 100)); diff --git a/zone/npc.cpp b/zone/npc.cpp index d1d9e8a44..1fc27dc5a 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -233,6 +233,7 @@ NPC::NPC(const NPCType* d, Spawn2* in_respawn, const glm::vec4& position, int if npc_spells_id = 0; HasAISpell = false; HasAISpellEffects = false; + innate_proc_spell_id = 0; if(GetClass() == MERCERNARY_MASTER && RuleB(Mercs, AllowMercs)) { @@ -2006,44 +2007,90 @@ void NPC::LevelScale() { float scaling = (((random_level / (float)level) - 1) * (scalerate / 100.0f)); - // Compensate for scale rates at low levels so they don't add too much - uint8 scale_adjust = 1; - if(level > 0 && level <= 5) - scale_adjust = 10; - if(level > 5 && level <= 10) - scale_adjust = 5; - if(level > 10 && level <= 15) - scale_adjust = 3; - if(level > 15 && level <= 25) - scale_adjust = 2; + if (RuleB(NPC, NewLevelScaling)) { + if (scalerate == 0 || maxlevel <= 25) { + // pre-pop seems to scale by 20 HP increments while newer by 100 + // We also don't want 100 increments on newer noobie zones, check level + if (zone->GetZoneID() < 200 || level < 48) { + max_hp += (random_level - level) * 20; + base_hp += (random_level - level) * 20; + } else { + max_hp += (random_level - level) * 100; + base_hp += (random_level - level) * 100; + } - base_hp += (int)(base_hp * scaling); - max_hp += (int)(max_hp * scaling); - cur_hp = max_hp; - STR += (int)(STR * scaling / scale_adjust); - STA += (int)(STA * scaling / scale_adjust); - AGI += (int)(AGI * scaling / scale_adjust); - DEX += (int)(DEX * scaling / scale_adjust); - INT += (int)(INT * scaling / scale_adjust); - WIS += (int)(WIS * scaling / scale_adjust); - CHA += (int)(CHA * scaling / scale_adjust); - if (MR) - MR += (int)(MR * scaling / scale_adjust); - if (CR) - CR += (int)(CR * scaling / scale_adjust); - if (DR) - DR += (int)(DR * scaling / scale_adjust); - if (FR) - FR += (int)(FR * scaling / scale_adjust); - if (PR) - PR += (int)(PR * scaling / scale_adjust); + cur_hp = max_hp; + max_dmg += (random_level - level) * 2; + } else { + uint8 scale_adjust = 1; + + base_hp += (int)(base_hp * scaling); + max_hp += (int)(max_hp * scaling); + cur_hp = max_hp; + + if (max_dmg) { + max_dmg += (int)(max_dmg * scaling / scale_adjust); + min_dmg += (int)(min_dmg * scaling / scale_adjust); + } + + STR += (int)(STR * scaling / scale_adjust); + STA += (int)(STA * scaling / scale_adjust); + AGI += (int)(AGI * scaling / scale_adjust); + DEX += (int)(DEX * scaling / scale_adjust); + INT += (int)(INT * scaling / scale_adjust); + WIS += (int)(WIS * scaling / scale_adjust); + CHA += (int)(CHA * scaling / scale_adjust); + if (MR) + MR += (int)(MR * scaling / scale_adjust); + if (CR) + CR += (int)(CR * scaling / scale_adjust); + if (DR) + DR += (int)(DR * scaling / scale_adjust); + if (FR) + FR += (int)(FR * scaling / scale_adjust); + if (PR) + PR += (int)(PR * scaling / scale_adjust); + } + } else { + // Compensate for scale rates at low levels so they don't add too much + uint8 scale_adjust = 1; + if(level > 0 && level <= 5) + scale_adjust = 10; + if(level > 5 && level <= 10) + scale_adjust = 5; + if(level > 10 && level <= 15) + scale_adjust = 3; + if(level > 15 && level <= 25) + scale_adjust = 2; + + base_hp += (int)(base_hp * scaling); + max_hp += (int)(max_hp * scaling); + cur_hp = max_hp; + STR += (int)(STR * scaling / scale_adjust); + STA += (int)(STA * scaling / scale_adjust); + AGI += (int)(AGI * scaling / scale_adjust); + DEX += (int)(DEX * scaling / scale_adjust); + INT += (int)(INT * scaling / scale_adjust); + WIS += (int)(WIS * scaling / scale_adjust); + CHA += (int)(CHA * scaling / scale_adjust); + if (MR) + MR += (int)(MR * scaling / scale_adjust); + if (CR) + CR += (int)(CR * scaling / scale_adjust); + if (DR) + DR += (int)(DR * scaling / scale_adjust); + if (FR) + FR += (int)(FR * scaling / scale_adjust); + if (PR) + PR += (int)(PR * scaling / scale_adjust); + + if (max_dmg) + { + max_dmg += (int)(max_dmg * scaling / scale_adjust); + min_dmg += (int)(min_dmg * scaling / scale_adjust); + } - if (max_dmg) - { - max_dmg += (int)(max_dmg * scaling / scale_adjust); - min_dmg += (int)(min_dmg * scaling / scale_adjust); } - level = random_level; return; diff --git a/zone/npc.h b/zone/npc.h index 0023d4273..d3e7928a2 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -407,6 +407,7 @@ public: void mod_npc_killed_merit(Mob* c); void mod_npc_killed(Mob* oos); void AISpellsList(Client *c); + uint16 GetInnateProcSpellID() const { return innate_proc_spell_id; } uint32 GetHeroForgeModel() const { return herosforgemodel; } void SetHeroForgeModel(uint32 model) { herosforgemodel = model; } @@ -454,6 +455,7 @@ protected: virtual bool AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore = 0); AISpellsVar_Struct AISpellVar; int16 GetFocusEffect(focusType type, uint16 spell_id); + uint16 innate_proc_spell_id; uint32 npc_spells_effects_id; std::vector AIspellsEffects; diff --git a/zone/perl_mob.cpp b/zone/perl_mob.cpp index 4a6fab4e4..f171d8d79 100644 --- a/zone/perl_mob.cpp +++ b/zone/perl_mob.cpp @@ -3982,12 +3982,12 @@ XS(XS_Mob_CastSpell) { dXSARGS; if (items < 3 || items > 7) - Perl_croak(aTHX_ "Usage: Mob::CastSpell(THIS, spell_id, target_id, slot= 10, casttime= -1, mana_cost= -1, resist_adjust = 0)"); + Perl_croak(aTHX_ "Usage: Mob::CastSpell(THIS, spell_id, target_id, slot= 22, casttime= -1, mana_cost= -1, resist_adjust = 0)"); { Mob * THIS; uint16 spell_id = (uint16)SvUV(ST(1)); uint16 target_id = (uint16)SvUV(ST(2)); - uint16 slot; + EQEmu::CastingSlot slot; int32 casttime; int32 mana_cost; int16 resist_adjust; @@ -4002,9 +4002,9 @@ XS(XS_Mob_CastSpell) Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); if (items < 4) - slot = 10; + slot = EQEmu::CastingSlot::Item; else { - slot = (uint16)SvUV(ST(3)); + slot = static_cast(SvUV(ST(3))); } if (items < 5) @@ -4085,7 +4085,7 @@ XS(XS_Mob_SpellFinished) resist_diff = spells[spell_id].ResistDiff; } - THIS->SpellFinished(spell_id, spell_target, 10, mana_cost, -1, resist_diff); + THIS->SpellFinished(spell_id, spell_target, EQEmu::CastingSlot::Item, mana_cost, -1, resist_diff); } XSRETURN_EMPTY; } diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index fdd9c3df2..78fc2b206 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -365,14 +365,14 @@ void QuestManager::castspell(int spell_id, int target_id) { if (owner) { Mob *tgt = entity_list.GetMob(target_id); if(tgt != nullptr) - owner->SpellFinished(spell_id, tgt, 10, 0, -1, spells[spell_id].ResistDiff); + owner->SpellFinished(spell_id, tgt, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } } void QuestManager::selfcast(int spell_id) { QuestManagerCurrentQuestVars(); if (initiator) - initiator->SpellFinished(spell_id, initiator, 10, 0, -1, spells[spell_id].ResistDiff); + initiator->SpellFinished(spell_id, initiator, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } void QuestManager::addloot(int item_id, int charges, bool equipitem) { diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index 8b4cf0af8..eb7f5aac1 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -149,7 +149,7 @@ void Mob::DoSpecialAttackDamage(Mob *who, EQEmu::skills::SkillType skill, int32 IsValidSpell(aabonuses.SkillAttackProc[2])) { float chance = aabonuses.SkillAttackProc[0] / 1000.0f; if (zone->random.Roll(chance)) - SpellFinished(aabonuses.SkillAttackProc[2], who, 10, 0, -1, + SpellFinished(aabonuses.SkillAttackProc[2], who, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SkillAttackProc[2]].ResistDiff); } who->Damage(this, max_damage, SPELL_UNKNOWN, skill, false); @@ -786,7 +786,7 @@ void Client::RangedAttack(Mob* other, bool CanDoubleAttack) { //EndlessQuiver AA base1 = 100% Chance to avoid consumption arrow. int ChanceAvoidConsume = aabonuses.ConsumeProjectile + itembonuses.ConsumeProjectile + spellbonuses.ConsumeProjectile; - if (!ChanceAvoidConsume || (ChanceAvoidConsume < 100 && zone->random.Int(0,99) > ChanceAvoidConsume)){ + if (RangeItem->ExpendableArrow || !ChanceAvoidConsume || (ChanceAvoidConsume < 100 && zone->random.Int(0,99) > ChanceAvoidConsume)){ DeleteItemInInventory(ammo_slot, 1, true); Log.Out(Logs::Detail, Logs::Combat, "Consumed one arrow from slot %d", ammo_slot); } else { @@ -2429,7 +2429,7 @@ void Mob::DoMeleeSkillAttackDmg(Mob* other, uint16 weapon_damage, EQEmu::skills: IsValidSpell(aabonuses.SkillAttackProc[2])) { float chance = aabonuses.SkillAttackProc[0] / 1000.0f; if (zone->random.Roll(chance)) - SpellFinished(aabonuses.SkillAttackProc[2], other, 10, 0, -1, + SpellFinished(aabonuses.SkillAttackProc[2], other, EQEmu::CastingSlot::Item, 0, -1, spells[aabonuses.SkillAttackProc[2]].ResistDiff); } other->Damage(this, damage, SPELL_UNKNOWN, skillinuse); diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 0209ba4df..8f66345d2 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -194,6 +194,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove bool SE_SpellTrigger_HasCast = false; + // if buff slot, use instrument mod there, otherwise calc it + uint32 instrument_mod = buffslot > -1 ? buffs[buffslot].instrument_mod : caster ? caster->GetInstrumentMod(spell_id) : 10; // iterate through the effects in the spell for (i = 0; i < EFFECT_COUNT; i++) { @@ -201,7 +203,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove continue; effect = spell.effectid[i]; - effect_value = CalcSpellEffectValue(spell_id, i, caster_level, buffslot > -1 ? buffs[buffslot].instrument_mod : 10, caster ? caster : this); + effect_value = CalcSpellEffectValue(spell_id, i, caster_level, instrument_mod, caster ? caster : this); if(spell_id == SPELL_LAY_ON_HANDS && caster && caster->GetAA(aaImprovedLayOnHands)) effect_value = GetMaxHP(); @@ -355,8 +357,9 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove break; int32 val = 0; - val = 7500*effect_value; - val = caster->GetActSpellHealing(spell_id, val, this); + val = 7500 * effect_value; + if (caster) + val = caster->GetActSpellHealing(spell_id, val, this); if (val > 0) HealDamage(val, caster); @@ -375,12 +378,14 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove snprintf(effect_desc, _EDLEN, "Current Mana: %+i", effect_value); #endif SetMana(GetMana() + effect_value); - caster->SetMana(caster->GetMana() + std::abs(effect_value)); + if (caster) + caster->SetMana(caster->GetMana() + std::abs(effect_value)); if (effect_value < 0) TryTriggerOnValueAmount(false, true); #ifdef SPELL_EFFECT_SPAM - caster->Message(0, "You have gained %+i mana!", effect_value); + if (caster) + caster->Message(0, "You have gained %+i mana!", effect_value); #endif } } @@ -532,7 +537,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove } } - if (effect == SE_GateCastersBindpoint && caster->IsClient()) + if (effect == SE_GateCastersBindpoint && caster && caster->IsClient()) { // Teleport Bind uses caster's bind point int index = spells[spell_id].base[i] - 1; if (index < 0 || index > 4) @@ -648,7 +653,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove //Added client messages to give some indication this effect is active. // Is there a message generated? Too disgusted by raids. uint32 time = spell.base[i] * 10 * 1000; - if (caster->IsClient()) { + if (caster && caster->IsClient()) { if (caster->IsGrouped()) { auto group = caster->GetGroup(); for (int i = 0; i < 6; ++i) @@ -695,7 +700,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove ((GetLevel() > max_level) && caster && (!caster->IsNPC() || (caster->IsNPC() && !RuleB(Spells, NPCIgnoreBaseImmunity)))))) { - caster->Message_StringID(MT_SpellFailure, IMMUNE_STUN); + if (caster) + caster->Message_StringID(MT_SpellFailure, IMMUNE_STUN); } else { int stun_resist = itembonuses.StunResist+spellbonuses.StunResist; if (IsClient()) @@ -704,7 +710,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove if (stun_resist <= 0 || zone->random.Int(0,99) >= stun_resist) { Log.Out(Logs::Detail, Logs::Combat, "Stunned. We had %d percent resist chance.", stun_resist); - if (caster->IsClient()) + if (caster && caster->IsClient()) effect_value += effect_value*caster->GetFocusEffect(focusFcStunTimeMod, spell_id)/100; Stun(effect_value); @@ -916,11 +922,11 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove cd->meleepush_xy = action->sequence; CastToClient()->QueuePacket(action_packet); - if(caster->IsClient() && caster != this) + if(caster && caster->IsClient() && caster != this) caster->CastToClient()->QueuePacket(action_packet); CastToClient()->QueuePacket(message_packet); - if(caster->IsClient() && caster != this) + if(caster && caster->IsClient() && caster != this) caster->CastToClient()->QueuePacket(message_packet); CastToClient()->SetBindPoint(spells[spell_id].base[i] - 1); @@ -1031,7 +1037,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove if(zone->random.Roll(effect_value)) Gate(spells[spell_id].base2[i] - 1); - else + else if (caster) caster->Message_StringID(MT_SpellFailure,GATE_FAIL); } break; @@ -1043,7 +1049,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove snprintf(effect_desc, _EDLEN, "Cancel Magic: %d", effect_value); #endif if(GetSpecialAbility(UNDISPELLABLE)){ - caster->Message_StringID(MT_SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); + if (caster) + caster->Message_StringID(MT_SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); break; } @@ -1053,7 +1060,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove spells[buffs[slot].spellid].dispel_flag == 0 && !IsDiscipline(buffs[slot].spellid)) { - if (TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ + if (caster && TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ BuffFadeBySlot(slot); slot = buff_count; } @@ -1068,7 +1075,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove snprintf(effect_desc, _EDLEN, "Dispel Detrimental: %d", effect_value); #endif if(GetSpecialAbility(UNDISPELLABLE)){ - caster->Message_StringID(MT_SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); + if (caster) + caster->Message_StringID(MT_SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); break; } @@ -1078,7 +1086,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove IsDetrimentalSpell(buffs[slot].spellid) && spells[buffs[slot].spellid].dispel_flag == 0) { - if (TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ + if (caster && TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ BuffFadeBySlot(slot); slot = buff_count; } @@ -1093,7 +1101,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove snprintf(effect_desc, _EDLEN, "Dispel Beneficial: %d", effect_value); #endif if(GetSpecialAbility(UNDISPELLABLE)){ - caster->Message_StringID(MT_SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); + if (caster) + caster->Message_StringID(MT_SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); break; } @@ -1103,7 +1112,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove IsBeneficialSpell(buffs[slot].spellid) && spells[buffs[slot].spellid].dispel_flag == 0) { - if (TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ + if (caster && TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ BuffFadeBySlot(slot); slot = buff_count; } @@ -1120,7 +1129,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove if (buffs[slot].spellid != SPELL_UNKNOWN && IsDetrimentalSpell(buffs[slot].spellid)) { - if (TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ + if (caster && TryDispel(caster->GetLevel(),buffs[slot].casterlevel, effect_value)){ BuffFadeBySlot(slot); } } @@ -1509,7 +1518,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove ((GetLevel() > max_level) && caster && (!caster->IsNPC() || (caster->IsNPC() && !RuleB(Spells, NPCIgnoreBaseImmunity))))) { - caster->Message_StringID(MT_Shout, IMMUNE_STUN); + if (caster) + caster->Message_StringID(MT_Shout, IMMUNE_STUN); } else { @@ -1726,7 +1736,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove break; } } - else { + else if (caster) { Raid *r = entity_list.GetRaidByClient(caster->CastToClient()); if(r) { @@ -1766,7 +1776,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove Message_StringID(4, CORPSE_CANT_SENSE); } } - else + else if (caster) caster->Message_StringID(MT_SpellFailure, SPELL_LEVEL_REQ); } else { @@ -2113,7 +2123,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove #ifdef SPELL_EFFECT_SPAM snprintf(effect_desc, _EDLEN, "Sacrifice"); #endif - if(!IsClient() || !caster->IsClient()){ + if(!caster || !IsClient() || !caster->IsClient()){ break; } CastToClient()->SacrificeConfirm(caster->CastToClient()); @@ -2122,12 +2132,15 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_SummonPC: { - if(IsClient()){ - CastToClient()->MovePC(zone->GetZoneID(), zone->GetInstanceID(), caster->GetX(), caster->GetY(), caster->GetZ(), caster->GetHeading(), 2, SummonPC); + if (!caster) + break; + if (IsClient()) { + CastToClient()->MovePC(zone->GetZoneID(), zone->GetInstanceID(), caster->GetX(), + caster->GetY(), caster->GetZ(), caster->GetHeading(), 2, + SummonPC); Message(15, "You have been summoned!"); entity_list.ClearAggro(this); - } - else + } else caster->Message(13, "This spell can only be cast on players."); break; @@ -2174,6 +2187,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_TemporaryPets: //Dook- swarms and wards: { + if (!caster) + break; // this makes necro epic 1.5/2.0 proc work properly if((spell_id != 6882) && (spell_id != 6884)) // Chaotic Jester/Steadfast Servant { @@ -2269,6 +2284,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove */ int16 focus = 0; int ReuseTime = spells[spell_id].recast_time + spells[spell_id].recovery_time; + if (!caster) + break; focus = caster->GetFocusEffect(focusFcBaseEffects, spell_id); @@ -2292,7 +2309,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove snprintf(effect_desc, _EDLEN, "Wake The Dead"); #endif //meh dupe issue with npc casting this - if(caster->IsClient()){ + if(caster && caster->IsClient()){ int dur = spells[spell_id].max[i]; if (!dur) dur = 60; @@ -2657,7 +2674,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_Taunt: { - if (IsNPC()){ + if (caster && IsNPC()){ caster->Taunt(this->CastToNPC(), false, static_cast(spell.base[i]), true, spell.base2[i]); } break; @@ -2745,7 +2762,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove if (caster && IsValidSpell(spells[spell_id].base2[i])){ if(zone->random.Roll(spells[spell_id].base[i])) - caster->SpellFinished(spells[spell_id].base2[i], this, 10, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); + caster->SpellFinished(spells[spell_id].base2[i], this, EQEmu::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); } break; } @@ -3668,10 +3685,11 @@ void Mob::DoBuffTic(const Buffs_Struct &buff, int slot, Mob *caster) break; } // These effects always trigger when they fade. + // Should we have this triggered from else where? case SE_CastOnFadeEffect: case SE_CastOnFadeEffectNPC: case SE_CastOnFadeEffectAlways: { - if (buff.ticsremaining == 1) { + if (buff.ticsremaining == 0) { SpellOnTarget(spells[buff.spellid].base[i], this); } break; @@ -5135,7 +5153,7 @@ int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, boo if (Caston_spell_id) { if (IsValidSpell(Caston_spell_id) && (Caston_spell_id != spell_id)) - SpellFinished(Caston_spell_id, this, 10, 0, -1, spells[Caston_spell_id].ResistDiff); + SpellFinished(Caston_spell_id, this, EQEmu::CastingSlot::Item, 0, -1, spells[Caston_spell_id].ResistDiff); } return (value * lvlModifier / 100); @@ -6226,7 +6244,7 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama { /*If return TRUE spell met all restrictions and can continue (this = target). This check is used when the spell_new field CastRestriction is defined OR spell effect '0'(DD/Heal) has a defined limit - Range 1 : UNKNOWN + Range 1 : UNKNOWN -- the spells with this seem to not have a restiction, true for now Range 100 : *Animal OR Humanoid Range 101 : *Dragon Range 102 : *Animal OR Insect @@ -6253,7 +6271,10 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama Range 124 : *Undead HP < 10% Range 125 : *Clockwork HP < 10% Range 126 : *Wisp HP < 10% - Range 127-130 : UNKNOWN + Range 127 : UNKNOWN + Range 128 : pure melee -- guess + Range 129 : pure caster -- guess + Range 130 : hybrid -- guess Range 150 : UNKNOWN Range 190 : No Raid boss flag *not implemented Range 191 : This spell will deal less damage to 'exceptionally strong targets' - Raid boss flag *not implemented @@ -6265,12 +6286,17 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama Range 300 - 303 : UNKOWN *not implemented Range 304 : Chain + Plate class (buffs) Range 399 - 409 : Heal if HP within a specified range (400 = 0-25% 401 = 25 - 35% 402 = 35-45% ect) - Range 410 - 411 : UNKOWN + Range 410 - 411 : UNKOWN -- examples are auras that cast on NPCs maybe in combat/out of combat? Range 500 - 599 : Heal if HP less than a specified value Range 600 - 699 : Limit to Body Type [base2 - 600 = Body] Range 700 : NPC only -- from patch notes "Wizard - Arcane Fusion no longer deals damage to non-NPC targets. This should ensure that wizards who fail their Bucolic Gambit are slightly less likely to annihilate themselves." Range 701 : NOT PET - Range 800 : UKNOWN + Range 800 : UKNOWN -- Target's Target Test (16598) + Range 812 : UNKNOWN -- triggered by Thaumatize Owner + Range 814 : UNKNOWN -- Vegetentacles + Range 815 : UNKNOWN -- Pumpkin Pulp Splash + Range 816 : UNKNOWN -- Rotten Fruit Splash + Range 817 : UNKNOWN -- Tainted Bixie Pollen Splash Range 818 - 819 : If Undead/If Not Undead Range 820 - 822 : UKNOWN Range 835 : Unknown *not implemented @@ -6290,6 +6316,9 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama switch(value) { + case 1: + return true; + case 100: if ((GetBodyType() == BT_Animal) || (GetBodyType() == BT_Humanoid)) return true; @@ -6683,10 +6712,10 @@ void Mob::TryTriggerThreshHold(int32 damage, int effect_id, Mob* attacker){ if (IsValidSpell(spell_id)) { if (IsBeneficialSpell(spell_id)) - SpellFinished(spell_id, this, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spell_id, this, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); else if(attacker) - SpellFinished(spell_id, attacker, 10, 0, -1, spells[spell_id].ResistDiff); + SpellFinished(spell_id, attacker, EQEmu::CastingSlot::Item, 0, -1, spells[spell_id].ResistDiff); } } } diff --git a/zone/spells.cpp b/zone/spells.cpp index b1565be7f..e12f8042f 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -104,6 +104,8 @@ extern Zone* zone; extern volatile bool is_zone_loaded; extern WorldServer worldserver; +using EQEmu::CastingSlot; + // this is run constantly for every mob void Mob::SpellProcess() { @@ -145,13 +147,13 @@ void NPC::SpellProcess() // the rule is you can cast one triggered (usually timed) spell at a time // but things like SpellFinished() can run concurrent with a triggered cast // to allow procs to work -bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, +bool Mob::CastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, int32 cast_time, int32 mana_cost, uint32* oSpellWillFinish, uint32 item_slot, uint32 timer, uint32 timer_duration, int16 *resist_adjust, uint32 aa_id) { Log.Out(Logs::Detail, Logs::Spells, "CastSpell called for spell %s (%d) on entity %d, slot %d, time %d, mana %d, from item slot %d", - (IsValidSpell(spell_id))?spells[spell_id].name:"UNKNOWN SPELL", spell_id, target_id, slot, cast_time, mana_cost, (item_slot==0xFFFFFFFF)?999:item_slot); + (IsValidSpell(spell_id))?spells[spell_id].name:"UNKNOWN SPELL", spell_id, target_id, static_cast(slot), cast_time, mana_cost, (item_slot==0xFFFFFFFF)?999:item_slot); if(casting_spell_id == spell_id) ZeroCastingVars(); @@ -178,7 +180,7 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, if(IsClient()) CastToClient()->SendSpellBarEnable(spell_id); if(casting_spell_id && IsNPC()) - CastToNPC()->AI_Event_SpellCastFinished(false, casting_spell_slot); + CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); return(false); } //It appears that the Sanctuary effect is removed by a check on the client side (keep this however for redundancy) @@ -201,7 +203,7 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, if(IsClient()) CastToClient()->SendSpellBarEnable(spell_id); if(casting_spell_id && IsNPC()) - CastToNPC()->AI_Event_SpellCastFinished(false, casting_spell_slot); + CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); return(false); } @@ -231,7 +233,7 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, // check for fizzle // note that CheckFizzle itself doesn't let NPCs fizzle, // but this code allows for it. - if(slot < MAX_PP_MEMSPELL && !CheckFizzle(spell_id)) + if(slot < CastingSlot::MaxGems && !CheckFizzle(spell_id)) { int fizzle_msg = IsBardSong(spell_id) ? MISS_NOTE : SPELL_FIZZLE; InterruptSpell(fizzle_msg, 0x121, spell_id); @@ -252,7 +254,7 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, } //Added to prevent MQ2 exploitation of equipping normally-unequippable/clickable items with effects and clicking them for benefits. - if(item_slot && IsClient() && ((slot == USE_ITEM_SPELL_SLOT) || (slot == POTION_BELT_SPELL_SLOT) || (slot == TARGET_RING_SPELL_SLOT))) + if(item_slot && IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt)) { ItemInst *itm = CastToClient()->GetInv().GetItem(item_slot); int bitmask = 1; @@ -336,14 +338,13 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, // this is the 2nd phase of CastSpell, broken up like this to make it easier // to repeat a spell for bard songs // -bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot, +bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, int32 cast_time, int32 mana_cost, uint32* oSpellWillFinish, uint32 item_slot, uint32 timer, uint32 timer_duration, int16 resist_adjust, uint32 aa_id) { Mob* pMob = nullptr; int32 orgcasttime; - EQApplicationPacket *outapp = nullptr; if(!IsValidSpell(spell_id)) { InterruptSpell(); @@ -353,7 +354,7 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot, const SPDat_Spell_Struct &spell = spells[spell_id]; Log.Out(Logs::Detail, Logs::Spells, "DoCastSpell called for spell %s (%d) on entity %d, slot %d, time %d, mana %d, from item %d", - spell.name, spell_id, target_id, slot, cast_time, mana_cost, item_slot==0xFFFFFFFF?999:item_slot); + spell.name, spell_id, target_id, static_cast(slot), cast_time, mana_cost, item_slot==0xFFFFFFFF?999:item_slot); casting_spell_id = spell_id; casting_spell_slot = slot; @@ -418,7 +419,7 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot, // If you're at full mana, let it cast even if you dont have enough mana // we calculated this above, now enforce it - if(mana_cost > 0 && slot != USE_ITEM_SPELL_SLOT) + if(mana_cost > 0 && slot != CastingSlot::Item) { int my_curmana = GetMana(); int my_maxmana = GetMaxMana(); @@ -453,8 +454,26 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot, Log.Out(Logs::Detail, Logs::Spells, "Spell %d: Casting time %d (orig %d), mana cost %d", spell_id, cast_time, orgcasttime, mana_cost); + // now tell the people in the area -- we ALWAYS want to send this, even instant cast spells. + // The only time this is skipped is for NPC innate procs and weapon procs. Procs from buffs + // oddly still send this. Since those cases don't reach here, we don't need to check them + if (slot != CastingSlot::Discipline) { + auto outapp = new EQApplicationPacket(OP_BeginCast,sizeof(BeginCast_Struct)); + BeginCast_Struct* begincast = (BeginCast_Struct*)outapp->pBuffer; + begincast->caster_id = GetID(); + begincast->spell_id = spell_id; + begincast->cast_time = orgcasttime; // client calculates reduced time by itself + outapp->priority = 3; + entity_list.QueueCloseClients(this, outapp, false, 200, 0, true); //IsClient() ? FILTER_PCSPELLS : FILTER_NPCSPELLS); + safe_delete(outapp); + } + // cast time is 0, just finish it right now and be done with it if(cast_time == 0) { + if (!DoCastingChecks()) { + StopCasting(); + return false; + } CastedSpellFinished(spell_id, target_id, slot, mana_cost, item_slot, resist_adjust); return(true); } @@ -476,25 +495,14 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, uint16 slot, if (oSpellWillFinish) *oSpellWillFinish = Timer::GetCurrentTime() + cast_time + 100; - // now tell the people in the area - outapp = new EQApplicationPacket(OP_BeginCast,sizeof(BeginCast_Struct)); - BeginCast_Struct* begincast = (BeginCast_Struct*)outapp->pBuffer; - begincast->caster_id = GetID(); - begincast->spell_id = spell_id; - begincast->cast_time = orgcasttime; // client calculates reduced time by itself - outapp->priority = 3; - entity_list.QueueCloseClients(this, outapp, false, 200, 0, true); //IsClient() ? FILTER_PCSPELLS : FILTER_NPCSPELLS); - safe_delete(outapp); - outapp = nullptr; - - if (IsClient() && slot == USE_ITEM_SPELL_SLOT &&item_slot != 0xFFFFFFFF) { + if (IsClient() && slot == CastingSlot::Item && item_slot != 0xFFFFFFFF) { auto item = CastToClient()->GetInv().GetItem(item_slot); if (item && item->GetItem()) Message_StringID(MT_Spells, BEGINS_TO_GLOW, item->GetItem()->Name); } if (!DoCastingChecks()) { - InterruptSpell(); + StopCasting(); return false; } @@ -550,6 +558,10 @@ bool Mob::DoCastingChecks() } } + if (IsClient() && spells[spell_id].EndurTimerIndex > 0 && casting_spell_slot < CastingSlot::MaxGems) + if (!CastToClient()->IsLinkedSpellReuseTimerReady(spells[spell_id].EndurTimerIndex)) + return false; + casting_spell_checks = true; return true; } @@ -767,7 +779,7 @@ void Mob::ZeroCastingVars() spellend_timer.Disable(); casting_spell_id = 0; casting_spell_targetid = 0; - casting_spell_slot = 0; + casting_spell_slot = CastingSlot::Gem1; casting_spell_mana = 0; casting_spell_inventory_slot = 0; casting_spell_timer = 0; @@ -802,7 +814,7 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) } if(casting_spell_id && IsNPC()) { - CastToNPC()->AI_Event_SpellCastFinished(false, casting_spell_slot); + CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); } if(casting_spell_aa_id && IsClient()) { //Rest AA Timer on failed cast @@ -874,18 +886,44 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) } +// this is like interrupt, just it doesn't spam interrupt packets to everyone +// There are a few cases where this is what live does :P +void Mob::StopCasting() +{ + if (casting_spell_id && IsNPC()) { + CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); + } + + if (IsClient()) { + auto c = CastToClient(); + if (casting_spell_aa_id) { //Rest AA Timer on failed cast + c->Message_StringID(MT_SpellFailure, ABILITY_FAILED); + c->ResetAlternateAdvancementTimer(casting_spell_aa_id); + } + + auto outapp = new EQApplicationPacket(OP_ManaChange, sizeof(ManaChange_Struct)); + auto mc = (ManaChange_Struct *)outapp->pBuffer; + mc->new_mana = GetMana(); + mc->stamina = GetEndurance(); + mc->spell_id = casting_spell_id; + mc->keepcasting = 0; + c->FastQueuePacket(&outapp); + } + ZeroCastingVars(); +} + // this is called after the timer is up and the spell is finished // casting. everything goes through here, including items with zero cast time // only to be used from SpellProcess // NOTE: do not put range checking, etc into this function. this should // just check timed spell specific things before passing off to SpellFinished // which figures out proper targets etc -void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, +void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slot, uint16 mana_used, uint32 inventory_slot, int16 resist_adjust) { bool IsFromItem = false; - if(IsClient() && slot != USE_ITEM_SPELL_SLOT && slot != POTION_BELT_SPELL_SLOT && slot != TARGET_RING_SPELL_SLOT && spells[spell_id].recast_time > 1000) { // 10 is item + if(IsClient() && slot != CastingSlot::Item && slot != CastingSlot::PotionBelt && spells[spell_id].recast_time > 1000) { // 10 is item if(!CastToClient()->GetPTimers().Expired(&database, pTimerSpellStart + spell_id, false)) { //should we issue a message or send them a spell gem packet? Message_StringID(13, SPELL_RECAST); @@ -895,7 +933,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, } } - if(IsClient() && ((slot == USE_ITEM_SPELL_SLOT) || (slot == POTION_BELT_SPELL_SLOT) || (slot == TARGET_RING_SPELL_SLOT))) + if(IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt)) { IsFromItem = true; ItemInst *itm = CastToClient()->GetInv().GetItem(inventory_slot); @@ -1193,7 +1231,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, int16 DeleteChargeFromSlot = -1; - if(IsClient() && ((slot == USE_ITEM_SPELL_SLOT) || (slot == POTION_BELT_SPELL_SLOT) || (slot == TARGET_RING_SPELL_SLOT)) + if(IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt) && inventory_slot != 0xFFFFFFFF) // 10 is an item { bool fromaug = false; @@ -1306,8 +1344,11 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, { if(IsClient()) { - this->CastToClient()->CheckSongSkillIncrease(spell_id); - this->CastToClient()->MemorizeSpell(slot, spell_id, memSpellSpellbar); + Client *c = CastToClient(); + c->CheckSongSkillIncrease(spell_id); + if (spells[spell_id].EndurTimerIndex > 0 && slot < CastingSlot::MaxGems) + c->SetLinkedSpellReuseTimer(spells[spell_id].EndurTimerIndex, spells[spell_id].recast_time / 1000); + c->MemorizeSpell(static_cast(slot), spell_id, memSpellSpellbar); } Log.Out(Logs::Detail, Logs::Spells, "Bard song %d should be started", spell_id); } @@ -1319,14 +1360,15 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, SendSpellBarEnable(spell_id); // this causes the delayed refresh of the spell bar gems - c->MemorizeSpell(slot, spell_id, memSpellSpellbar); + if (spells[spell_id].EndurTimerIndex > 0 && slot < CastingSlot::MaxGems) + c->SetLinkedSpellReuseTimer(spells[spell_id].EndurTimerIndex, spells[spell_id].recast_time / 1000); + c->MemorizeSpell(static_cast(slot), spell_id, memSpellSpellbar); // this tells the client that casting may happen again SetMana(GetMana()); // skills - if(slot < MAX_PP_MEMSPELL) - { + if (EQEmu::skills::IsCastingSkill(spells[spell_id].skill)) { c->CheckIncreaseSkill(spells[spell_id].skill, nullptr); // increased chance of gaining channel skill if you regained concentration @@ -1348,8 +1390,8 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, } -bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, uint16 slot) { - +bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, CastingSlot slot, bool isproc) +{ /* The basic types of spells: @@ -1395,6 +1437,11 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce targetType = ST_GroupClientAndPet; } + // NPC innate procs override the target type to single target. + // Yes. This code will cause issues if they have the proc as innate AND on a weapon. Oh well. + if (isproc && IsNPC() && CastToNPC()->GetInnateProcSpellID() == spell_id) + targetType = ST_Target; + if (spell_target && !spell_target->PassCastRestriction(true, spells[spell_id].CastRestriction)){ Message_StringID(13,SPELL_NEED_TAR); return false; @@ -1682,7 +1729,7 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce case ST_Group: case ST_GroupNoPets: { - if(IsClient() && CastToClient()->TGB() && IsTGBCompatibleSpell(spell_id) && slot != USE_ITEM_SPELL_SLOT) { + if(IsClient() && CastToClient()->TGB() && IsTGBCompatibleSpell(spell_id) && (slot != CastingSlot::Item || RuleB(Spells, AllowItemTGB))) { if( (!target) || (target->IsNPC() && !(target->GetOwner() && target->GetOwner()->IsClient())) || (target->IsCorpse()) ) @@ -1728,7 +1775,9 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce { if(IsGrouped()) { - group_id_caster = GetGroup()->GetID(); + if (Group* group = GetGroup()) { + group_id_caster = group->GetID(); + } } else if(IsRaidGrouped()) { @@ -1743,7 +1792,9 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce Mob *owner = GetOwner(); if(owner->IsGrouped()) { - group_id_caster = owner->GetGroup()->GetID(); + if (Group* group = owner->GetGroup()) { + group_id_caster = group->GetID(); + } } else if(owner->IsRaidGrouped()) { @@ -1772,7 +1823,9 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce { if(spell_target->IsGrouped()) { - group_id_target = spell_target->GetGroup()->GetID(); + if (Group* group = spell_target->GetGroup()) { + group_id_target = group->GetID(); + } } else if(spell_target->IsRaidGrouped()) { @@ -1787,7 +1840,9 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce Mob *owner = spell_target->GetOwner(); if(owner->IsGrouped()) { - group_id_target = owner->GetGroup()->GetID(); + if (Group* group = owner->GetGroup()) { + group_id_target = group->GetID(); + } } else if(owner->IsRaidGrouped()) { @@ -1904,7 +1959,7 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce // only used from CastedSpellFinished, and procs // we can't interrupt in this, or anything called from this! // if you need to abort the casting, return false -bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 mana_used, +bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, uint16 mana_used, uint32 inventory_slot, int16 resist_adjust, bool isproc, int level_override) { //EQApplicationPacket *outapp = nullptr; @@ -1974,7 +2029,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 //determine the type of spell target we have CastAction_type CastAction; - if(!DetermineSpellTargets(spell_id, spell_target, ae_center, CastAction, slot)) + if(!DetermineSpellTargets(spell_id, spell_target, ae_center, CastAction, slot, isproc)) return(false); Log.Out(Logs::Detail, Logs::Spells, "Spell %d: target type %d, target %s, AE center %s", spell_id, CastAction, spell_target?spell_target->GetName():"NONE", ae_center?ae_center->GetName():"NONE"); @@ -2263,7 +2318,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 bool mgb = HasMGB() && spells[spell_id].can_mgb; // if this was a spell slot or an ability use up the mana for it - if(slot != USE_ITEM_SPELL_SLOT && slot != POTION_BELT_SPELL_SLOT && slot != TARGET_RING_SPELL_SLOT && mana_used > 0) + if(slot != CastingSlot::Item && slot != CastingSlot::PotionBelt && mana_used > 0) { mana_used = GetActSpellCost(spell_id, mana_used); if (mgb) { @@ -2326,7 +2381,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 } } - if(IsClient() && ((slot == USE_ITEM_SPELL_SLOT) || (slot == POTION_BELT_SPELL_SLOT) || (slot == TARGET_RING_SPELL_SLOT))) + if(IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt)) { ItemInst *itm = CastToClient()->GetInv().GetItem(inventory_slot); if(itm && itm->GetItem()->RecastDelay > 0){ @@ -2345,7 +2400,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 } if(IsNPC()) - CastToNPC()->AI_Event_SpellCastFinished(true, slot); + CastToNPC()->AI_Event_SpellCastFinished(true, static_cast(slot)); return true; } @@ -2360,8 +2415,8 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 * * return false to stop the song */ -bool Mob::ApplyNextBardPulse(uint16 spell_id, Mob *spell_target, uint16 slot) { - if(slot == USE_ITEM_SPELL_SLOT) { +bool Mob::ApplyNextBardPulse(uint16 spell_id, Mob *spell_target, CastingSlot slot) { + if(slot == CastingSlot::Item) { //bard songs should never come from items... Log.Out(Logs::Detail, Logs::Spells, "Bard Song Pulse %d: Supposidly cast from an item. Killing song.", spell_id); return(false); @@ -2972,8 +3027,7 @@ int Mob::CheckStackConflict(uint16 spellid1, int caster_level1, uint16 spellid2, if ( effect1 == SE_AttackSpeed || - effect1 == SE_AttackSpeed2 || - effect1 == SE_AttackSpeed3 + effect1 == SE_AttackSpeed2 ) { sp1_value -= 100; @@ -3047,6 +3101,34 @@ bool Client::CheckSpellLevelRestriction(uint16 spell_id) return true; } +uint32 Mob::GetFirstBuffSlot(bool disc, bool song) +{ + return 0; +} + +uint32 Mob::GetLastBuffSlot(bool disc, bool song) +{ + return GetCurrentBuffSlots(); +} + +uint32 Client::GetFirstBuffSlot(bool disc, bool song) +{ + if (song) + return GetMaxBuffSlots(); + if (disc) + return GetMaxBuffSlots() + GetMaxSongSlots(); + return 0; +} + +uint32 Client::GetLastBuffSlot(bool disc, bool song) +{ + if (song) + return GetMaxBuffSlots() + GetCurrentSongSlots(); + if (disc) + return GetMaxBuffSlots() + GetMaxSongSlots() + GetCurrentDiscSlots(); + return GetCurrentBuffSlots(); +} + // returns the slot the buff was added to, -1 if it wasn't added due to // stacking problems, and -2 if this is not a buff // if caster is null, the buff will be added with the caster level being @@ -3084,18 +3166,8 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid // we also check if overwriting will occur. this is so after this loop // we can determine if there will be room for this buff int buff_count = GetMaxTotalSlots(); - uint32 start_slot = 0; - uint32 end_slot = 0; - if (IsDisciplineBuff(spell_id)) { - start_slot = GetMaxBuffSlots() + GetMaxSongSlots(); - end_slot = start_slot + GetCurrentDiscSlots(); - } else if(spells[spell_id].short_buff_box) { - start_slot = GetMaxBuffSlots(); - end_slot = start_slot + GetCurrentSongSlots(); - } else { - start_slot = 0; - end_slot = GetCurrentBuffSlots(); - } + uint32 start_slot = GetFirstBuffSlot(IsDisciplineBuff(spell_id), spells[spell_id].short_buff_box); + uint32 end_slot = GetLastBuffSlot(IsDisciplineBuff(spell_id), spells[spell_id].short_buff_box); for (buffslot = 0; buffslot < buff_count; buffslot++) { const Buffs_Struct &curbuf = buffs[buffslot]; @@ -3107,7 +3179,7 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid if (ret == -1) { // stop the spell Log.Out(Logs::Detail, Logs::Spells, "Adding buff %d failed: stacking prevented by spell %d in slot %d with caster level %d", spell_id, curbuf.spellid, buffslot, curbuf.casterlevel); - if (caster->IsClient() && RuleB(Client, UseLiveBlockedMessage)) { + if (caster && caster->IsClient() && RuleB(Client, UseLiveBlockedMessage)) { caster->Message(13, "Your %s did not take hold on %s. (Blocked by %s.)", spells[spell_id].name, this->GetName(), spells[curbuf.spellid].name); } return -1; @@ -3769,7 +3841,7 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, bool reflect, bool use_r } if (IsValidSpell(spells[spell_id].RecourseLink) && spells[spell_id].RecourseLink != spell_id) - SpellFinished(spells[spell_id].RecourseLink, this, 10, 0, -1, spells[spells[spell_id].RecourseLink].ResistDiff); + SpellFinished(spells[spell_id].RecourseLink, this, CastingSlot::Item, 0, -1, spells[spells[spell_id].RecourseLink].ResistDiff); if (IsDetrimentalSpell(spell_id)) { @@ -4713,6 +4785,7 @@ void Mob::SendSpellBarEnable(uint16 spell_id) manachange->new_mana = GetMana(); manachange->spell_id = spell_id; manachange->stamina = CastToClient()->GetEndurance(); + manachange->keepcasting = 0; outapp->priority = 6; CastToClient()->QueuePacket(outapp); safe_delete(outapp); @@ -5245,7 +5318,7 @@ bool Mob::UseBardSpellLogic(uint16 spell_id, int slot) spell_id = casting_spell_id; if(slot == -1) - slot = casting_spell_slot; + slot = static_cast(casting_spell_slot); // should we treat this as a bard singing? return @@ -5273,7 +5346,7 @@ void Mob::_StopSong() { bardsong = 0; bardsong_target_id = 0; - bardsong_slot = 0; + bardsong_slot = CastingSlot::Gem1; bardsong_timer.Disable(); } @@ -5351,8 +5424,8 @@ void Mob::SendPetBuffsToClient() int MaxSlots = GetMaxTotalSlots(); - if(MaxSlots > BUFF_COUNT) - MaxSlots = BUFF_COUNT; + if(MaxSlots > PET_BUFF_COUNT) + MaxSlots = PET_BUFF_COUNT; for(int buffslot = 0; buffslot < MaxSlots; buffslot++) { @@ -5449,10 +5522,14 @@ void Mob::BuffModifyDurationBySpellID(uint16 spell_id, int32 newDuration) int Client::GetCurrentBuffSlots() const { - if(15 + aabonuses.BuffSlotIncrease > 25) - return 25; - else - return 15 + aabonuses.BuffSlotIncrease; + int numbuffs = 15; + // client does check spells and items + numbuffs += aabonuses.BuffSlotIncrease + spellbonuses.BuffSlotIncrease + itembonuses.BuffSlotIncrease; + if (GetLevel() > 70) + numbuffs++; + if (GetLevel() > 74) + numbuffs++; + return EQEmu::ClampUpper(numbuffs, GetMaxBuffSlots()); } int Client::GetCurrentSongSlots() const @@ -5644,3 +5721,26 @@ void Mob::ConeDirectional(uint16 spell_id, int16 resist_adjust) ++iter; } } + +// duration in seconds +void Client::SetLinkedSpellReuseTimer(uint32 timer_id, uint32 duration) +{ + if (timer_id > 19) + return; + Log.Out(Logs::Detail, Logs::Spells, "Setting Linked Spell Reuse %d for %d", timer_id, duration); + GetPTimers().Start(pTimerLinkedSpellReuseStart + timer_id, duration); + auto outapp = new EQApplicationPacket(OP_LinkedReuse, sizeof(LinkedSpellReuseTimer_Struct)); + auto lr = (LinkedSpellReuseTimer_Struct *)outapp->pBuffer; + lr->timer_id = timer_id; + lr->start_time = Timer::GetCurrentTime() / 1000; + lr->end_time = lr->start_time + duration; + FastQueuePacket(&outapp); +} + +bool Client::IsLinkedSpellReuseTimerReady(uint32 timer_id) +{ + if (timer_id > 19) + return true; + return GetPTimers().Expired(&database, pTimerLinkedSpellReuseStart + timer_id, false); +} + diff --git a/zone/string_ids.h b/zone/string_ids.h index be05b1eb2..e1fb4de7c 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -297,6 +297,7 @@ #define ADVENTURE_COMPLETE 5147 //You received %1 points for successfully completing the adventure. #define SUCCOR_FAIL 5169 //The portal collapes before you can escape! #define PET_ATTACKING 5501 //%1 tells you, 'Attacking %2 Master.' +#define AVOID_STUNNING_BLOW 5753 //You avoid the stunning blow. #define FATAL_BOW_SHOT 5745 //%1 performs a FATAL BOW SHOT!! #define MELEE_SILENCE 5806 //You *CANNOT* use this melee ability, you are suffering from amnesia! #define DISCIPLINE_REUSE_MSG 5807 //You can use the ability %1 again in %2 hour(s) %3 minute(s) %4 seconds. @@ -354,7 +355,7 @@ #define HIT_NON_MELEE 9073 //%1 hit %2 for %3 points of non-melee damage. #define GLOWS_BLUE 9074 //Your %1 glows blue. #define GLOWS_RED 9075 //Your %1 glows red. -#define SHAKE_OFF_STUN 9077 +#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 NEW_SPELLS_AVAIL 9149 //You have new spells available to you. Check the merchants near your guild master. diff --git a/zone/trap.cpp b/zone/trap.cpp index 5a644bb03..87000f940 100644 --- a/zone/trap.cpp +++ b/zone/trap.cpp @@ -115,7 +115,7 @@ void Trap::Trigger(Mob* trigger) entity_list.MessageClose(trigger,false,100,13,"%s",message.c_str()); } if(hiddenTrigger){ - hiddenTrigger->SpellFinished(effectvalue, trigger, 10, 0, -1, spells[effectvalue].ResistDiff); + hiddenTrigger->SpellFinished(effectvalue, trigger, EQEmu::CastingSlot::Item, 0, -1, spells[effectvalue].ResistDiff); } break; case trapTypeAlarm: diff --git a/zone/water_map.h b/zone/water_map.h index 2b6e0ce3c..975703b40 100644 --- a/zone/water_map.h +++ b/zone/water_map.h @@ -8,7 +8,7 @@ extern const ZoneConfig *Config; -enum WaterRegionType { +enum WaterRegionType : int { RegionTypeUnsupported = -2, RegionTypeUntagged = -1, RegionTypeNormal = 0, @@ -33,6 +33,8 @@ public: virtual bool InVWater(const glm::vec3& location) const = 0; virtual bool InLava(const glm::vec3& location) const = 0; virtual bool InLiquid(const glm::vec3& location) const = 0; + virtual bool InPvP(const glm::vec3& location) const = 0; + virtual bool InZoneLine(const glm::vec3& location) const = 0; protected: virtual bool Load(FILE *fp) { return false; } diff --git a/zone/water_map_v1.cpp b/zone/water_map_v1.cpp index 431df4224..7d2479a3b 100644 --- a/zone/water_map_v1.cpp +++ b/zone/water_map_v1.cpp @@ -30,6 +30,14 @@ bool WaterMapV1::InLiquid(const glm::vec3& location) const { return InWater(location) || InLava(location); } +bool WaterMapV1::InPvP(const glm::vec3& location) const { + return ReturnRegionType(location) == RegionTypePVP; +} + +bool WaterMapV1::InZoneLine(const glm::vec3& location) const { + return ReturnRegionType(location) == RegionTypeZoneLine; +} + bool WaterMapV1::Load(FILE *fp) { uint32 bsp_tree_size; if (fread(&bsp_tree_size, sizeof(bsp_tree_size), 1, fp) != 1) { diff --git a/zone/water_map_v1.h b/zone/water_map_v1.h index 6697aa0c8..af4d4966f 100644 --- a/zone/water_map_v1.h +++ b/zone/water_map_v1.h @@ -24,6 +24,8 @@ public: virtual bool InVWater(const glm::vec3& location) const; virtual bool InLava(const glm::vec3& location) const; virtual bool InLiquid(const glm::vec3& location) const; + virtual bool InPvP(const glm::vec3& location) const; + virtual bool InZoneLine(const glm::vec3& location) const; protected: virtual bool Load(FILE *fp); diff --git a/zone/water_map_v2.cpp b/zone/water_map_v2.cpp index a57999a48..939b9be97 100644 --- a/zone/water_map_v2.cpp +++ b/zone/water_map_v2.cpp @@ -33,6 +33,14 @@ bool WaterMapV2::InLiquid(const glm::vec3& location) const { return InWater(location) || InLava(location); } +bool WaterMapV2::InPvP(const glm::vec3& location) const { + return ReturnRegionType(location) == RegionTypePVP; +} + +bool WaterMapV2::InZoneLine(const glm::vec3& location) const { + return ReturnRegionType(location) == RegionTypeZoneLine; +} + bool WaterMapV2::Load(FILE *fp) { uint32 region_count; if (fread(®ion_count, sizeof(region_count), 1, fp) != 1) { diff --git a/zone/water_map_v2.h b/zone/water_map_v2.h index 6429937f0..704d1a119 100644 --- a/zone/water_map_v2.h +++ b/zone/water_map_v2.h @@ -17,6 +17,8 @@ public: virtual bool InVWater(const glm::vec3& location) const; virtual bool InLava(const glm::vec3& location) const; virtual bool InLiquid(const glm::vec3& location) const; + virtual bool InPvP(const glm::vec3& location) const; + virtual bool InZoneLine(const glm::vec3& location) const; protected: virtual bool Load(FILE *fp); diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 969c4b947..0f00ed0c5 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -765,7 +765,7 @@ void WorldServer::Process() { zone->SetZoneHasCurrentTime(true); } - if (zone->is_zone_time_localized){ + if (zone && zone->is_zone_time_localized){ Log.Out(Logs::General, Logs::Zone_Server, "Received request to sync time from world, but our time is localized currently"); } break; diff --git a/zone/zone.cpp b/zone/zone.cpp index df48d0cfa..1165d7e9b 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -1594,7 +1594,9 @@ ZonePoint* Zone::GetClosestZonePoint(const glm::vec3& location, uint32 to, Clien iterator.Advance(); } - if(closest_dist > 400.0f && closest_dist < max_distance2) + // if we have a water map and it says we're in a zoneline, lets assume it's just a really big zone line + // this shouldn't open up any exploits since those situations are detected later on + if ((zone->HasWaterMap() && !zone->watermap->InZoneLine(glm::vec3(client->GetPosition()))) || (!zone->HasWaterMap() && closest_dist > 400.0f && closest_dist < max_distance2)) { if(client) client->CheatDetected(MQZoneUnknownDest, location.x, location.y, location.z); // Someone is trying to use /zone diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index d0c80f37f..7f86f5110 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -3178,7 +3178,8 @@ void ZoneDatabase::SavePetInfo(Client *client) query.clear(); // pet buffs! - for (int index = 0; index < RuleI(Spells, MaxTotalSlotsPET); index++) { + int max_slots = RuleI(Spells, MaxTotalSlotsPET); + for (int index = 0; index < max_slots; index++) { if (petinfo->Buffs[index].spellid == SPELL_UNKNOWN || petinfo->Buffs[index].spellid == 0) continue; if (query.length() == 0) diff --git a/zone/zonedb.h b/zone/zonedb.h index ef5a6810d..689f9a82f 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -125,7 +125,7 @@ struct PetInfo { uint32 HP; uint32 Mana; float size; - SpellBuff_Struct Buffs[BUFF_COUNT]; + SpellBuff_Struct Buffs[PET_BUFF_COUNT]; uint32 Items[EQEmu::legacy::EQUIPMENT_SIZE]; char Name[64]; };