diff --git a/zone/client.cpp b/zone/client.cpp index daccf2779..67f909283 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -10513,6 +10513,8 @@ std::vector Client::GetMemmedSpells() { std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { std::vector scribeable_spells; + std::unordered_map> spell_group_cache = LoadSpellGroupCache(min_level, max_level); + for (uint16 spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { bool scribeable = true; if (!IsValidSpell(spell_id)) { @@ -10547,13 +10549,33 @@ std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { continue; } - if (RuleB(Spells, EnableSpellGlobals) && !SpellGlobalCheck(spell_id, CharacterID())) { + if ( + RuleB(Spells, EnableSpellGlobals) && + !SpellGlobalCheck(spell_id, CharacterID()) + ) { scribeable = false; - } else if (RuleB(Spells, EnableSpellBuckets) && !SpellBucketCheck(spell_id, CharacterID())) { + } else if ( + RuleB(Spells, EnableSpellBuckets) && + !SpellBucketCheck(spell_id, CharacterID()) + ) { scribeable = false; } - if (scribeable) { + if (spells[spell_id].spell_group) { + const auto& g = spell_group_cache.find(spells[spell_id].spell_group); + if (g != spell_group_cache.end()) { + for (const auto& s : g->second) { + if ( + EQ::ValueWithin(spells[s].classes[m_pp.class_ - 1], min_level, max_level) && + s == spell_id && + scribeable + ) { + scribeable_spells.push_back(spell_id); + } + continue; + } + } + } else if (scribeable) { scribeable_spells.push_back(spell_id); } } @@ -10562,7 +10584,7 @@ std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { std::vector Client::GetScribedSpells() { std::vector scribed_spells; - for(int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { if (IsValidSpell(m_pp.spell_book[index])) { scribed_spells.push_back(m_pp.spell_book[index]); } diff --git a/zone/client.h b/zone/client.h index 6fea80883..ce8223ac6 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1062,6 +1062,7 @@ public: inline uint32 GetSpellByBookSlot(int book_slot) { return m_pp.spell_book[book_slot]; } inline bool HasSpellScribed(int spellid) { return FindSpellBookSlotBySpellID(spellid) != -1; } uint32 GetHighestScribedSpellinSpellGroup(uint32 spell_group); + std::unordered_map> LoadSpellGroupCache(uint8 min_level, uint8 max_level); uint16 GetMaxSkillAfterSpecializationRules(EQ::skills::SkillType skillid, uint16 maxSkill); void SendPopupToClient(const char *Title, const char *Text, uint32 PopupID = 0, uint32 Buttons = 0, uint32 Duration = 0); void SendFullPopup(const char *Title, const char *Text, uint32 PopupID = 0, uint32 NegativeID = 0, uint32 Buttons = 0, uint32 Duration = 0, const char *ButtonName0 = 0, const char *ButtonName1 = 0, uint32 SoundControls = 0); diff --git a/zone/spells.cpp b/zone/spells.cpp index 465f56586..3b3c6d090 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -182,7 +182,7 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, } //Goal of Spells:UseSpellImpliedTargeting is to replicate the EQ2 feature where spells will 'pass through' invalid targets to target's target to try to find a valid target. - if (RuleB(Spells,UseSpellImpliedTargeting) && IsClient()) { + if (RuleB(Spells,UseSpellImpliedTargeting) && IsClient()) { Mob* spell_target = entity_list.GetMobID(target_id); if (spell_target) { Mob* targets_target = spell_target->GetTarget(); @@ -202,7 +202,7 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, } } } - } + } if (!DoCastingChecksOnCaster(spell_id, slot) || !DoCastingChecksZoneRestrictions(true, spell_id) || @@ -2278,7 +2278,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in } } - //Guard Assist Code + //Guard Assist Code if (RuleB(Character, PVPEnableGuardFactionAssist) && spell_target && IsDetrimentalSpell(spell_id) && spell_target != this) { if (IsClient() && spell_target->IsClient()|| (HasOwner() && GetOwner()->IsClient() && spell_target->IsClient())) { auto& mob_list = entity_list.GetCloseMobList(spell_target); @@ -2591,8 +2591,8 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in case Beam: { BeamDirectional(spell_id, resist_adjust); - break; - } + break; + } case TargetRing: { @@ -2885,7 +2885,7 @@ int CalcBuffDuration_formula(int level, int formula, int duration) temp = 10 * (level + 10); break; case 50: // Permanent. Cancelled by casting/combat for perm invis, non-lev zones for lev, curing poison/curse - // counters, etc. + // counters, etc. return -1; case 51: // Permanent. Cancelled when out of range of aura. return -4; @@ -3108,7 +3108,7 @@ int Mob::CheckStackConflict(uint16 spellid1, int caster_level1, uint16 spellid2, continue; if (IsBardOnlyStackEffect(effect1) && GetSpellLevel(spellid1, BARD) != 255 && - GetSpellLevel(spellid2, BARD) != 255) + GetSpellLevel(spellid2, BARD) != 255) continue; // big ol' list according to the client, wasn't that nice! @@ -3541,7 +3541,7 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, int reflect_effectivenes // other AE spells this is redundant, oh well // 1 = PCs, 2 = NPCs if (spells[spell_id].pcnpc_only_flag && spells[spell_id].target_type != ST_AETargetHateList && - spells[spell_id].target_type != ST_HateList) { + spells[spell_id].target_type != ST_HateList) { if (spells[spell_id].pcnpc_only_flag == 1 && !spelltar->IsClient() && !spelltar->IsMerc() && !spelltar->IsBot()) return false; else if (spells[spell_id].pcnpc_only_flag == 2 && (spelltar->IsClient() || spelltar->IsMerc() || spelltar->IsBot())) @@ -5520,6 +5520,33 @@ uint32 Client::GetHighestScribedSpellinSpellGroup(uint32 spell_group) return highest_spell_id; } +std::unordered_map> Client::LoadSpellGroupCache(uint8 min_level, uint8 max_level) { + std::unordered_map> spell_group_cache; + + const auto query = fmt::format( + "SELECT a.spellgroup, a.id, a.rank " + "FROM spells_new a " + "INNER JOIN (" + "SELECT spellgroup, MAX(rank) rank " + "FROM spells_new " + "GROUP BY spellgroup) " + "b ON a.spellgroup = b.spellgroup AND a.rank = b.rank " + "WHERE a.spellgroup IN (SELECT DISTINCT spellgroup FROM spells_new WHERE spellgroup != 0 and classes{} BETWEEN {} AND {}) ORDER BY rank DESC", + m_pp.class_, min_level, max_level + ); + + auto results = database.QueryDatabase(query); + if (!results.Success() || !results.RowCount()) { + return spell_group_cache; + } + + for (auto row : results) { + spell_group_cache[std::stoul(row[0])].push_back(static_cast(std::stoul(row[1]))); + } + + return spell_group_cache; +} + bool Client::SpellGlobalCheck(uint16 spell_id, uint32 character_id) { std::string query = fmt::format( "SELECT qglobal, value FROM spell_globals WHERE spellid = {}", @@ -6340,7 +6367,7 @@ void Mob::BeamDirectional(uint16 spell_id, int16 resist_adjust) while (iter != targets_in_range.end()) { if (!(*iter) || (beneficial_targets && ((*iter)->IsNPC() && !(*iter)->IsPetOwnerClient())) || - (*iter)->BehindMob(this, (*iter)->GetX(), (*iter)->GetY())) { + (*iter)->BehindMob(this, (*iter)->GetX(), (*iter)->GetY())) { ++iter; continue; } @@ -6429,7 +6456,7 @@ void Mob::ConeDirectional(uint16 spell_id, int16 resist_adjust) } float heading_to_target = - (CalculateHeadingToTarget((*iter)->GetX(), (*iter)->GetY()) * 360.0f / 512.0f); + (CalculateHeadingToTarget((*iter)->GetX(), (*iter)->GetY()) * 360.0f / 512.0f); while (heading_to_target < 0.0f) heading_to_target += 360.0f; @@ -6473,7 +6500,7 @@ void Mob::ConeDirectional(uint16 spell_id, int16 resist_adjust) if (angle_start > angle_end) { if ((heading_to_target >= angle_start && heading_to_target <= 360.0f) || - (heading_to_target >= 0.0f && heading_to_target <= angle_end)) { + (heading_to_target >= 0.0f && heading_to_target <= angle_end)) { if (CheckLosFN((*iter)) || spells[spell_id].npc_no_los) { (*iter)->CalcSpellPowerDistanceMod(spell_id, 0, this); SpellOnTarget(spell_id, (*iter), 0, true, resist_adjust);