diff --git a/sql/updates/world/4.3.4/2025_06_21_00_world.sql b/sql/updates/world/4.3.4/2025_06_21_00_world.sql new file mode 100644 index 0000000000..e2a7639d5a --- /dev/null +++ b/sql/updates/world/4.3.4/2025_06_21_00_world.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `playercreateinfo_skills`; diff --git a/src/common/Utilities/Containers.h b/src/common/Utilities/Containers.h index c1d2aa8fbf..7369661948 100644 --- a/src/common/Utilities/Containers.h +++ b/src/common/Utilities/Containers.h @@ -70,6 +70,13 @@ namespace Trinity namespace Containers { + template + concept Map = requires (M) + { + typename M::key_type; + typename M::mapped_type; + }; + // replace with std::size in C++17 template constexpr inline std::size_t Size(C const& container) @@ -253,15 +260,28 @@ namespace Trinity /** * Returns a pointer to mapped value (or the value itself if map stores pointers) */ - template - inline auto MapGetValuePtr(M& map, typename M::key_type const& key) -> decltype(AddressOrSelf(map.find(key)->second)) + template + inline auto MapGetValuePtr(M& map, typename M::key_type const& key) { + using mapped_type = typename M::mapped_type; + auto itr = map.find(key); - return itr != map.end() ? AddressOrSelf(itr->second) : nullptr; + if constexpr (std::is_pointer_v) + return itr != map.end() ? itr->second : nullptr; // raw pointer + else if constexpr (requires(mapped_type const& p) { p.operator->(); }) + { + // smart pointers + if constexpr (std::is_copy_constructible_v) + return itr != map.end() ? itr->second : nullptr; // copyable (like shared_ptr) + else + return itr != map.end() ? std::to_address(itr->second) : nullptr; // non-copyable unique_ptr like, unwrap it to raw pointer + } + else + return itr != map.end() ? std::addressof(itr->second) : nullptr; // value } - template class M, class... Rest> - inline void MultimapErasePair(M& multimap, K const& key, V const& value) + template + inline void MultimapErasePair(M& multimap, typename M::key_type const& key, typename M::mapped_type const& value) { auto range = multimap.equal_range(key); for (auto itr = range.first; itr != range.second; ) diff --git a/src/server/game/DataStores/DBCEnums.h b/src/server/game/DataStores/DBCEnums.h index d89c9e4115..a809cb011d 100644 --- a/src/server/game/DataStores/DBCEnums.h +++ b/src/server/game/DataStores/DBCEnums.h @@ -532,10 +532,13 @@ enum class MapFlags : uint32 DEFINE_ENUM_FLAG(MapFlags); -enum AbilytyLearnType +enum class SkillLineAbilityAcquireMethod : int32 { - SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE = 1, // Spell state will update depending on skill value - SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN = 2 // Spell will be learned/removed together with entire skill + Learned = 0, + AutomaticSkillRank = 1, // Spell state will update depending on skill value + AutomaticCharLevel = 2, // Spell will be learned/removed together with entire skill + NeverLearned = 3, + LearnedOrAutomaticCharLevel = 4, }; enum ItemEnchantmentType diff --git a/src/server/game/DataStores/DBCStores.cpp b/src/server/game/DataStores/DBCStores.cpp index 4b3f988f69..fb11610b2a 100644 --- a/src/server/game/DataStores/DBCStores.cpp +++ b/src/server/game/DataStores/DBCStores.cpp @@ -648,7 +648,7 @@ void DBCManager::LoadStores(const std::string& dataPath, uint32 defaultLocale) if (skillLine->SkillLine != cFamily->SkillLine[0] && skillLine->SkillLine != cFamily->SkillLine[1]) continue; - if (skillLine->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN) + if (skillLine->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticCharLevel) continue; sPetFamilySpellsStore[cFamily->ID].insert(spellInfo->ID); diff --git a/src/server/game/DataStores/DBCStores.h b/src/server/game/DataStores/DBCStores.h index b70b56bd90..92d0b3ebd8 100644 --- a/src/server/game/DataStores/DBCStores.h +++ b/src/server/game/DataStores/DBCStores.h @@ -152,6 +152,7 @@ TC_GAME_API extern DBCStorage sResearchProjectSto TC_GAME_API extern DBCStorage sResearchSiteStore; TC_GAME_API extern DBCStorage sScalingStatDistributionStore; TC_GAME_API extern DBCStorage sScalingStatValuesStore; +TC_GAME_API extern DBCStorage sSkillRaceClassInfoStore; TC_GAME_API extern DBCStorage sSkillLineStore; TC_GAME_API extern DBCStorage sSkillLineAbilityStore; TC_GAME_API extern DBCStorage sSkillTiersStore; diff --git a/src/server/game/DataStores/DBCStructure.h b/src/server/game/DataStores/DBCStructure.h index 0ca165e984..1553160f59 100644 --- a/src/server/game/DataStores/DBCStructure.h +++ b/src/server/game/DataStores/DBCStructure.h @@ -1521,6 +1521,8 @@ struct SkillLineAbilityEntry uint32 TrivialSkillLineRankLow; // 11 uint32 NumSkillUps; // 12 uint32 UniqueBit; // 13 + + SkillLineAbilityAcquireMethod GetAcquireMethod() const { return static_cast(AcquireMethod); } }; struct SkillRaceClassInfoEntry @@ -1531,7 +1533,7 @@ struct SkillRaceClassInfoEntry uint32 ClassMask; // 3 uint32 Flags; // 4 uint32 Availability; // 5 - //uint32 MinLevel; // 6 + uint32 MinLevel; // 6 uint32 SkillTierID; // 7 //uint32 SkillCostIndex; // 8 }; @@ -1543,6 +1545,17 @@ struct SkillTiersEntry uint32 ID; // 0 //uint32 Cost[MAX_SKILL_STEP]; // 1-16 uint32 Value[MAX_SKILL_STEP]; // 17-32 + + uint32 GetValueForTierIndex(uint32 tierIndex) const + { + if (tierIndex >= MAX_SKILL_STEP) + tierIndex = MAX_SKILL_STEP - 1; + + while (Value[tierIndex] == 0 && tierIndex > 0) + --tierIndex; + + return Value[tierIndex]; + } }; struct SoundEntriesEntry diff --git a/src/server/game/DataStores/DBCfmt.h b/src/server/game/DataStores/DBCfmt.h index 74b96ea649..cf98bc43e6 100644 --- a/src/server/game/DataStores/DBCfmt.h +++ b/src/server/game/DataStores/DBCfmt.h @@ -138,7 +138,7 @@ char const ScalingStatDistributionfmt[] = "niiiiiiiiiiiiiiiiiiiiii"; char const ScalingStatValuesfmt[] = "iniiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"; char const SkillLinefmt[] = "nisxixi"; char const SkillLineAbilityfmt[] = "niiiixxiiiiiii"; -char const SkillRaceClassInfofmt[] = "diiiiixix"; +char const SkillRaceClassInfofmt[] = "diiiiiiix"; char const SkillTiersfmt[] = "nxxxxxxxxxxxxxxxxiiiiiiiiiiiiiiii"; char const SoundEntriesfmt[] = "nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; char const SpellCastTimefmt[] = "nixx"; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index abb9d68a01..b4c06db9d1 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -2952,13 +2952,44 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent // add dependent skills if this spell is not learned from adding skill already if (spellLearnSkill->skill != fromSkill) { - uint32 skill_value = GetPureSkillValue(spellLearnSkill->skill); - uint32 skill_max_value = GetPureMaxSkillValue(spellLearnSkill->skill); + uint16 skill_value = GetPureSkillValue(spellLearnSkill->skill); + uint16 skill_max_value = GetPureMaxSkillValue(spellLearnSkill->skill); if (skill_value < spellLearnSkill->value) skill_value = spellLearnSkill->value; - uint32 new_skill_max_value = spellLearnSkill->maxvalue == 0 ? GetMaxSkillValueForLevel() : spellLearnSkill->maxvalue; + uint16 new_skill_max_value = spellLearnSkill->maxvalue; + + if (new_skill_max_value == 0) + { + if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(spellLearnSkill->skill, getRace(), getClass())) + { + switch (GetSkillRangeType(rcInfo)) + { + case SKILL_RANGE_LANGUAGE: + skill_value = 300; + new_skill_max_value = 300; + break; + case SKILL_RANGE_LEVEL: + new_skill_max_value = GetMaxSkillValueForLevel(); + break; + case SKILL_RANGE_MONO: + new_skill_max_value = 1; + break; + case SKILL_RANGE_RANK: + { + SkillTiersEntry const* tier = sSkillTiersStore.AssertEntry(rcInfo->SkillTierID); + new_skill_max_value = tier->GetValueForTierIndex(spellLearnSkill->step - 1); + break; + } + default: + break; + } + + if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skill_value = new_skill_max_value; + } + } if (skill_max_value < new_skill_max_value) skill_max_value = new_skill_max_value; @@ -2978,10 +3009,10 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent if (pSkill->ID == fromSkill) continue; - ///@todo: confirm if rogues start with lockpicking skill at level 1 but only receive the spell to use it at level 16 - // Also added for runeforging. It's already confirmed this happens upon learning for Death Knights, not from character creation. - if ((_spell_idx->second->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN && !HasSkill(pSkill->ID)) || ((pSkill->ID == SKILL_LOCKPICKING || pSkill->ID == SKILL_RUNEFORGING) && _spell_idx->second->TrivialSkillLineRankHigh == 0)) - LearnDefaultSkill(pSkill->ID, 0); + // Runeforging special case + if ((_spell_idx->second->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticCharLevel && !HasSkill(_spell_idx->second->SkillLine)) || ((_spell_idx->second->SkillLine == SKILL_RUNEFORGING) && _spell_idx->second->TrivialSkillLineRankHigh == 0)) + if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(_spell_idx->second->SkillLine, getRace(), getClass())) + LearnDefaultSkill(rcInfo); } } @@ -3213,17 +3244,50 @@ void Player::RemoveSpell(uint32 spell_id, bool disabled, bool learn_low_rank) SetSkill(spellLearnSkill->skill, 0, 0, 0); else // set to prev. skill setting values { - uint32 skill_value = GetPureSkillValue(prevSkill->skill); - uint32 skill_max_value = GetPureMaxSkillValue(prevSkill->skill); + uint16 skill_value = GetPureSkillValue(prevSkill->skill); + uint16 skill_max_value = GetPureMaxSkillValue(prevSkill->skill); - if (skill_value > prevSkill->value) + uint16 new_skill_max_value = prevSkill->maxvalue; + + if (new_skill_max_value == 0) + { + if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(prevSkill->skill, getRace(), getClass())) + { + switch (GetSkillRangeType(rcInfo)) + { + case SKILL_RANGE_LANGUAGE: + skill_value = 300; + new_skill_max_value = 300; + break; + case SKILL_RANGE_LEVEL: + new_skill_max_value = GetMaxSkillValueForLevel(); + break; + case SKILL_RANGE_MONO: + new_skill_max_value = 1; + break; + case SKILL_RANGE_RANK: + { + SkillTiersEntry const* tier = sSkillTiersStore.AssertEntry(rcInfo->SkillTierID); + new_skill_max_value = tier->GetValueForTierIndex(prevSkill->step - 1); + break; + } + default: + break; + } + + if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skill_value = new_skill_max_value; + } + } + else if (skill_value > prevSkill->value) skill_value = prevSkill->value; - uint32 new_skill_max_value = prevSkill->maxvalue == 0 ? GetMaxSkillValueForLevel() : prevSkill->maxvalue; - if (skill_max_value > new_skill_max_value) skill_max_value = new_skill_max_value; + if (skill_value > new_skill_max_value) + skill_value = new_skill_max_value; + SetSkill(prevSkill->skill, prevSkill->step, skill_value, skill_max_value); } } @@ -5401,12 +5465,30 @@ void Player::InitializeSkillFields() // To "remove" a skill line, set it's values to zero void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal) { - if (!id) + SkillLineEntry const* skillEntry = sSkillLineStore.LookupEntry(id); + if (!skillEntry) + { + TC_LOG_ERROR("misc", "Player::SetSkill: Skill (SkillID: %u) not found in SkillLineStore for player '%s' (%s)", + id, GetName().c_str(), GetGUID().ToString().c_str()); return; + } uint16 currVal; SkillStatusMap::iterator itr = mSkillStatus.find(id); + auto refreshSkillBonusAuras = [&] + { + // Temporary bonuses + for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL)) + if (effect->GetMiscValue() == int32(id)) + effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true); + + // Permanent bonuses + for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL_TALENT)) + if (effect->GetMiscValue() == int32(id)) + effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true); + }; + // Handle already stored skills if (itr != mSkillStatus.end()) { @@ -5441,7 +5523,21 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal) if (itr->second.uState == SKILL_UNCHANGED) { if (currVal == 0) // activated skill, mark as new to save into database - itr->second.uState = SKILL_NEW; + { + itr->second.uState = itr->second.uState != SKILL_DELETED + ? SKILL_NEW + : SKILL_CHANGED; // skills marked as SKILL_DELETED already exist in database, mark as changed instead of new + + if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION) + { + if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1)) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1, id); + else if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1)) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1, id); + } + + refreshSkillBonusAuras(); + } else // updated skill, mark as changed to save into database itr->second.uState = SKILL_CHANGED; } @@ -5467,10 +5563,9 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal) SetSkillPermBonus(itr->second.pos, 0); // mark as deleted so the next save will delete the data from the database - if (itr->second.uState != SKILL_NEW) - itr->second.uState = SKILL_DELETED; - else - itr->second.uState = SKILL_UNCHANGED; + itr->second.uState = itr->second.uState != SKILL_NEW + ? SKILL_DELETED + : SKILL_UNCHANGED; // skills marked as SKILL_NEW don't exist in database (this distinction is not neccessary for deletion but for re-learning the same skill before save to db happens) // remove all spells that related to this skill if (std::vector const* skillLineAbilities = sDBCManager.GetSkillLineAbilitiesBySkill(id)) @@ -5509,13 +5604,6 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal) return; } - SkillLineEntry const* skillEntry = sSkillLineStore.LookupEntry(id); - if (!skillEntry) - { - TC_LOG_ERROR("misc", "Skill not found in SkillLineStore: skill #%u", id); - return; - } - if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION) { if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1)) @@ -5544,15 +5632,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal) UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id); - // temporary bonuses - for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL)) - if (effect->GetMiscValue() == int32(id)) - effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true); - - // permanent bonuses - for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL_TALENT)) - if (effect->GetMiscValue() == int32(id)) - effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true); + refreshSkillBonusAuras(); // archaeology skill learned if (id == SKILL_ARCHAEOLOGY) @@ -23128,21 +23208,20 @@ void Player::LearnDefaultSkills() ASSERT(info); for (PlayerCreateInfoSkills::const_iterator itr = info->skills.begin(); itr != info->skills.end(); ++itr) { - uint32 skillId = itr->SkillId; - if (HasSkill(skillId)) + SkillRaceClassInfoEntry const* rcInfo = *itr; + if (HasSkill(rcInfo->SkillID)) continue; - LearnDefaultSkill(skillId, itr->Rank); + if (rcInfo->MinLevel > getLevel()) + continue; + + LearnDefaultSkill(rcInfo); } } -void Player::LearnDefaultSkill(uint32 skillId, uint16 rank) +void Player::LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo) { - SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(skillId, getRace(), getClass()); - if (!rcInfo) - return; - - TC_LOG_DEBUG("entities.player.loading", "PLAYER (Class: %u Race: %u): Adding initial skill, id = %u", uint32(getClass()), uint32(getRace()), skillId); + uint16 skillId = rcInfo->SkillID; switch (GetSkillRangeType(rcInfo)) { case SKILL_RANGE_LANGUAGE: @@ -23169,16 +23248,13 @@ void Player::LearnDefaultSkill(uint32 skillId, uint16 rank) break; case SKILL_RANGE_RANK: { - if (!rank) - break; - SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID); - uint16 maxValue = tier->Value[std::max(rank - 1, 0)]; + uint16 maxValue = tier->GetValueForTierIndex(0); uint16 skillValue = 1; if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) skillValue = maxValue; else if (getClass() == CLASS_DEATH_KNIGHT) - skillValue = std::min(std::max({ uint16(1), uint16((getLevel() - 1) * 5) }), maxValue); + skillValue = std::min(std::max(uint16(1), uint16((getLevel() - 1) * 5)), maxValue); SetSkill(skillId, 1, skillValue, maxValue); break; @@ -23226,13 +23302,19 @@ void Player::LearnQuestRewardedSpells(Quest const* quest) uint32 learned_0 = spellInfo->Effects[0].TriggerSpell; if (!HasSpell(learned_0)) { - SpellInfo const* learnedInfo = sSpellMgr->GetSpellInfo(learned_0); - if (!learnedInfo) - return; + found = false; + SkillLineAbilityMapBounds skills = sSpellMgr->GetSkillLineAbilityMapBounds(learned_0); + for (auto skillItr = skills.first; skillItr != skills.second; ++skillItr) + { + if (skillItr->second->GetAcquireMethod() == SkillLineAbilityAcquireMethod::LearnedOrAutomaticCharLevel) + { + found = true; + break; + } + } - // profession specialization can be re-learned from npc - if (learnedInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL && learnedInfo->Effects[1].Effect == 0 && !learnedInfo->SpellLevel) - return; + if (!found) + return; } CastSpell(this, spell_id, true); @@ -23265,19 +23347,15 @@ void Player::LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue) if (!spellInfo) continue; - switch (ability->AcquireMethod) + switch (ability->GetAcquireMethod()) { - case SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE: - case SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN: + case SkillLineAbilityAcquireMethod::AutomaticSkillRank: + case SkillLineAbilityAcquireMethod::AutomaticCharLevel: break; default: continue; } - // AcquireMethod == 2 && NumSkillUps == 1 --> automatically learn riding skill spell, else we skip it (client shows riding in spellbook as trainable). - if (skillId == SKILL_RIDING && (ability->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN || ability->NumSkillUps != 1)) - continue; - // Check race if set if (ability->RaceMask && !(ability->RaceMask & raceMask)) continue; @@ -23286,12 +23364,13 @@ void Player::LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue) if (ability->ClassMask && !(ability->ClassMask & classMask)) continue; - // check level, skip class spells if not high enough - if (getLevel() < spellInfo->SpellLevel) + // Check level, skip class spells if not high enough + uint32 requiredLevel = std::max(spellInfo->SpellLevel, spellInfo->BaseLevel); + if (requiredLevel > getLevel()) continue; // need unlearn spell - if (skillValue < ability->MinSkillLineRank && ability->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE) + if (int32(skillValue) < ability->MinSkillLineRank && ability->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticSkillRank) RemoveSpell(ability->Spell); // need learn diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 737d826dde..453d433953 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -52,6 +52,7 @@ struct PlayerTalent; struct PlayerTalentInfo; struct ScalingStatDistributionEntry; struct ScalingStatValuesEntry; +struct SkillRaceClassInfoEntry; struct TrainerSpell; struct VendorItem; struct GuildMemberProfessionData; @@ -1549,7 +1550,7 @@ class TC_GAME_API Player : public Unit, public GridObject void ResetSpells(bool myClassOnly = false); void LearnCustomSpells(); void LearnDefaultSkills(); - void LearnDefaultSkill(uint32 skillId, uint16 rank); + void LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo); void LearnQuestRewardedSpells(); void LearnQuestRewardedSpells(Quest const* quest); void LearnSpellHighestRank(uint32 spellid); diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 421ee22a6f..bc45979482 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -247,9 +247,6 @@ ObjectMgr::ObjectMgr(): _gameObjectSpawnId(1), DBCLocaleIndex(LOCALE_enUS) { - for (uint8 i = 0; i < MAX_CLASSES; ++i) - for (uint8 j = 0; j < MAX_RACES; ++j) - _playerInfo[j][i] = nullptr; } ObjectMgr* ObjectMgr::instance() @@ -263,16 +260,6 @@ ObjectMgr::~ObjectMgr() for (PetLevelInfoContainer::iterator i = _petInfoStore.begin(); i != _petInfoStore.end(); ++i) delete[] i->second; - for (int race = 0; race < MAX_RACES; ++race) - { - for (int class_ = 0; class_ < MAX_CLASSES; ++class_) - { - if (_playerInfo[race][class_]) - delete[] _playerInfo[race][class_]->levelInfo; - delete _playerInfo[race][class_]; - } - } - for (CacheVendorItemContainer::iterator itr = _cacheVendorItemStore.begin(); itr != _cacheVendorItemStore.end(); ++itr) itr->second.Clear(); @@ -3321,11 +3308,12 @@ PetLevelInfo const* ObjectMgr::GetPetLevelInfo(uint32 creature_id, uint8 level) void ObjectMgr::PlayerCreateInfoAddItemHelper(uint32 race_, uint32 class_, uint32 itemId, int32 count) { - if (!_playerInfo[race_][class_]) + PlayerInfo* playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race_), Classes(class_) }); + if (!playerInfo) return; if (count > 0) - _playerInfo[race_][class_]->item.push_back(PlayerCreateInfoItem(itemId, count)); + playerInfo->item.emplace_back(itemId, count); else { if (count < -1) @@ -3421,7 +3409,7 @@ void ObjectMgr::LoadPlayerInfo() continue; } - PlayerInfo* info = new PlayerInfo(); + std::unique_ptr info = std::make_unique(); info->mapId = mapId; info->areaId = areaId; info->positionX = positionX; @@ -3430,7 +3418,7 @@ void ObjectMgr::LoadPlayerInfo() info->orientation = orientation; info->displayId_m = rEntry->MaleDisplayID; info->displayId_f = rEntry->FemaleDisplayID; - _playerInfo[current_race][current_class] = info; + _playerInfo[{ Races(current_race), Classes(current_class) }] = std::move(info); ++count; } @@ -3516,77 +3504,19 @@ void ObjectMgr::LoadPlayerInfo() { uint32 oldMSTime = getMSTime(); - QueryResult result = WorldDatabase.PQuery("SELECT raceMask, classMask, skill, `rank` FROM playercreateinfo_skills"); - - if (!result) - { - TC_LOG_ERROR("server.loading", ">> Loaded 0 player create skills. DB table `playercreateinfo_skills` is empty."); - } - else - { - uint32 count = 0; - - do - { - Field* fields = result->Fetch(); - uint32 raceMask = fields[0].GetUInt32(); - uint32 classMask = fields[1].GetUInt32(); - PlayerCreateInfoSkill skill; - skill.SkillId = fields[2].GetUInt16(); - skill.Rank = fields[3].GetUInt16(); - - if (skill.Rank >= MAX_SKILL_STEP) - { - TC_LOG_ERROR("sql.sql", "Skill rank value %hu set for skill %hu raceMask %u classMask %u is too high, max allowed value is %d", skill.Rank, skill.SkillId, raceMask, classMask, MAX_SKILL_STEP); - continue; - } - - if (raceMask != 0 && !(raceMask & RACEMASK_ALL_PLAYABLE)) - { - TC_LOG_ERROR("sql.sql", "Wrong race mask %u in `playercreateinfo_skills` table, ignoring.", raceMask); - continue; - } - - if (classMask != 0 && !(classMask & CLASSMASK_ALL_PLAYABLE)) - { - TC_LOG_ERROR("sql.sql", "Wrong class mask %u in `playercreateinfo_skills` table, ignoring.", classMask); - continue; - } - - if (!sSkillLineStore.LookupEntry(skill.SkillId)) - { - TC_LOG_ERROR("sql.sql", "Wrong skill id %u in `playercreateinfo_skills` table, ignoring.", skill.SkillId); - continue; - } - + for (SkillRaceClassInfoEntry const* rcInfo : sSkillRaceClassInfoStore) + if (rcInfo->Availability == 1) for (uint32 raceIndex = RACE_HUMAN; raceIndex < MAX_RACES; ++raceIndex) - { - if (raceMask == 0 || ((1 << (raceIndex - 1)) & raceMask)) - { + if (rcInfo->RaceMask == 0 || (rcInfo->RaceMask & (1 << (raceIndex - 1)) != 0)) for (uint32 classIndex = CLASS_WARRIOR; classIndex < MAX_CLASSES; ++classIndex) - { - if (classMask == 0 || ((1 << (classIndex - 1)) & classMask)) - { - if (!sDBCManager.GetSkillRaceClassInfo(skill.SkillId, raceIndex, classIndex)) - continue; + if (rcInfo->ClassMask == -1 || rcInfo->ClassMask == 0 || ((1 << (classIndex - 1)) & rcInfo->ClassMask)) + if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(raceIndex), Classes(classIndex) })) + playerInfo->skills.push_back(rcInfo); - if (PlayerInfo* info = _playerInfo[raceIndex][classIndex]) - { - info->skills.push_back(skill); - ++count; - } - } - } - } - } - } while (result->NextRow()); - - TC_LOG_INFO("server.loading", ">> Loaded %u player create skills in %u ms", count, GetMSTimeDiffToNow(oldMSTime)); - } + TC_LOG_INFO("server.loading", ">> Loaded player create skills in %u ms", GetMSTimeDiffToNow(oldMSTime)); } - // Load playercreate spells - TC_LOG_INFO("server.loading", "Loading Player Create Spell Data..."); + TC_LOG_INFO("server.loading", "Loading Player Create Custom Spell Data..."); { uint32 oldMSTime = getMSTime(); @@ -3627,9 +3557,9 @@ void ObjectMgr::LoadPlayerInfo() { if (classMask == 0 || ((1 << (classIndex - 1)) & classMask)) { - if (PlayerInfo* info = _playerInfo[raceIndex][classIndex]) + if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(raceIndex), Classes(classIndex) })) { - info->customSpells.push_back(spellId); + playerInfo->customSpells.push_back(spellId); ++count; } // We need something better here, the check is not accounting for spells used by multiple races/classes but not all of them. @@ -3687,9 +3617,9 @@ void ObjectMgr::LoadPlayerInfo() { if (classMask == 0 || ((1 << (classIndex - 1)) & classMask)) { - if (PlayerInfo* info = _playerInfo[raceIndex][classIndex]) + if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(raceIndex), Classes(classIndex) })) { - info->castSpells.push_back(spellId); + playerInfo->castSpells.push_back(spellId); ++count; } } @@ -3736,8 +3666,8 @@ void ObjectMgr::LoadPlayerInfo() continue; } - if (PlayerInfo* info = _playerInfo[current_race][current_class]) - info->action.push_back(PlayerCreateInfoAction(fields[2].GetUInt16(), fields[3].GetUInt32(), fields[4].GetUInt16())); + if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(current_race), Classes(current_class) })) + playerInfo->action.push_back(PlayerCreateInfoAction(fields[2].GetUInt16(), fields[3].GetUInt32(), fields[4].GetUInt16())); ++count; } @@ -3822,12 +3752,12 @@ void ObjectMgr::LoadPlayerInfo() for (std::size_t race = 0; race < raceStatModifiers.size(); ++race) { - if (PlayerInfo* info = _playerInfo[race][current_class]) + if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(current_class) })) { - if (!info->levelInfo) - info->levelInfo = new PlayerLevelInfo[sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)]; + if (!playerInfo->levelInfo) + playerInfo->levelInfo = std::make_unique(sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)); - PlayerLevelInfo& levelInfo = info->levelInfo[current_level - 1]; + PlayerLevelInfo& levelInfo = playerInfo->levelInfo[current_level - 1]; for (uint8 i = 0; i < MAX_STATS; i++) levelInfo.stats[i] = fields[i + 2].GetUInt16() + raceStatModifiers[race].StatModifier[i]; } @@ -3850,8 +3780,8 @@ void ObjectMgr::LoadPlayerInfo() if (!sChrClassesStore.LookupEntry(class_)) continue; - PlayerInfo* info = _playerInfo[race][class_]; - if (!info) + auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(class_) }); + if (!playerInfo) continue; // skip expansion races if not playing with expansion @@ -3867,7 +3797,7 @@ void ObjectMgr::LoadPlayerInfo() continue; // fatal error if no level 1 data - if (!info->levelInfo || info->levelInfo[0].stats[0] == 0) + if (!playerInfo->levelInfo || playerInfo->levelInfo[0].stats[0] == 0) { TC_LOG_ERROR("sql.sql", "Race %i Class %i Level 1 does not have stats data!", race, class_); exit(1); @@ -3876,10 +3806,10 @@ void ObjectMgr::LoadPlayerInfo() // fill level gaps for (uint8 level = 1; level < sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); ++level) { - if (info->levelInfo[level].stats[0] == 0) + if (playerInfo->levelInfo[level].stats[0] == 0) { TC_LOG_ERROR("sql.sql", "Race %i Class %i Level %i does not have stats data. Using stats data of level %i.", race, class_, level + 1, level); - info->levelInfo[level] = info->levelInfo[level - 1]; + playerInfo->levelInfo[level] = playerInfo->levelInfo[level - 1]; } } } @@ -3972,7 +3902,7 @@ void ObjectMgr::GetPlayerLevelInfo(uint32 race, uint32 class_, uint8 level, Play if (level < 1 || race >= MAX_RACES || class_ >= MAX_CLASSES) return; - PlayerInfo const* pInfo = _playerInfo[race][class_]; + PlayerInfo const* pInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, {Races(race), Classes(class_)}); if (!pInfo) return; @@ -3985,7 +3915,7 @@ void ObjectMgr::GetPlayerLevelInfo(uint32 race, uint32 class_, uint8 level, Play void ObjectMgr::BuildPlayerLevelInfo(uint8 race, uint8 _class, uint8 level, PlayerLevelInfo* info) const { // base data (last known level) - *info = _playerInfo[race][_class]->levelInfo[sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)-1]; + *info = ASSERT_NOTNULL(Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(_class) }))->levelInfo[sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL) - 1]; // if conversion from uint32 to uint8 causes unexpected behaviour, change lvl to uint32 for (uint8 lvl = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)-1; lvl < level; ++lvl) @@ -10111,10 +10041,7 @@ PlayerInfo const* ObjectMgr::GetPlayerInfo(uint32 race, uint32 class_) const return nullptr; if (class_ >= MAX_CLASSES) return nullptr; - PlayerInfo const* info = _playerInfo[race][class_]; - if (!info) - return nullptr; - return info; + return Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(class_) }); } CreatureStaticFlagsOverride const* ObjectMgr::GetCreatureStaticFlagsOverride(ObjectGuid::LowType spawnId, Difficulty difficultyId) const diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index b28f4184af..2780f3ddc1 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -688,15 +688,7 @@ struct PlayerCreateInfoAction typedef std::vector PlayerCreateInfoActions; -struct PlayerCreateInfoSkill -{ - PlayerCreateInfoSkill() : SkillId(0), Rank(0) { } - - uint16 SkillId; - uint16 Rank; -}; - -typedef std::vector PlayerCreateInfoSkills; +typedef std::vector PlayerCreateInfoSkills; // existence checked by displayId != 0 struct PlayerInfo @@ -716,7 +708,7 @@ struct PlayerInfo PlayerCreateInfoSkills skills; //[level-1] 0..MaxPlayerLevel-1 - PlayerLevelInfo* levelInfo = nullptr; + std::unique_ptr levelInfo; }; @@ -1734,7 +1726,7 @@ class TC_GAME_API ObjectMgr void BuildPlayerLevelInfo(uint8 race, uint8 class_, uint8 level, PlayerLevelInfo* plinfo) const; - PlayerInfo* _playerInfo[MAX_RACES][MAX_CLASSES]; + std::unordered_map, std::unique_ptr> _playerInfo; typedef std::vector PlayerXPperLevel; // [level] PlayerXPperLevel _playerXPperLevel; diff --git a/src/server/game/Guilds/GuildMgr.cpp b/src/server/game/Guilds/GuildMgr.cpp index ef4f850ff2..13085cef67 100644 --- a/src/server/game/Guilds/GuildMgr.cpp +++ b/src/server/game/Guilds/GuildMgr.cpp @@ -592,7 +592,7 @@ void GuildMgr::LoadGuildProfessionData() GuildProfession prof; prof.SpellId = entry->Spell; prof.UniqueBits = entry->UniqueBit; - prof.AutoLearn = entry->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE; + prof.AutoLearn = entry->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticSkillRank; GuildProfessionStore[skill->ID].push_back(prof); } } diff --git a/src/server/game/Miscellaneous/SharedDefines.h b/src/server/game/Miscellaneous/SharedDefines.h index ae663592a4..e7a687c8df 100644 --- a/src/server/game/Miscellaneous/SharedDefines.h +++ b/src/server/game/Miscellaneous/SharedDefines.h @@ -693,7 +693,7 @@ enum SpellAttr9 SPELL_ATTR9_UNK3 = 0x00000008, // 3 SPELL_ATTR9_SPECIAL_DELAY_CALCULATION = 0x00000010, // 4 SPELL_ATTR9_SUMMON_PLAYER_TOTEM = 0x00000020, // 5 - SPELL_ATTR9_UNK6 = 0x00000040, // 6 + SPELL_ATTR9_ITEM_CAST_GRANTS_SKILL_GAIN = 0x00000040, // 6 Item Cast Grants Skill Gain SPELL_ATTR9_UNK7 = 0x00000080, // 7 SPELL_ATTR9_AIMED_SHOT = 0x00000100, // 8 SPELL_ATTR9_NOT_USABLE_IN_ARENA = 0x00000200, // 9 Cannot be used in arenas diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index ebac673710..8df1f37c8f 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -3732,6 +3732,11 @@ void Spell::_cast(bool skipCheck) player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL, m_spellInfo->Id); } + // Spells that don't create items can have this attribute - handle here + if (m_CastItem && m_spellInfo->HasAttribute(SPELL_ATTR9_ITEM_CAST_GRANTS_SKILL_GAIN)) + if (Player* playerCaster = m_caster->ToPlayer()) + playerCaster->UpdateCraftSkill(m_spellInfo); + if (!(_triggeredCastFlags & TRIGGERED_IGNORE_POWER_AND_REAGENT_COST)) { // Powers have to be taken before SendSpellGo diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp index 4e1c6770dc..aab87cab9d 100644 --- a/src/server/game/Spells/SpellEffects.cpp +++ b/src/server/game/Spells/SpellEffects.cpp @@ -1401,7 +1401,8 @@ void Spell::DoCreateItem(uint32 /*i*/, uint32 itemtype) // we succeeded in creating at least one item, so a levelup is possible if (bgType == 0) - player->UpdateCraftSkill(m_spellInfo); + if (!m_CastItem) + player->UpdateCraftSkill(m_spellInfo); } /* @@ -1437,7 +1438,8 @@ void Spell::EffectCreateItem2(SpellEffIndex effIndex) if (m_spellInfo->IsLootCrafting()) { player->AutoStoreLoot(m_spellInfo->Id, LootTemplates_Spell, false); - player->UpdateCraftSkill(m_spellInfo); + if (!m_CastItem) + player->UpdateCraftSkill(m_spellInfo); } else // If there's no random loot entries for this spell, pick the item associated with this spell { @@ -2274,14 +2276,15 @@ void Spell::EffectLearnSkill(SpellEffIndex effIndex) if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET) return; - if (unitTarget->GetTypeId() != TYPEID_PLAYER) + Player* playerTarget = Object::ToPlayer(unitTarget); + if (!playerTarget) return; if (damage < 0) return; uint32 skillid = m_spellInfo->Effects[effIndex].MiscValue; - SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skillid, unitTarget->getRace(), unitTarget->getClass()); + SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skillid, playerTarget->getRace(), playerTarget->getClass()); if (!rcEntry) return; @@ -2289,8 +2292,13 @@ void Spell::EffectLearnSkill(SpellEffIndex effIndex) if (!tier) return; - uint16 skillval = unitTarget->ToPlayer()->GetPureSkillValue(skillid); - unitTarget->ToPlayer()->SetSkill(skillid, m_spellInfo->Effects[effIndex].CalcValue(), std::max(skillval, 1), tier->Value[damage - 1]); + uint16 skillval = std::max(1, playerTarget->GetPureSkillValue(skillid)); + uint16 maxSkillVal = tier->Value[damage - 1]; + + if (rcEntry->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skillval = maxSkillVal; + + playerTarget->SetSkill(skillid, damage, skillval, maxSkillVal); } void Spell::EffectPlayMovie(SpellEffIndex effIndex) @@ -2347,7 +2355,7 @@ void Spell::EffectEnchantItemPerm(SpellEffIndex effIndex) else { // do not increase skill if vellum used - if (!(m_CastItem && m_CastItem->GetTemplate()->GetFlags() & ITEM_FLAG_NO_REAGENT_COST)) + if (!m_CastItem) player->UpdateCraftSkill(m_spellInfo); uint32 enchant_id = m_spellInfo->Effects[effIndex].MiscValue; @@ -3932,7 +3940,9 @@ void Spell::EffectDisEnchant(SpellEffIndex /*effIndex*/) if (Player* caster = m_caster->ToPlayer()) { - caster->UpdateCraftSkill(m_spellInfo); + if (!m_CastItem) + caster->UpdateCraftSkill(m_spellInfo); + caster->SendLoot(itemTarget->GetGUID(), LOOT_DISENCHANTING); } @@ -4988,12 +4998,37 @@ void Spell::EffectMilling(SpellEffIndex /*effIndex*/) player->SendLoot(itemTarget->GetGUID(), LOOT_MILLING); } -void Spell::EffectSkill(SpellEffIndex /*effIndex*/) +void Spell::EffectSkill(SpellEffIndex effIndex) { if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT) return; - TC_LOG_DEBUG("spells", "WORLD: SkillEFFECT"); + Player* playerTarget = Object::ToPlayer(GetCaster()); + if (!playerTarget) + return; + + if (damage < 1) + return; + + uint32 skillid = m_spellInfo->Effects[effIndex].MiscValue; + if (playerTarget->GetSkillStep(skillid) >= damage) + return; + + SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skillid, playerTarget->getRace(), playerTarget->getClass()); + if (!rcEntry) + return; + + SkillTiersEntry const* tier =sSkillTiersStore.AssertEntry(rcEntry->SkillTierID); + if (!tier) + return; + + uint16 skillval = std::max(1, playerTarget->GetPureSkillValue(skillid)); + uint16 maxSkillVal = tier->Value[damage - 1]; + + if (rcEntry->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skillval = maxSkillVal; + + playerTarget->SetSkill(skillid, damage, skillval, maxSkillVal); } /* There is currently no need for this effect. We handle it in Battleground.cpp diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index ef63512d1b..b4879738d7 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -1264,7 +1264,7 @@ bool SpellInfo::IsAbilityLearnedWithProfession() const for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) { SkillLineAbilityEntry const* pAbility = _spell_idx->second; - if (!pAbility || pAbility->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE) + if (!pAbility || pAbility->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticSkillRank) continue; if (pAbility->MinSkillLineRank > 0) diff --git a/src/server/game/Spells/SpellMgr.cpp b/src/server/game/Spells/SpellMgr.cpp index 29a078801f..3e355194eb 100644 --- a/src/server/game/Spells/SpellMgr.cpp +++ b/src/server/game/Spells/SpellMgr.cpp @@ -1024,11 +1024,8 @@ void SpellMgr::LoadSpellLearnSkills() case SPELL_EFFECT_SKILL: dbc_node.skill = entry->Effects[i].MiscValue; dbc_node.step = entry->Effects[i].CalcValue(); - if (dbc_node.skill != SKILL_RIDING) - dbc_node.value = 1; - else - dbc_node.value = dbc_node.step * 75; - dbc_node.maxvalue = dbc_node.step * 75; + dbc_node.value = 0; + dbc_node.maxvalue = 0; break; case SPELL_EFFECT_DUAL_WIELD: dbc_node.skill = SKILL_DUAL_WIELD; @@ -2164,7 +2161,7 @@ void SpellMgr::LoadPetLevelupSpellMap() if (skillLine->SkillLine != creatureFamily->SkillLine[j]) continue; - if (skillLine->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN) + if (skillLine->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticCharLevel) continue; SpellInfo const* spell = GetSpellInfo(skillLine->Spell);