From 0aa7df2f5303fa54868cd2f1471cf3797af143f5 Mon Sep 17 00:00:00 2001 From: hondacrx Date: Thu, 27 Jul 2017 17:29:22 -0400 Subject: [PATCH] Rewrite trainer handling to properly support multiple trainers on the same creature --- Framework/Constants/AccountConst.cs | 2 +- Framework/Constants/GossipConst.cs | 2 +- Framework/Constants/SharedConst.cs | 17 +- Framework/Database/Databases/WorldDatabase.cs | 2 +- Game/Chat/Commands/ReloadCommand.cs | 22 +- Game/Entities/Creature/Creature.cs | 11 +- Game/Entities/Creature/CreatureData.cs | 36 -- Game/Entities/Creature/Gossip.cs | 32 +- Game/Entities/Creature/Trainer.cs | 167 +++++++++ Game/Entities/Player/Player.Fields.cs | 2 + Game/Entities/Player/Player.cs | 114 +----- Game/Game.csproj | 1 + Game/Globals/ObjectManager.cs | 336 +++++++++--------- Game/Handlers/NPCHandler.cs | 151 +------- Game/Handlers/SkillHandler.cs | 6 + Game/Network/Packets/NPCPackets.cs | 16 +- Game/Server/WorldManager.cs | 10 +- Scripts/World/NpcSpecial.cs | 15 - 18 files changed, 434 insertions(+), 508 deletions(-) create mode 100644 Game/Entities/Creature/Trainer.cs diff --git a/Framework/Constants/AccountConst.cs b/Framework/Constants/AccountConst.cs index 933ddeae7..84a04702e 100644 --- a/Framework/Constants/AccountConst.cs +++ b/Framework/Constants/AccountConst.cs @@ -578,7 +578,7 @@ namespace Framework.Constants CommandReloadMailLootTemplate = 669, CommandReloadMillingLootTemplate = 670, CommandReloadNpcSpellclickSpells = 671, - CommandReloadNpcTrainer = 672, + CommandReloadTrainer = 672, CommandReloadNpcVendor = 673, CommandReloadPageText = 674, CommandReloadPickpocketingLootTemplate = 675, diff --git a/Framework/Constants/GossipConst.cs b/Framework/Constants/GossipConst.cs index 02b02cf9d..05cce01ab 100644 --- a/Framework/Constants/GossipConst.cs +++ b/Framework/Constants/GossipConst.cs @@ -36,7 +36,7 @@ namespace Framework.Constants Stablepet = 14, //Unit_Npc_Flag_Stable (4194304) Armorer = 15, //Unit_Npc_Flag_Armorer (4096) Unlearntalents = 16, //Unit_Npc_Flag_Trainer (16) (Bonus Option For Trainer) - Unlearnpettalents = 17, //Unit_Npc_Flag_Trainer (16) (Bonus Option For Trainer) + Unlearnpettalents_Old = 17, // deprecated Learndualspec = 18, //Unit_Npc_Flag_Trainer (16) (Bonus Option For Trainer) Outdoorpvp = 19, //Added By Code (Option For Outdoor Pvp Creatures) Max diff --git a/Framework/Constants/SharedConst.cs b/Framework/Constants/SharedConst.cs index a0d59df35..5e00e987c 100644 --- a/Framework/Constants/SharedConst.cs +++ b/Framework/Constants/SharedConst.cs @@ -571,17 +571,22 @@ namespace Framework.Constants public enum TrainerType { - Class = 0, - Mounts = 1, + None = 0, + Talent = 1, Tradeskills = 2, Pets = 3 } public enum TrainerSpellState { - Gray = 0, - Green = 1, - Red = 2, - GreenDisabled = 10 + Known = 0, + Available = 1, + Unavailable = 2, + } + + public enum TrainerFailReason + { + Unavailable = 0, + NotEnoughMoney = 1 } public enum ChatMsg : uint diff --git a/Framework/Database/Databases/WorldDatabase.cs b/Framework/Database/Databases/WorldDatabase.cs index b7e5e303a..272f8214c 100644 --- a/Framework/Database/Databases/WorldDatabase.cs +++ b/Framework/Database/Databases/WorldDatabase.cs @@ -75,7 +75,7 @@ namespace Framework.Database PrepareStatement(WorldStatements.SEL_WAYPOINT_SCRIPT_ID_BY_GUID, "SELECT id FROM waypoint_scripts WHERE guid = ?"); PrepareStatement(WorldStatements.DEL_CREATURE, "DELETE FROM creature WHERE guid = ?"); PrepareStatement(WorldStatements.SEL_COMMANDS, "SELECT name, permission, help FROM command"); - PrepareStatement(WorldStatements.SEL_CREATURE_TEMPLATE, "SELECT entry, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, modelid1, modelid2, modelid3, modelid4, name, femaleName, subname, IconName, gossip_menu_id, minlevel, maxlevel, HealthScalingExpansion, RequiredExpansion, VignetteID, faction, npcflag, speed_walk, speed_run, scale, rank, dmgschool, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, unit_class, unit_flags, unit_flags2, unit_flags3, dynamicflags, family, trainer_type, trainer_class, trainer_race, type, type_flags, type_flags2, lootid, pickpocketloot, skinloot, resistance1, resistance2, resistance3, resistance4, resistance5, resistance6, spell1, spell2, spell3, spell4, spell5, spell6, spell7, spell8, VehicleId, mingold, maxgold, AIName, MovementType, InhabitType, HoverHeight, HealthModifier, HealthModifierExtra, ManaModifier, ManaModifierExtra, ArmorModifier, DamageModifier, ExperienceModifier, RacialLeader, movementId, RegenHealth, mechanic_immune_mask, flags_extra, ScriptName FROM creature_template WHERE entry = ?"); + PrepareStatement(WorldStatements.SEL_CREATURE_TEMPLATE, "SELECT entry, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, modelid1, modelid2, modelid3, modelid4, name, femaleName, subname, IconName, gossip_menu_id, minlevel, maxlevel, HealthScalingExpansion, RequiredExpansion, VignetteID, faction, npcflag, speed_walk, speed_run, scale, rank, dmgschool, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, unit_class, unit_flags, unit_flags2, unit_flags3, dynamicflags, family, trainer_class, type, type_flags, type_flags2, lootid, pickpocketloot, skinloot, resistance1, resistance2, resistance3, resistance4, resistance5, resistance6, spell1, spell2, spell3, spell4, spell5, spell6, spell7, spell8, VehicleId, mingold, maxgold, AIName, MovementType, InhabitType, HoverHeight, HealthModifier, HealthModifierExtra, ManaModifier, ManaModifierExtra, ArmorModifier, DamageModifier, ExperienceModifier, RacialLeader, movementId, RegenHealth, mechanic_immune_mask, flags_extra, ScriptName FROM creature_template WHERE entry = ?"); PrepareStatement(WorldStatements.SEL_WAYPOINT_SCRIPT_BY_ID, "SELECT guid, delay, command, datalong, datalong2, dataint, x, y, z, o FROM waypoint_scripts WHERE id = ?"); PrepareStatement(WorldStatements.SEL_CREATURE_BY_ID, "SELECT guid FROM creature WHERE id = ?"); PrepareStatement(WorldStatements.SEL_GAMEOBJECT_NEAREST, "SELECT guid, id, position_x, position_y, position_z, map, (POW(position_x - ?, 2) + POW(position_y - ?, 2) + POW(position_z - ?, 2)) AS order_ FROM gameobject WHERE map = ? AND (POW(position_x - ?, 2) + POW(position_y - ?, 2) + POW(position_z - ?, 2)) <= ? ORDER BY order_"); diff --git a/Game/Chat/Commands/ReloadCommand.cs b/Game/Chat/Commands/ReloadCommand.cs index 838d8a9f1..36e069836 100644 --- a/Game/Chat/Commands/ReloadCommand.cs +++ b/Game/Chat/Commands/ReloadCommand.cs @@ -527,15 +527,6 @@ namespace Game.Chat return true; } - [Command("npc_trainer", RBACPermissions.CommandReloadNpcTrainer, true)] - static bool HandleReloadNpcTrainerCommand(StringArguments args, CommandHandler handler) - { - Log.outInfo(LogFilter.Server, "Re-Loading `npc_trainer` Table!"); - Global.ObjectMgr.LoadTrainerSpell(); - handler.SendGlobalGMSysMessage("DB table `npc_trainer` reloaded."); - return true; - } - [Command("npc_vendor", RBACPermissions.CommandReloadNpcVendor, true)] static bool HandleReloadNpcVendorCommand(StringArguments args, CommandHandler handler) { @@ -885,6 +876,17 @@ namespace Game.Chat return true; } + [Command("trainer", RBACPermissions.CommandReloadTrainer, true)] + static bool HandleReloadTrainerCommand(StringArguments args, CommandHandler handler) + { + Log.outInfo(LogFilter.Misc, "Re-Loading `trainer` Table!"); + Global.ObjectMgr.LoadTrainers(); + handler.SendGlobalGMSysMessage("DB table `trainer` reloaded."); + handler.SendGlobalGMSysMessage("DB table `trainer_locale` reloaded."); + handler.SendGlobalGMSysMessage("DB table `trainer_spell` reloaded."); + return true; + } + [Command("vehicle_accessory", RBACPermissions.CommandReloadVehicleAccesory, true)] static bool HandleReloadVehicleAccessoryCommand(StringArguments args, CommandHandler handler) { @@ -1051,7 +1053,7 @@ namespace Game.Chat { if (args != null) // will be reloaded from all_gossips { - HandleReloadNpcTrainerCommand(null, handler); + HandleReloadTrainerCommand(null, handler); HandleReloadNpcVendorCommand(null, handler); HandleReloadPointsOfInterestCommand(null, handler); HandleReloadSpellClickSpellsCommand(null, handler); diff --git a/Game/Entities/Creature/Creature.cs b/Game/Entities/Creature/Creature.cs index 51e606317..ce5cd14df 100644 --- a/Game/Entities/Creature/Creature.cs +++ b/Game/Entities/Creature/Creature.cs @@ -860,11 +860,9 @@ namespace Game.Entities return true; } - public bool isCanTrainingAndResetTalentsOf(Player player) + public bool CanResetTalents(Player player) { - return player.getLevel() >= 10 - && GetCreatureTemplate().TrainerType == TrainerType.Class - && player.GetClass() == GetCreatureTemplate().TrainerClass; + return player.getLevel() >= 15 && player.GetClass() == GetCreatureTemplate().TrainerClass; } public void SetTextRepeatId(byte textGroup, byte id) @@ -2457,11 +2455,6 @@ namespace Game.Entities return vCount.count; } - public TrainerSpellData GetTrainerSpells() - { - return Global.ObjectMgr.GetNpcTrainerSpells(GetEntry()); - } - public override string GetName(LocaleConstant locale_idx = LocaleConstant.enUS) { if (locale_idx != LocaleConstant.enUS) diff --git a/Game/Entities/Creature/CreatureData.cs b/Game/Entities/Creature/CreatureData.cs index 2c66e0e12..52f833777 100644 --- a/Game/Entities/Creature/CreatureData.cs +++ b/Game/Entities/Creature/CreatureData.cs @@ -58,9 +58,7 @@ namespace Game.Entities public uint UnitFlags3; public uint DynamicFlags; public CreatureFamily Family; - public TrainerType TrainerType; public Class TrainerClass; - public Race TrainerRace; public CreatureType CreatureType; public CreatureTypeFlags TypeFlags; public uint TypeFlags2; @@ -386,38 +384,4 @@ namespace Game.Entities m_items.Clear(); } } - - public class TrainerSpell - { - public uint SpellID; - public uint MoneyCost; - public uint ReqSkillLine; - public uint ReqSkillRank; - public uint ReqLevel; - public uint[] ReqAbility = new uint[SharedConst.MaxTrainerspellAbilityReqs]; - public uint Index; - - // helpers - public bool IsCastable() - { - return ReqAbility[0] != SpellID; - } - } - - public class TrainerSpellData - { - public TrainerSpellData() - { - trainerType = 0; - } - - public Dictionary spellList = new Dictionary(); - public uint trainerType; // 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. - - public TrainerSpell Find(uint spell_id) - { - return spellList.LookupByKey(spell_id); - } - } } diff --git a/Game/Entities/Creature/Gossip.cs b/Game/Entities/Creature/Gossip.cs index 6976af86b..c56380b23 100644 --- a/Game/Entities/Creature/Gossip.cs +++ b/Game/Entities/Creature/Gossip.cs @@ -30,22 +30,22 @@ namespace Game.Misc { public class GossipMenu { - public void AddMenuItem(int menuItemId, GossipOptionIcon icon, string message, uint sender, uint action, string boxMessage, uint boxMoney, bool coded = false) + public uint AddMenuItem(int optionIndex, GossipOptionIcon icon, string message, uint sender, uint action, string boxMessage, uint boxMoney, bool coded = false) { Contract.Assert(_menuItems.Count <= SharedConst.MaxGossipMenuItems); // Find a free new id - script case - if (menuItemId == -1) + if (optionIndex == -1) { - menuItemId = 0; + optionIndex = 0; if (!_menuItems.Empty()) { foreach (var item in _menuItems) { - if (item.Key > menuItemId) + if (item.Key > optionIndex) break; - menuItemId = (int)item.Key + 1; + optionIndex = (int)item.Key + 1; } } } @@ -60,7 +60,8 @@ namespace Game.Misc menuItem.BoxMessage = boxMessage; menuItem.BoxMoney = boxMoney; - _menuItems[(uint)menuItemId] = menuItem; + _menuItems[(uint)optionIndex] = menuItem; + return (uint)optionIndex; } /// @@ -124,18 +125,20 @@ namespace Game.Misc } // Add menu item with existing method. Menu item id -1 is also used in ADD_GOSSIP_ITEM macro. - AddMenuItem(-1, item.OptionIcon, strOptionText, sender, action, strBoxText, item.BoxMoney, item.BoxCoded); + uint optionIndex = AddMenuItem(-1, item.OptionIcon, strOptionText, sender, action, strBoxText, item.BoxMoney, item.BoxCoded); + AddGossipMenuItemData(optionIndex, item.ActionMenuId, item.ActionPoiId, item.TrainerId); } } - public void AddGossipMenuItemData(uint menuItemId, uint gossipActionMenuId, uint gossipActionPoi) + public void AddGossipMenuItemData(uint optionIndex, uint gossipActionMenuId, uint gossipActionPoi, uint trainerId) { GossipMenuItemData itemData = new GossipMenuItemData(); itemData.GossipActionMenuId = gossipActionMenuId; itemData.GossipActionPoi = gossipActionPoi; + itemData.TrainerId = trainerId; - _menuItemData[menuItemId] = itemData; + _menuItemData[optionIndex] = itemData; } public uint GetMenuItemSender(uint menuItemId) @@ -162,6 +165,15 @@ namespace Game.Misc return false; } + public bool HasMenuItemType(uint optionType) + { + foreach (var menuItemPair in _menuItems) + if (menuItemPair.Value.OptionType == optionType) + return true; + + return false; + } + public void ClearMenu() { _menuItems.Clear(); @@ -796,6 +808,7 @@ namespace Game.Misc { public uint GossipActionMenuId; // MenuId of the gossip triggered by this action public uint GossipActionPoi; + public uint TrainerId; } public struct NpcTextData @@ -829,6 +842,7 @@ namespace Game.Misc public uint BoxMoney; public string BoxText; public uint BoxBroadcastTextId; + public uint TrainerId; public List Conditions = new List(); } diff --git a/Game/Entities/Creature/Trainer.cs b/Game/Entities/Creature/Trainer.cs new file mode 100644 index 000000000..aaa78270a --- /dev/null +++ b/Game/Entities/Creature/Trainer.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Framework.Constants; +using Game.Network.Packets; +using Game.Spells; + +namespace Game.Entities +{ + public class TrainerSpell + { + public uint SpellId; + public uint MoneyCost; + public uint ReqSkillLine; + public uint ReqSkillRank; + public Array ReqAbility = new Array(3); + public byte ReqLevel; + + public uint CastSpellId; + } + + public class Trainer + { + public Trainer(uint id, TrainerType type, string greeting, List spells) + { + _id = id; + _type = type; + _spells = spells; + + _greeting[(int)LocaleConstant.enUS] = greeting; + } + + public void SendSpells(Creature npc, Player player, LocaleConstant locale) + { + float reputationDiscount = player.GetReputationPriceDiscount(npc); + + TrainerList trainerList = new TrainerList(); + trainerList.TrainerGUID = npc.GetGUID(); + trainerList.TrainerType = (int)_type; + trainerList.TrainerID = (int)_id; + trainerList.Greeting = GetGreeting(locale); + + foreach (TrainerSpell trainerSpell in _spells) + { + if (!player.IsSpellFitByClassAndRace(trainerSpell.SpellId)) + continue; + + TrainerListSpell trainerListSpell = new TrainerListSpell(); + trainerListSpell.SpellID = trainerSpell.SpellId; + trainerListSpell.MoneyCost = (uint)(trainerSpell.MoneyCost * reputationDiscount); + trainerListSpell.ReqSkillLine = trainerSpell.ReqSkillLine; + trainerListSpell.ReqSkillRank = trainerSpell.ReqSkillRank; + trainerListSpell.ReqAbility = trainerSpell.ReqAbility.ToArray(); + trainerListSpell.Usable = GetSpellState(player, trainerSpell); + trainerListSpell.ReqLevel = trainerSpell.ReqLevel; + trainerList.Spells.Add(trainerListSpell); + } + + player.SendPacket(trainerList); + } + + public void TeachSpell(Creature npc, Player player, uint spellId) + { + TrainerSpell trainerSpell = GetSpell(spellId); + if (trainerSpell == null || !CanTeachSpell(player, trainerSpell)) + { + SendTeachFailure(npc, player, spellId, TrainerFailReason.Unavailable); + return; + } + + float reputationDiscount = player.GetReputationPriceDiscount(npc); + long moneyCost = (long)(trainerSpell.MoneyCost * reputationDiscount); + if (!player.HasEnoughMoney(moneyCost)) + { + SendTeachFailure(npc, player, spellId, TrainerFailReason.NotEnoughMoney); + return; + } + + player.ModifyMoney(-moneyCost); + + npc.SendPlaySpellVisualKit(179, 0, 0); // 53 SpellCastDirected + player.SendPlaySpellVisualKit(362, 1, 0); // 113 EmoteSalute + + // learn explicitly or cast explicitly + if (trainerSpell.CastSpellId != 0) + player.CastSpell(player, trainerSpell.CastSpellId, true); + else + player.LearnSpell(trainerSpell.SpellId, false); + } + + TrainerSpell GetSpell(uint spellId) + { + return _spells.Find(trainerSpell => trainerSpell.SpellId == spellId); + } + + bool CanTeachSpell(Player player, TrainerSpell trainerSpell) + { + TrainerSpellState state = GetSpellState(player, trainerSpell); + if (state != TrainerSpellState.Available) + return false; + + SpellInfo trainerSpellInfo = Global.SpellMgr.GetSpellInfo(trainerSpell.SpellId); + if (trainerSpellInfo.IsPrimaryProfessionFirstRank() && player.GetFreePrimaryProfessionPoints() == 0) + return false; + + return true; + } + + TrainerSpellState GetSpellState(Player player, TrainerSpell trainerSpell) + { + if (player.HasSpell(trainerSpell.SpellId)) + return TrainerSpellState.Known; + + // check race/class requirement + if (!player.IsSpellFitByClassAndRace(trainerSpell.SpellId)) + return TrainerSpellState.Unavailable; + + // check skill requirement + if (trainerSpell.ReqSkillLine != 0 && player.GetBaseSkillValue((SkillType)trainerSpell.ReqSkillLine) < trainerSpell.ReqSkillRank) + return TrainerSpellState.Unavailable; + + foreach (uint reqAbility in trainerSpell.ReqAbility) + if (reqAbility != 0 && !player.HasSpell(reqAbility)) + return TrainerSpellState.Unavailable; + + // check level requirement + if (player.getLevel() < trainerSpell.ReqLevel) + return TrainerSpellState.Unavailable; + + // check additional spell requirement + foreach (var spellId in Global.SpellMgr.GetSpellsRequiredForSpellBounds(trainerSpell.SpellId)) + if (!player.HasSpell(spellId)) + return TrainerSpellState.Unavailable; + + return TrainerSpellState.Available; + } + + void SendTeachFailure(Creature npc, Player player, uint spellId, TrainerFailReason reason) + { + TrainerBuyFailed trainerBuyFailed = new TrainerBuyFailed(); + trainerBuyFailed.TrainerGUID = npc.GetGUID(); + trainerBuyFailed.SpellID = spellId; + trainerBuyFailed.TrainerFailedReason = reason; + player.SendPacket(trainerBuyFailed); + } + + string GetGreeting(LocaleConstant locale) + { + if (_greeting[(int)locale].IsEmpty()) + return _greeting[(int)LocaleConstant.enUS]; + + return _greeting[(int)locale]; + } + + public void AddGreetingLocale(LocaleConstant locale, string greeting) + { + _greeting[(int)locale] = greeting; + } + + uint _id; + TrainerType _type; + List _spells; + Array _greeting = new Array((int)LocaleConstant.Total); + } +} diff --git a/Game/Entities/Player/Player.Fields.cs b/Game/Entities/Player/Player.Fields.cs index a3d9e0b1e..bfc40a08d 100644 --- a/Game/Entities/Player/Player.Fields.cs +++ b/Game/Entities/Player/Player.Fields.cs @@ -140,6 +140,8 @@ namespace Game.Entities RestMgr _restMgr; + uint _currentTrainerId; + //Combat int[] baseRatingValue = new int[(int)CombatRating.Max]; public float[][] m_auraBaseMod = new float[(int)BaseModGroup.End][]; diff --git a/Game/Entities/Player/Player.cs b/Game/Entities/Player/Player.cs index f509ef603..fe40fe148 100644 --- a/Game/Entities/Player/Player.cs +++ b/Game/Entities/Player/Player.cs @@ -1034,6 +1034,9 @@ namespace Game.Entities public RestMgr GetRestMgr() { return _restMgr; } + public uint GetCurrentTrainerId() { return _currentTrainerId; } + public void SetCurrentTrainerId(uint trainerId) { _currentTrainerId = trainerId; } + public bool IsAdvancedCombatLoggingEnabled() { return _advancedCombatLoggingEnabled; } public void SetAdvancedCombatLogging(bool enabled) { _advancedCombatLoggingEnabled = enabled; } @@ -1093,26 +1096,6 @@ namespace Game.Entities m_temporaryUnsummonedPetNumber = 0; } - public void ResetPetTalents() - { - /* ODO: 6.x remove/update pet talents - // This needs another gossip option + NPC text as a confirmation. - // The confirmation gossip listid has the text: "Yes, please do." - Pet pet = GetPet(); - - if (!pet || pet.getPetType() != PetType.Hunter || pet.m_usedTalentCount == 0) - return; - - CharmInfo charmInfo = pet.GetCharmInfo(); - if (charmInfo == null) - { - Log.outError(LogFilter.Player, "Object (GUID: {0} TypeId: {1}) is considered pet-like but doesn't have a charminfo!", pet.GetGUID().ToString(), pet.GetTypeId()); - return; - } - pet.resetTalents(); - SendTalentsInfoData(true); - */ - } public bool IsPetNeedBeTemporaryUnsummoned() { @@ -2352,10 +2335,10 @@ namespace Game.Entities foreach (var menuItems in menuItemBounds) { - bool canTalk = true; if (!Global.ConditionMgr.IsObjectMeetToConditions(this, source, menuItems.Conditions)) continue; + bool canTalk = true; GameObject go = source.ToGameObject(); Creature creature = source.ToCreature(); if (creature) @@ -2384,11 +2367,7 @@ namespace Game.Entities canTalk = false; break; case GossipOption.Unlearntalents: - if (!creature.isCanTrainingAndResetTalentsOf(this)) - canTalk = false; - break; - case GossipOption.Unlearnpettalents: - if (GetPet() == null || GetPet().getPetType() != PetType.Hunter || GetPet().m_spells.Count <= 1 || creature.GetCreatureTemplate().TrainerType != TrainerType.Pets || creature.GetCreatureTemplate().TrainerClass != Class.Hunter) + if (!creature.CanResetTalents(this)) canTalk = false; break; case GossipOption.Taxivendor: @@ -2407,11 +2386,6 @@ namespace Game.Entities canTalk = false; break; case GossipOption.Trainer: - if (GetClass() != creature.GetCreatureTemplate().TrainerClass && creature.GetCreatureTemplate().TrainerType == TrainerType.Class) - Log.outError(LogFilter.Sql, "GOSSIP_OPTION_TRAINER. Player {0} (GUID: {1}) request wrong gossip menu: {2} with wrong class: {3} at Creature: {4} (Entry: {5}, Trainer Class: {6})", - GetName(), GetGUID().ToString(), menu.GetGossipMenu().GetMenuId(), GetClass(), creature.GetName(), creature.GetEntry(), creature.GetCreatureTemplate().TrainerClass); - canTalk = false; - break; case GossipOption.Gossip: case GossipOption.Spiritguide: case GossipOption.Innkeeper: @@ -2482,7 +2456,7 @@ namespace Game.Entities } menu.GetGossipMenu().AddMenuItem((int)menuItems.OptionIndex, menuItems.OptionIcon, strOptionText, 0, (uint)menuItems.OptionType, strBoxText, menuItems.BoxMoney, menuItems.BoxCoded); - menu.GetGossipMenu().AddGossipMenuItemData(menuItems.OptionIndex, menuItems.ActionMenuId, menuItems.ActionPoiId); + menu.GetGossipMenu().AddGossipMenuItemData(menuItems.OptionIndex, menuItems.ActionMenuId, menuItems.ActionPoiId, menuItems.TrainerId); } } } @@ -2580,7 +2554,7 @@ namespace Game.Entities GetSession().SendStablePet(guid); break; case GossipOption.Trainer: - GetSession().SendTrainerList(guid); + GetSession().SendTrainerList(guid, menuItemData.TrainerId); break; case GossipOption.Learndualspec: break; @@ -2588,10 +2562,6 @@ namespace Game.Entities PlayerTalkClass.SendCloseGossip(); SendRespecWipeConfirm(guid, GetNextResetTalentsCost()); break; - case GossipOption.Unlearnpettalents: - PlayerTalkClass.SendCloseGossip(); - ResetPetTalents(); - break; case GossipOption.Taxivendor: GetSession().SendTaxiMenu(source.ToCreature()); break; @@ -5484,76 +5454,6 @@ namespace Game.Entities return null; } - public TrainerSpellState GetTrainerSpellState(TrainerSpell trainer_spell) - { - if (trainer_spell == null) - return TrainerSpellState.Red; - - bool hasSpell = true; - for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i) - { - if (trainer_spell.ReqAbility[i] == 0) - continue; - - if (!HasSpell(trainer_spell.ReqAbility[i])) - { - hasSpell = false; - break; - } - } - // known spell - if (hasSpell) - return TrainerSpellState.Gray; - - // check skill requirement - if (trainer_spell.ReqSkillLine != 0 && GetBaseSkillValue((SkillType)trainer_spell.ReqSkillLine) < trainer_spell.ReqSkillRank) - return TrainerSpellState.Red; - - // check level requirement - if (getLevel() < trainer_spell.ReqLevel) - return TrainerSpellState.Red; - - for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i) - { - if (trainer_spell.ReqAbility[i] == 0) - continue; - - // check race/class requirement - if (!IsSpellFitByClassAndRace(trainer_spell.ReqAbility[i])) - return TrainerSpellState.Red; - - uint prevSpell = Global.SpellMgr.GetPrevSpellInChain(trainer_spell.ReqAbility[i]); - if (prevSpell != 0) - { - // check prev.rank requirement - if (!HasSpell(prevSpell)) - return TrainerSpellState.Red; - } - - var spellsRequired = Global.SpellMgr.GetSpellsRequiredForSpellBounds(trainer_spell.ReqAbility[i]); - foreach (var spellId in spellsRequired) - { - // check additional spell requirement - if (!HasSpell(spellId)) - return TrainerSpellState.Red; - } - } - - // check primary prof. limit - // first rank of primary profession spell when there are no proffesions avalible is disabled - for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i) - { - if (trainer_spell.ReqAbility[i] == 0) - continue; - - SpellInfo learnedSpellInfo = Global.SpellMgr.GetSpellInfo(trainer_spell.ReqAbility[i]); - if (learnedSpellInfo != null && learnedSpellInfo.IsPrimaryProfessionFirstRank() && (GetFreePrimaryProfessionPoints() == 0)) - return TrainerSpellState.GreenDisabled; - } - - return TrainerSpellState.Green; - } - public void SendInitialPacketsBeforeAddToMap() { if (!m_teleport_options.HasAnyFlag(TeleportToOptions.Seamless)) diff --git a/Game/Game.csproj b/Game/Game.csproj index 7ced8b5cf..6bae2ef5c 100644 --- a/Game/Game.csproj +++ b/Game/Game.csproj @@ -148,6 +148,7 @@ + diff --git a/Game/Globals/ObjectManager.cs b/Game/Globals/ObjectManager.cs index 90dc2c1de..8318c6cdc 100644 --- a/Game/Globals/ObjectManager.cs +++ b/Game/Globals/ObjectManager.cs @@ -631,11 +631,16 @@ namespace Game uint oldMSTime = Time.GetMSTime(); gossipMenuItemsStorage.Clear(); - - SQLResult result = DB.World.Query( - // 0 1 2 3 4 5 6 7 8 9 10 11 12 - "SELECT menu_id, id, option_icon, option_text, OptionBroadcastTextID, option_id, npc_option_npcflag, action_menu_id, action_poi_id, box_coded, box_money, box_text, BoxBroadcastTextID " + - "FROM gossip_menu_option ORDER BY menu_id, id"); + + // 0 1 2 3 4 5 6 + SQLResult result = DB.World.Query("SELECT o.MenuId, o.OptionIndex, o.OptionIcon, o.OptionText, o.OptionBroadcastTextId, o.OptionType, o.OptionNpcflag, " + + // 7 8 9 10 11 12 13 + "oa.ActionMenuId, oa.ActionPoiId, ob.BoxCoded, ob.BoxMoney, ob.BoxText, ob.BoxBroadcastTextId, ot.TrainerId " + + "FROM gossip_menu_option o " + + "LEFT JOIN gossip_menu_option_action oa ON o.MenuId = oa.MenuId AND o.OptionIndex = oa.OptionIndex " + + "LEFT JOIN gossip_menu_option_box ob ON o.MenuId = ob.MenuId AND o.OptionIndex = ob.OptionIndex " + + "LEFT JOIN gossip_menu_option_trainer ot ON o.MenuId = ot.MenuId AND o.OptionIndex = ot.OptionIndex " + + "ORDER BY o.MenuId, o.OptionIndex"); if (result.IsEmpty()) { @@ -650,7 +655,7 @@ namespace Game gMenuItem.MenuId = result.Read(0); gMenuItem.OptionIndex = result.Read(1); - gMenuItem.OptionIcon = (GossipOptionIcon)result.Read(2); + gMenuItem.OptionIcon = (GossipOptionIcon)result.Read(2); gMenuItem.OptionText = result.Read(3); gMenuItem.OptionBroadcastTextId = result.Read(4); gMenuItem.OptionType = (GossipOption)result.Read(5); @@ -661,10 +666,11 @@ namespace Game gMenuItem.BoxMoney = result.Read(10); gMenuItem.BoxText = result.Read(11); gMenuItem.BoxBroadcastTextId = result.Read(12); + gMenuItem.TrainerId = result.Read(13); if (gMenuItem.OptionIcon >= GossipOptionIcon.Max) { - Log.outError(LogFilter.Sql, "Table gossip_menu_option for menu {0}, id {1} has unknown icon id {2}. Replacing with GOSSIPICONCHAT", gMenuItem.MenuId, gMenuItem.OptionIndex, gMenuItem.OptionIcon); + Log.outError(LogFilter.Sql, $"Table gossip_menu_option for MenuId {gMenuItem.MenuId}, OptionIndex {gMenuItem.OptionIndex} has unknown icon id {gMenuItem.OptionIcon}. Replacing with GOSSIPICONCHAT"); gMenuItem.OptionIcon = GossipOptionIcon.Chat; } @@ -672,17 +678,17 @@ namespace Game { if (!CliDB.BroadcastTextStorage.ContainsKey(gMenuItem.OptionBroadcastTextId)) { - Log.outError(LogFilter.Sql, "Table `gossip_menu_option` for menu {0}, id {1} has non-existing or incompatible OptionBroadcastTextId {2}, ignoring.", gMenuItem.MenuId, gMenuItem.OptionIndex, gMenuItem.OptionBroadcastTextId); + Log.outError(LogFilter.Sql, $"Table `gossip_menu_option` for MenuId {gMenuItem.MenuId}, OptionIndex {gMenuItem.OptionIndex} has non-existing or incompatible OptionBroadcastTextId {gMenuItem.OptionBroadcastTextId}, ignoring."); gMenuItem.OptionBroadcastTextId = 0; } } if (gMenuItem.OptionType >= GossipOption.Max) - Log.outError(LogFilter.Sql, "Table gossip_menu_option for menu {0}, id {1} has unknown option id {2}. Option will not be used", gMenuItem.MenuId, gMenuItem.OptionIndex, gMenuItem.OptionType); + Log.outError(LogFilter.Sql, $"Table gossip_menu_option for MenuId {gMenuItem.MenuId}, OptionIndex {gMenuItem.OptionIndex} has unknown option id {gMenuItem.OptionType}. Option will not be used"); if (gMenuItem.ActionPoiId != 0 && GetPointOfInterest(gMenuItem.ActionPoiId) == null) { - Log.outError(LogFilter.Sql, "Table gossip_menu_option for menu {0}, id {1} use non-existing actionpoiid {2}, ignoring", gMenuItem.MenuId, gMenuItem.OptionIndex, gMenuItem.ActionPoiId); + Log.outError(LogFilter.Sql, $"Table gossip_menu_option for MenuId {gMenuItem.MenuId}, OptionIndex {gMenuItem.OptionIndex} use non-existing actionpoiid {gMenuItem.ActionPoiId}, ignoring"); gMenuItem.ActionPoiId = 0; } @@ -690,11 +696,17 @@ namespace Game { if (!CliDB.BroadcastTextStorage.ContainsKey(gMenuItem.BoxBroadcastTextId)) { - Log.outError(LogFilter.Sql, "Table `gossip_menu_option` for menu {0}, id {1} has non-existing or incompatible BoxBroadcastTextId {2}, ignoring.", gMenuItem.MenuId, gMenuItem.OptionIndex, gMenuItem.BoxBroadcastTextId); + Log.outError(LogFilter.Sql, $"Table `gossip_menu_option` for MenuId {gMenuItem.MenuId}, OptionIndex {gMenuItem.OptionIndex} has non-existing or incompatible BoxBroadcastTextId {gMenuItem.BoxBroadcastTextId}, ignoring."); gMenuItem.BoxBroadcastTextId = 0; } } + if (gMenuItem.TrainerId != 0 && GetTrainer(gMenuItem.TrainerId) == null) + { + Log.outError(LogFilter.Sql, $"Table `gossip_menu_option_trainer` for MenuId {gMenuItem.MenuId}, OptionIndex {gMenuItem.OptionIndex} use non-existing TrainerId {gMenuItem.TrainerId}, ignoring"); + gMenuItem.TrainerId = 0; + } + gossipMenuItemsStorage.Add(gMenuItem.MenuId, gMenuItem); ++count; @@ -1703,15 +1715,15 @@ namespace Game "modelid4, name, femaleName, subname, IconName, gossip_menu_id, minlevel, maxlevel, HealthScalingExpansion, RequiredExpansion, VignetteID, " + //20 21 22 23 24 25 26 27 28 29 30 "faction, npcflag, speed_walk, speed_run, scale, rank, dmgschool, BaseAttackTime, RangeAttackTime, BaseVariance, RangeVariance, " + - //31 32 33 34 35 36 37 38 39 40 - "unit_class, unit_flags, unit_flags2, unit_flags3, dynamicflags, family, trainer_type, trainer_class, trainer_race, type, " + - //41 42 43 44 45 46 47 48 49 50 51 + //31 32 33 34 35 36 37 38 + "unit_class, unit_flags, unit_flags2, unit_flags3, dynamicflags, family, trainer_class, type, " + + //39 40 41 42 43 44 45 46 47 48 49 "type_flags, type_flags2, lootid, pickpocketloot, skinloot, resistance1, resistance2, resistance3, resistance4, resistance5, resistance6, " + - //52 53 54 55 56 57 58 59 60 61 62 63 64 + //50 51 52 53 54 55 56 57 58 59 60 61 62 "spell1, spell2, spell3, spell4, spell5, spell6, spell7, spell8, VehicleId, mingold, maxgold, AIName, MovementType, " + - //65 66 67 68 69 70 71 72 73 + //63 64 65 66 67 68 69 70 71 "InhabitType, HoverHeight, HealthModifier, HealthModifierExtra, ManaModifier, ManaModifierExtra, ArmorModifier, DamageModifier, ExperienceModifier, " + - //74 75 76 77 78 79 + //72 73 74 75 76 77 "RacialLeader, movementId, RegenHealth, mechanic_immune_mask, flags_extra, ScriptName FROM creature_template"); if (result.IsEmpty()) @@ -1777,42 +1789,40 @@ namespace Game creature.UnitFlags3 = fields.Read(34); creature.DynamicFlags = fields.Read(35); creature.Family = (CreatureFamily)fields.Read(36); - creature.TrainerType = (TrainerType)fields.Read(37); - creature.TrainerClass = (Class)fields.Read(38); - creature.TrainerRace = (Race)fields.Read(39); - creature.CreatureType = (CreatureType)fields.Read(40); - creature.TypeFlags = (CreatureTypeFlags)fields.Read(41); - creature.TypeFlags2 = fields.Read(42); - creature.LootId = fields.Read(43); - creature.PickPocketId = fields.Read(44); - creature.SkinLootId = fields.Read(45); + creature.TrainerClass = (Class)fields.Read(37); + creature.CreatureType = (CreatureType)fields.Read(38); + creature.TypeFlags = (CreatureTypeFlags)fields.Read(39); + creature.TypeFlags2 = fields.Read(40); + creature.LootId = fields.Read(41); + creature.PickPocketId = fields.Read(42); + creature.SkinLootId = fields.Read(43); for (var i = (int)SpellSchools.Holy; i < (int)SpellSchools.Max; ++i) - creature.Resistance[i] = fields.Read(46 + i - 1); + creature.Resistance[i] = fields.Read(44 + i - 1); for (var i = 0; i < SharedConst.MaxCreatureSpells; ++i) - creature.Spells[i] = fields.Read(52 + i); + creature.Spells[i] = fields.Read(50 + i); - creature.VehicleId = fields.Read(60); - creature.MinGold = fields.Read(61); - creature.MaxGold = fields.Read(62); - creature.AIName = fields.Read(63); - creature.MovementType = fields.Read(64); - creature.InhabitType = (InhabitType)fields.Read(65); - creature.HoverHeight = fields.Read(66); - creature.ModHealth = fields.Read(67); - creature.ModHealthExtra = fields.Read(68); - creature.ModMana = fields.Read(69); - creature.ModManaExtra = fields.Read(70); - creature.ModArmor = fields.Read(71); - creature.ModDamage = fields.Read(72); - creature.ModExperience = fields.Read(73); - creature.RacialLeader = fields.Read(74); - creature.MovementId = fields.Read(75); - creature.RegenHealth = fields.Read(76); - creature.MechanicImmuneMask = fields.Read(77); - creature.FlagsExtra = (CreatureFlagsExtra)fields.Read(78); - creature.ScriptID = GetScriptId(fields.Read(79)); + creature.VehicleId = fields.Read(58); + creature.MinGold = fields.Read(59); + creature.MaxGold = fields.Read(60); + creature.AIName = fields.Read(61); + creature.MovementType = fields.Read(62); + creature.InhabitType = (InhabitType)fields.Read(63); + creature.HoverHeight = fields.Read(64); + creature.ModHealth = fields.Read(65); + creature.ModHealthExtra = fields.Read(66); + creature.ModMana = fields.Read(67); + creature.ModManaExtra = fields.Read(68); + creature.ModArmor = fields.Read(69); + creature.ModDamage = fields.Read(70); + creature.ModExperience = fields.Read(71); + creature.RacialLeader = fields.Read(72); + creature.MovementId = fields.Read(73); + creature.RegenHealth = fields.Read(74); + creature.MechanicImmuneMask = fields.Read(75); + creature.FlagsExtra = (CreatureFlagsExtra)fields.Read(76); + creature.ScriptID = GetScriptId(fields.Read(77)); creatureTemplateStorage.Add(entry, creature); } @@ -2382,18 +2392,6 @@ namespace Game continue; } - if (cInfo.TrainerRace != difficultyInfo.TrainerRace) - { - Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has different `trainer_race` in difficulty {1} mode (Entry: {2}).", cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff]); - continue; - } - - if (cInfo.TrainerType != difficultyInfo.TrainerType) - { - Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has different `trainer_type` in difficulty {1} mode (Entry: {2}).", cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff]); - continue; - } - if (cInfo.CreatureType != difficultyInfo.CreatureType) { Log.outError(LogFilter.Sql, "Creature (Entry: {0}, type: {1}) has different `type` in difficulty {2} mode (Entry: {3}, type: {4}).", @@ -2551,9 +2549,6 @@ namespace Game if (cInfo.RangeAttackTime == 0) cInfo.RangeAttackTime = SharedConst.BaseAttackTime; - if (cInfo.Npcflag.HasAnyFlag(NPCFlags.Trainer) && (uint)cInfo.TrainerType >= 4) - Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong trainer type {1}.", cInfo.Entry, cInfo.TrainerType); - if (cInfo.SpeedWalk == 0.0f) { Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong value ({1}) in speed_walk, set to 1.", cInfo.Entry, cInfo.SpeedWalk); @@ -2906,110 +2901,121 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loaded {0} npc texts in {1} ms", _npcTextStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime)); } - public void LoadTrainerSpell() + public void LoadTrainers() { - var time = Time.GetMSTime(); - SQLResult result = DB.World.Query("SELECT b.ID, a.SpellID, a.MoneyCost, a.ReqSkillLine, a.ReqSkillRank, a.Reqlevel, a.Index FROM npc_trainer AS a " + - "INNER JOIN npc_trainer AS b ON a.ID = -(b.SpellID) UNION SELECT * FROM npc_trainer WHERE SpellID > 0"); + uint oldMSTime = Time.GetMSTime(); - if (result.IsEmpty()) + // For reload case + _trainers.Clear(); + + MultiMap spellsByTrainer = new MultiMap(); + SQLResult trainerSpellsResult = DB.World.Query("SELECT TrainerId, SpellId, MoneyCost, ReqSkillLine, ReqSkillRank, ReqAbility1, ReqAbility2, ReqAbility3, ReqLevel FROM trainer_spell"); + if (!trainerSpellsResult.IsEmpty()) { - Log.outError(LogFilter.ServerLoading, "Loaded 0 Trainers. DB table `npc_trainer` is empty!"); - return; - } - - uint count = 0; - do - { - uint ID = result.Read(0); - uint SpellID = result.Read(1); - uint MoneyCost = result.Read(2); - uint ReqSkillLine = result.Read(3); - uint ReqSkillRank = result.Read(4); - uint Reqlevel = result.Read(5); - uint Index = result.Read(6); - - AddSpellToTrainer(ID, SpellID, MoneyCost, ReqSkillLine, ReqSkillRank, Reqlevel, Index); - count++; - } while (result.NextRow()); - - Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Trainers in {1} ms", count, Time.GetMSTimeDiffToNow(time)); - } - void AddSpellToTrainer(uint ID, uint SpellID, uint MoneyCost, uint ReqSkillLine, uint ReqSkillRank, uint Reqlevel, uint Index) - { - if (ID >= 200000) - return; - - CreatureTemplate cInfo = GetCreatureTemplate(ID); - if (cInfo == null) - { - Log.outError(LogFilter.Sql, "Table `npc_trainer` contains entries for a non-existing creature template (ID: {0}), ignoring", ID); - return; - } - - if (!cInfo.Npcflag.HasAnyFlag(NPCFlags.Trainer)) - { - Log.outError(LogFilter.Sql, "Table `npc_trainer` contains entries for a creature template (ID: {0}) without trainer flag, ignoring", ID); - return; - } - - SpellInfo spellinfo = Global.SpellMgr.GetSpellInfo(SpellID); - if (spellinfo == null) - { - Log.outError(LogFilter.Sql, "Table `npc_trainer` contains an ID ({0}) for a non-existing spell (Spell: {1}), ignoring", ID, SpellID); - return; - } - - if (!Global.SpellMgr.IsSpellValid(spellinfo)) - { - Log.outError(LogFilter.Sql, "Table `npc_trainer` contains an ID ({0}) for a broken spell (Spell: {1}), ignoring", ID, SpellID); - return; - } - - if (cacheTrainerSpellStorage.LookupByKey(ID) == null) - cacheTrainerSpellStorage.Add(ID, new TrainerSpellData()); - - TrainerSpellData data = cacheTrainerSpellStorage[ID]; - TrainerSpell trainerSpell = new TrainerSpell(); - trainerSpell.SpellID = SpellID; - trainerSpell.MoneyCost = MoneyCost; - trainerSpell.ReqSkillLine = ReqSkillLine; - trainerSpell.ReqSkillRank = ReqSkillRank; - trainerSpell.ReqLevel = Reqlevel; - trainerSpell.Index = Index; - - if (trainerSpell.ReqLevel == 0) - trainerSpell.ReqLevel = spellinfo.SpellLevel; - - // calculate learned spell for profession case when stored cast-spell - trainerSpell.ReqAbility[0] = SpellID; - - foreach (SpellEffectInfo effect in spellinfo.GetEffectsForDifficulty(Difficulty.None)) - { - if (effect == null || effect.Effect != SpellEffectName.LearnSpell) - continue; - - if (trainerSpell.ReqAbility[0] == SpellID) - trainerSpell.ReqAbility[0] = 0; - // player must be able to cast spell on himself - if (effect.TargetA.GetTarget() != 0 && effect.TargetA.GetTarget() != Targets.UnitAlly - && effect.TargetA.GetTarget() != Targets.UnitAny && effect.TargetA.GetTarget() != Targets.UnitCaster) + do { - Log.outError(LogFilter.Sql, "Table `npc_trainer` has spell {0} for trainer entry {1} with learn effect which has incorrect target type, ignoring learn effect!", SpellID, ID); - continue; - } + TrainerSpell spell = new TrainerSpell(); + uint trainerId = trainerSpellsResult.Read(0); + spell.SpellId = trainerSpellsResult.Read(1); + spell.MoneyCost = trainerSpellsResult.Read(2); + spell.ReqSkillLine = trainerSpellsResult.Read(3); + spell.ReqSkillRank = trainerSpellsResult.Read(4); + spell.ReqAbility[0] = trainerSpellsResult.Read(5); + spell.ReqAbility[1] = trainerSpellsResult.Read(6); + spell.ReqAbility[2] = trainerSpellsResult.Read(7); + spell.ReqLevel = trainerSpellsResult.Read(8); - trainerSpell.ReqAbility[effect.EffectIndex] = effect.TriggerSpell; + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(spell.SpellId); + if (spellInfo == null) + { + Log.outError(LogFilter.Sql, $"Table `trainer_spell` references non-existing spell (SpellId: {spell.SpellId}) for TrainerId {trainerId}, ignoring"); + continue; + } - if (trainerSpell.ReqAbility[effect.EffectIndex] != 0) - { - SpellInfo learnedSpellInfo = Global.SpellMgr.GetSpellInfo(trainerSpell.ReqAbility[effect.EffectIndex]); - if (learnedSpellInfo != null && learnedSpellInfo.IsProfession()) - data.trainerType = 2; - } + if (spell.ReqSkillLine != 0 && !CliDB.SkillLineStorage.ContainsKey(spell.ReqSkillLine)) + { + Log.outError(LogFilter.Sql, $"Table `trainer_spell` references non-existing skill (ReqSkillLine: {spell.ReqSkillLine}) for TrainerId {spell.SpellId} and SpellId {trainerId}, ignoring"); + continue; + } + + bool allReqValid = true; + for (var i = 0; i < spell.ReqAbility.Count; ++i) + { + uint requiredSpell = spell.ReqAbility[i]; + if (requiredSpell != 0 && !Global.SpellMgr.HasSpellInfo(requiredSpell)) + { + Log.outError(LogFilter.Sql, $"Table `trainer_spell` references non-existing spell (ReqAbility {i + 1}: {requiredSpell}) for TrainerId {spell.SpellId} and SpellId {trainerId}, ignoring"); + allReqValid = false; + } + } + + if (!allReqValid) + continue; + + foreach (SpellEffectInfo spellEffect in spellInfo.GetEffectsForDifficulty(Difficulty.None)) + { + if (spellEffect.IsEffect(SpellEffectName.LearnSpell)) + { + spell.CastSpellId = spell.SpellId; + spell.SpellId = spellEffect.TriggerSpell; + break; + } + } + + spellsByTrainer.Add(trainerId, spell); + + } while (trainerSpellsResult.NextRow()); } - data.spellList[SpellID] = trainerSpell; - return; + + SQLResult trainersResult = DB.World.Query("SELECT Id, Type, Greeting FROM trainer"); + if (!trainersResult.IsEmpty()) + { + do + { + uint trainerId = trainersResult.Read(0); + TrainerType trainerType = (TrainerType)trainersResult.Read(1); + string greeting = trainersResult.Read(2); + List spells = new List(); + var spellList = spellsByTrainer.LookupByKey(trainerId); + if (spellList != null) + { + spells = spellList; + spellsByTrainer.Remove(trainerId); + } + + _trainers.Add(trainerId, new Trainer(trainerId, trainerType, greeting, spells)); + + } while (trainersResult.NextRow()); + } + + foreach (var unusedSpells in spellsByTrainer) + { + Log.outError(LogFilter.Sql, $"Table `trainer_spell` references non-existing trainer (TrainerId: {unusedSpells.Key}) for SpellId {unusedSpells.Value.SpellId}, ignoring"); + + } + + SQLResult trainerLocalesResult = DB.World.Query("SELECT Id, locale, Greeting_lang FROM trainer_locale"); + if (!trainerLocalesResult.IsEmpty()) + { + do + { + uint trainerId = trainerLocalesResult.Read(0); + string localeName = trainerLocalesResult.Read(1); + + LocaleConstant locale = Extensions.ToEnum(localeName); + if (locale == LocaleConstant.enUS) + continue; + + Trainer trainer = _trainers.LookupByKey(trainerId); + if (trainer != null) + trainer.AddGreetingLocale(locale, trainerLocalesResult.Read(2)); + else + Log.outError(LogFilter.Sql, $"Table `trainer_locale` references non-existing trainer (TrainerId: {trainerId}) for locale {localeName}, ignoring"); + + } while (trainerLocalesResult.NextRow()); + } + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {_trainers.Count} Trainers in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); } public void LoadVendors() { @@ -3488,10 +3494,6 @@ namespace Game { return creatureModelStorage.LookupByKey(modelId); } - public TrainerSpellData GetNpcTrainerSpells(uint entry) - { - return cacheTrainerSpellStorage.LookupByKey(entry); - } public NpcText GetNpcText(uint textId) { return _npcTextStorage.LookupByKey(textId); @@ -4585,6 +4587,10 @@ namespace Game { return ItemTemplateStorage; } + public Trainer GetTrainer(uint trainerId) + { + return _trainers.LookupByKey(trainerId); + } public void AddVendorItem(uint entry, uint item, int maxcount, uint incrtime, uint extendedCost, ItemVendorType type, bool persist = true) { VendorItemData vList = cacheVendorItemStorage[entry]; @@ -9095,7 +9101,7 @@ namespace Game Dictionary linkedRespawnStorage = new Dictionary(); Dictionary creatureBaseStatsStorage = new Dictionary(); Dictionary cacheVendorItemStorage = new Dictionary(); - Dictionary cacheTrainerSpellStorage = new Dictionary(); + Dictionary _trainers = new Dictionary(); List[] _difficultyEntries = new List[SharedConst.MaxCreatureDifficulties]; // already loaded difficulty 1 value in creatures, used in CheckCreatureTemplate List[] _hasDifficultyEntries = new List[SharedConst.MaxCreatureDifficulties]; // already loaded creatures with difficulty 1 values, used in CheckCreatureTemplate Dictionary _npcTextStorage = new Dictionary(); diff --git a/Game/Handlers/NPCHandler.cs b/Game/Handlers/NPCHandler.cs index 44ac3c00b..695432710 100644 --- a/Game/Handlers/NPCHandler.cs +++ b/Game/Handlers/NPCHandler.cs @@ -56,20 +56,15 @@ namespace Game [WorldPacketHandler(ClientOpcodes.TrainerList)] void HandleTrainerList(Hello packet) { - SendTrainerList(packet.Unit); + Log.outInfo(LogFilter.Network, $"{GetPlayerInfo()} sent legacy gossipless trainer hello for unit {packet.Unit.ToString()}, no trainer list available"); } - public void SendTrainerList(ObjectGuid guid, uint index = 0) - { - string str = Global.ObjectMgr.GetCypherString(CypherStrings.NpcTainerHello); - SendTrainerList(guid, str, index); - } - void SendTrainerList(ObjectGuid guid, string title, uint index = 0) + public void SendTrainerList(ObjectGuid guid, uint trainerId) { Creature unit = GetPlayer().GetNPCIfCanInteractWith(guid, NPCFlags.Trainer); if (unit == null) { - Log.outDebug(LogFilter.Network, "WORLD: SendTrainerList - {0} not found or you can not interact with him.", guid.ToString()); + Log.outDebug(LogFilter.Network, $"WORLD: SendTrainerList - {guid.ToString()} not found or you can not interact with him."); return; } @@ -77,92 +72,24 @@ namespace Game if (GetPlayer().HasUnitState(UnitState.Died)) GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - TrainerSpellData trainer_spells = unit.GetTrainerSpells(); - if (trainer_spells == null) + Trainer trainer = Global.ObjectMgr.GetTrainer(trainerId); + if (trainer == null) { - Log.outDebug(LogFilter.Network, "WORLD: SendTrainerList - Training spells not found for {0}", guid.ToString()); + Log.outDebug(LogFilter.Network, $"WORLD: SendTrainerList - trainer spells not found for trainer {guid.ToString()} id {trainerId}"); return; } - TrainerList packet = new TrainerList(); - packet.TrainerGUID = guid; - packet.TrainerType = (int)trainer_spells.trainerType; - packet.Greeting = title; - - // reputation discount - float fDiscountMod = GetPlayer().GetReputationPriceDiscount(unit); - - foreach (var tSpell in trainer_spells.spellList.Values) - { - if (index != 0 && tSpell.Index != index) - continue; - - bool valid = true; - for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; i++) - { - if (tSpell.ReqAbility[i] == 0) - continue; - - if (!GetPlayer().IsSpellFitByClassAndRace(tSpell.ReqAbility[i])) - { - valid = false; - break; - } - } - - if (!valid) - continue; - - TrainerSpellState state = GetPlayer().GetTrainerSpellState(tSpell); - - TrainerListSpell spell = new TrainerListSpell(); - spell.SpellID = (int)tSpell.SpellID; - spell.MoneyCost = (int)Math.Floor(tSpell.MoneyCost * fDiscountMod); - spell.ReqSkillLine = (int)tSpell.ReqSkillLine; - spell.ReqSkillRank = (int)tSpell.ReqSkillRank; - spell.ReqLevel = (byte)tSpell.ReqLevel; - spell.Usable = (state == TrainerSpellState.GreenDisabled ? TrainerSpellState.Green : state); - - byte maxReq = 0; - for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i) - { - if (tSpell.ReqAbility[i] == 0) - continue; - - uint prevSpellId = Global.SpellMgr.GetPrevSpellInChain(tSpell.ReqAbility[i]); - if (prevSpellId != 0) - { - spell.ReqAbility[maxReq] = (int)prevSpellId; - ++maxReq; - } - - if (maxReq == 2) - break; - - var spellsRequired = Global.SpellMgr.GetSpellsRequiredForSpellBounds(tSpell.ReqAbility[i]); - for (var c = 0; c < spellsRequired.Count && maxReq < SharedConst.MaxTrainerspellAbilityReqs; ++c) - { - spell.ReqAbility[maxReq] = (int)spellsRequired[c]; - ++maxReq; - } - - if (maxReq == 2) - break; - } - - packet.Spells.Add(spell); - } - - SendPacket(packet); + _player.SetCurrentTrainerId(trainerId); + trainer.SendSpells(unit, _player, GetSessionDbLocaleIndex()); } [WorldPacketHandler(ClientOpcodes.TrainerBuySpell)] void HandleTrainerBuySpell(TrainerBuySpell packet) { - Creature trainer = _player.GetNPCIfCanInteractWith(packet.TrainerGUID, NPCFlags.Trainer); - if (trainer == null) + Creature npc = _player.GetNPCIfCanInteractWith(packet.TrainerGUID, NPCFlags.Trainer); + if (npc == null) { - Log.outDebug(LogFilter.Network, "WORLD: HandleTrainerBuySpell - {0} not found or you can not interact with him.", packet.TrainerGUID.ToString()); + Log.outDebug(LogFilter.Network, $"WORLD: HandleTrainerBuySpell - {packet.TrainerGUID.ToString()} not found or you can not interact with him."); return; } @@ -170,64 +97,18 @@ namespace Game if (_player.HasUnitState(UnitState.Died)) _player.RemoveAurasByType(AuraType.FeignDeath); - // check race for mount trainers - if (trainer.GetCreatureTemplate().TrainerType == TrainerType.Mounts) - { - Race trainerRace = trainer.GetCreatureTemplate().TrainerRace; - if (trainerRace != 0) - if (_player.GetRace() != trainerRace) - return; - } - - // check class for class trainers - if (_player.GetClass() != trainer.GetCreatureTemplate().TrainerClass && trainer.GetCreatureTemplate().TrainerType == TrainerType.Class) + if (_player.GetCurrentTrainerId() != packet.TrainerID) return; // check present spell in trainer spell list - var trainerSpells = trainer.GetTrainerSpells(); - if (trainerSpells == null) - { - SendTrainerBuyFailed(packet.TrainerGUID, packet.SpellID, 0); + Trainer trainer = Global.ObjectMgr.GetTrainer(packet.TrainerID); + if (trainer == null) return; - } - var trainerSpell = trainerSpells.spellList.LookupByKey(packet.SpellID); - if (trainerSpell == null) - { - SendTrainerBuyFailed(packet.TrainerGUID, packet.SpellID, 0); - return; - } - - // can't be learn, cheat? Or double learn with lags... - if (_player.GetTrainerSpellState(trainerSpell) != TrainerSpellState.Green) - { - SendTrainerBuyFailed(packet.TrainerGUID, packet.SpellID, 0); - return; - } - - // apply reputation discount - uint nSpellCost = (uint)(Math.Floor((double)trainerSpell.MoneyCost) * _player.GetReputationPriceDiscount(trainer)); - - // check money requirement - if (!_player.HasEnoughMoney(nSpellCost)) - { - SendTrainerBuyFailed(packet.TrainerGUID, packet.SpellID, 1); - return; - } - - _player.ModifyMoney(-nSpellCost); - - trainer.SendPlaySpellVisualKit(179, 0, 0); // 53 SpellCastDirected - _player.SendPlaySpellVisualKit(362, 1, 0); // 113 EmoteSalute - - // learn explicitly or cast explicitly - if (trainerSpell.IsCastable()) - _player.CastSpell(GetPlayer(), trainerSpell.SpellID, true); - else - _player.LearnSpell(packet.SpellID, false); + trainer.TeachSpell(npc, _player, packet.SpellID); } - void SendTrainerBuyFailed(ObjectGuid trainerGUID, uint spellID, uint trainerFailedReason) + void SendTrainerBuyFailed(ObjectGuid trainerGUID, uint spellID, TrainerFailReason trainerFailedReason) { TrainerBuyFailed trainerBuyFailed = new TrainerBuyFailed(); trainerBuyFailed.TrainerGUID = trainerGUID; diff --git a/Game/Handlers/SkillHandler.cs b/Game/Handlers/SkillHandler.cs index f0517b472..c58238513 100644 --- a/Game/Handlers/SkillHandler.cs +++ b/Game/Handlers/SkillHandler.cs @@ -68,6 +68,12 @@ namespace Game return; } + if (!unit.CanResetTalents(_player)) + return; + + if (!_player.PlayerTalkClass.GetGossipMenu().HasMenuItemType((uint)GossipOption.Unlearntalents)) + return; + // remove fake death if (GetPlayer().HasUnitState(UnitState.Died)) GetPlayer().RemoveAurasByType(AuraType.FeignDeath); diff --git a/Game/Network/Packets/NPCPackets.cs b/Game/Network/Packets/NPCPackets.cs index 757da47f2..ed726224d 100644 --- a/Game/Network/Packets/NPCPackets.cs +++ b/Game/Network/Packets/NPCPackets.cs @@ -170,11 +170,11 @@ namespace Game.Network.Packets _worldPacket.WriteString(Greeting); } - public string Greeting; - public int TrainerType = 0; public ObjectGuid TrainerGUID; + public int TrainerType; public int TrainerID = 1; public List Spells = new List(); + public string Greeting; } public class ShowBank : ServerPacket @@ -276,7 +276,7 @@ namespace Game.Network.Packets public ObjectGuid TrainerGUID; public uint SpellID; - public uint TrainerFailedReason; + public TrainerFailReason TrainerFailedReason; } class RequestStabledPets : ClientPacket @@ -344,11 +344,11 @@ namespace Game.Network.Packets public class TrainerListSpell { - public int SpellID; - public int MoneyCost; - public int ReqSkillLine; - public int ReqSkillRank; - public int[] ReqAbility = new int[SharedConst.MaxTrainerspellAbilityReqs]; + public uint SpellID; + public uint MoneyCost; + public uint ReqSkillLine; + public uint ReqSkillRank; + public uint[] ReqAbility = new uint[SharedConst.MaxTrainerspellAbilityReqs]; public TrainerSpellState Usable; public byte ReqLevel; } diff --git a/Game/Server/WorldManager.cs b/Game/Server/WorldManager.cs index 0e66c937e..84836b25d 100644 --- a/Game/Server/WorldManager.cs +++ b/Game/Server/WorldManager.cs @@ -702,17 +702,17 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loading GameTeleports..."); Global.ObjectMgr.LoadGameTele(); + Log.outInfo(LogFilter.ServerLoading, "Loading Trainers..."); + Global.ObjectMgr.LoadTrainers(); // must be after load CreatureTemplate + Log.outInfo(LogFilter.ServerLoading, "Loading Gossip menu..."); Global.ObjectMgr.LoadGossipMenu(); Log.outInfo(LogFilter.ServerLoading, "Loading Gossip menu options..."); - Global.ObjectMgr.LoadGossipMenuItems(); + Global.ObjectMgr.LoadGossipMenuItems(); // must be after LoadTrainers Log.outInfo(LogFilter.ServerLoading, "Loading Vendors..."); - Global.ObjectMgr.LoadVendors(); // must be after load CreatureTemplate and ItemTemplate - - Log.outInfo(LogFilter.ServerLoading, "Loading Trainers..."); - Global.ObjectMgr.LoadTrainerSpell(); + Global.ObjectMgr.LoadVendors(); // must be after load CreatureTemplate and ItemTemplate Log.outInfo(LogFilter.ServerLoading, "Loading Waypoints..."); Global.WaypointMgr.Load(); diff --git a/Scripts/World/NpcSpecial.cs b/Scripts/World/NpcSpecial.cs index b63e93d4f..95d72a3a8 100644 --- a/Scripts/World/NpcSpecial.cs +++ b/Scripts/World/NpcSpecial.cs @@ -1699,21 +1699,6 @@ namespace Scripts.World.NpcSpecial } } - [Script] - class npc_pet_trainer : ScriptedAI - { - public npc_pet_trainer(Creature creature) : base(creature) { } - - public override void sGossipSelect(Player player, uint menuId, uint gossipListId) - { - if (menuId == GossipMenus.MenuIdPetUnlearn && gossipListId == GossipMenus.OptionIdPleaseDo) - { - player.ResetPetTalents(); - player.CLOSE_GOSSIP_MENU(); - } - } - } - [Script] class npc_experience : CreatureScript {