Merge 21b2ae3aed0660e10e59ffb3617cf025f33f49c4 into 5bef92d5eaca3e2ecc317f9d599312bc23eb71aa

This commit is contained in:
sogladev 2025-11-10 10:07:42 +08:00 committed by GitHub
commit aed4d2cc5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 8916 additions and 605 deletions

View File

@ -0,0 +1,49 @@
--
DROP TABLE IF EXISTS `trainer`;
CREATE TABLE `trainer` (
`Id` INT UNSIGNED DEFAULT 0 NOT NULL,
`Type` TINYINT UNSIGNED DEFAULT 2 NOT NULL,
`Requirement` MEDIUMINT UNSIGNED DEFAULT 0 NOT NULL,
`Greeting` MEDIUMTEXT NULL,
`VerifiedBuild` INT DEFAULT 0 NULL,
CONSTRAINT trainer_pk PRIMARY KEY (Id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DROP TABLE IF EXISTS `trainer_locale`;
CREATE TABLE `trainer_locale` (
`Id` INT UNSIGNED NOT NULL DEFAULT 0,
`locale` varchar(4) NOT NULL,
`Greeting_lang` MEDIUMTEXT NULL,
`VerifiedBuild` INT DEFAULT 0 NULL,
CONSTRAINT `trainer_locale_pk` PRIMARY KEY (`Id`,`locale`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DROP TABLE IF EXISTS `trainer_spell`;
CREATE TABLE `trainer_spell` (
`TrainerId` int unsigned DEFAULT 0 NOT NULL,
`SpellId` int unsigned DEFAULT 0 NOT NULL,
`MoneyCost` int unsigned DEFAULT 0 NOT NULL,
`ReqSkillLine` int unsigned DEFAULT 0 NOT NULL,
`ReqSkillRank` int unsigned DEFAULT 0 NOT NULL,
`ReqAbility1` int unsigned DEFAULT 0 NOT NULL,
`ReqAbility2` int unsigned DEFAULT 0 NOT NULL,
`ReqAbility3` int unsigned DEFAULT 0 NOT NULL,
`ReqLevel` tinyint unsigned DEFAULT 0 NOT NULL,
`VerifiedBuild` int DEFAULT 0 NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DROP TABLE IF EXISTS `creature_default_trainer`;
CREATE TABLE `creature_default_trainer` (
`CreatureId` int unsigned NOT NULL,
`TrainerId` int unsigned DEFAULT 0 NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Drop unused table
DROP TABLE IF EXISTS `npc_trainer`;
-- Drop removed columns
ALTER TABLE `creature_template`
DROP `trainer_type`,
DROP `trainer_spell`,
DROP `trainer_class`,
DROP `trainer_race`;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
--
-- Truesilver gauntlets require skill 225, not 245
UPDATE `trainer_spell` SET `ReqSkillRank`=225 WHERE `TrainerId`=124 AND `SpellId`=9954;
-- Woo Ping polearm training requires level 20
UPDATE `trainer_spell` SET `ReqLevel`=20 WHERE `TrainerId`=53 AND `SpellId`=200;
-- Guvan should train priest spells past level 6
UPDATE `creature_default_trainer` SET `TrainerId`=11 WHERE `CreatureId`=16502;
-- Portal to Theramore and Stonard should be trainable at 35
UPDATE `trainer_spell` SET `ReqLevel`=35 WHERE `TrainerId` IN (28, 29) AND `SpellId` IN (49361, 49360);
-- Add missing ReqAbility1 for some Gnomish and Goblin Engineering spells
UPDATE `trainer_spell` SET `ReqAbility1`=20219 WHERE `SpellId` IN (12759, 12895, 12897, 12899, 12902, 12903, 12905, 12906, 12907, 30568, 30570);
UPDATE `trainer_spell` SET `ReqAbility1`=20222 WHERE `SpellId` IN (8895, 12715, 12717, 12718, 12754, 12755, 12758, 12760, 12908, 30558, 30560);
-- Shattrath Bookshelves should not train Northrend recipes
UPDATE `creature_default_trainer` SET `TrainerId`=58 WHERE `CreatureId`=33609;
UPDATE `creature_default_trainer` SET `TrainerId`=66 WHERE `CreatureId`=33608;
UPDATE `creature_default_trainer` SET `TrainerId`=70 WHERE `CreatureId`=33616;
UPDATE `creature_default_trainer` SET `TrainerId`=112 WHERE `CreatureId`=33614;
UPDATE `creature_default_trainer` SET `TrainerId`=79 WHERE `CreatureId`=33617;
UPDATE `creature_default_trainer` SET `TrainerId`=120 WHERE `CreatureId`=33615;
UPDATE `creature_default_trainer` SET `TrainerId`=95 WHERE `CreatureId`=33610;
UPDATE `creature_default_trainer` SET `TrainerId`=91 WHERE `CreatureId`=33612;
UPDATE `creature_default_trainer` SET `TrainerId`=62 WHERE `CreatureId`=33611;
UPDATE `creature_default_trainer` SET `TrainerId`=73 WHERE `CreatureId`=33613;
UPDATE `creature_default_trainer` SET `TrainerId`=101 WHERE `CreatureId`=33618;
UPDATE `creature_default_trainer` SET `TrainerId`=77 WHERE `CreatureId`=33619;
UPDATE `creature_default_trainer` SET `TrainerId`=99 WHERE `CreatureId`=33623;
UPDATE `creature_default_trainer` SET `TrainerId`=83 WHERE `CreatureId`=33621;
-- Hamanar Shattrath Jewelcrafting trainer should not be able to learn grand master jewelcrafter.
UPDATE `creature_default_trainer` SET `TrainerId`=112 WHERE `CreatureId`=19063;
-- make Flying Carpet available from TBC Tailoring trainers
DELETE FROM `trainer_spell` WHERE `TrainerId`=73 AND `SpellId` = 60969;
INSERT INTO `trainer_spell` (`TrainerId`, `SpellId`, `MoneyCost`, `ReqSkillLine`, `ReqSkillRank`, `ReqAbility1`, `ReqAbility2`, `ReqAbility3`, `ReqLevel`, `VerifiedBuild`) VALUES
(73, 60969, 105000, 197, 300, 34090, 0, 0, 0, 0);

View File

@ -77,7 +77,7 @@ void WorldDatabaseConnection::DoPrepareStatements()
PrepareStatement(WORLD_SEL_WAYPOINT_SCRIPT_ID_BY_GUID, "SELECT id FROM waypoint_scripts WHERE guid = ?", CONNECTION_SYNCH);
PrepareStatement(WORLD_DEL_CREATURE, "DELETE FROM creature WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(WORLD_SEL_COMMANDS, "SELECT name, security, help FROM command", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_CREATURE_TEMPLATE, "SELECT entry, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, name, subname, IconName, gossip_menu_id, minlevel, maxlevel, exp, faction, npcflag, speed_walk, speed_run, speed_swim, speed_flight, detection_range, scale, `rank`, dmgschool, DamageModifier, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, unit_class, unit_flags, unit_flags2, dynamicflags, family, trainer_type, trainer_spell, trainer_class, trainer_race, type, type_flags, lootid, pickpocketloot, skinloot, PetSpellDataId, VehicleId, mingold, maxgold, AIName, MovementType, ctm.Ground, ctm.Swim, ctm.Flight, ctm.Rooted, ctm.Chase, ctm.Random, ctm.InteractionPauseTimer, HoverHeight, HealthModifier, ManaModifier, ArmorModifier, ExperienceModifier, RacialLeader, movementId, RegenHealth, mechanic_immune_mask, spell_school_immune_mask, flags_extra, ScriptName FROM creature_template ct LEFT JOIN creature_template_movement ctm ON ct.entry = ctm.CreatureId WHERE entry = ?", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_CREATURE_TEMPLATE, "SELECT entry, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, name, subname, IconName, gossip_menu_id, minlevel, maxlevel, exp, faction, npcflag, speed_walk, speed_run, speed_swim, speed_flight, detection_range, scale, `rank`, dmgschool, DamageModifier, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, unit_class, unit_flags, unit_flags2, dynamicflags, family, type, type_flags, lootid, pickpocketloot, skinloot, PetSpellDataId, VehicleId, mingold, maxgold, AIName, MovementType, ctm.Ground, ctm.Swim, ctm.Flight, ctm.Rooted, ctm.Chase, ctm.Random, ctm.InteractionPauseTimer, HoverHeight, HealthModifier, ManaModifier, ArmorModifier, ExperienceModifier, RacialLeader, movementId, RegenHealth, mechanic_immune_mask, spell_school_immune_mask, flags_extra, ScriptName FROM creature_template ct LEFT JOIN creature_template_movement ctm ON ct.entry = ctm.CreatureId WHERE entry = ?", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_WAYPOINT_SCRIPT_BY_ID, "SELECT guid, delay, command, datalong, datalong2, dataint, x, y, z, o FROM waypoint_scripts WHERE id = ?", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_ITEM_TEMPLATE_BY_NAME, "SELECT entry FROM item_template WHERE name = ?", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_CREATURE_BY_ID, "SELECT guid FROM creature WHERE id1 = ? OR id2 = ? OR id3 = ?", CONNECTION_SYNCH);

View File

@ -80,15 +80,6 @@ std::string CreatureMovementData::ToString() const
return str.str();
}
TrainerSpell const* TrainerSpellData::Find(uint32 spell_id) const
{
TrainerSpellMap::const_iterator itr = spellList.find(spell_id);
if (itr != spellList.end())
return &itr->second;
return nullptr;
}
bool VendorItemData::RemoveItem(uint32 item_id)
{
bool found = false;
@ -1268,52 +1259,13 @@ bool Creature::isCanInteractWithBattleMaster(Player* player, bool msg) const
return true;
}
bool Creature::isCanTrainingAndResetTalentsOf(Player* player) const
bool Creature::CanResetTalents(Player* player) const
{
return player->GetLevel() >= 10
&& GetCreatureTemplate()->trainer_type == TRAINER_TYPE_CLASS
&& player->IsClass((Classes)GetCreatureTemplate()->trainer_class, CLASS_CONTEXT_CLASS_TRAINER);
}
bool Creature::IsValidTrainerForPlayer(Player* player, uint32* npcFlags /*= nullptr*/) const
{
if (!IsTrainer())
{
Trainer::Trainer const* trainer = sObjectMgr->GetTrainer(GetEntry());
if (!trainer)
return false;
}
switch (m_creatureInfo->trainer_type)
{
case TRAINER_TYPE_CLASS:
case TRAINER_TYPE_PETS:
if (m_creatureInfo->trainer_class && !player->IsClass((Classes)m_creatureInfo->trainer_class, CLASS_CONTEXT_CLASS_TRAINER))
{
if (npcFlags)
*npcFlags &= ~UNIT_NPC_FLAG_TRAINER_CLASS;
return false;
}
break;
case TRAINER_TYPE_MOUNTS:
if (m_creatureInfo->trainer_race && m_creatureInfo->trainer_race != player->getRace())
{
return false;
}
break;
case TRAINER_TYPE_TRADESKILLS:
if (m_creatureInfo->trainer_spell && !player->HasSpell(m_creatureInfo->trainer_spell))
{
if (npcFlags)
*npcFlags &= ~UNIT_NPC_FLAG_TRAINER_PROFESSION;
return false;
}
break;
default:
break;
}
return true;
return player->GetLevel() >= 10 && trainer->IsTrainerValidForPlayer(player);
}
Player* Creature::GetLootRecipient() const
@ -3147,11 +3099,6 @@ uint32 Creature::UpdateVendorItemCurrentCount(VendorItem const* vItem, uint32 us
return vCount->count;
}
TrainerSpellData const* Creature::GetTrainerSpells() const
{
return sObjectMgr->GetNpcTrainerSpells(GetEntry());
}
// overwrite WorldObject function for proper name localization
std::string const& Creature::GetNameForLocaleIdx(LocaleConstant loc_idx) const
{

View File

@ -103,8 +103,7 @@ public:
///// @todo RENAME THIS!!!!!
bool isCanInteractWithBattleMaster(Player* player, bool msg) const;
bool isCanTrainingAndResetTalentsOf(Player* player) const;
[[nodiscard]] bool IsValidTrainerForPlayer(Player* player, uint32* npcFlags = nullptr) const;
bool CanResetTalents(Player* player) const;
bool CanCreatureAttack(Unit const* victim, bool skipDistCheck = false) const;
void LoadSpellTemplateImmunity();
bool IsImmunedToSpell(SpellInfo const* spellInfo, Spell const* spell = nullptr) override;
@ -203,8 +202,6 @@ public:
uint32 GetVendorItemCurrentCount(VendorItem const* vItem);
uint32 UpdateVendorItemCurrentCount(VendorItem const* vItem, uint32 used_count);
[[nodiscard]] TrainerSpellData const* GetTrainerSpells() const;
[[nodiscard]] CreatureTemplate const* GetCreatureTemplate() const { return m_creatureInfo; }
[[nodiscard]] CreatureData const* GetCreatureData() const { return m_creatureData; }
void SetDetectionDistance(float dist){ m_detectionDistance = dist; }

View File

@ -215,10 +215,6 @@ struct CreatureTemplate
uint32 unit_flags2; // enum UnitFlags2 mask values
uint32 dynamicflags;
uint32 family; // enum CreatureFamily values (optional)
uint32 trainer_type;
uint32 trainer_spell;
uint32 trainer_class;
uint32 trainer_race;
uint32 type; // enum CreatureType values
uint32 type_flags; // enum CreatureTypeFlags mask values
uint32 lootid;
@ -503,39 +499,6 @@ struct VendorItemCount
typedef std::list<VendorItemCount> VendorItemCounts;
struct TrainerSpell
{
TrainerSpell()
{
for (unsigned int & i : learnedSpell)
i = 0;
}
uint32 spell{0};
uint32 spellCost{0};
uint32 reqSkill{0};
uint32 reqSkillValue{0};
uint32 reqLevel{0};
uint32 learnedSpell[3];
uint32 reqSpell{0};
// helpers
[[nodiscard]] bool IsCastable() const { return learnedSpell[0] != spell; }
};
typedef std::unordered_map<uint32 /*spellid*/, TrainerSpell> TrainerSpellMap;
struct TrainerSpellData
{
TrainerSpellData() = default;
~TrainerSpellData() { spellList.clear(); }
TrainerSpellMap spellList;
uint32 trainerType{0}; // trainer type based at trainer spells, can be different from creature_template value.
// req. for correct show non-prof. trainers like weaponmaster, allowed values 0 and 2.
[[nodiscard]] TrainerSpell const* Find(uint32 spell_id) const;
};
struct CreatureSpellCooldown
{
CreatureSpellCooldown() = default;

View File

@ -0,0 +1,262 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by the
* Free Software Foundation; either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Trainer.h"
#include "Creature.h"
#include "NPCPackets.h"
#include "Player.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
namespace Trainer
{
bool Spell::IsCastable() const
{
return sSpellMgr->AssertSpellInfo(SpellId)->HasEffect(SPELL_EFFECT_LEARN_SPELL);
}
Trainer::Trainer(uint32 trainerId, Type type, uint32 requirement, std::string greeting, std::vector<Spell> spells) : _trainerId(trainerId), _type(type), _requirement(requirement), _spells(std::move(spells))
{
_greeting[DEFAULT_LOCALE] = std::move(greeting);
}
void Trainer::SendSpells(Creature* npc, Player* player, LocaleConstant locale) const
{
float reputationDiscount = player->GetReputationPriceDiscount(npc);
WorldPackets::NPC::TrainerList trainerList;
trainerList.TrainerGUID = npc->GetGUID();
trainerList.TrainerType = AsUnderlyingType(_type);
trainerList.Greeting = GetGreeting(locale);
trainerList.Spells.reserve(_spells.size());
for (Spell const& trainerSpell : _spells)
{
if (!player->IsSpellFitByClassAndRace(trainerSpell.SpellId))
continue;
SpellInfo const* trainerSpellInfo = sSpellMgr->AssertSpellInfo(trainerSpell.SpellId);
bool primaryProfessionFirstRank = false;
for (SpellEffectInfo const& spellEffectInfo : trainerSpellInfo->GetEffects())
{
if (!spellEffectInfo.IsEffect(SPELL_EFFECT_LEARN_SPELL))
continue;
SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(spellEffectInfo.TriggerSpell);
if (learnedSpellInfo && learnedSpellInfo->IsPrimaryProfessionFirstRank())
primaryProfessionFirstRank = true;
}
trainerList.Spells.emplace_back();
WorldPackets::NPC::TrainerListSpell& trainerListSpell = trainerList.Spells.back();
trainerListSpell.SpellID = trainerSpell.SpellId;
trainerListSpell.Usable = AsUnderlyingType(GetSpellState(player, &trainerSpell));
trainerListSpell.MoneyCost = int32(trainerSpell.MoneyCost * reputationDiscount);
trainerListSpell.PointCost[0] = 0; // spells don't cost talent points
trainerListSpell.PointCost[1] = (primaryProfessionFirstRank ? 1 : 0);
trainerListSpell.ReqLevel = trainerSpell.ReqLevel;
trainerListSpell.ReqSkillLine = trainerSpell.ReqSkillLine;
trainerListSpell.ReqSkillRank = trainerSpell.ReqSkillRank;
std::copy(trainerSpell.ReqAbility.begin(), trainerSpell.ReqAbility.end(), trainerListSpell.ReqAbility.begin());
}
player->SendDirectMessage(trainerList.Write());
}
void Trainer::TeachSpell(Creature* npc, Player* player, uint32 spellId)
{
if (!IsTrainerValidForPlayer(player))
return;
Spell const* trainerSpell = GetSpell(spellId);
if (!trainerSpell)
{
SendTeachFailure(npc, player, spellId, FailReason::Unavailable);
return;
}
if (!CanTeachSpell(player, trainerSpell))
{
SendTeachFailure(npc, player, spellId, FailReason::NotEnoughSkill);
return;
}
float reputationDiscount = player->GetReputationPriceDiscount(npc);
int32 moneyCost = int32(trainerSpell->MoneyCost * reputationDiscount);
if (!player->HasEnoughMoney(moneyCost))
{
SendTeachFailure(npc, player, spellId, FailReason::NotEnoughMoney);
return;
}
player->ModifyMoney(-moneyCost);
npc->SendPlaySpellVisual(179); // 53 SpellCastDirected
npc->SendPlaySpellImpact(player->GetGUID(), 362); // 113 EmoteSalute
// learn explicitly or cast explicitly
if (trainerSpell->IsCastable())
player->CastSpell(player, trainerSpell->SpellId, true);
else
player->learnSpell(trainerSpell->SpellId, false);
SendTeachSucceeded(npc, player, spellId);
}
Spell const* Trainer::GetSpell(uint32 spellId) const
{
auto itr = std::find_if(_spells.begin(), _spells.end(), [spellId](Spell const& trainerSpell)
{
return trainerSpell.SpellId == spellId;
});
if (itr != _spells.end())
return &(*itr);
return nullptr;
}
bool Trainer::CanTeachSpell(Player const* player, Spell const* trainerSpell)
{
SpellState state = GetSpellState(player, trainerSpell);
if (state != SpellState::Available)
return false;
SpellInfo const* trainerSpellInfo = sSpellMgr->AssertSpellInfo(trainerSpell->SpellId);
for (SpellEffectInfo const& spellEffectInfo : trainerSpellInfo->GetEffects())
{
if (!spellEffectInfo.IsEffect(SPELL_EFFECT_LEARN_SPELL))
continue;
SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(spellEffectInfo.TriggerSpell);
if (learnedSpellInfo && learnedSpellInfo->IsPrimaryProfessionFirstRank() && !player->GetFreePrimaryProfessionPoints())
return false;
}
return true;
}
SpellState Trainer::GetSpellState(Player const* player, Spell const* trainerSpell) const
{
if (player->HasSpell(trainerSpell->SpellId))
return SpellState::Known;
// check race/class requirement
if (!player->IsSpellFitByClassAndRace(trainerSpell->SpellId))
return SpellState::Unavailable;
// check skill requirement
if (trainerSpell->ReqSkillLine && player->GetBaseSkillValue(trainerSpell->ReqSkillLine) < trainerSpell->ReqSkillRank)
return SpellState::Unavailable;
for (int32 reqAbility : trainerSpell->ReqAbility)
if (reqAbility && !player->HasSpell(reqAbility))
return SpellState::Unavailable;
// check level requirement
if (player->GetLevel() < trainerSpell->ReqLevel)
return SpellState::Unavailable;
// check ranks
bool hasLearnSpellEffect = false;
bool knowsAllLearnedSpells = true;
for (SpellEffectInfo const& spellEffectInfo : sSpellMgr->AssertSpellInfo(trainerSpell->SpellId)->GetEffects())
{
if (!spellEffectInfo.IsEffect(SPELL_EFFECT_LEARN_SPELL))
continue;
hasLearnSpellEffect = true;
if (!player->HasSpell(spellEffectInfo.TriggerSpell))
knowsAllLearnedSpells = false;
if (uint32 previousRankSpellId = sSpellMgr->GetPrevSpellInChain(spellEffectInfo.TriggerSpell))
if (!player->HasSpell(previousRankSpellId))
return SpellState::Unavailable;
}
if (!hasLearnSpellEffect)
{
if (uint32 previousRankSpellId = sSpellMgr->GetPrevSpellInChain(trainerSpell->SpellId))
if (!player->HasSpell(previousRankSpellId))
return SpellState::Unavailable;
}
else if (knowsAllLearnedSpells)
return SpellState::Known;
// check additional spell requirement
for (auto const& requirePair : sSpellMgr->GetSpellsRequiredForSpellBounds(trainerSpell->SpellId))
if (!player->HasSpell(requirePair.second))
return SpellState::Unavailable;
return SpellState::Available;
}
bool Trainer::IsTrainerValidForPlayer(Player const* player) const
{
if (!GetTrainerRequirement())
return true;
switch (GetTrainerType())
{
case Type::Class:
case Type::Pet:
// check class for class trainers
return player->getClass() == GetTrainerRequirement();
case Type::Mount:
// check race for mount trainers
return player->getRace() == GetTrainerRequirement();
case Type::Tradeskill:
// check spell for profession trainers
return player->HasSpell(GetTrainerRequirement());
default:
break;
}
return true;
}
void Trainer::SendTeachFailure(Creature const* npc, Player const* player, uint32 spellId, FailReason reason) const
{
WorldPackets::NPC::TrainerBuyFailed trainerBuyFailed;
trainerBuyFailed.TrainerGUID = npc->GetGUID();
trainerBuyFailed.SpellID = spellId;
trainerBuyFailed.TrainerFailedReason = AsUnderlyingType(reason);
player->SendDirectMessage(trainerBuyFailed.Write());
}
void Trainer::SendTeachSucceeded(Creature const* npc, Player const* player, uint32 spellId) const
{
WorldPackets::NPC::TrainerBuySucceeded trainerBuySucceeded;
trainerBuySucceeded.TrainerGUID = npc->GetGUID();
trainerBuySucceeded.SpellID = spellId;
player->SendDirectMessage(trainerBuySucceeded.Write());
}
std::string const& Trainer::GetGreeting(LocaleConstant locale) const
{
if (_greeting[locale].empty())
return _greeting[DEFAULT_LOCALE];
return _greeting[locale];
}
void Trainer::AddGreetingLocale(LocaleConstant locale, std::string greeting)
{
_greeting[locale] = std::move(greeting);
}
}

View File

@ -0,0 +1,97 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by the
* Free Software Foundation; either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef Trainer_h__
#define Trainer_h__
#include "Common.h"
#include <array>
#include <vector>
class Creature;
class ObjectMgr;
class Player;
namespace Trainer
{
enum class Type : uint32
{
Class = 0,
Mount = 1,
Tradeskill = 2,
Pet = 3
};
enum class SpellState : uint8
{
Available = 0,
Unavailable = 1,
Known = 2
};
enum class FailReason : uint32
{
Unavailable = 0,
NotEnoughMoney = 1,
NotEnoughSkill = 2
};
struct AC_GAME_API Spell
{
uint32 SpellId = 0;
uint32 MoneyCost = 0;
uint32 ReqSkillLine = 0;
uint32 ReqSkillRank = 0;
std::array<uint32, 3> ReqAbility = { };
uint8 ReqLevel = 0;
[[nodiscard]] bool IsCastable() const;
};
class AC_GAME_API Trainer
{
public:
Trainer(uint32 trainerId, Type type, uint32 requirement, std::string greeting, std::vector<Spell> spells);
[[nodiscard]] Spell const* GetSpell(uint32 spellId) const;
[[nodiscard]] std::vector<Spell> const& GetSpells() const { return _spells; }
void SendSpells(Creature* npc, Player* player, LocaleConstant locale) const;
bool CanTeachSpell(Player const* player, Spell const* trainerSpell);
void TeachSpell(Creature* npc, Player* player, uint32 spellId);
[[nodiscard]] Type GetTrainerType() const { return _type; }
[[nodiscard]] uint32 GetTrainerRequirement() const { return _requirement; }
bool IsTrainerValidForPlayer(Player const* player) const;
private:
SpellState GetSpellState(Player const* player, Spell const* trainerSpell) const;
void SendTeachFailure(Creature const* npc, Player const* player, uint32 spellId, FailReason reason) const;
void SendTeachSucceeded(Creature const* npc, Player const* player, uint32 spellId) const;
[[nodiscard]] std::string const& GetGreeting(LocaleConstant locale) const;
friend ObjectMgr;
void AddGreetingLocale(LocaleConstant locale, std::string greeting);
uint32 _trainerId;
Type _type;
uint32 _requirement;
std::vector<Spell> _spells;
std::array<std::string, TOTAL_LOCALES> _greeting;
};
}
#endif // Trainer_h__

View File

@ -77,6 +77,7 @@
#include "StringConvert.h"
#include "TicketMgr.h"
#include "Tokenize.h"
#include "Trainer.h"
#include "Transport.h"
#include "Unit.h"
#include "UpdateData.h"
@ -2120,11 +2121,6 @@ Creature* Player::GetNPCIfCanInteractWith(ObjectGuid const& guid, uint32 npcflag
if (!creature->IsWithinDistInMap(this, INTERACTION_DISTANCE))
return nullptr;
// pussywizard: many npcs have missing conditions for class training and rogue trainer can for eg. train dual wield to a shaman :/ too many to change in sql and watch in the future
// pussywizard: this function is not used when talking, but when already taking action (buy spell, reset talents, show spell list)
if (npcflagmask & (UNIT_NPC_FLAG_TRAINER | UNIT_NPC_FLAG_TRAINER_CLASS) && creature->GetCreatureTemplate()->trainer_type == TRAINER_TYPE_CLASS && !IsClass((Classes)creature->GetCreatureTemplate()->trainer_class, CLASS_CONTEXT_CLASS_TRAINER))
return nullptr;
return creature;
}
@ -3906,74 +3902,6 @@ bool Player::HasActiveSpell(uint32 spell) const
return (itr != m_spells.end() && itr->second->State != PLAYERSPELL_REMOVED && itr->second->Active && itr->second->IsInSpec(m_activeSpec));
}
TrainerSpellState Player::GetTrainerSpellState(TrainerSpell const* trainer_spell) const
{
if (!trainer_spell)
return TRAINER_SPELL_RED;
bool hasSpell = true;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (!trainer_spell->learnedSpell[i])
continue;
if (!HasSpell(trainer_spell->learnedSpell[i]))
{
hasSpell = false;
break;
}
}
// known spell
if (hasSpell)
return TRAINER_SPELL_GRAY;
// check skill requirement
if (trainer_spell->reqSkill && GetBaseSkillValue(trainer_spell->reqSkill) < trainer_spell->reqSkillValue)
return TRAINER_SPELL_RED;
// check level requirement
if (GetLevel() < trainer_spell->reqLevel)
return TRAINER_SPELL_RED;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (!trainer_spell->learnedSpell[i])
continue;
// check race/class requirement
if (!IsSpellFitByClassAndRace(trainer_spell->learnedSpell[i]))
return TRAINER_SPELL_RED;
if (uint32 prevSpell = sSpellMgr->GetPrevSpellInChain(trainer_spell->learnedSpell[i]))
{
// check prev.rank requirement
if (prevSpell && !HasSpell(prevSpell))
return TRAINER_SPELL_RED;
}
SpellsRequiringSpellMapBounds spellsRequired = sSpellMgr->GetSpellsRequiredForSpellBounds(trainer_spell->learnedSpell[i]);
for (SpellsRequiringSpellMap::const_iterator itr = spellsRequired.first; itr != spellsRequired.second; ++itr)
{
// check additional spell requirement
if (!HasSpell(itr->second))
return TRAINER_SPELL_RED;
}
}
// check primary prof. limit
// first rank of primary profession spell when there are no proffesions avalible is disabled
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (!trainer_spell->learnedSpell[i])
continue;
SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(trainer_spell->learnedSpell[i]);
if (learnedSpellInfo && learnedSpellInfo->IsPrimaryProfessionFirstRank() && (GetFreePrimaryProfessionPoints() == 0))
return TRAINER_SPELL_GREEN_DISABLED;
}
return TRAINER_SPELL_GREEN;
}
/**
* Deletes a character from the database
*
@ -14431,6 +14359,18 @@ bool Player::CanSeeVendor(Creature const* creature) const
return true;
}
bool Player::CanSeeTrainer(Creature const* creature) const
{
if (!creature->HasNpcFlag(UNIT_NPC_FLAG_TRAINER))
return true;
if (auto trainer = sObjectMgr->GetTrainer(creature->GetEntry()))
if (!trainer || !trainer->IsTrainerValidForPlayer(this))
return false;
return true;
}
void Player::BuildPlayerTalentsInfoData(WorldPacket* data)
{
*data << uint32(GetFreeTalentPoints()); // unspentTalentPoints

View File

@ -210,14 +210,6 @@ struct SpellCooldown
typedef std::map<uint32, SpellCooldown> SpellCooldowns;
typedef std::unordered_map<uint32 /*instanceId*/, time_t/*releaseTime*/> InstanceTimeMap;
enum TrainerSpellState
{
TRAINER_SPELL_GREEN = 0,
TRAINER_SPELL_RED = 1,
TRAINER_SPELL_GRAY = 2,
TRAINER_SPELL_GREEN_DISABLED = 10 // custom value, not send to client: formally green but learn not allowed
};
enum ActionButtonUpdateState
{
ACTIONBUTTON_UNCHANGED = 0,
@ -1684,7 +1676,6 @@ public:
void SendRemoveControlBar();
[[nodiscard]] bool HasSpell(uint32 spell) const override;
[[nodiscard]] bool HasActiveSpell(uint32 spell) const; // show in spellbook
TrainerSpellState GetTrainerSpellState(TrainerSpell const* trainer_spell) const;
[[nodiscard]] bool IsSpellFitByClassAndRace(uint32 spell_id) const;
bool IsNeedCastPassiveSpellAtLearn(SpellInfo const* spellInfo) const;
@ -2550,6 +2541,8 @@ public:
//bool isActiveObject() const { return true; }
bool CanSeeSpellClickOn(Creature const* creature) const;
[[nodiscard]] bool CanSeeVendor(Creature const* creature) const;
[[nodiscard]] bool CanSeeTrainer(Creature const* creature) const;
private:
[[nodiscard]] bool AnyVendorOptionAvailable(uint32 menuId, Creature const* creature) const;
public:

View File

@ -90,15 +90,15 @@ void Player::PrepareGossipMenu(WorldObject* source, uint32 menuId /*= 0*/, bool
}
case GOSSIP_OPTION_LEARNDUALSPEC:
case GOSSIP_OPTION_DUALSPEC_INFO:
if (!(GetSpecsCount() == 1 && creature->isCanTrainingAndResetTalentsOf(this) && !(GetLevel() < sWorld->getIntConfig(CONFIG_MIN_DUALSPEC_LEVEL))))
if (!(GetSpecsCount() == 1 && creature->CanResetTalents(this) && !(GetLevel() < sWorld->getIntConfig(CONFIG_MIN_DUALSPEC_LEVEL))))
canTalk = false;
break;
case GOSSIP_OPTION_UNLEARNTALENTS:
if (!creature->isCanTrainingAndResetTalentsOf(this))
if (!creature->CanResetTalents(this))
canTalk = false;
break;
case GOSSIP_OPTION_UNLEARNPETTALENTS:
if (!GetPet() || GetPet()->getPetType() != HUNTER_PET || GetPet()->m_spells.size() <= 1 || creature->GetCreatureTemplate()->trainer_type != TRAINER_TYPE_PETS || creature->GetCreatureTemplate()->trainer_class != CLASS_HUNTER)
if (!GetPet() || GetPet()->getPetType() != HUNTER_PET || GetPet()->m_spells.size() <= 1 || !creature->CanResetTalents(this))
canTalk = false;
break;
case GOSSIP_OPTION_TAXIVENDOR:
@ -117,11 +117,16 @@ void Player::PrepareGossipMenu(WorldObject* source, uint32 menuId /*= 0*/, bool
canTalk = false;
break;
case GOSSIP_OPTION_TRAINER:
if (!creature->IsValidTrainerForPlayer(this))
{
Trainer::Trainer const* trainer = sObjectMgr->GetTrainer(creature->GetEntry());
if (!trainer || !trainer->IsTrainerValidForPlayer(this))
{
LOG_ERROR("sql.sql", "GOSSIP_OPTION_TRAINER:: Player %s (GUID: %u) requested wrong gossip menu: %u at Creature: %s (Entry: %u)",
GetName().c_str(), GetGUID().GetCounter(), menu->GetGossipMenu().GetMenuId(), creature->GetName().c_str(), creature->GetEntry());
canTalk = false;
}
break;
}
[[fallthrough]];
case GOSSIP_OPTION_GOSSIP:
if (creature->isVendorWithIconSpeak())
{
@ -328,7 +333,7 @@ void Player::OnGossipSelect(WorldObject* source, uint32 gossipListId, uint32 men
GetSession()->SendStablePet(guid);
break;
case GOSSIP_OPTION_TRAINER:
GetSession()->SendTrainerList(guid);
GetSession()->SendTrainerList(source->ToCreature());
break;
case GOSSIP_OPTION_LEARNDUALSPEC:
if (GetSpecsCount() == 1 && GetLevel() >= sWorld->getIntConfig(CONFIG_MIN_DUALSPEC_LEVEL))

View File

@ -20737,7 +20737,7 @@ void Unit::PatchValuesUpdate(ByteBuffer& valuesUpdateBuf, BuildValuesCachePosPoi
appendValue &= ~UNIT_NPC_FLAG_VENDOR_MASK;
}
if (!creature->IsValidTrainerForPlayer(target, &appendValue))
if (!target->CanSeeTrainer(creature))
appendValue &= ~UNIT_NPC_FLAG_TRAINER;
valuesUpdateBuf.put(posPointers.UnitNPCFlagsPos, appendValue);

View File

@ -342,8 +342,6 @@ ObjectMgr::~ObjectMgr()
for (CacheVendorItemContainer::iterator itr = _cacheVendorItemStore.begin(); itr != _cacheVendorItemStore.end(); ++itr)
itr->second.Clear();
_cacheTrainerSpellStore.clear();
for (DungeonEncounterContainer::iterator itr = _dungeonEncounterStore.begin(); itr != _dungeonEncounterStore.end(); ++itr)
for (DungeonEncounterList::iterator encounterItr = itr->second.begin(); encounterItr != itr->second.end(); ++encounterItr)
delete *encounterItr;
@ -523,19 +521,19 @@ void ObjectMgr::LoadCreatureTemplates()
{
uint32 oldMSTime = getMSTime();
// 0 1 2 3 4 5 6 7 8
// 0 1 2 3 4 5 6 7 8
QueryResult result = WorldDatabase.Query("SELECT entry, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, name, subname, IconName, "
// 9 10 11 12 13 14 15 16 17 18 19 20 21 22
"gossip_menu_id, minlevel, maxlevel, exp, faction, npcflag, speed_walk, speed_run, speed_swim, speed_flight, detection_range, scale, `rank`, dmgschool, "
// 23 24 25 26 27 28 29 30 31 32 33 34
"DamageModifier, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, unit_class, unit_flags, unit_flags2, dynamicflags, family, trainer_type, trainer_spell, "
// 35 36 37 38 39 40 41
"trainer_class, trainer_race, type, type_flags, lootid, pickpocketloot, skinloot, "
// 42 43 44 45 46 47 48 49 50 51
// 23 24 25 26 27 28 29 30 31 32
"DamageModifier, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, unit_class, unit_flags, unit_flags2, dynamicflags, family, "
// 33 34 35 36 37
"type, type_flags, lootid, pickpocketloot, skinloot, "
// 38 39 40 41 42 43 44 45 46 47
"PetSpellDataId, VehicleId, mingold, maxgold, AIName, MovementType, ctm.Ground, ctm.Swim, ctm.Flight, ctm.Rooted, "
// 52 53 54 55 56 57 58 59 60 61 62 63
// 48 49 50 51 52 53 54 55 56 57 58 59
"ctm.Chase, ctm.Random, ctm.InteractionPauseTimer, HoverHeight, HealthModifier, ManaModifier, ArmorModifier, ExperienceModifier, RacialLeader, movementId, RegenHealth, mechanic_immune_mask, "
// 64 65 66
// 60 61 62
"spell_school_immune_mask, flags_extra, ScriptName "
"FROM creature_template ct LEFT JOIN creature_template_movement ctm ON ct.entry = ctm.CreatureId ORDER BY entry DESC;");
@ -635,15 +633,11 @@ void ObjectMgr::LoadCreatureTemplate(Field* fields, bool triggerHook)
creatureTemplate.unit_flags2 = fields[30].Get<uint32>();
creatureTemplate.dynamicflags = fields[31].Get<uint32>();
creatureTemplate.family = uint32(fields[32].Get<uint8>());
creatureTemplate.trainer_type = uint32(fields[33].Get<uint8>());
creatureTemplate.trainer_spell = fields[34].Get<uint32>();
creatureTemplate.trainer_class = uint32(fields[35].Get<uint8>());
creatureTemplate.trainer_race = uint32(fields[36].Get<uint8>());
creatureTemplate.type = uint32(fields[37].Get<uint8>());
creatureTemplate.type_flags = fields[38].Get<uint32>();
creatureTemplate.lootid = fields[39].Get<uint32>();
creatureTemplate.pickpocketLootId = fields[40].Get<uint32>();
creatureTemplate.SkinLootId = fields[41].Get<uint32>();
creatureTemplate.type = uint32(fields[33].Get<uint8>());
creatureTemplate.type_flags = fields[34].Get<uint32>();
creatureTemplate.lootid = fields[35].Get<uint32>();
creatureTemplate.pickpocketLootId = fields[36].Get<uint32>();
creatureTemplate.SkinLootId = fields[37].Get<uint32>();
for (uint8 i = SPELL_SCHOOL_HOLY; i < MAX_SPELL_SCHOOL; ++i)
{
@ -655,49 +649,49 @@ void ObjectMgr::LoadCreatureTemplate(Field* fields, bool triggerHook)
creatureTemplate.spells[i] = 0;
}
creatureTemplate.PetSpellDataId = fields[42].Get<uint32>();
creatureTemplate.VehicleId = fields[43].Get<uint32>();
creatureTemplate.mingold = fields[44].Get<uint32>();
creatureTemplate.maxgold = fields[45].Get<uint32>();
creatureTemplate.AIName = fields[46].Get<std::string>(); // stopped here, fix it
creatureTemplate.MovementType = uint32(fields[47].Get<uint8>());
if (!fields[48].IsNull())
creatureTemplate.PetSpellDataId = fields[38].Get<uint32>();
creatureTemplate.VehicleId = fields[39].Get<uint32>();
creatureTemplate.mingold = fields[40].Get<uint32>();
creatureTemplate.maxgold = fields[41].Get<uint32>();
creatureTemplate.AIName = fields[42].Get<std::string>();
creatureTemplate.MovementType = uint32(fields[43].Get<uint8>());
if (!fields[44].IsNull())
{
creatureTemplate.Movement.Ground = static_cast<CreatureGroundMovementType>(fields[48].Get<uint8>());
}
creatureTemplate.Movement.Swim = fields[49].Get<bool>();
creatureTemplate.Movement.Swim = fields[45].Get<bool>();
if (!fields[46].IsNull())
{
creatureTemplate.Movement.Flight = static_cast<CreatureFlightMovementType>(fields[46].Get<uint8>());
}
creatureTemplate.Movement.Rooted = fields[47].Get<bool>();
if (!fields[48].IsNull())
{
creatureTemplate.Movement.Chase = static_cast<CreatureChaseMovementType>(fields[48].Get<uint8>());
}
if (!fields[49].IsNull())
{
creatureTemplate.Movement.Random = static_cast<CreatureRandomMovementType>(fields[49].Get<uint8>());
}
if (!fields[50].IsNull())
{
creatureTemplate.Movement.Flight = static_cast<CreatureFlightMovementType>(fields[50].Get<uint8>());
creatureTemplate.Movement.InteractionPauseTimer = fields[50].Get<uint32>();
}
creatureTemplate.Movement.Rooted = fields[51].Get<bool>();
if (!fields[52].IsNull())
{
creatureTemplate.Movement.Chase = static_cast<CreatureChaseMovementType>(fields[52].Get<uint8>());
}
if (!fields[53].IsNull())
{
creatureTemplate.Movement.Random = static_cast<CreatureRandomMovementType>(fields[53].Get<uint8>());
}
if (!fields[54].IsNull())
{
creatureTemplate.Movement.InteractionPauseTimer = fields[54].Get<uint32>();
}
creatureTemplate.HoverHeight = fields[55].Get<float>();
creatureTemplate.ModHealth = fields[56].Get<float>();
creatureTemplate.ModMana = fields[57].Get<float>();
creatureTemplate.ModArmor = fields[58].Get<float>();
creatureTemplate.ModExperience = fields[59].Get<float>();
creatureTemplate.RacialLeader = fields[60].Get<bool>();
creatureTemplate.movementId = fields[61].Get<uint32>();
creatureTemplate.RegenHealth = fields[62].Get<bool>();
creatureTemplate.MechanicImmuneMask = fields[63].Get<uint32>();
creatureTemplate.SpellSchoolImmuneMask = fields[64].Get<uint8>();
creatureTemplate.flags_extra = fields[65].Get<uint32>();
creatureTemplate.ScriptID = GetScriptId(fields[66].Get<std::string>());
creatureTemplate.HoverHeight = fields[51].Get<float>();
creatureTemplate.ModHealth = fields[52].Get<float>();
creatureTemplate.ModMana = fields[53].Get<float>();
creatureTemplate.ModArmor = fields[54].Get<float>();
creatureTemplate.ModExperience = fields[55].Get<float>();
creatureTemplate.RacialLeader = fields[56].Get<bool>();
creatureTemplate.movementId = fields[57].Get<uint32>();
creatureTemplate.RegenHealth = fields[58].Get<bool>();
creatureTemplate.MechanicImmuneMask = fields[59].Get<uint32>();
creatureTemplate.SpellSchoolImmuneMask = fields[60].Get<uint8>();
creatureTemplate.flags_extra = fields[61].Get<uint32>();
creatureTemplate.ScriptID = GetScriptId(fields[62].Get<std::string>());
// useful if the creature template load is being triggered from outside this class
if (triggerHook)
@ -1037,30 +1031,6 @@ void ObjectMgr::CheckCreatureTemplate(CreatureTemplate const* cInfo)
cInfo->Entry, cInfo->family, diff + 1, cInfo->DifficultyEntry[diff], difficultyInfo->family);
}
if (cInfo->trainer_class != difficultyInfo->trainer_class)
{
LOG_ERROR("sql.sql", "Creature (Entry: {}) has different `trainer_class` in difficulty {} mode (Entry: {}).", cInfo->Entry, diff + 1, cInfo->DifficultyEntry[diff]);
continue;
}
if (cInfo->trainer_race != difficultyInfo->trainer_race)
{
LOG_ERROR("sql.sql", "Creature (Entry: {}) has different `trainer_race` in difficulty {} mode (Entry: {}).", cInfo->Entry, diff + 1, cInfo->DifficultyEntry[diff]);
continue;
}
if (cInfo->trainer_type != difficultyInfo->trainer_type)
{
LOG_ERROR("sql.sql", "Creature (Entry: {}) has different `trainer_type` in difficulty {} mode (Entry: {}).", cInfo->Entry, diff + 1, cInfo->DifficultyEntry[diff]);
continue;
}
if (cInfo->trainer_spell != difficultyInfo->trainer_spell)
{
LOG_ERROR("sql.sql", "Creature (Entry: {}) has different `trainer_spell` in difficulty {} mode (Entry: {}).", cInfo->Entry, diff + 1, cInfo->DifficultyEntry[diff]);
continue;
}
if (cInfo->type != difficultyInfo->type)
{
LOG_ERROR("sql.sql", "Creature (Entry: {}, type {}) has different `type` in difficulty {} mode (Entry: {}, type {}).",
@ -1157,9 +1127,6 @@ void ObjectMgr::CheckCreatureTemplate(CreatureTemplate const* cInfo)
if (cInfo->RangeAttackTime == 0)
const_cast<CreatureTemplate*>(cInfo)->RangeAttackTime = BASE_ATTACK_TIME;
if ((cInfo->npcflag & UNIT_NPC_FLAG_TRAINER) && cInfo->trainer_type >= MAX_TRAINER_TYPE)
LOG_ERROR("sql.sql", "Creature (Entry: {}) has wrong trainer type {}.", cInfo->Entry, cInfo->trainer_type);
if (cInfo->speed_walk == 0.0f)
{
LOG_ERROR("sql.sql", "Creature (Entry: {}) has wrong value ({}) in speed_walk, set to 1.", cInfo->Entry, cInfo->speed_walk);
@ -9310,130 +9277,158 @@ void ObjectMgr::LoadMailLevelRewards()
LOG_INFO("server.loading", " ");
}
void ObjectMgr::AddSpellToTrainer(uint32 entry, uint32 spell, uint32 spellCost, uint32 reqSkill, uint32 reqSkillValue, uint32 reqLevel, uint32 reqSpell)
{
if (entry >= ACORE_TRAINER_START_REF)
return;
CreatureTemplate const* cInfo = GetCreatureTemplate(entry);
if (!cInfo)
{
LOG_ERROR("sql.sql", "Table `npc_trainer` contains an entry for a non-existing creature template (Entry: {}), ignoring", entry);
return;
}
if (!(cInfo->npcflag & UNIT_NPC_FLAG_TRAINER))
{
LOG_ERROR("sql.sql", "Table `npc_trainer` contains an entry for a creature template (Entry: {}) without trainer flag, ignoring", entry);
return;
}
SpellInfo const* spellinfo = sSpellMgr->GetSpellInfo(spell);
if (!spellinfo)
{
LOG_ERROR("sql.sql", "Table `npc_trainer` contains an entry (Entry: {}) for a non-existing spell (Spell: {}), ignoring", entry, spell);
return;
}
if (!SpellMgr::ComputeIsSpellValid(spellinfo))
{
LOG_ERROR("sql.sql", "Table `npc_trainer` contains an entry (Entry: {}) for a broken spell (Spell: {}), ignoring", entry, spell);
return;
}
if (GetTalentSpellCost(spell))
{
LOG_ERROR("sql.sql", "Table `npc_trainer` contains an entry (Entry: {}) for a non-existing spell (Spell: {}) which is a talent, ignoring", entry, spell);
return;
}
if (reqSpell && !sSpellMgr->GetSpellInfo(reqSpell))
{
LOG_ERROR("sql.sql", "Table `npc_trainer` contains an entry (Entry: {}) for a non-existing reqSpell (Spell: {}), ignoring", entry, reqSpell);
return;
}
TrainerSpellData& data = _cacheTrainerSpellStore[entry];
TrainerSpell& trainerSpell = data.spellList[spell];
trainerSpell.spell = spell;
trainerSpell.spellCost = spellCost;
trainerSpell.reqSkill = reqSkill;
trainerSpell.reqSkillValue = reqSkillValue;
trainerSpell.reqLevel = reqLevel;
trainerSpell.reqSpell = reqSpell;
if (!trainerSpell.reqLevel)
trainerSpell.reqLevel = spellinfo->SpellLevel;
// calculate learned spell for profession case when stored cast-spell
trainerSpell.learnedSpell[0] = spell;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (spellinfo->Effects[i].Effect != SPELL_EFFECT_LEARN_SPELL)
continue;
if (trainerSpell.learnedSpell[0] == spell)
trainerSpell.learnedSpell[0] = 0;
// player must be able to cast spell on himself
if (spellinfo->Effects[i].TargetA.GetTarget() != 0 && spellinfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ALLY
&& spellinfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ANY && spellinfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER)
{
LOG_ERROR("sql.sql", "Table `npc_trainer` has spell {} for trainer entry {} with learn effect which has incorrect target type, ignoring learn effect!", spell, entry);
continue;
}
trainerSpell.learnedSpell[i] = spellinfo->Effects[i].TriggerSpell;
if (trainerSpell.learnedSpell[i])
{
SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(trainerSpell.learnedSpell[i]);
if (learnedSpellInfo && learnedSpellInfo->IsProfession())
data.trainerType = 2;
}
}
return;
}
void ObjectMgr::LoadTrainerSpell()
void ObjectMgr::LoadTrainers()
{
uint32 oldMSTime = getMSTime();
// For reload case
_cacheTrainerSpellStore.clear();
_trainers.clear();
QueryResult result = WorldDatabase.Query("SELECT b.ID, a.SpellID, a.MoneyCost, a.ReqSkillLine, a.ReqSkillRank, a.ReqLevel, a.ReqSpell FROM npc_trainer AS a "
"INNER JOIN npc_trainer AS b ON a.ID = -(b.SpellID) "
"UNION SELECT * FROM npc_trainer WHERE SpellID > 0");
if (!result)
std::unordered_map<int32, std::vector<Trainer::Spell>> spellsByTrainer;
if (QueryResult trainerSpellsResult = WorldDatabase.Query("SELECT TrainerId, SpellId, MoneyCost, ReqSkillLine, ReqSkillRank, ReqAbility1, ReqAbility2, ReqAbility3, ReqLevel FROM trainer_spell"))
{
LOG_WARN("server.loading", ">> Loaded 0 Trainers. DB table `npc_trainer` is empty!");
LOG_INFO("server.loading", " ");
return;
do
{
Field* fields = trainerSpellsResult->Fetch();
Trainer::Spell spell;
uint32 trainerId = fields[0].Get<uint32>();
spell.SpellId = fields[1].Get<uint32>();
spell.MoneyCost = fields[2].Get<uint32>();
spell.ReqSkillLine = fields[3].Get<uint32>();
spell.ReqSkillRank = fields[4].Get<uint32>();
spell.ReqAbility[0] = fields[5].Get<uint32>();
spell.ReqAbility[1] = fields[6].Get<uint32>();
spell.ReqAbility[2] = fields[7].Get<uint32>();
spell.ReqLevel = fields[8].Get<uint8>();
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell.SpellId);
if (!spellInfo)
{
LOG_ERROR("sql.sql", "Table `trainer_spell` references non-existing spell (SpellId: {}) for TrainerId {}, ignoring", spell.SpellId, trainerId);
continue;
}
if (GetTalentSpellCost(spell.SpellId))
{
LOG_ERROR("sql.sql", "Table `trainer_spell` references non-existing spell (SpellId: {}) which is a talent, for TrainerId {}, ignoring", spell.SpellId, trainerId);
continue;
}
if (spell.ReqSkillLine && !sSkillLineStore.LookupEntry(spell.ReqSkillLine))
{
LOG_ERROR("sql.sql", "Table `trainer_spell` references non-existing skill (ReqSkillLine: {}) for TrainerId {} and SpellId {}, ignoring",
spell.ReqSkillLine, spell.SpellId, trainerId);
continue;
}
bool allReqValid = true;
for (std::size_t i = 0; i < spell.ReqAbility.size(); ++i)
{
uint32 requiredSpell = spell.ReqAbility[i];
if (requiredSpell && !sSpellMgr->GetSpellInfo(requiredSpell))
{
LOG_ERROR("sql.sql", "Table `trainer_spell` references non-existing spell (ReqAbility {} : {}) for TrainerId {} and SpellId {}, ignoring",
i + 1, requiredSpell, trainerId, spell.SpellId);
allReqValid = false;
}
}
if (!allReqValid)
continue;
spellsByTrainer[trainerId].push_back(spell);
} while (trainerSpellsResult->NextRow());
}
uint32 count = 0;
do
if (QueryResult trainersResult = WorldDatabase.Query("SELECT Id, Type, Requirement, Greeting FROM trainer"))
{
Field* fields = result->Fetch();
do
{
Field* fields = trainersResult->Fetch();
uint32 entry = fields[0].Get<uint32>();
uint32 spell = fields[1].Get<uint32>();
uint32 spellCost = fields[2].Get<uint32>();
uint32 reqSkill = fields[3].Get<uint16>();
uint32 reqSkillValue = fields[4].Get<uint16>();
uint32 reqLevel = fields[5].Get<uint8>();
uint32 reqSpell = fields[6].Get<uint32>();
uint32 trainerId = fields[0].Get<uint32>();
Trainer::Type trainerType = Trainer::Type(fields[1].Get<uint8>());
uint32 requirement = fields[2].Get<uint32>();
std::string greeting = fields[3].Get<std::string>();
std::vector<Trainer::Spell> spells;
auto spellsItr = spellsByTrainer.find(trainerId);
if (spellsItr != spellsByTrainer.end())
{
spells = std::move(spellsItr->second);
spellsByTrainer.erase(spellsItr);
}
AddSpellToTrainer(entry, spell, spellCost, reqSkill, reqSkillValue, reqLevel, reqSpell);
_trainers.emplace(std::piecewise_construct, std::forward_as_tuple(trainerId), std::forward_as_tuple(trainerId, trainerType, requirement, std::move(greeting), std::move(spells)));
} while (trainersResult->NextRow());
}
++count;
} while (result->NextRow());
for (auto const& unusedSpells : spellsByTrainer)
{
for (Trainer::Spell const& unusedSpell : unusedSpells.second)
{
LOG_ERROR("sql.sql", "Table `trainer_spell` references non-existing trainer (TrainerId: {}) for SpellId {}, ignoring", unusedSpells.first, unusedSpell.SpellId);
}
}
LOG_INFO("server.loading", ">> Loaded {} Trainers in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
if (QueryResult trainerLocalesResult = WorldDatabase.Query("SELECT Id, locale, Greeting_lang FROM trainer_locale"))
{
do
{
Field* fields = trainerLocalesResult->Fetch();
uint32 trainerId = fields[0].Get<uint32>();
std::string localeName = fields[1].Get<std::string>();
LocaleConstant locale = GetLocaleByName(localeName);
if (locale == LOCALE_enUS)
continue;
if (Trainer::Trainer* trainer = Acore::Containers::MapGetValuePtr(_trainers, trainerId))
trainer->AddGreetingLocale(locale, fields[2].Get<std::string>());
else
LOG_ERROR("sql.sql", "Table `trainer_locale` references non-existing trainer (TrainerId: {}) for locale %s, ignoring",
trainerId, localeName.c_str());
} while (trainerLocalesResult->NextRow());
}
LOG_INFO("server.loading", ">> Loaded {} Trainers in {} ms", _trainers.size(), GetMSTimeDiffToNow(oldMSTime));
}
void ObjectMgr::LoadCreatureDefaultTrainers()
{
uint32 oldMSTime = getMSTime();
_creatureDefaultTrainers.clear();
if (QueryResult result = WorldDatabase.Query("SELECT CreatureId, TrainerId FROM creature_default_trainer"))
{
do
{
Field* fields = result->Fetch();
uint32 creatureId = fields[0].Get<uint32>();
uint32 trainerId = fields[1].Get<uint32>();
if (!GetCreatureTemplate(creatureId))
{
LOG_ERROR("sql.sql", "Table `creature_default_trainer` references non-existing creature template (CreatureId: %u), ignoring", creatureId);
continue;
}
_creatureDefaultTrainers[creatureId] = trainerId;
} while (result->NextRow());
}
LOG_INFO("server.loading", ">> Loaded {} default trainers in {} ms", _creatureDefaultTrainers.size(), GetMSTimeDiffToNow(oldMSTime));
}
Trainer::Trainer* ObjectMgr::GetTrainer(uint32 creatureId)
{
auto itr = _creatureDefaultTrainers.find(creatureId);
if (itr != _creatureDefaultTrainers.end())
return Acore::Containers::MapGetValuePtr(_trainers, itr->second);
return nullptr;
}
int ObjectMgr::LoadReferenceVendor(int32 vendor, int32 item, std::set<uint32>* skip_vendors)

View File

@ -33,6 +33,7 @@
#include "ObjectDefines.h"
#include "QuestDef.h"
#include "TemporarySummon.h"
#include "Trainer.h"
#include "VehicleDefines.h"
#include <functional>
#include <limits>
@ -664,7 +665,6 @@ typedef std::unordered_map<uint32, QuestPOIVector> QuestPOIContainer;
typedef std::map<std::pair<uint32, uint8>, QuestGreeting> QuestGreetingContainer;
typedef std::unordered_map<uint32, VendorItemData> CacheVendorItemContainer;
typedef std::unordered_map<uint32, TrainerSpellData> CacheTrainerSpellContainer;
typedef std::vector<uint32> CreatureCustomIDsContainer;
@ -1106,8 +1106,8 @@ public:
void LoadGossipMenuItems();
void LoadVendors();
void LoadTrainerSpell();
void AddSpellToTrainer(uint32 entry, uint32 spell, uint32 spellCost, uint32 reqSkill, uint32 reqSkillValue, uint32 reqLevel, uint32 reqSpell);
void LoadTrainers();
void LoadCreatureDefaultTrainers();
std::string GeneratePetName(uint32 entry);
std::string GeneratePetNameLocale(uint32 entry, LocaleConstant locale);
@ -1364,14 +1364,7 @@ public:
bool AddGameTele(GameTele& data);
bool DeleteGameTele(std::string_view name);
[[nodiscard]] TrainerSpellData const* GetNpcTrainerSpells(uint32 entry) const
{
CacheTrainerSpellContainer::const_iterator iter = _cacheTrainerSpellStore.find(entry);
if (iter == _cacheTrainerSpellStore.end())
return nullptr;
return &iter->second;
}
Trainer::Trainer* GetTrainer(uint32 creatureId);
[[nodiscard]] VendorItemData const* GetNpcVendorItemList(uint32 entry) const
{
@ -1617,7 +1610,8 @@ private:
PointOfInterestLocaleContainer _pointOfInterestLocaleStore;
CacheVendorItemContainer _cacheVendorItemStore;
CacheTrainerSpellContainer _cacheTrainerSpellStore;
std::unordered_map<uint32, Trainer::Trainer> _trainers;
std::unordered_map<uint32, uint32> _creatureDefaultTrainers;
std::set<uint32> _difficultyEntries[MAX_DIFFICULTY - 1]; // already loaded difficulty 1 value in creatures, used in CheckCreatureTemplate
std::set<uint32> _hasDifficultyEntries[MAX_DIFFICULTY - 1]; // already loaded creatures with difficulty 1 values, used in CheckCreatureTemplate

View File

@ -21,6 +21,7 @@
#include "DatabaseEnv.h"
#include "GameGraveyard.h"
#include "Language.h"
#include "NPCPackets.h"
#include "ObjectMgr.h"
#include "Opcodes.h"
#include "Pet.h"
@ -29,6 +30,7 @@
#include "ScriptMgr.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "Trainer.h"
#include "WorldPacket.h"
#include "WorldSession.h"
#include <cmath>
@ -76,28 +78,48 @@ void WorldSession::SendShowMailBox(ObjectGuid guid)
SendPacket(&data);
}
void WorldSession::HandleTrainerListOpcode(WorldPacket& recvData)
void WorldSession::HandleTrainerListOpcode(WorldPackets::NPC::Hello& packet)
{
ObjectGuid guid;
recvData >> guid;
SendTrainerList(guid);
}
void WorldSession::SendTrainerList(ObjectGuid guid)
{
std::string str = GetAcoreString(LANG_NPC_TAINER_HELLO);
SendTrainerList(guid, str);
}
void WorldSession::SendTrainerList(ObjectGuid guid, const std::string& strTitle)
{
LOG_DEBUG("network", "WORLD: SendTrainerList");
Creature* unit = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_TRAINER);
if (!unit)
Creature* npc = GetPlayer()->GetNPCIfCanInteractWith(packet.Unit, UNIT_NPC_FLAG_TRAINER);
if (!npc)
{
LOG_DEBUG("network", "WORLD: SendTrainerList - Unit ({}) not found or you can not interact with him.", guid.ToString());
LOG_DEBUG("network", "WorldSession: SendTrainerList - {} not found or you can not interact with him.", packet.Unit.ToString().c_str());
return;
}
SendTrainerList(npc);
}
void WorldSession::SendTrainerList(Creature* npc)
{
// remove fake death
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
Trainer::Trainer const* trainer = sObjectMgr->GetTrainer(npc->GetEntry());
if (!trainer)
{
LOG_DEBUG("network", "WorldSession: SendTrainerList - trainer spells not found for {}", npc->GetGUID().ToString().c_str());
return;
}
if (!trainer->IsTrainerValidForPlayer(_player))
{
LOG_DEBUG("network", "WorldSession: SendTrainerList - trainer {} not valid for player {}", npc->GetGUID().ToString().c_str(), GetPlayerInfo().c_str());
return;
}
trainer->SendSpells(npc, _player, GetSessionDbLocaleIndex());
}
void WorldSession::HandleTrainerBuySpellOpcode(WorldPackets::NPC::TrainerBuySpell& packet)
{
LOG_DEBUG("network", "WORLD: Received CMSG_TRAINER_BUY_SPELL {}, learn spell id is: {}", packet.TrainerGUID.ToString().c_str(), packet.SpellID);
Creature* npc = GetPlayer()->GetNPCIfCanInteractWith(packet.TrainerGUID, UNIT_NPC_FLAG_TRAINER);
if (!npc)
{
LOG_DEBUG("network", "WORLD: HandleTrainerBuySpellOpcode - {} not found or you can not interact with him.", packet.TrainerGUID.ToString().c_str());
return;
}
@ -105,169 +127,11 @@ void WorldSession::SendTrainerList(ObjectGuid guid, const std::string& strTitle)
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
CreatureTemplate const* ci = unit->GetCreatureTemplate();
if (!ci)
{
LOG_DEBUG("network", "WORLD: SendTrainerList - ({}) NO CREATUREINFO!", guid.ToString());
return;
}
TrainerSpellData const* trainer_spells = unit->GetTrainerSpells();
if (!trainer_spells)
{
LOG_DEBUG("network", "WORLD: SendTrainerList - Training spells not found for creature ({})", guid.ToString());
return;
}
WorldPacket data(SMSG_TRAINER_LIST, 8 + 4 + 4 + trainer_spells->spellList.size() * 38 + strTitle.size() + 1);
data << guid;
data << uint32(trainer_spells->trainerType);
std::size_t count_pos = data.wpos();
data << uint32(trainer_spells->spellList.size());
// reputation discount
float fDiscountMod = _player->GetReputationPriceDiscount(unit);
bool can_learn_primary_prof = GetPlayer()->GetFreePrimaryProfessionPoints() > 0;
uint32 count = 0;
for (TrainerSpellMap::const_iterator itr = trainer_spells->spellList.begin(); itr != trainer_spells->spellList.end(); ++itr)
{
TrainerSpell const* tSpell = &itr->second;
bool valid = true;
bool primary_prof_first_rank = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (!tSpell->learnedSpell[i])
continue;
if (!_player->IsSpellFitByClassAndRace(tSpell->learnedSpell[i]))
{
valid = false;
break;
}
SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(tSpell->learnedSpell[i]);
if (learnedSpellInfo && learnedSpellInfo->IsPrimaryProfessionFirstRank())
primary_prof_first_rank = true;
}
if (!valid)
continue;
if (tSpell->reqSpell && !_player->HasSpell(tSpell->reqSpell))
{
continue;
}
TrainerSpellState state = _player->GetTrainerSpellState(tSpell);
data << uint32(tSpell->spell); // learned spell (or cast-spell in profession case)
data << uint8(state == TRAINER_SPELL_GREEN_DISABLED ? TRAINER_SPELL_GREEN : state);
data << uint32(std::floor(tSpell->spellCost * fDiscountMod));
data << uint32(primary_prof_first_rank && can_learn_primary_prof ? 1 : 0);
// primary prof. learn confirmation dialog
data << uint32(primary_prof_first_rank ? 1 : 0); // must be equal prev. field to have learn button in enabled state
data << uint8(tSpell->reqLevel);
data << uint32(tSpell->reqSkill);
data << uint32(tSpell->reqSkillValue);
//prev + req or req + 0
uint8 maxReq = 0;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (!tSpell->learnedSpell[i])
continue;
if (uint32 prevSpellId = sSpellMgr->GetPrevSpellInChain(tSpell->learnedSpell[i]))
{
data << uint32(prevSpellId);
++maxReq;
}
if (maxReq == 3)
break;
SpellsRequiringSpellMapBounds spellsRequired = sSpellMgr->GetSpellsRequiredForSpellBounds(tSpell->learnedSpell[i]);
for (SpellsRequiringSpellMap::const_iterator itr2 = spellsRequired.first; itr2 != spellsRequired.second && maxReq < 3; ++itr2)
{
data << uint32(itr2->second);
++maxReq;
}
if (maxReq == 3)
break;
}
while (maxReq < 3)
{
data << uint32(0);
++maxReq;
}
++count;
}
data << strTitle;
data.put<uint32>(count_pos, count);
SendPacket(&data);
}
void WorldSession::HandleTrainerBuySpellOpcode(WorldPacket& recvData)
{
ObjectGuid guid;
uint32 spellId = 0;
recvData >> guid >> spellId;
Creature* unit = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_TRAINER);
if (!unit)
{
LOG_DEBUG("network", "WORLD: HandleTrainerBuySpellOpcode - Unit ({}) not found or you can not interact with him.", guid.ToString());
return;
}
// remove fake death
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
// check present spell in trainer spell list
TrainerSpellData const* trainer_spells = unit->GetTrainerSpells();
if (!trainer_spells)
Trainer::Trainer* trainer = sObjectMgr->GetTrainer(npc->GetEntry());
if (!trainer)
return;
// not found, cheat?
TrainerSpell const* trainer_spell = trainer_spells->Find(spellId);
if (!trainer_spell)
return;
if (trainer_spell->reqSpell && !_player->HasSpell(trainer_spell->reqSpell))
{
return;
}
// can't be learn, cheat? Or double learn with lags...
if (_player->GetTrainerSpellState(trainer_spell) != TRAINER_SPELL_GREEN)
return;
// apply reputation discount
uint32 nSpellCost = uint32(std::floor(trainer_spell->spellCost * _player->GetReputationPriceDiscount(unit)));
// check money requirement
if (!_player->HasEnoughMoney(nSpellCost))
return;
_player->ModifyMoney(-int32(nSpellCost));
unit->SendPlaySpellVisual(179); // 53 SpellCastDirected
unit->SendPlaySpellImpact(_player->GetGUID(), 362); // 113 EmoteSalute
// learn explicitly or cast explicitly
if (trainer_spell->IsCastable())
_player->CastSpell(_player, trainer_spell->spell, true);
else
_player->learnSpell(spellId);
WorldPacket data(SMSG_TRAINER_BUY_SUCCEEDED, 12);
data << guid;
data << uint32(spellId); // should be same as in packet from client
SendPacket(&data);
trainer->TeachSpell(npc, _player, packet.SpellID);
}
void WorldSession::HandleGossipHelloOpcode(WorldPacket& recvData)

View File

@ -68,7 +68,7 @@ void WorldSession::HandleTalentWipeConfirmOpcode(WorldPacket& recvData)
return;
}
if (!unit->isCanTrainingAndResetTalentsOf(_player))
if (!unit->CanResetTalents(_player))
return;
// remove fake death

View File

@ -28,6 +28,7 @@
#include "ItemPackets.h"
#include "LFGPackets.h"
#include "MiscPackets.h"
#include "NPCPackets.h"
#include "PetPackets.h"
#include "QueryPackets.h"
#include "TotemPackets.h"

View File

@ -0,0 +1,69 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by the
* Free Software Foundation; either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "NPCPackets.h"
void WorldPackets::NPC::Hello::Read()
{
_worldPacket >> Unit;
}
WorldPacket const* WorldPackets::NPC::TrainerList::Write()
{
_worldPacket << TrainerGUID;
_worldPacket << int32(TrainerType);
_worldPacket << int32(Spells.size());
for (TrainerListSpell const& spell : Spells)
{
_worldPacket << int32(spell.SpellID);
_worldPacket << uint8(spell.Usable);
_worldPacket << int32(spell.MoneyCost);
_worldPacket.append(spell.PointCost.data(), spell.PointCost.size());
_worldPacket << uint8(spell.ReqLevel);
_worldPacket << int32(spell.ReqSkillLine);
_worldPacket << int32(spell.ReqSkillRank);
_worldPacket.append(spell.ReqAbility.data(), spell.ReqAbility.size());
}
_worldPacket << Greeting;
return &_worldPacket;
}
void WorldPackets::NPC::TrainerBuySpell::Read()
{
_worldPacket >> TrainerGUID;
_worldPacket >> SpellID;
}
WorldPacket const* WorldPackets::NPC::TrainerBuyFailed::Write()
{
_worldPacket << TrainerGUID;
_worldPacket << int32(SpellID);
_worldPacket << int32(TrainerFailedReason);
return &_worldPacket;
}
WorldPacket const* WorldPackets::NPC::TrainerBuySucceeded::Write()
{
_worldPacket << TrainerGUID;
_worldPacket << int32(SpellID);
return &_worldPacket;
}

View File

@ -0,0 +1,103 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by the
* Free Software Foundation; either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NPCPackets_h__
#define NPCPackets_h__
#include "Packet.h"
#include "ObjectGuid.h"
#include <array>
namespace WorldPackets::NPC
{
// CMSG_BANKER_ACTIVATE
// CMSG_BATTLEMASTER_HELLO
// CMSG_BINDER_ACTIVATE
// CMSG_GOSSIP_HELLO
// CMSG_LIST_INVENTORY
// CMSG_TRAINER_LIST
class Hello final : public ClientPacket
{
public:
explicit Hello(WorldPacket&& packet) : ClientPacket(std::move(packet)) { }
void Read() override;
ObjectGuid Unit;
};
struct TrainerListSpell
{
int32 SpellID = 0;
uint8 Usable = 0;
int32 MoneyCost = 0;
std::array<int32, 2> PointCost = { }; // compared with PLAYER_CHARACTER_POINTS in Lua
uint8 ReqLevel = 0;
int32 ReqSkillLine = 0;
int32 ReqSkillRank = 0;
std::array<int32, 3> ReqAbility = { };
};
class TrainerList final : public ServerPacket
{
public:
TrainerList() : ServerPacket(SMSG_TRAINER_LIST) { }
WorldPacket const* Write() override;
ObjectGuid TrainerGUID;
int32 TrainerType = 0;
std::vector<TrainerListSpell> Spells;
std::string Greeting;
};
class TrainerBuySpell final : public ClientPacket
{
public:
explicit TrainerBuySpell(WorldPacket&& packet) : ClientPacket(CMSG_TRAINER_BUY_SPELL, std::move(packet)) { }
void Read() override;
ObjectGuid TrainerGUID;
int32 SpellID = 0;
};
class TrainerBuyFailed final : public ServerPacket
{
public:
TrainerBuyFailed() : ServerPacket(SMSG_TRAINER_BUY_FAILED, 8 + 4 + 4) { }
WorldPacket const* Write() override;
ObjectGuid TrainerGUID;
int32 SpellID = 0;
int32 TrainerFailedReason = 0;
};
class TrainerBuySucceeded final : public ServerPacket
{
public:
TrainerBuySucceeded() : ServerPacket(SMSG_TRAINER_BUY_SUCCEEDED, 8 + 4) { }
WorldPacket const* Write() override;
ObjectGuid TrainerGUID;
int32 SpellID = 0;
};
}
#endif // NPCPackets_h__

View File

@ -198,6 +198,12 @@ namespace WorldPackets
class ArenaTeam;
class CalendarComplain;
}
namespace NPC
{
class Hello;
class TrainerBuySpell;
}
}
enum AccountDataType
@ -478,8 +484,7 @@ public:
//void SendTestCreatureQueryOpcode(uint32 entry, ObjectGuid guid, uint32 testvalue);
void SendNameQueryOpcode(ObjectGuid guid);
void SendTrainerList(ObjectGuid guid);
void SendTrainerList(ObjectGuid guid, std::string const& strTitle);
void SendTrainerList(Creature* npc);
void SendListInventory(ObjectGuid guid, uint32 vendorEntry = 0);
void SendShowBank(ObjectGuid guid);
bool CanOpenMailBox(ObjectGuid guid);
@ -785,8 +790,8 @@ public: // opcodes handlers
void SendActivateTaxiReply(ActivateTaxiReply reply);
void HandleTabardVendorActivateOpcode(WorldPacket& recvPacket);
void HandleTrainerListOpcode(WorldPacket& recvPacket);
void HandleTrainerBuySpellOpcode(WorldPacket& recvPacket);
void HandleTrainerListOpcode(WorldPackets::NPC::Hello& packet);
void HandleTrainerBuySpellOpcode(WorldPackets::NPC::TrainerBuySpell& packet);
void HandlePetitionShowListOpcode(WorldPacket& recvPacket);
void HandleGossipHelloOpcode(WorldPacket& recvPacket);
void HandleGossipSelectOptionOpcode(WorldPacket& recvPacket);

View File

@ -605,9 +605,9 @@ uint32 SpellMgr::GetSpellWithRank(uint32 spell_id, uint32 rank, bool strict) con
return spell_id;
}
SpellRequiredMapBounds SpellMgr::GetSpellsRequiredForSpellBounds(uint32 spell_id) const
Acore::IteratorPair<SpellRequiredMap::const_iterator> SpellMgr::GetSpellsRequiredForSpellBounds(uint32 spell_id) const
{
return mSpellReq.equal_range(spell_id);
return Acore::Containers::MapEqualRange(mSpellReq, spell_id);
}
SpellsRequiringSpellMapBounds SpellMgr::GetSpellsRequiringSpellBounds(uint32 spell_id) const

View File

@ -22,6 +22,7 @@
#include "Common.h"
#include "Log.h"
#include "IteratorPair.h"
#include "SharedDefines.h"
#include "Unit.h"
@ -669,7 +670,7 @@ public:
[[nodiscard]] uint32 GetSpellWithRank(uint32 spell_id, uint32 rank, bool strict = false) const;
// Spell Required table
[[nodiscard]] SpellRequiredMapBounds GetSpellsRequiredForSpellBounds(uint32 spell_id) const;
[[nodiscard]] Acore::IteratorPair<SpellRequiredMap::const_iterator>GetSpellsRequiredForSpellBounds(uint32 spell_id) const;
[[nodiscard]] SpellsRequiringSpellMapBounds GetSpellsRequiringSpellBounds(uint32 spell_id) const;
[[nodiscard]] bool IsSpellRequiringSpell(uint32 spellid, uint32 req_spellid) const;

View File

@ -766,6 +766,12 @@ void World::SetInitialWorldSettings()
LOG_INFO("server.loading", "Loading GameTeleports...");
sObjectMgr->LoadGameTele();
LOG_INFO("server.loading", "Loading Trainers..."); // must be after LoadCreatureTemplates
sObjectMgr->LoadTrainers();
LOG_INFO("server.loading", "Loading Creature default trainers...");
sObjectMgr->LoadCreatureDefaultTrainers();
LOG_INFO("server.loading", "Loading Gossip Menu...");
sObjectMgr->LoadGossipMenu();
@ -775,9 +781,6 @@ void World::SetInitialWorldSettings()
LOG_INFO("server.loading", "Loading Vendors...");
sObjectMgr->LoadVendors(); // must be after load CreatureTemplate and ItemTemplate
LOG_INFO("server.loading", "Loading Trainers...");
sObjectMgr->LoadTrainerSpell(); // must be after load CreatureTemplate
LOG_INFO("server.loading", "Loading Waypoints...");
sWaypointMgr->Load();

View File

@ -129,7 +129,7 @@ public:
{ "mail_server_template", HandleReloadMailServerTemplateCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "milling_loot_template", HandleReloadLootTemplatesMillingCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "npc_spellclick_spells", HandleReloadSpellClickSpellsCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "npc_trainer", HandleReloadNpcTrainerCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "trainer", HandleReloadTrainerCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "npc_vendor", HandleReloadNpcVendorCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "game_event_npc_vendor", HandleReloadGameEventNPCVendorCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "page_text", HandleReloadPageTextsCommand, SEC_ADMINISTRATOR, Console::Yes },
@ -254,7 +254,7 @@ public:
static bool HandleReloadAllNpcCommand(ChatHandler* handler)
{
HandleReloadNpcTrainerCommand(handler);
HandleReloadTrainerCommand(handler);
HandleReloadNpcVendorCommand(handler);
HandleReloadPointsOfInterestCommand(handler);
HandleReloadSpellClickSpellsCommand(handler);
@ -746,11 +746,15 @@ public:
return true;
}
static bool HandleReloadNpcTrainerCommand(ChatHandler* handler)
static bool HandleReloadTrainerCommand(ChatHandler* handler)
{
LOG_INFO("server.loading", "Reloading `npc_trainer` Table!");
sObjectMgr->LoadTrainerSpell();
handler->SendGlobalGMSysMessage("DB table `npc_trainer` reloaded.");
LOG_INFO("server.loading", "Reloading `trainer` Tables!");
sObjectMgr->LoadTrainers();
sObjectMgr->LoadCreatureDefaultTrainers();
handler->SendGlobalGMSysMessage("DB table `trainer` reloaded.");
handler->SendGlobalGMSysMessage("DB table `trainer_locale` reloaded.");
handler->SendGlobalGMSysMessage("DB table `trainer_spell` reloaded.");
handler->SendGlobalGMSysMessage("DB table `creature_default_trainer` reloaded.");
return true;
}

View File

@ -709,7 +709,7 @@ public:
switch (action)
{
case GOSSIP_ACTION_TRAIN:
player->GetSession()->SendTrainerList(creature->GetGUID());
player->GetSession()->SendTrainerList(creature);
break;
case GOSSIP_ACTION_TRADE:
player->GetSession()->SendListInventory(creature->GetGUID());

View File

@ -514,7 +514,7 @@ public:
player->GetSession()->SendListInventory(creature->GetGUID());
break;
case GOSSIP_ACTION_TRAIN:
player->GetSession()->SendTrainerList(creature->GetGUID());
player->GetSession()->SendTrainerList(creature);
break;
//Learn Alchemy
case GOSSIP_ACTION_INFO_DEF + 1:
@ -707,7 +707,7 @@ public:
player->GetSession()->SendListInventory(creature->GetGUID());
break;
case GOSSIP_ACTION_TRAIN:
player->GetSession()->SendTrainerList(creature->GetGUID());
player->GetSession()->SendTrainerList(creature);
break;
//Learn Armor/Weapon
case GOSSIP_ACTION_INFO_DEF + 1:
@ -1019,7 +1019,7 @@ public:
switch (action)
{
case GOSSIP_ACTION_TRAIN:
player->GetSession()->SendTrainerList(creature->GetGUID());
player->GetSession()->SendTrainerList(creature);
break;
case GOSSIP_MENU_UNLEARN_CONFIRM_DRAGONSCALE:
AddGossipItemFor(player, GOSSIP_MENU_UNLEARN_CONFIRM_DRAGONSCALE, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_DRAGONSCALE, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1, DoMedUnlearnCost(player));
@ -1121,7 +1121,7 @@ public:
player->GetSession()->SendListInventory(creature->GetGUID());
break;
case GOSSIP_ACTION_TRAIN:
player->GetSession()->SendTrainerList(creature->GetGUID());
player->GetSession()->SendTrainerList(creature);
break;
//Learn Tailor
case GOSSIP_ACTION_INFO_DEF + 1:

View File

@ -2623,16 +2623,6 @@ enum LockType
LOCKTYPE_OPEN_FROM_VEHICLE = 21
};
enum TrainerType // this is important type for npcs!
{
TRAINER_TYPE_CLASS = 0,
TRAINER_TYPE_MOUNTS = 1, // on blizz it's 2
TRAINER_TYPE_TRADESKILLS = 2,
TRAINER_TYPE_PETS = 3
};
#define MAX_TRAINER_TYPE 4
// CreatureType.dbc
enum CreatureType
{