/*
 * Copyright (C) 2012-2017 CypherCore <http://github.com/CypherCore>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 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/>.
 */

using Framework.Collections;
using Framework.Constants;
using Framework.Database;
using Framework.GameMath;
using Game.Conditions;
using Game.DataStorage;
using Game.Entities;
using Game.Mails;
using Game.Maps;
using Game.Misc;
using Game.Scripting;
using Game.Spells;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Runtime.InteropServices;

namespace Game
{
    public sealed class ObjectManager : Singleton<ObjectManager>
    {
        ObjectManager()
        {
            for (var i = 0; i < SharedConst.MaxCreatureDifficulties; ++i)
            {
                _difficultyEntries[i] = new List<uint>();
                _hasDifficultyEntries[i] = new List<uint>();
            }

            lang_description = new LanguageDesc[]
            {
                new LanguageDesc(Language.Addon,            0,      0                               ),
                new LanguageDesc(Language.Universal,        0,      0                               ),
                new LanguageDesc(Language.Orcish,           669,    SkillType.LangOrcish            ),
                new LanguageDesc(Language.Darnassian,       671,    SkillType.LangDarnassian        ),
                new LanguageDesc(Language.Taurahe,          670,    SkillType.LangTaurahe           ),
                new LanguageDesc(Language.Dwarvish,         672,    SkillType.LangDwarven           ),
                new LanguageDesc(Language.Common,           668,    SkillType.LangCommon            ),
                new LanguageDesc(Language.Demonic,          815,    SkillType.LangDemonTongue       ),
                new LanguageDesc(Language.Titan,            816,    SkillType.LangTitan             ),
                new LanguageDesc(Language.Thalassian,       813,    SkillType.LangThalassian        ),
                new LanguageDesc(Language.Draconic,         814,    SkillType.LangDraconic          ),
                new LanguageDesc(Language.Kalimag,          817,    SkillType.LangOldTongue         ),
                new LanguageDesc(Language.Gnomish,          7340,   SkillType.LangGnomish           ),
                new LanguageDesc(Language.Troll,            7341,   SkillType.LangTroll             ),
                new LanguageDesc(Language.Gutterspeak,      17737,  SkillType.LangForsaken          ),
                new LanguageDesc(Language.Draenei,          29932,  SkillType.LangDraenei           ),
                new LanguageDesc(Language.Zombie,           0,      0                               ),
                new LanguageDesc(Language.GnomishBinary,    0,      0                               ),
                new LanguageDesc(Language.GoblinBinary,     0,      0                               ),
                new LanguageDesc(Language.Worgen,           69270,  SkillType.LangGilnean           ),
                new LanguageDesc(Language.Goblin,           69269,  SkillType.LangGoblin            ),
                new LanguageDesc(Language.PandarenNeutral,  108127, SkillType.LangPandarenNeutral   ),
                new LanguageDesc(Language.PandarenAlliance, 108130, SkillType.LangPandarenAlliance  ),
                new LanguageDesc(Language.PandarenHorde,    108131, SkillType.LangPandarenHorde     ),
            };
        }

        //Static Methods
        public static bool NormalizePlayerName(ref string name)
        {
            if (name.IsEmpty())
                return false;

            //CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture;
            //TextInfo textInfo = cultureInfo.TextInfo;

            //str = textInfo.ToTitleCase(str);

            name = name.ToLower();

            var charArray = name.ToCharArray();
            charArray[0] = char.ToUpper(charArray[0]);

            name = new string(charArray);
            return true;
        }
        public static ExtendedPlayerName ExtractExtendedPlayerName(string name)
        {
            int pos = name.IndexOf('-');
            if (pos != -1)
                return new ExtendedPlayerName(name.Substring(0, pos), name.Substring(pos + 1));
            else
                return new ExtendedPlayerName(name, "");
        }
        public static LanguageDesc GetLanguageDescByID(Language lang)
        {
            for (byte i = 0; i < lang_description.Length; ++i)
            {
                if (lang_description[i].lang_id == lang)
                    return lang_description[i];
            }

            return null;
        }
        static LanguageType GetRealmLanguageType(bool create)
        {
            switch ((RealmZones)WorldConfig.GetIntValue(WorldCfg.RealmZone))
            {
                case RealmZones.Unknown:                            // any language
                case RealmZones.Development:
                case RealmZones.TestServer:
                case RealmZones.QaServer:
                    return LanguageType.Any;
                case RealmZones.UnitedStates:                      // extended-Latin
                case RealmZones.Oceanic:
                case RealmZones.LatinAmerica:
                case RealmZones.English:
                case RealmZones.German:
                case RealmZones.French:
                case RealmZones.Spanish:
                    return LanguageType.ExtendenLatin;
                case RealmZones.Korea:                              // East-Asian
                case RealmZones.Taiwan:
                case RealmZones.China:
                    return LanguageType.EastAsia;
                case RealmZones.Russian:                            // Cyrillic
                    return LanguageType.Cyrillic;
                default:
                    return create ? LanguageType.BasicLatin : LanguageType.Any;        // basic-Latin at create, any at login
            }
        }

        public static uint ChooseDisplayId(CreatureTemplate cinfo, CreatureData data = null)
        {
            // Load creature model (display id)
            if (data != null && data.displayid != 0)
                return data.displayid;

            if (!cinfo.FlagsExtra.HasAnyFlag(CreatureFlagsExtra.Trigger))
                return cinfo.GetRandomValidModelId();

            // Triggers by default receive the invisible model
            return cinfo.GetFirstInvisibleModel();
        }

        public static void ChooseCreatureFlags(CreatureTemplate cInfo, out ulong npcFlag, out uint unitFlags, out uint unitFlags2, out uint unitFlags3, out uint dynamicFlags, CreatureData data = null)
        {
            npcFlag = (ulong)cInfo.Npcflag;
            unitFlags = (uint)cInfo.UnitFlags;
            unitFlags2 = cInfo.UnitFlags2;
            unitFlags3 = cInfo.UnitFlags3;
            dynamicFlags = cInfo.DynamicFlags;

            if (data != null)
            {
                if (data.npcflag != 0)
                    npcFlag = data.npcflag;

                if (data.unit_flags != 0)
                    unitFlags = data.unit_flags;

                if (data.unit_flags2 != 0)
                    unitFlags2 = data.unit_flags2;

                if (data.unit_flags3 != 0)
                    unitFlags3 = data.unit_flags3;

                if (data.dynamicflags != 0)
                    dynamicFlags = data.dynamicflags;
            }
        }
        public static ObjectGuid GetPlayerGUIDByName(string name)
        {
            PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_GUID_BY_NAME);
            stmt.AddValue(0, name);
            SQLResult result = DB.Characters.Query(stmt);

            if (!result.IsEmpty())
                return ObjectGuid.Create(HighGuid.Player, result.Read<ulong>(0));

            return ObjectGuid.Empty;
        }
        public static bool GetPlayerNameByGUID(ObjectGuid guid, out string name)
        {
            name = "";

            Player player = Global.ObjAccessor.FindConnectedPlayer(guid);
            if (player)
            {
                name = player.GetName();
                return true;
            }

            CharacterInfo characterInfo = Global.WorldMgr.GetCharacterInfo(guid);
            if (characterInfo == null)
                return false;

            name = characterInfo.Name;
            return true;
        }
        public static bool GetPlayerNameAndClassByGUID(ObjectGuid guid, out string name, out byte _class)
        {
            name = "";
            _class = 0;

            Player player = Global.ObjAccessor.FindConnectedPlayer(guid);
            if (player)
            {
                name = player.GetName();
                _class = (byte)player.GetClass();
                return true;
            }

            CharacterInfo characterInfo = Global.WorldMgr.GetCharacterInfo(guid);
            if (characterInfo != null)
            {
                name = characterInfo.Name;
                _class = (byte)characterInfo.ClassID;
                return true;
            }

            return false;
        }
        public static Team GetPlayerTeamByGUID(ObjectGuid guid)
        {
            CharacterInfo characterInfo = Global.WorldMgr.GetCharacterInfo(guid);
            if (characterInfo != null)
                return Player.TeamForRace(characterInfo.RaceID);

            return 0;
        }
        public static uint GetPlayerAccountIdByGUID(ObjectGuid guid)
        {
            CharacterInfo characterInfo = Global.WorldMgr.GetCharacterInfo(guid);
            if (characterInfo != null)
                return characterInfo.AccountId;

            return 0;
        }

        public static ResponseCodes CheckPlayerName(string name, LocaleConstant locale, bool create = false)
        {
            if (name.Length > 12)
                return ResponseCodes.CharNameTooLong;

            uint minName = WorldConfig.GetUIntValue(WorldCfg.MinPlayerName);
            if (name.Length < minName)
                return ResponseCodes.CharNameTooShort;

            uint strictMask = WorldConfig.GetUIntValue(WorldCfg.StrictPlayerNames);
            if (!IsValidString(name, strictMask, false, create))
                return ResponseCodes.CharNameMixedLanguages;

            name = name.ToLower();
            for (int i = 2; i < name.Length; ++i)
                if (name[i] == name[i - 1] && name[i] == name[i - 2])
                    return ResponseCodes.CharNameThreeConsecutive;

            return Global.DB2Mgr.ValidateName(name, locale);
        }
        public static PetNameInvalidReason CheckPetName(string name)
        {
            if (name.Length > 12)
                return PetNameInvalidReason.TooLong;

            uint minName = WorldConfig.GetUIntValue(WorldCfg.MinPetName);
            if (name.Length < minName)
                return PetNameInvalidReason.TooShort;

            uint strictMask = WorldConfig.GetUIntValue(WorldCfg.StrictPetNames);
            if (!IsValidString(name, strictMask, false))
                return PetNameInvalidReason.MixedLanguages;

            return PetNameInvalidReason.Success;
        }
        public static bool IsValidCharterName(string name)
        {
            if (name.Length > 24)
                return false;

            uint minName = WorldConfig.GetUIntValue(WorldCfg.MinCharterName);
            if (name.Length < minName)
                return false;

            uint strictMask = WorldConfig.GetUIntValue(WorldCfg.StrictCharterNames);

            return IsValidString(name, strictMask, true);
        }
        public static void AddLocaleString(string value, LocaleConstant locale, StringArray data)
        {
            if (!string.IsNullOrEmpty(value))
                data[(int)locale] = value;
        }
        public static void GetLocaleString(StringArray data, LocaleConstant locale, ref string value)
        {
            if (data.Length > (int)locale && !string.IsNullOrEmpty(data[(int)locale]))
                value = data[(int)locale];
        }

        static bool IsValidString(string str, uint strictMask, bool numericOrSpace, bool create = false)
        {
            if (strictMask == 0)                                       // any language, ignore realm
            {
                if (IsCultureString(LanguageType.BasicLatin, str, numericOrSpace))
                    return true;
                if (IsCultureString(LanguageType.ExtendenLatin, str, numericOrSpace))
                    return true;
                if (IsCultureString(LanguageType.Cyrillic, str, numericOrSpace))
                    return true;
                if (IsCultureString(LanguageType.EastAsia, str, numericOrSpace))
                    return true;
                return false;
            }

            if (Convert.ToBoolean(strictMask & 0x2))                                    // realm zone specific
            {
                LanguageType lt = GetRealmLanguageType(create);
                if (lt.HasAnyFlag(LanguageType.ExtendenLatin))
                {
                    if (IsCultureString(LanguageType.BasicLatin, str, numericOrSpace))
                        return true;
                    if (IsCultureString(LanguageType.ExtendenLatin, str, numericOrSpace))
                        return true;
                }
                if (lt.HasAnyFlag(LanguageType.Cyrillic))
                    if (IsCultureString(LanguageType.Cyrillic, str, numericOrSpace))
                        return true;
                if (lt.HasAnyFlag(LanguageType.EastAsia))
                    if (IsCultureString(LanguageType.EastAsia, str, numericOrSpace))
                        return true;
            }

            if (Convert.ToBoolean(strictMask & 0x1))                                    // basic Latin
            {
                if (IsCultureString(LanguageType.BasicLatin, str, numericOrSpace))
                    return true;
            }

            return false;
        }
        static bool IsCultureString(LanguageType culture, string str, bool numericOrSpace)
        {
            foreach (var wchar in str)
            {
                if (numericOrSpace && (char.IsNumber(wchar) || char.IsWhiteSpace(wchar)))
                    return true;

                switch (culture)
                {
                    case LanguageType.BasicLatin:
                        if (wchar >= 'a' && wchar <= 'z')                      // LATIN SMALL LETTER A - LATIN SMALL LETTER Z
                            return true;
                        if (wchar >= 'A' && wchar <= 'Z')                      // LATIN CAPITAL LETTER A - LATIN CAPITAL LETTER Z
                            return true;
                        return false;
                    case LanguageType.ExtendenLatin:
                        if (wchar >= 0x00C0 && wchar <= 0x00D6)                  // LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER O WITH DIAERESIS
                            return true;
                        if (wchar >= 0x00D8 && wchar <= 0x00DE)                  // LATIN CAPITAL LETTER O WITH STROKE - LATIN CAPITAL LETTER THORN
                            return true;
                        if (wchar == 0x00DF)                                     // LATIN SMALL LETTER SHARP S
                            return true;
                        if (wchar >= 0x00E0 && wchar <= 0x00F6)                  // LATIN SMALL LETTER A WITH GRAVE - LATIN SMALL LETTER O WITH DIAERESIS
                            return true;
                        if (wchar >= 0x00F8 && wchar <= 0x00FE)                  // LATIN SMALL LETTER O WITH STROKE - LATIN SMALL LETTER THORN
                            return true;
                        if (wchar >= 0x0100 && wchar <= 0x012F)                  // LATIN CAPITAL LETTER A WITH MACRON - LATIN SMALL LETTER I WITH OGONEK
                            return true;
                        if (wchar == 0x1E9E)                                     // LATIN CAPITAL LETTER SHARP S
                            return true;
                        return false;
                    case LanguageType.Cyrillic:
                        if (wchar >= 0x0410 && wchar <= 0x044F)                  // CYRILLIC CAPITAL LETTER A - CYRILLIC SMALL LETTER YA
                            return true;
                        if (wchar == 0x0401 || wchar == 0x0451)                  // CYRILLIC CAPITAL LETTER IO, CYRILLIC SMALL LETTER IO
                            return true;
                        return false;
                    case LanguageType.EastAsia:
                        if (wchar >= 0x1100 && wchar <= 0x11F9)                  // Hangul Jamo
                            return true;
                        if (wchar >= 0x3041 && wchar <= 0x30FF)                  // Hiragana + Katakana
                            return true;
                        if (wchar >= 0x3131 && wchar <= 0x318E)                  // Hangul Compatibility Jamo
                            return true;
                        if (wchar >= 0x31F0 && wchar <= 0x31FF)                  // Katakana Phonetic Ext.
                            return true;
                        if (wchar >= 0x3400 && wchar <= 0x4DB5)                  // CJK Ideographs Ext. A
                            return true;
                        if (wchar >= 0x4E00 && wchar <= 0x9FC3)                  // Unified CJK Ideographs
                            return true;
                        if (wchar >= 0xAC00 && wchar <= 0xD7A3)                  // Hangul Syllables
                            return true;
                        if (wchar >= 0xFF01 && wchar <= 0xFFEE)                  // Halfwidth forms
                            return true;
                        return false;
                }
            }

            return false;
        }

        //General
        public void LoadCypherStrings()
        {
            var time = Time.GetMSTime();
            CypherStringStorage.Clear();

            SQLResult result = DB.World.Query("SELECT entry, content_default, content_loc1, content_loc2, content_loc3, content_loc4, content_loc5, content_loc6, content_loc7, content_loc8 FROM trinity_string");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 CypherStrings. DB table `trinity_string` is empty.");
                Global.WorldMgr.StopNow();
                return;
            }
            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);

                CypherStringStorage[entry] = new StringArray((int)SharedConst.DefaultLocale + 1);
                count++;

                for (var i = SharedConst.DefaultLocale; i >= 0; --i)
                    AddLocaleString(result.Read<string>((int)i + 1).ConvertFormatSyntax(), i, CypherStringStorage[entry]);
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} CypherStrings in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }

        public void LoadRaceAndClassExpansionRequirements()
        {
            uint oldMSTime = Time.GetMSTime();
            _raceExpansionRequirementStorage.Clear();

            //                                            0       1
            SQLResult result = DB.World.Query("SELECT raceID, expansion FROM `race_expansion_requirement`");
            if (!result.IsEmpty())
            {
                uint count = 0;
                do
                {
                    byte raceID = result.Read<byte>(0);
                    byte expansion = result.Read<byte>(1);

                    ChrRacesRecord raceEntry = CliDB.ChrRacesStorage.LookupByKey(raceID);
                    if (raceEntry == null)
                    {
                        Log.outError(LogFilter.Sql, "Race {0} defined in `race_expansion_requirement` does not exists, skipped.", raceID);
                        continue;
                    }

                    if (expansion >= (int)Expansion.Max)
                    {
                        Log.outError(LogFilter.Sql, "Race {0} defined in `race_expansion_requirement` has incorrect expansion {1}, skipped.", raceID, expansion);
                        continue;
                    }

                    _raceExpansionRequirementStorage[raceID] = expansion;

                    ++count;
                }
                while (result.NextRow());
                Log.outInfo(LogFilter.ServerLoading, "Loaded {0} race expansion requirements in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
            }
            else
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 race expansion requirements. DB table `race_expansion_requirement` is empty.");

            oldMSTime = Time.GetMSTime();
            _classExpansionRequirementStorage.Clear();

            //                                  0        1
            result = DB.World.Query("SELECT classID, expansion FROM `class_expansion_requirement`");
            if (!result.IsEmpty())
            {
                uint count = 0;
                do
                {
                    byte classID = result.Read<byte>(0);
                    byte expansion = result.Read<byte>(1);

                    ChrClassesRecord classEntry = CliDB.ChrClassesStorage.LookupByKey(classID);
                    if (classEntry == null)
                    {
                        Log.outError(LogFilter.Sql, "Class {0} defined in `class_expansion_requirement` does not exists, skipped.", classID);
                        continue;
                    }

                    if (expansion >= (int)Expansion.Max)
                    {
                        Log.outError(LogFilter.Sql, "Class {0} defined in `class_expansion_requirement` has incorrect expansion {1}, skipped.", classID, expansion);
                        continue;
                    }

                    _classExpansionRequirementStorage[classID] = expansion;

                    ++count;
                }
                while (result.NextRow());
                Log.outInfo(LogFilter.ServerLoading, "Loaded {0} class expansion requirements in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
            }
            else
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 class expansion requirements. DB table `class_expansion_requirement` is empty.");
        }
        public void LoadRealmNames()
        {
            uint oldMSTime = Time.GetMSTime();
            _realmNameStorage.Clear();

            //                                         0   1
            SQLResult result = DB.Login.Query("SELECT id, name FROM `realmlist`");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 realm names. DB table `realmlist` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint realm = result.Read<uint>(0);
                string realmName = result.Read<string>(1);

                _realmNameStorage[realm] = realmName;

                ++count;
            }
            while (result.NextRow());
            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} realm names in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public string GetCypherString(uint entry, LocaleConstant locale = LocaleConstant.enUS)
        {
            if (!CypherStringStorage.ContainsKey(entry))
            {
                Log.outError(LogFilter.Sql, "Cypher string entry {0} not found in DB.", entry);
                return "<Error>";
            }

            var cs = CypherStringStorage[entry];
            if (cs.Length > (int)locale && !string.IsNullOrEmpty(cs[(int)locale]))
                return cs[(int)locale];

            return cs[(int)SharedConst.DefaultLocale];
        }
        public string GetCypherString(CypherStrings cmd, LocaleConstant locale = LocaleConstant.enUS)
        {
            return GetCypherString((uint)cmd, locale);
        }

        public string GetRealmName(uint realm)
        {
            return _realmNameStorage.LookupByKey(realm);
        }
        public bool GetRealmName(uint realmId, ref string name, ref string normalizedName)
        {
            var realmName = _realmNameStorage.LookupByKey(realmId);
            if (realmName != null)
            {
                name = realmName;
                normalizedName = realmName.Normalize();
                return true;
            }
            return false;
        }
        public Dictionary<byte, byte> GetRaceExpansionRequirements() { return _raceExpansionRequirementStorage; }
        public Expansion GetRaceExpansionRequirement(Race race)
        {
            if (_raceExpansionRequirementStorage.ContainsKey((byte)race))
                return (Expansion)_raceExpansionRequirementStorage[(byte)race];
            return Expansion.Classic;
        }
        public Dictionary<byte, byte> GetClassExpansionRequirements() { return _classExpansionRequirementStorage; }
        public Expansion GetClassExpansionRequirement(Class class_)
        {
            var exp = _classExpansionRequirementStorage.LookupByKey(class_);
            if (_classExpansionRequirementStorage.ContainsKey((byte)class_))
                return (Expansion)_classExpansionRequirementStorage[(byte)class_];
            return Expansion.Classic;
        }

        //Gossip
        public void LoadGossipMenu()
        {
            uint oldMSTime = Time.GetMSTime();

            gossipMenusStorage.Clear();

            SQLResult result = DB.World.Query("SELECT entry, text_id FROM gossip_menu");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0  gossip_menu entries. DB table `gossip_menu` is empty!");
                return;
            }

            uint count = 0;

            do
            {
                GossipMenus gMenu = new GossipMenus();

                gMenu.entry = result.Read<uint>(0);
                gMenu.text_id = result.Read<uint>(1);

                if (GetNpcText(gMenu.text_id) == null)
                {
                    Log.outError(LogFilter.Sql, "Table gossip_menu entry {0} are using non-existing textid {1}", gMenu.entry, gMenu.text_id);
                    continue;
                }

                gossipMenusStorage.Add(gMenu.entry, gMenu);

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gossip_menu entries in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadGossipMenuItems()
        {
            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");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 gossip_menu_option entries. DB table `gossip_menu_option` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                GossipMenuItems gMenuItem = new GossipMenuItems();

                gMenuItem.MenuId = result.Read<uint>(0);
                gMenuItem.OptionIndex = result.Read<uint>(1);
                gMenuItem.OptionIcon = (GossipOptionIcon)result.Read<uint>(2);
                gMenuItem.OptionText = result.Read<string>(3);
                gMenuItem.OptionBroadcastTextId = result.Read<uint>(4);
                gMenuItem.OptionType = (GossipOption)result.Read<uint>(5);
                gMenuItem.OptionNpcflag = (NPCFlags)result.Read<ulong>(6);
                gMenuItem.ActionMenuId = result.Read<uint>(7);
                gMenuItem.ActionPoiId = result.Read<uint>(8);
                gMenuItem.BoxCoded = result.Read<bool>(9);
                gMenuItem.BoxMoney = result.Read<uint>(10);
                gMenuItem.BoxText = result.Read<string>(11);
                gMenuItem.BoxBroadcastTextId = result.Read<uint>(12);

                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);
                    gMenuItem.OptionIcon = GossipOptionIcon.Chat;
                }

                if (gMenuItem.OptionBroadcastTextId != 0)
                {
                    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);
                        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);

                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);
                    gMenuItem.ActionPoiId = 0;
                }

                if (gMenuItem.BoxBroadcastTextId != 0)
                {
                    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);
                        gMenuItem.BoxBroadcastTextId = 0;
                    }
                }

                gossipMenuItemsStorage.Add(gMenuItem.MenuId, gMenuItem);
                ++count;

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gossip_menu_option entries in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadPointsOfInterest()
        {
            uint oldMSTime = Time.GetMSTime();

            pointsOfInterestStorage.Clear(); // need for reload case

            //                                         0   1          2          3     4      5           6
            SQLResult result = DB.World.Query("SELECT ID, PositionX, PositionY, Icon, Flags, Importance, Name FROM points_of_interest");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 Points of Interest definitions. DB table `points_of_interest` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint id = result.Read<uint>(0);

                PointOfInterest POI = new PointOfInterest();
                POI.ID = id;
                POI.Pos = new Vector2(result.Read<float>(1), result.Read<float>(2));
                POI.Icon = result.Read<uint>(3);
                POI.Flags = result.Read<uint>(4);
                POI.Importance = result.Read<uint>(5);
                POI.Name = result.Read<string>(6);

                if (!GridDefines.IsValidMapCoord(POI.Pos.X, POI.Pos.Y))
                {
                    Log.outError(LogFilter.Sql, "Table `points_of_interest` (ID: {0}) have invalid coordinates (PositionX: {1} PositionY: {2}), ignored.", id, POI.Pos.X, POI.Pos.Y);
                    continue;
                }

                pointsOfInterestStorage[id] = POI;

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Points of Interest definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public List<GossipMenus> GetGossipMenusMapBounds(uint uiMenuId)
        {
            return gossipMenusStorage.LookupByKey(uiMenuId);
        }
        public List<GossipMenuItems> GetGossipMenuItemsMapBounds(uint uiMenuId)
        {
            return gossipMenuItemsStorage.LookupByKey(uiMenuId);
        }
        public PointOfInterest GetPointOfInterest(uint id)
        {
            return pointsOfInterestStorage.LookupByKey(id);
        }

        public void LoadGraveyardZones()
        {
            uint oldMSTime = Time.GetMSTime();

            GraveYardStorage.Clear();                                  // need for reload case

            //                                         0       1         2
            SQLResult result = DB.World.Query("SELECT ID, GhostZone, faction FROM graveyard_zone");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 graveyard-zone links. DB table `graveyard_zone` is empty.");
                return;
            }

            uint count = 0;

            do
            {
                ++count;
                uint safeLocId = result.Read<uint>(0);
                uint zoneId = result.Read<uint>(1);
                Team team = (Team)result.Read<uint>(2);

                WorldSafeLocsRecord entry = CliDB.WorldSafeLocsStorage.LookupByKey(safeLocId);
                if (entry == null)
                {
                    Log.outError(LogFilter.Sql, "Table `graveyard_zone` has a record for not existing graveyard (WorldSafeLocs.dbc id) {0}, skipped.", safeLocId);
                    continue;
                }

                AreaTableRecord areaEntry = CliDB.AreaTableStorage.LookupByKey(zoneId);
                if (areaEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Table `graveyard_zone` has a record for not existing zone id ({0}), skipped.", zoneId);
                    continue;
                }

                if (team != 0 && team != Team.Horde && team != Team.Alliance)
                {
                    Log.outError(LogFilter.Sql, "Table `graveyard_zone` has a record for non player faction ({0}), skipped.", team);
                    continue;
                }

                if (!AddGraveYardLink(safeLocId, zoneId, team, false))
                    Log.outError(LogFilter.Sql, "Table `graveyard_zone` has a duplicate record for Graveyard (ID: {0}) and Zone (ID: {1}), skipped.", safeLocId, zoneId);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} graveyard-zone links in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        WorldSafeLocsRecord GetDefaultGraveYard(Team team)
        {
            if (team == Team.Horde)
                return CliDB.WorldSafeLocsStorage.LookupByKey(10);
            else if (team == Team.Alliance)
                return CliDB.WorldSafeLocsStorage.LookupByKey(4);
            else return null;
        }
        public WorldSafeLocsRecord GetClosestGraveYard(float x, float y, float z, uint MapId, Team team)
        {
            // search for zone associated closest graveyard
            uint zoneId = Global.MapMgr.GetZoneId(MapId, x, y, z);
            if (zoneId == 0)
            {
                if (z > -500)
                {
                    Log.outError(LogFilter.Server, "ZoneId not found for map {0} coords ({1}, {2}, {3})", MapId, x, y, z);
                    return GetDefaultGraveYard(team);
                }
            }

            // Simulate std. algorithm:
            //   found some graveyard associated to (ghost_zone, ghost_map)
            //
            //   if mapId == graveyard.mapId (ghost in plain zone or city or Battleground) and search graveyard at same map
            //     then check faction
            //   if mapId != graveyard.mapId (ghost in instance) and search any graveyard associated
            //     then check faction
            var range = GraveYardStorage.LookupByKey(zoneId);
            MapRecord mapEntry = CliDB.MapStorage.LookupByKey(MapId);

            // not need to check validity of map object; MapId _MUST_ be valid here
            if (range.Empty() && !mapEntry.IsBattlegroundOrArena())
            {
                if (zoneId != 0) // zone == 0 can't be fixed, used by bliz for bugged zones
                    Log.outError(LogFilter.Sql, "Table `game_graveyard_zone` incomplete: Zone {0} Team {1} does not have a linked graveyard.", zoneId, team);
                return GetDefaultGraveYard(team);
            }

            // at corpse map
            bool foundNear = false;
            float distNear = 10000;
            WorldSafeLocsRecord entryNear = null;

            // at entrance map for corpse map
            bool foundEntr = false;
            float distEntr = 10000;
            WorldSafeLocsRecord entryEntr = null;

            // some where other
            WorldSafeLocsRecord entryFar = null;

            foreach (var data in range)
            {
                WorldSafeLocsRecord entry = CliDB.WorldSafeLocsStorage.LookupByKey(data.safeLocId);
                if (entry == null)
                {
                    Log.outError(LogFilter.Sql, "Table `game_graveyard_zone` has record for not existing graveyard (WorldSafeLocs.dbc id) {0}, skipped.", data.safeLocId);
                    continue;
                }

                // skip enemy faction graveyard
                // team == 0 case can be at call from .neargrave
                if (data.team != 0 && team != 0 && data.team != (uint)team)
                    continue;

                // find now nearest graveyard at other map
                if (MapId != entry.MapID)
                {
                    // if find graveyard at different map from where entrance placed (or no entrance data), use any first
                    if (mapEntry == null
                        || mapEntry.CorpseMapID < 0
                        || mapEntry.CorpseMapID != entry.MapID
                        || (mapEntry.CorpsePos.X == 0 && mapEntry.CorpsePos.Y == 0))
                    {
                        // not have any corrdinates for check distance anyway
                        entryFar = entry;
                        continue;
                    }

                    // at entrance map calculate distance (2D);
                    float dist2 = (entry.Loc.X - mapEntry.CorpsePos.X) * (entry.Loc.X - mapEntry.CorpsePos.X)
                        + (entry.Loc.Y - mapEntry.CorpsePos.Y) * (entry.Loc.Y - mapEntry.CorpsePos.Y);
                    if (foundEntr)
                    {
                        if (dist2 < distEntr)
                        {
                            distEntr = dist2;
                            entryEntr = entry;
                        }
                    }
                    else
                    {
                        foundEntr = true;
                        distEntr = dist2;
                        entryEntr = entry;
                    }
                }
                // find now nearest graveyard at same map
                else
                {
                    float dist2 = (entry.Loc.X - x) * (entry.Loc.X - x) + (entry.Loc.Y - y) * (entry.Loc.Y - y) + (entry.Loc.Z - z) * (entry.Loc.Z - z);
                    if (foundNear)
                    {
                        if (dist2 < distNear)
                        {
                            distNear = dist2;
                            entryNear = entry;
                        }
                    }
                    else
                    {
                        foundNear = true;
                        distNear = dist2;
                        entryNear = entry;
                    }
                }
            }

            if (entryNear != null)
                return entryNear;

            if (entryEntr != null)
                return entryEntr;

            return entryFar;
        }
        public GraveYardData FindGraveYardData(uint id, uint zoneId)
        {
            var range = GraveYardStorage.LookupByKey(zoneId);
            foreach (var data in range)
            {
                if (data.safeLocId == id)
                    return data;
            }
            return null;
        }

        public bool AddGraveYardLink(uint id, uint zoneId, Team team, bool persist = true)
        {
            if (FindGraveYardData(id, zoneId) != null)
                return false;

            // add link to loaded data
            GraveYardData data = new GraveYardData();
            data.safeLocId = id;
            data.team = (uint)team;

            GraveYardStorage.Add(zoneId, data);

            // add link to DB
            if (persist)
            {
                PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.INS_GRAVEYARD_ZONE);

                stmt.AddValue(0, id);
                stmt.AddValue(1, zoneId);
                stmt.AddValue(2, team);

                DB.World.Execute(stmt);
            }

            return true;
        }
        public void RemoveGraveYardLink(uint id, uint zoneId, Team team, bool persist = false)
        {
            var range = GraveYardStorage.LookupByKey(zoneId);
            if (range.Empty())
            {
                Log.outError(LogFilter.Sql, "Table `game_graveyard_zone` incomplete: Zone {0} Team {1} does not have a linked graveyard.", zoneId, team);
                return;
            }

            bool found = false;


            foreach (var data in range)
            {
                // skip not matching safezone id
                if (data.safeLocId != id)
                    continue;

                // skip enemy faction graveyard at same map (normal area, city, or Battleground)
                // team == 0 case can be at call from .neargrave
                if (data.team != 0 && team != 0 && data.team != (uint)team)
                    continue;

                found = true;
                break;
            }

            // no match, return
            if (!found)
                return;

            // remove from links
            GraveYardStorage.Remove(zoneId);

            // remove link from DB
            if (persist)
            {
                PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.DEL_GRAVEYARD_ZONE);

                stmt.AddValue(0, id);
                stmt.AddValue(1, zoneId);
                stmt.AddValue(2, team);

                DB.World.Execute(stmt);
            }
        }

        //Scripts
        public void LoadScriptNames()
        {
            uint oldMSTime = Time.GetMSTime();

            scriptNamesStorage.Add("");
            SQLResult result = DB.World.Query(
              "SELECT DISTINCT(ScriptName) FROM Battleground_template WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM creature WHERE ScriptName <> '' " +        
              "UNION SELECT DISTINCT(ScriptName) FROM creature_template WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM criteria_data WHERE ScriptName <> '' AND type = 11 " +
              "UNION SELECT DISTINCT(ScriptName) FROM gameobject WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM gameobject_template WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM item_script_names WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM areatrigger_scripts WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM areatrigger_template WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM spell_script_names WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM transports WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM game_weather WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM conditions WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM outdoorpvp_template WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(ScriptName) FROM scene_template WHERE ScriptName <> '' " +
              "UNION SELECT DISTINCT(script) FROM instance_template WHERE script <> ''");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded empty set of Script Names!");
                return;
            }

            uint count = 1;
            do
            {
                scriptNamesStorage.Add(result.Read<string>(0));
                ++count;
            }
            while (result.NextRow());

            scriptNamesStorage.Sort();
            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Script Names in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadAreaTriggerScripts()
        {
            uint oldMSTime = Time.GetMSTime();

            areaTriggerScriptStorage.Clear();                            // need for reload case
            SQLResult result = DB.World.Query("SELECT entry, ScriptName FROM areatrigger_scripts");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 areatrigger scripts. DB table `areatrigger_scripts` is empty.");
                return;
            }
            uint count = 0;
            do
            {
                uint id = result.Read<uint>(0);
                string scriptName = result.Read<string>(1);

                AreaTriggerRecord atEntry = CliDB.AreaTriggerStorage.LookupByKey(id);
                if (atEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Area trigger (Id:{0}) does not exist in `AreaTrigger.dbc`.", id);
                    continue;
                }
                ++count;
                areaTriggerScriptStorage[id] = GetScriptId(scriptName);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} areatrigger scripts in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        void LoadScripts(ScriptsType type)
        {
            uint oldMSTime = Time.GetMSTime();

            var scripts = GetScriptsMapByType(type);
            if (scripts == null)
                return;

            string tableName = GetScriptsTableNameByType(type);
            if (string.IsNullOrEmpty(tableName))
                return;

            if (Global.MapMgr.IsScriptScheduled())                    // function cannot be called when scripts are in use.
                return;

            Log.outInfo(LogFilter.ServerLoading, "Loading {0}...", tableName);

            scripts.Clear();                                       // need for reload support

            bool isSpellScriptTable = (type == ScriptsType.Spell);
            //                                         0    1       2         3         4          5    6  7  8  9
            SQLResult result = DB.World.Query("SELECT id, delay, command, datalong, datalong2, dataint, x, y, z, o{0} FROM {1}", isSpellScriptTable ? ", effIndex" : "", tableName);
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 script definitions. DB table `{0}` is empty!", tableName);
                return;
            }

            uint count = 0;
            do
            {
                ScriptInfo tmp = new ScriptInfo();
                tmp.type = type;
                tmp.id = result.Read<uint>(0);
                if (isSpellScriptTable)
                    tmp.id |= result.Read<uint>(10) << 24;
                tmp.delay = result.Read<uint>(1);
                tmp.command = (ScriptCommands)result.Read<uint>(2);
                unsafe
                {
                    tmp.Raw.nData[0] = result.Read<uint>(3);
                    tmp.Raw.nData[1] = result.Read<uint>(4);
                    tmp.Raw.nData[2] = (uint)result.Read<int>(5);
                    tmp.Raw.fData[0] = result.Read<float>(6);
                    tmp.Raw.fData[1] = result.Read<float>(7);
                    tmp.Raw.fData[2] = result.Read<float>(8);
                    tmp.Raw.fData[3] = result.Read<float>(9);
                }

                // generic command args check
                switch (tmp.command)
                {
                    case ScriptCommands.Talk:
                        {
                            if (tmp.Talk.ChatType > ChatMsg.RaidBossWhisper)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid talk type (datalong = {1}) in SCRIPT_COMMAND_TALK for script id {2}",
                                    tableName, tmp.Talk.ChatType, tmp.id);
                                continue;
                            }
                            if (!CliDB.BroadcastTextStorage.ContainsKey((uint)tmp.Talk.TextID))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid talk text id (dataint = {1}) in SCRIPT_COMMAND_TALK for script id {2}",
                                    tableName, tmp.Talk.TextID, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.Emote:
                        {
                            if (!CliDB.EmotesStorage.ContainsKey(tmp.Emote.EmoteID))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid emote id (datalong = {1}) in SCRIPT_COMMAND_EMOTE for script id {2}",
                                    tableName, tmp.Emote.EmoteID, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.TeleportTo:
                        {
                            if (!CliDB.MapStorage.ContainsKey(tmp.TeleportTo.MapID))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid map (Id: {1}) in SCRIPT_COMMAND_TELEPORT_TO for script id {2}",
                                    tableName, tmp.TeleportTo.MapID, tmp.id);
                                continue;
                            }

                            if (!GridDefines.IsValidMapCoord(tmp.TeleportTo.DestX, tmp.TeleportTo.DestY, tmp.TeleportTo.DestZ, tmp.TeleportTo.Orientation))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid coordinates (X: {1} Y: {2} Z: {3} O: {4}) in SCRIPT_COMMAND_TELEPORT_TO for script id {5}",
                                    tableName, tmp.TeleportTo.DestX, tmp.TeleportTo.DestY, tmp.TeleportTo.DestZ, tmp.TeleportTo.Orientation, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.QuestExplored:
                        {
                            Quest quest = GetQuestTemplate(tmp.QuestExplored.QuestID);
                            if (quest == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid quest (ID: {1}) in SCRIPT_COMMAND_QUEST_EXPLORED in `datalong` for script id {2}",
                                    tableName, tmp.QuestExplored.QuestID, tmp.id);
                                continue;
                            }

                            if (!quest.HasSpecialFlag(QuestSpecialFlags.ExplorationOrEvent))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has quest (ID: {1}) in SCRIPT_COMMAND_QUEST_EXPLORED in `datalong` for script id {2}, but quest not have flag QUEST_SPECIAL_FLAGS_EXPLORATION_OR_EVENT in quest flags. Script command or quest flags wrong. Quest modified to require objective.",
                                    tableName, tmp.QuestExplored.QuestID, tmp.id);

                                // this will prevent quest completing without objective
                                quest.SetSpecialFlag(QuestSpecialFlags.ExplorationOrEvent);

                                // continue; - quest objective requirement set and command can be allowed
                            }

                            if (tmp.QuestExplored.Distance > SharedConst.DefaultVisibilityDistance)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has too large distance ({1}) for exploring objective complete in `datalong2` in SCRIPT_COMMAND_QUEST_EXPLORED in `datalong` for script id {2}",
                                    tableName, tmp.QuestExplored.Distance, tmp.id);
                                continue;
                            }

                            if (tmp.QuestExplored.Distance != 0 && tmp.QuestExplored.Distance > SharedConst.DefaultVisibilityDistance)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has too large distance ({1}) for exploring objective complete in `datalong2` in SCRIPT_COMMAND_QUEST_EXPLORED in `datalong` for script id {2}, max distance is {3} or 0 for disable distance check",
                                    tableName, tmp.QuestExplored.Distance, tmp.id, SharedConst.DefaultVisibilityDistance);
                                continue;
                            }

                            if (tmp.QuestExplored.Distance != 0 && tmp.QuestExplored.Distance < SharedConst.InteractionDistance)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has too small distance ({1}) for exploring objective complete in `datalong2` in SCRIPT_COMMAND_QUEST_EXPLORED in `datalong` for script id {2}, min distance is {3} or 0 for disable distance check",
                                    tableName, tmp.QuestExplored.Distance, tmp.id, SharedConst.InteractionDistance);
                                continue;
                            }

                            break;
                        }

                    case ScriptCommands.KillCredit:
                        {
                            if (GetCreatureTemplate(tmp.KillCredit.CreatureEntry) == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid creature (Entry: {1}) in SCRIPT_COMMAND_KILL_CREDIT for script id {2}",
                                    tableName, tmp.KillCredit.CreatureEntry, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.RespawnGameobject:
                        {
                            GameObjectData data = GetGOData(tmp.RespawnGameObject.GOGuid);
                            if (data == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid gameobject (GUID: {1}) in SCRIPT_COMMAND_RESPAWN_GAMEOBJECT for script id {2}",
                                    tableName, tmp.RespawnGameObject.GOGuid, tmp.id);
                                continue;
                            }

                            GameObjectTemplate info = GetGameObjectTemplate(data.id);
                            if (info == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has gameobject with invalid entry (GUID: {1} Entry: {2}) in SCRIPT_COMMAND_RESPAWN_GAMEOBJECT for script id {3}",
                                    tableName, tmp.RespawnGameObject.GOGuid, data.id, tmp.id);
                                continue;
                            }

                            if (info.type == GameObjectTypes.FishingNode || info.type == GameObjectTypes.FishingHole || info.type == GameObjectTypes.Door ||
                                info.type == GameObjectTypes.Button || info.type == GameObjectTypes.Trap)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` have gameobject type ({1}) unsupported by command SCRIPT_COMMAND_RESPAWN_GAMEOBJECT for script id {2}",
                                    tableName, info.entry, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.TempSummonCreature:
                        {
                            if (!GridDefines.IsValidMapCoord(tmp.TempSummonCreature.PosX, tmp.TempSummonCreature.PosY, tmp.TempSummonCreature.PosZ, tmp.TempSummonCreature.Orientation))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid coordinates (X: {1} Y: {2} Z: {3} O: {4}) in SCRIPT_COMMAND_TEMP_SUMMON_CREATURE for script id {5}",
                                    tableName, tmp.TempSummonCreature.PosX, tmp.TempSummonCreature.PosY, tmp.TempSummonCreature.PosZ, tmp.TempSummonCreature.Orientation, tmp.id);
                                continue;
                            }

                            if (GetCreatureTemplate(tmp.TempSummonCreature.CreatureEntry) == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid creature (Entry: {1}) in SCRIPT_COMMAND_TEMP_SUMMON_CREATURE for script id {2}",
                                    tableName, tmp.TempSummonCreature.CreatureEntry, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.OpenDoor:
                    case ScriptCommands.CloseDoor:
                        {
                            GameObjectData data = GetGOData(tmp.ToggleDoor.GOGuid);
                            if (data == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid gameobject (GUID: {1}) in {2} for script id {3}",
                                    tableName, tmp.ToggleDoor.GOGuid, tmp.command, tmp.id);
                                continue;
                            }

                            GameObjectTemplate info = GetGameObjectTemplate(data.id);
                            if (info == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has gameobject with invalid entry (GUID: {1} Entry: {2}) in {3} for script id {4}",
                                    tableName, tmp.ToggleDoor.GOGuid, data.id, tmp.command, tmp.id);
                                continue;
                            }

                            if (info.type != GameObjectTypes.Door)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has gameobject type ({1}) non supported by command {2} for script id {3}",
                                    tableName, info.entry, tmp.command, tmp.id);
                                continue;
                            }

                            break;
                        }

                    case ScriptCommands.RemoveAura:
                        {
                            if (Global.SpellMgr.GetSpellInfo(tmp.RemoveAura.SpellID) == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` using non-existent spell (id: {1}) in SCRIPT_COMMAND_REMOVE_AURA for script id {2}",
                                    tableName, tmp.RemoveAura.SpellID, tmp.id);
                                continue;
                            }
                            if (Convert.ToBoolean((int)tmp.RemoveAura.Flags & ~0x1))                    // 1 bits (0, 1)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` using unknown flags in datalong2 ({1}) in SCRIPT_COMMAND_REMOVE_AURA for script id {2}",
                                    tableName, tmp.RemoveAura.Flags, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.CastSpell:
                        {
                            if (Global.SpellMgr.GetSpellInfo(tmp.CastSpell.SpellID) == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` using non-existent spell (id: {1}) in SCRIPT_COMMAND_CAST_SPELL for script id {2}",
                                    tableName, tmp.CastSpell.SpellID, tmp.id);
                                continue;
                            }
                            if ((int)tmp.CastSpell.Flags > 4)                      // targeting type
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` using unknown target in datalong2 ({1}) in SCRIPT_COMMAND_CAST_SPELL for script id {2}",
                                    tableName, tmp.CastSpell.Flags, tmp.id);
                                continue;
                            }
                            if ((int)tmp.CastSpell.Flags != 4 && Convert.ToBoolean(tmp.CastSpell.CreatureEntry & ~0x1))                      // 1 bit (0, 1)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` using unknown flags in dataint ({1}) in SCRIPT_COMMAND_CAST_SPELL for script id {2}",
                                    tableName, tmp.CastSpell.CreatureEntry, tmp.id);
                                continue;
                            }
                            else if ((int)tmp.CastSpell.Flags == 4 && GetCreatureTemplate((uint)tmp.CastSpell.CreatureEntry) == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` using invalid creature entry in dataint ({1}) in SCRIPT_COMMAND_CAST_SPELL for script id {2}",
                                    tableName, tmp.CastSpell.CreatureEntry, tmp.id);
                                continue;
                            }
                            break;
                        }

                    case ScriptCommands.CreateItem:
                        {
                            if (GetItemTemplate(tmp.CreateItem.ItemEntry) == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has nonexistent item (entry: {1}) in SCRIPT_COMMAND_CREATE_ITEM for script id {2}",
                                    tableName, tmp.CreateItem.ItemEntry, tmp.id);
                                continue;
                            }
                            if (tmp.CreateItem.Amount == 0)
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` SCRIPT_COMMAND_CREATE_ITEM but amount is {1} for script id {2}",
                                    tableName, tmp.CreateItem.Amount, tmp.id);
                                continue;
                            }
                            break;
                        }
                    case ScriptCommands.PlayAnimkit:
                        {
                            if (!CliDB.AnimKitStorage.ContainsKey(tmp.PlayAnimKit.AnimKitID))
                            {
                                Log.outError(LogFilter.Sql, "Table `{0}` has invalid AnimKid id (datalong = {1}) in SCRIPT_COMMAND_PLAY_ANIMKIT for script id {2}",
                                    tableName, tmp.PlayAnimKit.AnimKitID, tmp.id);
                                continue;
                            }
                            break;
                        }
                    default:
                        break;
                }

                if (!scripts.ContainsKey(tmp.id))
                    scripts[tmp.id] = new MultiMap<uint, ScriptInfo>();

                scripts[tmp.id].Add(tmp.delay, tmp);

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} script definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadSpellScripts()
        {
            LoadScripts(ScriptsType.Spell);

            // check ids
            foreach (var script in sSpellScripts)
            {
                uint spellId = script.Key & 0x00FFFFFF;
                SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(spellId);
                if (spellInfo == null)
                {
                    Log.outError(LogFilter.Sql, "Table `spell_scripts` has not existing spell (Id: {0}) as script id", spellId);
                    continue;
                }

                byte i = (byte)((script.Key >> 24) & 0x000000FF);
                //check for correct spellEffect
                SpellEffectInfo effect = spellInfo.GetEffect(i);
                if (effect != null && (effect.Effect == 0 || (effect.Effect != SpellEffectName.ScriptEffect && effect.Effect != SpellEffectName.Dummy)))
                    Log.outError(LogFilter.Sql, "Table `spell_scripts` - spell {0} effect {1} is not SPELL_EFFECT_SCRIPT_EFFECT or SPELL_EFFECT_DUMMY", spellId, i);
            }
        }
        public void LoadEventScripts()
        {
            LoadScripts(ScriptsType.Event);

            List<uint> evt_scripts = new List<uint>();
            // Load all possible script entries from gameobjects
            foreach (var go in gameObjectTemplateStorage)
            {
                uint eventId = go.Value.GetEventScriptId();
                if (eventId != 0)
                    evt_scripts.Add(eventId);
            }

            // Load all possible script entries from spells
            foreach (var spell in Global.SpellMgr.GetSpellInfoStorage().Values)
            {
                foreach (SpellEffectInfo effect in spell.GetEffectsForDifficulty(Difficulty.None))
                {
                    if (effect == null)
                        continue;

                    if (effect.Effect == SpellEffectName.SendEvent)
                        if (effect.MiscValue != 0)
                            evt_scripts.Add((uint)effect.MiscValue);
                }
            }

            foreach (var path_idx in CliDB.TaxiPathNodesByPath)
            {
                for (uint node_idx = 0; node_idx < path_idx.Value.Length; ++node_idx)
                {
                    TaxiPathNodeRecord node = path_idx.Value[node_idx];

                    if (node.ArrivalEventID != 0)
                        evt_scripts.Add(node.ArrivalEventID);

                    if (node.DepartureEventID != 0)
                        evt_scripts.Add(node.DepartureEventID);
                }
            }

            // Then check if all scripts are in above list of possible script entries
            foreach (var script in sEventScripts)
            {
                var id = evt_scripts.Find(p => p == script.Key);
                if (id == 0)
                    Log.outError(LogFilter.Sql, "Table `event_scripts` has script (Id: {0}) not referring to any gameobject_template type 10 data2 field, type 3 data6 field, type 13 data 2 field or any spell effect {1}",
                        script.Key, SpellEffectName.SendEvent);
            }
        }

        //Load WP Scripts
        public void LoadWaypointScripts()
        {
            LoadScripts(ScriptsType.Waypoint);

            List<uint> actionSet = new List<uint>();

            foreach (var script in sWaypointScripts)
                actionSet.Add(script.Key);

            PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.SEL_WAYPOINT_DATA_ACTION);
            SQLResult result = DB.World.Query(stmt);

            if (!result.IsEmpty())
            {
                do
                {
                    uint action = result.Read<uint>(0);

                    actionSet.Remove(action);
                }
                while (result.NextRow());
            }

            foreach (var id in actionSet)
                Log.outError(LogFilter.Sql, "There is no waypoint which links to the waypoint script {0}", id);
        }
        public void LoadSpellScriptNames()
        {
            uint oldMSTime = Time.GetMSTime();

            spellScriptsStorage.Clear();                            // need for reload case

            SQLResult result = DB.World.Query("SELECT spell_id, ScriptName FROM spell_script_names");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 spell script names. DB table `spell_script_names` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                int spellId = result.Read<int>(0);
                string scriptName = result.Read<string>(1);

                bool allRanks = false;
                if (spellId < 0)
                {
                    allRanks = true;
                    spellId = -spellId;
                }

                SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)spellId);
                if (spellInfo == null)
                {
                    Log.outError(LogFilter.Sql, "Scriptname: `{0}` spell (Id: {1}) does not exist.", scriptName, spellId);
                    continue;
                }

                if (allRanks)
                {
                    if (!spellInfo.IsRanked())
                        Log.outError(LogFilter.Sql, "Scriptname: `{0}` spell (Id: {1}) has no ranks of spell.", scriptName, spellId);

                    if (spellInfo.GetFirstRankSpell().Id != spellId)
                    {
                        Log.outError(LogFilter.Sql, "Scriptname: `{0}` spell (Id: {1}) is not first rank of spell.", scriptName, spellId);
                        continue;
                    }
                    while (spellInfo != null)
                    {
                        spellScriptsStorage.Add(spellInfo.Id, GetScriptId(scriptName));
                        spellInfo = spellInfo.GetNextRankSpell();
                    }
                }
                else
                {
                    if (spellInfo.IsRanked())
                        Log.outError(LogFilter.Sql, "Scriptname: `{0}` spell (Id: {1}) is ranked spell. Perhaps not all ranks are assigned to this script.", scriptName, spellId);

                    spellScriptsStorage.Add(spellInfo.Id, GetScriptId(scriptName));
                }

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} spell script names in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void ValidateSpellScripts()
        {
            uint oldMSTime = Time.GetMSTime();

            if (spellScriptsStorage.Empty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Validated 0 scripts.");
                return;
            }

            uint count = 0;

            foreach (var script in spellScriptsStorage.ToList())
            {
                SpellInfo spellEntry = Global.SpellMgr.GetSpellInfo(script.Key);
                Dictionary<SpellScriptLoader, uint> SpellScriptLoaders = Global.ScriptMgr.CreateSpellScriptLoaders(script.Key);

                foreach (var pair in SpellScriptLoaders)
                {
                    SpellScript spellScript = pair.Key.GetSpellScript();
                    AuraScript auraScript = pair.Key.GetAuraScript();
                    bool valid = true;

                    if (spellScript == null && auraScript == null)
                    {
                        Log.outError(LogFilter.Scripts, "Functions GetSpellScript() and GetAuraScript() of script `{0}` do not return objects - script skipped", GetScriptName(pair.Value));
                        valid = false;
                    }

                    if (spellScript != null)
                    {
                        spellScript._Init(pair.Key.GetName(), spellEntry.Id);
                        spellScript._Register();
                        if (!spellScript._Validate(spellEntry))
                            valid = false;
                    }

                    if (auraScript != null)
                    {
                        auraScript._Init(pair.Key.GetName(), spellEntry.Id);
                        auraScript._Register();
                        if (!auraScript._Validate(spellEntry))
                            valid = false;
                    }

                    if (!valid)
                        spellScriptsStorage.Remove(pair.Value);
                }
                ++count;
            }

            Log.outInfo(LogFilter.ServerLoading, "Validated {0} scripts in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public List<uint> GetSpellScriptsBounds(uint spellId)
        {
            return spellScriptsStorage.LookupByKey(spellId);
        }
        public string GetScriptName(uint id)
        {
            return id < scriptNamesStorage.Count ? scriptNamesStorage[(int)id] : "";
        }
        public uint GetScriptId(string name)
        {
            // use binary search to find the script name in the sorted vector
            // assume "" is the first element
            if (string.IsNullOrEmpty(name))
                return 0;

            return (uint)scriptNamesStorage.IndexOf(name);
        }
        public uint GetAreaTriggerScriptId(uint triggerid)
        {
            return areaTriggerScriptStorage.LookupByKey(triggerid);
        }
        public Dictionary<uint, MultiMap<uint, ScriptInfo>> GetScriptsMapByType(ScriptsType type)
        {
            switch (type)
            {
                case ScriptsType.Spell:
                    return sSpellScripts;
                case ScriptsType.Event:
                    return sEventScripts;
                case ScriptsType.Waypoint:
                    return sWaypointScripts;
                default:
                    return null;
            }
        }
        public string GetScriptsTableNameByType(ScriptsType type)
        {
            switch (type)
            {
                case ScriptsType.Spell:
                    return "spell_scripts";
                case ScriptsType.Event:
                    return "event_scripts";
                case ScriptsType.Waypoint:
                    return "waypoint_scripts";
                default:
                    return "";
            }
        }

        //Creatures
        public void LoadCreatureTemplates()
        {
            var time = Time.GetMSTime();

            //                                          0      1                   2                   3                   4            5            6         7         8
            SQLResult result = DB.World.Query("SELECT entry, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, modelid1, modelid2, modelid3, " +
                //9         10    11          12       13        14              15        16        17                      18                 19
                "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
                "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
                "spell1, spell2, spell3, spell4, spell5, spell6, spell7, spell8, VehicleId, mingold, maxgold, AIName, MovementType, " +
                //65           66           67              68                   69            70                 71             72              73
                "InhabitType, HoverHeight, HealthModifier, HealthModifierExtra, ManaModifier, ManaModifierExtra, ArmorModifier, DamageModifier, ExperienceModifier, " +
                //74            75          76           77                    78           79
                "RacialLeader, movementId, RegenHealth, mechanic_immune_mask, flags_extra, ScriptName FROM creature_template");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creatures. DB table `creature_template` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                LoadCreatureTemplate(result.GetFields());
                ++count;
            } while (result.NextRow());

            // Checking needs to be done after loading because of the difficulty self referencing
            foreach (var template in creatureTemplateStorage.Values)
                CheckCreatureTemplate(template);

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature definitions in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadCreatureTemplate(SQLFields fields)
        {
            uint entry = fields.Read<uint>(0);

            CreatureTemplate creature = new CreatureTemplate();
            creature.Entry = entry;

            for (var i = 0; i < SharedConst.MaxCreatureDifficulties; ++i)
                creature.DifficultyEntry[i] = fields.Read<uint>(1 + i);

            for (var i = 0; i < 2; ++i)
                creature.KillCredit[i] = fields.Read<uint>(4 + i);

            creature.ModelId1 = fields.Read<uint>(6);
            creature.ModelId2 = fields.Read<uint>(7);
            creature.ModelId3 = fields.Read<uint>(8);
            creature.ModelId4 = fields.Read<uint>(9);
            creature.Name = fields.Read<string>(10);
            creature.FemaleName = fields.Read<string>(11);
            creature.SubName = fields.Read<string>(12);
            creature.IconName = fields.Read<string>(13);
            creature.GossipMenuId = fields.Read<uint>(14);
            creature.Minlevel = fields.Read<short>(15);
            creature.Maxlevel = fields.Read<short>(16);
            creature.HealthScalingExpansion = fields.Read<int>(17);
            creature.RequiredExpansion = fields.Read<uint>(18);
            creature.VignetteID = fields.Read<uint>(19);
            creature.Faction = fields.Read<uint>(20);
            creature.Npcflag = (NPCFlags)fields.Read<uint>(21);
            creature.SpeedWalk = fields.Read<float>(22);
            creature.SpeedRun = fields.Read<float>(23);
            creature.Scale = fields.Read<float>(24);
            creature.Rank = (CreatureEliteType)fields.Read<uint>(25);
            creature.DmgSchool = fields.Read<uint>(26);
            creature.BaseAttackTime = fields.Read<uint>(27);
            creature.RangeAttackTime = fields.Read<uint>(28);
            creature.BaseVariance = fields.Read<float>(29);
            creature.RangeVariance = fields.Read<float>(30);
            creature.UnitClass = fields.Read<uint>(31);
            creature.UnitFlags = (UnitFlags)fields.Read<uint>(32);
            creature.UnitFlags2 = fields.Read<uint>(33);
            creature.UnitFlags3 = fields.Read<uint>(34);
            creature.DynamicFlags = fields.Read<uint>(35);
            creature.Family = (CreatureFamily)fields.Read<byte>(36);
            creature.TrainerType = (TrainerType)fields.Read<byte>(37);
            creature.TrainerClass = (Class)fields.Read<byte>(38);
            creature.TrainerRace = (Race)fields.Read<byte>(39);
            creature.CreatureType = (CreatureType)fields.Read<uint>(40);
            creature.TypeFlags = (CreatureTypeFlags)fields.Read<uint>(41);
            creature.TypeFlags2 = fields.Read<uint>(42);
            creature.LootId = fields.Read<uint>(43);
            creature.PickPocketId = fields.Read<uint>(44);
            creature.SkinLootId = fields.Read<uint>(45);

            for (var i = (int)SpellSchools.Holy; i < (int)SpellSchools.Max; ++i)
                creature.Resistance[i] = fields.Read<int>(46 + i - 1);

            for (var i = 0; i < SharedConst.MaxCreatureSpells; ++i)
                creature.Spells[i] = fields.Read<uint>(52 + i);

            creature.VehicleId = fields.Read<uint>(60);
            creature.MinGold = fields.Read<uint>(61);
            creature.MaxGold = fields.Read<uint>(62);
            creature.AIName = fields.Read<string>(63);
            creature.MovementType = fields.Read<uint>(64);
            creature.InhabitType = (InhabitType)fields.Read<uint>(65);
            creature.HoverHeight = fields.Read<float>(66);
            creature.ModHealth = fields.Read<float>(67);
            creature.ModHealthExtra = fields.Read<float>(68);
            creature.ModMana = fields.Read<float>(69);
            creature.ModManaExtra = fields.Read<float>(70);
            creature.ModArmor = fields.Read<float>(71);
            creature.ModDamage = fields.Read<float>(72);
            creature.ModExperience = fields.Read<float>(73);
            creature.RacialLeader = fields.Read<bool>(74);
            creature.MovementId = fields.Read<uint>(75);
            creature.RegenHealth = fields.Read<bool>(76);
            creature.MechanicImmuneMask = fields.Read<uint>(77);
            creature.FlagsExtra = (CreatureFlagsExtra)fields.Read<uint>(78);
            creature.ScriptID = GetScriptId(fields.Read<string>(79));

            creatureTemplateStorage.Add(entry, creature);
        }
        public void LoadCreatureTemplateAddons()
        {
            var time = Time.GetMSTime();
            //                                        0       1       2      3       4       5        6             7              8          9
            SQLResult result = DB.World.Query("SELECT entry, path_id, mount, bytes1, bytes2, emote, aiAnimKit, movementAnimKit, meleeAnimKit, auras FROM creature_template_addon");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creature template addon definitions. DB table `creature_template_addon` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);
                if (GetCreatureTemplate(entry) == null)
                {
                    Log.outError(LogFilter.Sql, "Creature template (Entry: {0}) does not exist but has a record in `creature_template_addon`", entry);
                    continue;
                }

                CreatureAddon creatureAddon = new CreatureAddon();
                creatureAddon.path_id = result.Read<uint>(1);
                creatureAddon.mount = result.Read<uint>(2);
                creatureAddon.bytes1 = result.Read<uint>(3);
                creatureAddon.bytes2 = result.Read<uint>(4);
                creatureAddon.emote = result.Read<uint>(5);
                creatureAddon.aiAnimKit = result.Read<ushort>(6);
                creatureAddon.movementAnimKit = result.Read<ushort>(7);
                creatureAddon.meleeAnimKit = result.Read<ushort>(8);

                var tokens = new StringArray(result.Read<string>(9), ' ');

                creatureAddon.auras = new uint[tokens.Length];
                byte i = 0;
                for (var c = 0; c < tokens.Length; ++c)
                {
                    uint spellId = uint.Parse(tokens[c].Replace(",", ""));
                    SpellInfo AdditionalSpellInfo = Global.SpellMgr.GetSpellInfo(spellId);
                    if (AdditionalSpellInfo == null)
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong spell {1} defined in `auras` field in `creature_template_addon`.", entry, spellId);
                        continue;
                    }

                    if (AdditionalSpellInfo.HasAura(Difficulty.None, AuraType.ControlVehicle))
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has SPELL_AURA_CONTROL_VEHICLE aura {1} defined in `auras` field in `creature_template_addon`.", entry, spellId);

                    if (creatureAddon.auras.Contains(spellId))
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has duplicate aura (spell {1}) in `auras` field in `creature_template_addon`.", entry, spellId);
                        continue;
                    }

                    creatureAddon.auras[i++] = spellId;
                }

                if (creatureAddon.mount != 0)
                {
                    if (CliDB.CreatureDisplayInfoStorage.LookupByKey(creatureAddon.mount) == null)
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid displayInfoId ({1}) for mount defined in `creature_template_addon`", entry, creatureAddon.mount);
                        creatureAddon.mount = 0;
                    }
                }

                if (!CliDB.EmotesStorage.ContainsKey(creatureAddon.emote))
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid emote ({1}) defined in `creatureaddon`.", entry, creatureAddon.emote);
                    creatureAddon.emote = 0;
                }

                if (creatureAddon.aiAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(creatureAddon.aiAnimKit))
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid aiAnimKit ({1}) defined in `creature_template_addon`.", entry, creatureAddon.aiAnimKit);
                    creatureAddon.aiAnimKit = 0;
                }

                if (creatureAddon.movementAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(creatureAddon.movementAnimKit))
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid movementAnimKit ({1}) defined in `creature_template_addon`.", entry, creatureAddon.movementAnimKit);
                    creatureAddon.movementAnimKit = 0;
                }

                if (creatureAddon.meleeAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(creatureAddon.meleeAnimKit))
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid meleeAnimKit ({1}) defined in `creature_template_addon`.", entry, creatureAddon.meleeAnimKit);
                    creatureAddon.meleeAnimKit = 0;
                }

                creatureTemplateAddonStorage.Add(entry, creatureAddon);
                count++;
            }
            while (result.NextRow());
            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature template addons in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadCreatureAddons()
        {
            var time = Time.GetMSTime();
            //                                        0       1       2      3       4       5        6             7              8          9
            SQLResult result = DB.World.Query("SELECT guid, path_id, mount, bytes1, bytes2, emote, aiAnimKit, movementAnimKit, meleeAnimKit, auras FROM creature_addon");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creature addon definitions. DB table `creature_addon` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                ulong guid = result.Read<ulong>(0);
                CreatureData creData = GetCreatureData(guid);
                if (creData == null)
                {
                    Log.outError(LogFilter.Sql, "Creature (GUID: {0}) does not exist but has a record in `creatureaddon`", guid);
                    continue;
                }

                CreatureAddon creatureAddon = new CreatureAddon();

                creatureAddon.path_id = result.Read<uint>(1);
                if (creData.movementType == (byte)MovementGeneratorType.Waypoint && creatureAddon.path_id == 0)
                {
                    creData.movementType = (byte)MovementGeneratorType.Idle;
                    Log.outError(LogFilter.Sql, "Creature (GUID {0}) has movement type set to WAYPOINTMOTIONTYPE but no path assigned", guid);
                }

                creatureAddon.mount = result.Read<uint>(2);
                creatureAddon.bytes1 = result.Read<uint>(3);
                creatureAddon.bytes2 = result.Read<uint>(4);
                creatureAddon.emote = result.Read<uint>(5);
                creatureAddon.aiAnimKit = result.Read<ushort>(6);
                creatureAddon.movementAnimKit = result.Read<ushort>(7);
                creatureAddon.meleeAnimKit = result.Read<ushort>(8);

                var tokens = new StringArray(result.Read<string>(9), ' ');
                byte i = 0;
                creatureAddon.auras = new uint[tokens.Length];
                for (var c = 0; c < tokens.Length; ++c)
                {
                    uint spellId = uint.Parse(tokens[c]);
                    SpellInfo AdditionalSpellInfo = Global.SpellMgr.GetSpellInfo(spellId);
                    if (AdditionalSpellInfo == null)
                    {
                        Log.outError(LogFilter.Sql, "Creature (GUID: {0}) has wrong spell {1} defined in `auras` field in `creatureaddon`.", guid, spellId);
                        continue;
                    }

                    if (AdditionalSpellInfo.HasAura(Difficulty.None, AuraType.ControlVehicle))
                        Log.outError(LogFilter.Sql, "Creature (GUID: {0}) has SPELL_AURA_CONTROL_VEHICLE aura {1} defined in `auras` field in `creature_addon`.", guid, spellId);

                    if (creatureAddon.auras.Contains(spellId))
                    {
                        Log.outError(LogFilter.Sql, "Creature (GUID: {0}) has duplicate aura (spell {1}) in `auras` field in `creature_addon`.", guid, spellId);
                        continue;
                    }

                    creatureAddon.auras[i++] = spellId;
                }

                if (creatureAddon.mount != 0)
                {
                    if (!CliDB.CreatureDisplayInfoStorage.ContainsKey(creatureAddon.mount))
                    {
                        Log.outError(LogFilter.Sql, "Creature (GUID: {0}) has invalid displayInfoId ({1}) for mount defined in `creatureaddon`", guid, creatureAddon.mount);
                        creatureAddon.mount = 0;
                    }
                }

                if (!CliDB.EmotesStorage.ContainsKey(creatureAddon.emote))
                {
                    Log.outError(LogFilter.Sql, "Creature (GUID: {0}) has invalid emote ({1}) defined in `creatureaddon`.", guid, creatureAddon.emote);
                    creatureAddon.emote = 0;
                }


                if (creatureAddon.aiAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(creatureAddon.aiAnimKit))
                {
                    Log.outError(LogFilter.Sql, "Creature (Guid: {0}) has invalid aiAnimKit ({1}) defined in `creature_addon`.", guid, creatureAddon.aiAnimKit);
                    creatureAddon.aiAnimKit = 0;
                }

                if (creatureAddon.movementAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(creatureAddon.movementAnimKit))
                {
                    Log.outError(LogFilter.Sql, "Creature (Guid: {0}) has invalid movementAnimKit ({1}) defined in `creature_addon`.", guid, creatureAddon.movementAnimKit);
                    creatureAddon.movementAnimKit = 0;
                }

                if (creatureAddon.meleeAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(creatureAddon.meleeAnimKit))
                {
                    Log.outError(LogFilter.Sql, "Creature (Guid: {0}) has invalid meleeAnimKit ({1}) defined in `creature_addon`.", guid, creatureAddon.meleeAnimKit);
                    creatureAddon.meleeAnimKit = 0;
                }

                creatureAddonStorage.Add(guid, creatureAddon);
                count++;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature addons in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadCreatureQuestItems()
        {
            uint oldMSTime = Time.GetMSTime();

            //                                          0              1      2
            SQLResult result = DB.World.Query("SELECT CreatureEntry, ItemId, Idx FROM creature_questitem ORDER BY Idx ASC");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 creature quest items. DB table `creature_questitem` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);
                uint item = result.Read<uint>(1);
                uint idx = result.Read<uint>(2);

                if (!creatureTemplateStorage.ContainsKey(entry))
                {
                    Log.outError(LogFilter.Sql, "Table `creature_questitem` has data for nonexistent creature (entry: {0}, idx: {1}), skipped", entry, idx);
                    continue;
                }

                if (!CliDB.ItemStorage.ContainsKey(item))
                {
                    Log.outError(LogFilter.Sql, "Table `creature_questitem` has nonexistent item (ID: {0}) in creature (entry: {1}, idx: {2}), skipped", item, entry, idx);
                    continue;
                };

                _creatureQuestItemStorage.Add(entry, item);

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature quest items in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadEquipmentTemplates()
        {
            var time = Time.GetMSTime();
            //                                                0   1        2                 3            4
            SQLResult result = DB.World.Query("SELECT CreatureID, ID, ItemID1, AppearanceModID1, ItemVisual1, " +
                //5                 6            7       8                 9           10
                "ItemID2, AppearanceModID2, ItemVisual2, ItemID3, AppearanceModID3, ItemVisual3 " +
                "FROM creature_equip_template");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creature equipment templates. DB table `creature_equip_template` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);

                if (GetCreatureTemplate(entry) == null)
                {
                    Log.outError(LogFilter.Sql, "Creature template (CreatureID: {0}) does not exist but has a record in `creature_equip_template`", entry);
                    continue;
                }

                uint id = result.Read<uint>(1);

                EquipmentInfo equipmentInfo = new EquipmentInfo();

                for (var i = 0; i < SharedConst.MaxEquipmentItems; ++i)
                {
                    equipmentInfo.Items[i].ItemId = result.Read<uint>(2 + i * 3);
                    equipmentInfo.Items[i].AppearanceModId = result.Read<ushort>(3 + i * 3);
                    equipmentInfo.Items[i].ItemVisual = result.Read<ushort>(4 + i * 3);

                    if (equipmentInfo.Items[i].ItemId == 0)
                        continue;

                    var dbcItem = CliDB.ItemStorage.LookupByKey(equipmentInfo.Items[i].ItemId);
                    if (dbcItem == null)
                    {
                        Log.outError(LogFilter.Sql, "Unknown item (ID: {0}) in creature_equip_template.ItemID{1} for CreatureID  = {2}, forced to 0.",
                            equipmentInfo.Items[i].ItemId, i + 1, entry);
                        equipmentInfo.Items[i].ItemId = 0;
                        continue;
                    }

                    if (Global.DB2Mgr.GetItemModifiedAppearance(equipmentInfo.Items[i].ItemId, equipmentInfo.Items[i].AppearanceModId) == null)
                    {
                        Log.outError(LogFilter.Sql, "Unknown item appearance for (ID: {0}, AppearanceModID: {1}) pair in creature_equip_template.ItemID{2} creature_equip_template.AppearanceModID{3} " +
                            "for CreatureID: {4} and ID: {5}, forced to default.",
                            equipmentInfo.Items[i].ItemId, equipmentInfo.Items[i].AppearanceModId, i + 1, i + 1, entry, id);
                        ItemModifiedAppearanceRecord defaultAppearance = Global.DB2Mgr.GetDefaultItemModifiedAppearance(equipmentInfo.Items[i].ItemId);
                        if (defaultAppearance != null)
                            equipmentInfo.Items[i].AppearanceModId = defaultAppearance.AppearanceModID;
                        else
                            equipmentInfo.Items[i].AppearanceModId = 0;
                        continue;
                    }

                    if (dbcItem.inventoryType != InventoryType.Weapon &&
                        dbcItem.inventoryType != InventoryType.Shield &&
                        dbcItem.inventoryType != InventoryType.Ranged &&
                        dbcItem.inventoryType != InventoryType.Weapon2Hand &&
                        dbcItem.inventoryType != InventoryType.WeaponMainhand &&
                        dbcItem.inventoryType != InventoryType.WeaponOffhand &&
                        dbcItem.inventoryType != InventoryType.Holdable &&
                        dbcItem.inventoryType != InventoryType.Thrown &&
                        dbcItem.inventoryType != InventoryType.RangedRight)
                    {
                        Log.outError(LogFilter.Sql, "Item (ID {0}) in creature_equip_template.ItemID{1} for CreatureID  = {2} is not equipable in a hand, forced to 0.",
                            equipmentInfo.Items[i].ItemId, i + 1, entry);
                        equipmentInfo.Items[i].ItemId = 0;
                    }
                }

                equipmentInfoStorage.Add(entry, Tuple.Create(id, equipmentInfo));
                ++count;

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} equipment templates in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadCreatureClassLevelStats()
        {
            var time = Time.GetMSTime();

            creatureBaseStatsStorage.Clear();
            //                                            0      1      2        3        4                5               6            7           8            9            10           11
            SQLResult result = DB.World.Query("SELECT level, class, basemana, basearmor, attackpower, rangedattackpower, damage_base, damage_exp1, damage_exp2, damage_exp3, damage_exp4, damage_exp5 FROM creature_classlevelstats");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creature base stats. DB table `creature_classlevelstats` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                byte Level = result.Read<byte>(0);
                byte _class = result.Read<byte>(1);

                if (_class == 0 || ((1 << (_class - 1)) & (int)Class.ClassMaskAllCreatures) == 0)
                    Log.outError(LogFilter.Sql, "Creature base stats for level {0} has invalid class {1}", Level, _class);

                CreatureBaseStats stats = new CreatureBaseStats();

                for (var i = 0; i < (int)Expansion.Max; ++i)
                {
                    stats.BaseHealth[i] = (uint)CliDB.GetGameTableColumnForClass(CliDB.NpcTotalHpGameTable[i].GetRow(Level), (Class)_class);
                    stats.BaseDamage[i] = CliDB.GetGameTableColumnForClass(CliDB.NpcDamageByClassGameTable[i].GetRow(Level), (Class)_class);
                    if (stats.BaseDamage[i] < 0.0f)
                    {
                        Log.outError(LogFilter.Sql, "Creature base stats for class {0}, level {1} has invalid negative base damage[{2}] - set to 0.0", _class, Level, i);
                        stats.BaseDamage[i] = 0.0f;
                    }
                }

                stats.BaseMana = result.Read<uint>(2);
                stats.BaseArmor = result.Read<uint>(3);

                stats.AttackPower = result.Read<ushort>(4);
                stats.RangedAttackPower = result.Read<ushort>(5);

                creatureBaseStatsStorage.Add(MathFunctions.MakePair16(Level, _class), stats);

                ++count;
            } while (result.NextRow());

            foreach (var creatureTemplate in creatureTemplateStorage.Values)
            {
                for (short lvl = creatureTemplate.Minlevel; lvl <= creatureTemplate.Maxlevel; ++lvl)
                {
                    if (creatureBaseStatsStorage.LookupByKey(MathFunctions.MakePair16((uint)lvl, creatureTemplate.UnitClass)) == null)
                        Log.outError(LogFilter.Sql, "Missing base stats for creature class {0} level {1}", creatureTemplate.UnitClass, lvl);
                }
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature base stats in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadCreatureModelInfo()
        {
            var time = Time.GetMSTime();
            SQLResult result = DB.World.Query("SELECT DisplayID, BoundingRadius, CombatReach, DisplayID_Other_Gender FROM creature_model_info");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creature model definitions. DB table `creaturemodelinfo` is empty.");
                return;
            }

            // List of model FileDataIDs that the client treats as invisible stalker
            uint[] trigggerCreatureModelFileID = { 124640, 124641, 124642, 343863, 439302 };

            uint count = 0;
            do
            {
                uint displayId = result.Read<uint>(0);

                var creatureDisplay = CliDB.CreatureDisplayInfoStorage.LookupByKey(displayId);
                if (creatureDisplay == null)
                {
                    Log.outError(LogFilter.Sql, "Table `creature_model_info` has a non-existent DisplayID (ID: {0}). Skipped.", displayId);
                    continue;
                }

                CreatureModelInfo modelInfo = new CreatureModelInfo();
                modelInfo.BoundingRadius = result.Read<float>(1);
                modelInfo.CombatReach = result.Read<float>(2);
                modelInfo.DisplayIdOtherGender = result.Read<uint>(3);
                modelInfo.gender = (sbyte)creatureDisplay.Gender;

                // Checks
                if (modelInfo.gender == (sbyte)Gender.Unknown)
                    modelInfo.gender = (sbyte)Gender.Male;

                if (modelInfo.DisplayIdOtherGender != 0 && !CliDB.CreatureDisplayInfoStorage.ContainsKey(modelInfo.DisplayIdOtherGender))
                {
                    Log.outError(LogFilter.Sql, "Table `creature_model_info` has a non-existent DisplayID_Other_Gender (ID: {0}) being used by DisplayID (ID: {1}).", modelInfo.DisplayIdOtherGender, displayId);
                    modelInfo.DisplayIdOtherGender = 0;
                }

                if (modelInfo.CombatReach < 0.1f)
                    modelInfo.CombatReach = SharedConst.DefaultCombatReach;

                CreatureModelDataRecord modelData = CliDB.CreatureModelDataStorage.LookupByKey(creatureDisplay.ModelID);
                if (modelData != null)
                {
                    for (uint i = 0; i < 5; ++i)
                    {
                        if (modelData.FileDataID == trigggerCreatureModelFileID[i])
                        {
                            modelInfo.IsTrigger = true;
                            break;
                        }
                    }
                }

                creatureModelStorage.Add(displayId, modelInfo);
                count++;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature model based info in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void CheckCreatureTemplate(CreatureTemplate cInfo)
        {
            if (cInfo == null)
                return;

            bool ok = true;                                     // bool to allow continue outside this loop
            for (uint diff = 0; diff < SharedConst.MaxCreatureDifficulties && ok; ++diff)
            {
                if (cInfo.DifficultyEntry[diff] == 0)
                    continue;
                ok = false;                                     // will be set to true at the end of this loop again

                CreatureTemplate difficultyInfo = GetCreatureTemplate(cInfo.DifficultyEntry[diff]);
                if (difficultyInfo == null)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has `difficulty_entry_{1}`={2} but creature entry {3} does not exist.",
                        cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff], cInfo.DifficultyEntry[diff]);
                    continue;
                }

                bool ok2 = true;
                for (uint diff2 = 0; diff2 < SharedConst.MaxCreatureDifficulties && ok2; ++diff2)
                {
                    ok2 = false;
                    if (_difficultyEntries[diff2].Contains(cInfo.Entry))
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) is listed as `difficulty_entry_{1}` of another creature, but itself lists {2} in `difficulty_entry_{3}`.",
                            cInfo.Entry, diff2 + 1, cInfo.DifficultyEntry[diff], diff + 1);
                        continue;
                    }

                    if (_difficultyEntries[diff2].Contains(cInfo.DifficultyEntry[diff]))
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) already listed as `difficulty_entry_{1}` for another entry.", cInfo.DifficultyEntry[diff], diff2 + 1);
                        continue;
                    }

                    if (_hasDifficultyEntries[diff2].Contains(cInfo.DifficultyEntry[diff]))
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has `difficulty_entry_{1}`={2} but creature entry {3} has itself a value in `difficulty_entry_{4}`.",
                            cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff], cInfo.DifficultyEntry[diff], diff2 + 1);
                        continue;
                    }
                    ok2 = true;
                }
                if (!ok2)
                    continue;

                if (cInfo.HealthScalingExpansion > difficultyInfo.HealthScalingExpansion)
                {
                    Log.outError(LogFilter.Sql, "Creature (Id: {0}, Expansion {1}) has different `HealthScalingExpansion` in difficulty {2} mode (Id: {3}, Expansion: {4}).",
                        cInfo.Entry, cInfo.HealthScalingExpansion, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.HealthScalingExpansion);
                }

                if (cInfo.Minlevel > difficultyInfo.Minlevel)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, minlevel: {1}) has lower `minlevel` in difficulty {2} mode (Entry: {3}, minlevel: {4}).",
                        cInfo.Entry, cInfo.Minlevel, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.Minlevel);
                }

                if (cInfo.Maxlevel > difficultyInfo.Maxlevel)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, maxlevel: {1}) has lower `maxlevel` in difficulty {2} mode (Entry: {3}, maxlevel: {4}).",
                        cInfo.Entry, cInfo.Maxlevel, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.Maxlevel);
                }

                if (cInfo.Faction != difficultyInfo.Faction)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, faction: {1}) has different `faction` in difficulty {2} mode (Entry: {3}, faction: {4}).",
                        cInfo.Entry, cInfo.Faction, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.Faction);
                }

                if (cInfo.UnitClass != difficultyInfo.UnitClass)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, class: {1}) has different `unit_class` in difficulty {2} mode (Entry: {3}, class: {4}).",
                        cInfo.Entry, cInfo.UnitClass, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.UnitClass);
                    continue;
                }

                if (cInfo.Npcflag != difficultyInfo.Npcflag)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has different `npcflag` in difficulty {1} mode (Entry: {2}).", cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff]);
                    Log.outError(LogFilter.Sql, "Possible FIX: UPDATE `creature_template` SET `npcflag`=`npcflag`^{0} WHERE `entry`={1};", cInfo.Npcflag ^ difficultyInfo.Npcflag, cInfo.DifficultyEntry[diff]);
                    continue;
                }

                if (cInfo.DmgSchool != difficultyInfo.DmgSchool)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, `dmgschool`: {1}) has different `dmgschool` in difficulty {2} mode (Entry: {3}, `dmgschool`: {4}).", 
                        cInfo.Entry, cInfo.DmgSchool, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.DmgSchool);
                    Log.outError(LogFilter.Sql, "Possible FIX: UPDATE `creature_template` SET `dmgschool`={0} WHERE `entry`={1};", cInfo.DmgSchool, cInfo.DifficultyEntry[diff]);
                }

                if (cInfo.UnitFlags2 != difficultyInfo.UnitFlags2)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, `unit_flags2`: {1}) has different `unit_flags2` in difficulty {2} mode (Entry: {3}, `unit_flags2`: {4}).", 
                        cInfo.Entry, cInfo.UnitFlags2, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.UnitFlags2);
                    Log.outError(LogFilter.Sql, "Possible FIX: UPDATE `creature_template` SET `unit_flags2`=`unit_flags2`^{0} WHERE `entry`={1};", cInfo.UnitFlags2 ^ difficultyInfo.UnitFlags2, cInfo.DifficultyEntry[diff]);
                }

                if (cInfo.Family != difficultyInfo.Family)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, family: {1}) has different `family` in difficulty {2} mode (Entry: {3}, family: {4}).",
                        cInfo.Entry, cInfo.Family, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.Family);
                }

                if (cInfo.TrainerClass != difficultyInfo.TrainerClass)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has different `trainer_class` in difficulty {1} mode (Entry: {2}).", cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff]);
                    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}).",
                        cInfo.Entry, cInfo.CreatureType, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.CreatureType);
                }

                if (cInfo.VehicleId == 0 && difficultyInfo.VehicleId != 0)
                {
                    Log.outError(LogFilter.Sql, "Non-vehicle Creature (Entry: {0}, VehicleId: {1}) has `VehicleId` set in difficulty {2} mode (Entry: {3}, VehicleId: {4}).",
                        cInfo.Entry, cInfo.VehicleId, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.VehicleId);
                }

                if (cInfo.RegenHealth != difficultyInfo.RegenHealth)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, RegenHealth: {1}) has different `RegenHealth` in difficulty {2} mode (Entry: {3}, RegenHealth: {4}).", 
                        cInfo.Entry, cInfo.RegenHealth, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.RegenHealth);
                    Log.outError(LogFilter.Sql, "Possible FIX: UPDATE `creature_template` SET `RegenHealth`={0} WHERE `entry`={1};", cInfo.RegenHealth, cInfo.DifficultyEntry[diff]);
                }

                uint differenceMask = cInfo.MechanicImmuneMask & (~difficultyInfo.MechanicImmuneMask);
                if (differenceMask != 0)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, mechanic_immune_mask: {1}) has weaker immunities in difficulty {2} mode (Entry: {3}, mechanic_immune_mask: {4}).",
                        cInfo.Entry, cInfo.MechanicImmuneMask, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.MechanicImmuneMask);
                    Log.outError(LogFilter.Sql, "Possible FIX: UPDATE `creature_template` SET `mechanic_immune_mask`=`mechanic_immune_mask`|{0} WHERE `entry`={1};", differenceMask, cInfo.DifficultyEntry[diff]);
                }

                differenceMask = (uint)((cInfo.FlagsExtra ^ difficultyInfo.FlagsExtra) & (~CreatureFlagsExtra.InstanceBind));
                if (differenceMask != 0)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}, flags_extra: {1}) has different `flags_extra` in difficulty {2} mode (Entry: {3}, flags_extra: {4}).",
                        cInfo.Entry, cInfo.FlagsExtra, diff + 1, cInfo.DifficultyEntry[diff], difficultyInfo.FlagsExtra);
                    Log.outError(LogFilter.Sql, "Possible FIX: UPDATE `creature_template` SET `flags_extra`=`flags_extra`^{0} WHERE `entry`={1};", differenceMask, cInfo.DifficultyEntry[diff]);
                }

                if (difficultyInfo.AIName.IsEmpty())
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists difficulty {1} mode entry {2} with `AIName` filled in. `AIName` of difficulty 0 mode creature is always used instead.",
                        cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff]);
                    continue;
                }

                if (difficultyInfo.ScriptID != 0)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists difficulty {1} mode entry {2} with `ScriptName` filled in. `ScriptName` of difficulty 0 mode creature is always used instead.",
                        cInfo.Entry, diff + 1, cInfo.DifficultyEntry[diff]);
                    continue;
                }

                _hasDifficultyEntries[diff].Add(cInfo.Entry);
                _difficultyEntries[diff].Add(cInfo.DifficultyEntry[diff]);
                ok = true;
            }

            if (!CliDB.FactionTemplateStorage.ContainsKey(cInfo.Faction))
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has non-existing faction template ({1}).", cInfo.Entry, cInfo.Faction);

            // used later for scale
            CreatureDisplayInfoRecord displayScaleEntry = null;

            if (cInfo.ModelId1 != 0)
            {
                CreatureDisplayInfoRecord displayEntry = CliDB.CreatureDisplayInfoStorage.LookupByKey(cInfo.ModelId1);
                if (displayEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists non-existing Modelid1 id ({1}), this can crash the client.", cInfo.Entry, cInfo.ModelId1);
                    //cInfo.ModelId1 = 0;
                }
                else if (displayScaleEntry == null)
                    displayScaleEntry = displayEntry;

                CreatureModelInfo modelInfo = GetCreatureModelInfo(cInfo.ModelId1);
                if (modelInfo == null)
                    Log.outError(LogFilter.Sql, "No model data exist for `Modelid1` = {0} listed by creature (Entry: {1}).", cInfo.ModelId1, cInfo.Entry);
            }

            if (cInfo.ModelId2 != 0)
            {
                CreatureDisplayInfoRecord displayEntry = CliDB.CreatureDisplayInfoStorage.LookupByKey(cInfo.ModelId2);
                if (displayEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists non-existing Modelid2 id ({1}), this can crash the client.", cInfo.Entry, cInfo.ModelId2);
                    cInfo.ModelId2 = 0;
                }
                else if (displayScaleEntry == null)
                    displayScaleEntry = displayEntry;

                CreatureModelInfo modelInfo = GetCreatureModelInfo(cInfo.ModelId2);
                if (modelInfo == null)
                    Log.outError(LogFilter.Sql, "No model data exist for `Modelid2` = {0} listed by creature (Entry: {1}).", cInfo.ModelId2, cInfo.Entry);
            }

            if (cInfo.ModelId3 != 0)
            {
                CreatureDisplayInfoRecord displayEntry = CliDB.CreatureDisplayInfoStorage.LookupByKey(cInfo.ModelId3);
                if (displayEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists non-existing Modelid3 id ({1}), this can crash the client.", cInfo.Entry, cInfo.ModelId3);
                    cInfo.ModelId3 = 0;
                }
                else if (displayScaleEntry == null)
                    displayScaleEntry = displayEntry;

                CreatureModelInfo modelInfo = GetCreatureModelInfo(cInfo.ModelId3);
                if (modelInfo == null)
                    Log.outError(LogFilter.Sql, "No model data exist for `Modelid3` = {0} listed by creature (Entry: {1}).", cInfo.ModelId3, cInfo.Entry);
            }

            if (cInfo.ModelId4 != 0)
            {
                CreatureDisplayInfoRecord displayEntry = CliDB.CreatureDisplayInfoStorage.LookupByKey(cInfo.ModelId4);
                if (displayEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists non-existing Modelid4 id ({1}), this can crash the client.", cInfo.Entry, cInfo.ModelId4);
                    cInfo.ModelId4 = 0;
                }
                else if (displayScaleEntry == null)
                    displayScaleEntry = displayEntry;

                CreatureModelInfo modelInfo = GetCreatureModelInfo(cInfo.ModelId4);
                if (modelInfo == null)
                    Log.outError(LogFilter.Sql, "No model data exist for `Modelid4` = {0} listed by creature (Entry: {1}).", cInfo.ModelId4, cInfo.Entry);
            }

            if (displayScaleEntry == null)
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) does not have any existing display id in Modelid1/Modelid2/Modelid3/Modelid4.", cInfo.Entry);

            for (int k = 0; k < SharedConst.MaxCreatureKillCredit; ++k)
            {
                if (cInfo.KillCredit[k] != 0)
                {
                    if (GetCreatureTemplate(cInfo.KillCredit[k]) == null)
                    {
                        Log.outError(LogFilter.Sql, "Creature (Entry: {0}) lists non-existing creature entry {1} in `KillCredit{2}`.", cInfo.Entry, cInfo.KillCredit[k], k + 1);
                        cInfo.KillCredit[k] = 0;
                    }
                }
            }

            if (cInfo.UnitClass == 0 || ((1 << ((int)cInfo.UnitClass - 1)) & (int)Class.ClassMaskAllCreatures) == 0)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid unit_class ({1}) in creature_template. Set to 1 (UNIT_CLASS_WARRIOR).", cInfo.Entry, cInfo.UnitClass);
                cInfo.UnitClass = (uint)Class.Warrior;
            }

            if (cInfo.DmgSchool >= (uint)SpellSchools.Max)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid spell school value ({1}) in `dmgschool`.", cInfo.Entry, cInfo.DmgSchool);
                cInfo.DmgSchool = (uint)SpellSchools.Normal;
            }

            if (cInfo.BaseAttackTime == 0)
                cInfo.BaseAttackTime = SharedConst.BaseAttackTime;

            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);
                cInfo.SpeedWalk = 1.0f;
            }

            if (cInfo.SpeedRun == 0.0f)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong value ({1}) in speed_run, set to 1.14286.", cInfo.Entry, cInfo.SpeedRun);
                cInfo.SpeedRun = 1.14286f;
            }

            if (cInfo.CreatureType != 0 && !CliDB.CreatureTypeStorage.ContainsKey((uint)cInfo.CreatureType))
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid creature type ({1}) in `type`.", cInfo.Entry, cInfo.CreatureType);
                cInfo.CreatureType = CreatureType.Humanoid;
            }

            // must exist or used hidden but used in data horse case
            if (cInfo.Family != 0 && !CliDB.CreatureFamilyStorage.ContainsKey(cInfo.Family) && cInfo.Family != CreatureFamily.HorseCustom)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has invalid creature family ({1}) in `family`.", cInfo.Entry, cInfo.Family);
                cInfo.Family = CreatureFamily.None;
            }

            if (cInfo.InhabitType <= 0 || cInfo.InhabitType > InhabitType.Anywhere)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong value ({1}) in `InhabitType`, creature will not correctly walk/swim/fly.", cInfo.Entry, cInfo.InhabitType);
                cInfo.InhabitType = InhabitType.Anywhere;
            }

            if (cInfo.HoverHeight < 0.0f)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong value ({1}) in `HoverHeight`", cInfo.Entry, cInfo.HoverHeight);
                cInfo.HoverHeight = 1.0f;
            }

            if (cInfo.VehicleId != 0)
            {
                if (!CliDB.VehicleStorage.ContainsKey(cInfo.VehicleId))
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has a non-existing VehicleId ({1}). This *WILL* cause the client to freeze!", cInfo.Entry, cInfo.VehicleId);
                    cInfo.VehicleId = 0;
                }
            }

            for (byte j = 0; j < SharedConst.MaxCreatureSpells; ++j)
            {
                if (cInfo.Spells[j] != 0 && !Global.SpellMgr.HasSpellInfo(cInfo.Spells[j]))
                {
                    Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has non-existing Spell{1} ({2}), set to 0.", cInfo.Entry, j + 1, cInfo.Spells[j]);
                    cInfo.Spells[j] = 0;
                }
            }

            if (cInfo.MovementType >= (uint)MovementGeneratorType.MaxDB)
            {
                Log.outError(LogFilter.Sql, "Creature (Entry: {0}) has wrong movement generator type ({1}), ignored and set to IDLE.", cInfo.Entry, cInfo.MovementType);
                cInfo.MovementType = (uint)MovementGeneratorType.Idle;
            }

            /// if not set custom creature scale then load scale from CreatureDisplayInfo.dbc
            if (cInfo.Scale <= 0.0f)
            {
                if (displayScaleEntry != null)
                    cInfo.Scale = displayScaleEntry.CreatureModelScale;
                else
                    cInfo.Scale = 1.0f;
            }

            if (cInfo.HealthScalingExpansion < (int)Expansion.LevelCurrent || cInfo.HealthScalingExpansion > ((int)Expansion.Max - 1))
            {
                Log.outError(LogFilter.Sql, "Table `creature_template` lists creature (Id: {0}) with invalid `HealthScalingExpansion` {1}. Ignored and set to 0.", cInfo.Entry, cInfo.HealthScalingExpansion);
                cInfo.HealthScalingExpansion = 0;
            }

            if (cInfo.RequiredExpansion > (int)(Expansion.Max - 1))
            {
                Log.outError(LogFilter.Sql, "Table `creature_template` lists creature (Entry: {0}) with `RequiredExpansion` {1}. Ignored and set to 0.", cInfo.Entry, cInfo.RequiredExpansion);
                cInfo.RequiredExpansion = 0;
            }

            uint badFlags = (uint)(cInfo.FlagsExtra & ~CreatureFlagsExtra.DBAllowed);
            if (badFlags != 0)
            {
                Log.outError(LogFilter.Sql, "Table `creature_template` lists creature (Entry: {0}) with disallowed `flags_extra` {1}, removing incorrect flag.", cInfo.Entry, badFlags);
                cInfo.FlagsExtra &= CreatureFlagsExtra.DBAllowed;
            }

            // -1, as expansion, is used in CreatureDifficulty.db2 for
            // auto-updating the levels of creatures having their expansion
            // set to that value to the current expansion's max leveling level
            if (cInfo.HealthScalingExpansion == (int)Expansion.LevelCurrent)
            {
                cInfo.Minlevel = (short)(WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel) + cInfo.Minlevel);
                cInfo.Maxlevel = (short)(WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel) + cInfo.Maxlevel);
                cInfo.HealthScalingExpansion = (int)Expansion.WarlordsOfDraenor;
            }

            if (cInfo.Minlevel < 1 || cInfo.Minlevel > SharedConst.StrongMaxLevel)
            {
                Log.outError(LogFilter.Sql, "Creature (ID: {0}): MinLevel {1} is not within [1, 255], value has been set to 1.", cInfo.Entry, cInfo.Minlevel);
                cInfo.Minlevel = 1;
            }

            if (cInfo.Maxlevel < 1 || cInfo.Maxlevel > SharedConst.StrongMaxLevel)
            {
                Log.outError(LogFilter.Sql, "Creature (ID: {0}): MaxLevel {1} is not within [1, 255], value has been set to 1.", cInfo.Entry, cInfo.Maxlevel);
                cInfo.Maxlevel = 1;
            }

            cInfo.ModDamage *= Creature._GetDamageMod(cInfo.Rank);
        }
        public void LoadLinkedRespawn()
        {
            uint oldMSTime = Time.GetMSTime();

            linkedRespawnStorage.Clear();
            //                                                 0        1          2
            SQLResult result = DB.World.Query("SELECT guid, linkedGuid, linkType FROM linked_respawn ORDER BY guid ASC");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 linked respawns. DB table `linked_respawn` is empty.");
                return;
            }

            do
            {
                ulong guidLow = result.Read<ulong>(0);
                ulong linkedGuidLow = result.Read<ulong>(1);
                byte linkType = result.Read<byte>(2);

                ObjectGuid guid = ObjectGuid.Empty;
                ObjectGuid linkedGuid = ObjectGuid.Empty;
                bool error = false;
                switch ((CreatureLinkedRespawnType)linkType)
                {
                    case CreatureLinkedRespawnType.CreatureToCreature:
                        {
                            CreatureData slave = GetCreatureData(guidLow);
                            if (slave == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get creature data for GUIDLow {0}", guidLow);
                                error = true;
                                break;
                            }

                            CreatureData master = GetCreatureData(linkedGuidLow);
                            if (master == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get creature data for GUIDLow {0}", linkedGuidLow);
                                error = true;
                                break;
                            }

                            MapRecord map = CliDB.MapStorage.LookupByKey(master.mapid);
                            if (map == null || !map.Instanceable() || (master.mapid != slave.mapid))
                            {
                                Log.outError(LogFilter.Sql, "Creature '{0}' linking to '{1}' on an unpermitted map.", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            if (!Convert.ToBoolean(master.spawnMask & slave.spawnMask))  // they must have a possibility to meet (normal/heroic difficulty)
                            {
                                Log.outError(LogFilter.Sql, "LinkedRespawn: Creature '{0}' linking to '{1}' with not corresponding spawnMask", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            guid = ObjectGuid.Create(HighGuid.Creature, slave.mapid, slave.id, guidLow);
                            linkedGuid = ObjectGuid.Create(HighGuid.Creature, master.mapid, master.id, linkedGuidLow);
                            break;
                        }
                    case CreatureLinkedRespawnType.CreatureToGO:
                        {
                            CreatureData slave = GetCreatureData(guidLow);
                            if (slave == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get creature data for GUIDLow {0}", guidLow);
                                error = true;
                                break;
                            }

                            GameObjectData master = GetGOData(linkedGuidLow);
                            if (master == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get gameobject data for GUIDLow {0}", linkedGuidLow);
                                error = true;
                                break;
                            }

                            MapRecord map = CliDB.MapStorage.LookupByKey(master.mapid);
                            if (map == null || !map.Instanceable() || (master.mapid != slave.mapid))
                            {
                                Log.outError(LogFilter.Sql, "Creature '{0}' linking to '{1}' on an unpermitted map.", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            if (!Convert.ToBoolean(master.spawnMask & slave.spawnMask))  // they must have a possibility to meet (normal/heroic difficulty)
                            {
                                Log.outError(LogFilter.Sql, "LinkedRespawn: Creature '{0}' linking to '{1}' with not corresponding spawnMask", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            guid = ObjectGuid.Create(HighGuid.Creature, slave.mapid, slave.id, guidLow);
                            linkedGuid = ObjectGuid.Create(HighGuid.GameObject, master.mapid, master.id, linkedGuidLow);
                            break;
                        }
                    case CreatureLinkedRespawnType.GOToGO:
                        {
                            GameObjectData slave = GetGOData(guidLow);
                            if (slave == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get gameobject data for GUIDLow {0}", guidLow);
                                error = true;
                                break;
                            }

                            GameObjectData master = GetGOData(linkedGuidLow);
                            if (master == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get gameobject data for GUIDLow {0}", linkedGuidLow);
                                error = true;
                                break;
                            }

                            MapRecord map = CliDB.MapStorage.LookupByKey(master.mapid);
                            if (map == null || !map.Instanceable() || (master.mapid != slave.mapid))
                            {
                                Log.outError(LogFilter.Sql, "Creature '{0}' linking to '{1}' on an unpermitted map.", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            if (!Convert.ToBoolean(master.spawnMask & slave.spawnMask))  // they must have a possibility to meet (normal/heroic difficulty)
                            {
                                Log.outError(LogFilter.Sql, "LinkedRespawn: Creature '{0}' linking to '{1}' with not corresponding spawnMask", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            guid = ObjectGuid.Create(HighGuid.GameObject, slave.mapid, slave.id, guidLow);
                            linkedGuid = ObjectGuid.Create(HighGuid.GameObject, master.mapid, master.id, linkedGuidLow);
                            break;
                        }
                    case CreatureLinkedRespawnType.GOToCreature:
                        {
                            GameObjectData slave = GetGOData(guidLow);
                            if (slave == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get gameobject data for GUIDLow {0}", guidLow);
                                error = true;
                                break;
                            }

                            CreatureData master = GetCreatureData(linkedGuidLow);
                            if (master == null)
                            {
                                Log.outError(LogFilter.Sql, "Couldn't get creature data for GUIDLow {0}", linkedGuidLow);
                                error = true;
                                break;
                            }

                            MapRecord map = CliDB.MapStorage.LookupByKey(master.mapid);
                            if (map == null || !map.Instanceable() || (master.mapid != slave.mapid))
                            {
                                Log.outError(LogFilter.Sql, "Creature '{0}' linking to '{1}' on an unpermitted map.", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            if (!Convert.ToBoolean(master.spawnMask & slave.spawnMask))  // they must have a possibility to meet (normal/heroic difficulty)
                            {
                                Log.outError(LogFilter.Sql, "LinkedRespawn: Creature '{0}' linking to '{1}' with not corresponding spawnMask", guidLow, linkedGuidLow);
                                error = true;
                                break;
                            }

                            guid = ObjectGuid.Create(HighGuid.GameObject, slave.mapid, slave.id, guidLow);
                            linkedGuid = ObjectGuid.Create(HighGuid.Creature, master.mapid, master.id, linkedGuidLow);
                            break;
                        }

                }

                if (!error)
                    linkedRespawnStorage[guid] = linkedGuid;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} linked respawns in {1} ms", linkedRespawnStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadNPCText()
        {
            uint oldMSTime = Time.GetMSTime();

            _npcTextStorage.Clear();

            SQLResult result = DB.World.Query("SELECT ID, Probability0, Probability1, Probability2, Probability3, Probability4, Probability5, Probability6, Probability7, " +
                "BroadcastTextID0, BroadcastTextID1, BroadcastTextID2, BroadcastTextID3, BroadcastTextID4, BroadcastTextID5, BroadcastTextID6, BroadcastTextID7 FROM npc_text");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 npc texts, table is empty!");
                return;
            }

            do
            {
                uint textID = result.Read<uint>(0);
                if (textID == 0)
                {
                    Log.outError(LogFilter.Sql, "Table `npc_text` has record wit reserved id 0, ignore.");
                    continue;
                }

                NpcText npcText = new NpcText();
                for (int i = 0; i < SharedConst.MaxNpcTextOptions; i++)
                {
                    npcText.Data[i].Probability = result.Read<float>(1 + i);
                    npcText.Data[i].BroadcastTextID = result.Read<uint>(9 + i);
                }

                for (int i = 0; i < SharedConst.MaxNpcTextOptions; i++)
                {
                    if (npcText.Data[i].BroadcastTextID != 0)
                    {
                        if (!CliDB.BroadcastTextStorage.ContainsKey(npcText.Data[i].BroadcastTextID))
                        {
                            Log.outError(LogFilter.Sql, "NPCText (Id: {0}) has a non-existing or incompatible BroadcastText (ID: {1}, Index: {2})", textID, npcText.Data[i].BroadcastTextID, i);
                            npcText.Data[i].BroadcastTextID = 0;
                        }
                    }
                }

                for (byte i = 0; i < SharedConst.MaxNpcTextOptions; i++)
                {
                    if (npcText.Data[i].Probability > 0 && npcText.Data[i].BroadcastTextID == 0)
                    {
                        Log.outError(LogFilter.Sql, "NPCText (ID: {0}) has a probability (Index: {1}) set, but no BroadcastTextID to go with it", textID, i);
                        npcText.Data[i].Probability = 0;
                    }
                }
                _npcTextStorage[textID] = npcText;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} npc texts in {1} ms", _npcTextStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public void LoadTrainerSpell()
        {
            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");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 Trainers. DB table `npc_trainer` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                uint ID = result.Read<uint>(0);
                uint SpellID = result.Read<uint>(1);
                uint MoneyCost = result.Read<uint>(2);
                uint ReqSkillLine = result.Read<ushort>(3);
                uint ReqSkillRank = result.Read<ushort>(4);
                uint Reqlevel = result.Read<byte>(5);
                uint Index = result.Read<byte>(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)
                {
                    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.ReqAbility[effect.EffectIndex] = effect.TriggerSpell;

                if (trainerSpell.ReqAbility[effect.EffectIndex] != 0)
                {
                    SpellInfo learnedSpellInfo = Global.SpellMgr.GetSpellInfo(trainerSpell.ReqAbility[effect.EffectIndex]);
                    if (learnedSpellInfo != null && learnedSpellInfo.IsProfession())
                        data.trainerType = 2;
                }
            }
            data.spellList[SpellID] = trainerSpell;
            return;
        }
        public void LoadVendors()
        {
            var time = Time.GetMSTime();
            // For reload case
            cacheVendorItemStorage.Clear();

            List<uint> skipvendors = new List<uint>();

            SQLResult result = DB.World.Query("SELECT entry, item, maxcount, incrtime, ExtendedCost, type FROM npc_vendor ORDER BY entry, slot ASC");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 Vendors. DB table `npc_vendor` is empty!");
                return;
            }

            uint count = 0;

            do
            {
                uint entry = result.Read<uint>(0);
                int itemid = result.Read<int>(1);

                // if item is a negative, its a reference
                if (itemid < 0)
                    count += LoadReferenceVendor((int)entry, -itemid, 0, skipvendors);
                else
                {
                    int maxcount = result.Read<int>(2);
                    uint incrtime = result.Read<uint>(3);
                    uint ExtendedCost = result.Read<uint>(4);
                    ItemVendorType type = (ItemVendorType)result.Read<byte>(5);

                    if (!IsVendorItemValid(entry, (uint)itemid, maxcount, incrtime, ExtendedCost, type, null, skipvendors))
                        continue;

                    if (cacheVendorItemStorage.LookupByKey(entry) == null)
                        cacheVendorItemStorage.Add(entry, new VendorItemData());

                    cacheVendorItemStorage[entry].AddItem((uint)itemid, maxcount, incrtime, ExtendedCost, type);
                    ++count;
                }
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Vendors in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        uint LoadReferenceVendor(int vendor, int item, byte type, List<uint> skip_vendors)
        {
            // find all items from the reference vendor
            PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.SEL_NPC_VENDOR_REF);
            stmt.AddValue(0, item);
            stmt.AddValue(1, type);
            SQLResult result = DB.World.Query(stmt);

            if (result.IsEmpty())
                return 0;

            uint count = 0;
            do
            {
                int item_id = result.Read<int>(0);

                // if item is a negative, its a reference
                if (item_id < 0)
                    count += LoadReferenceVendor(vendor, -item_id, type, skip_vendors);
                else
                {
                    int maxcount = result.Read<int>(1);
                    uint incrtime = result.Read<uint>(2);
                    uint ExtendedCost = result.Read<uint>(3);
                    ItemVendorType _type = (ItemVendorType)result.Read<byte>(4);

                    if (!IsVendorItemValid((uint)vendor, (uint)item_id, maxcount, incrtime, ExtendedCost, _type, null, skip_vendors))
                        continue;

                    VendorItemData vList = cacheVendorItemStorage[(uint)vendor];

                    vList.AddItem((uint)item_id, maxcount, incrtime, ExtendedCost, _type);
                    ++count;
                }
            } while (result.NextRow());

            return count;
        }
        public void LoadCreatures()
        {
            var time = Time.GetMSTime();

            //                                         0              1   2    3        4             5           6           7           8            9              10
            SQLResult result = DB.World.Query("SELECT creature.guid, id, map, modelid, equipment_id, position_x, position_y, position_z, orientation, spawntimesecs, spawndist, " +
                //   11               12         13       14            15         16          17          18                19                   20                    21
                "currentwaypoint, curhealth, curmana, MovementType, spawnMask, eventEntry, pool_entry, creature.npcflag, creature.unit_flags, creature.unit_flags2, creature.unit_flags3, " +
                //   22                     23                24                   25
                "creature.dynamicflags, creature.phaseid, creature.phasegroup, creature.ScriptName " +        
                "FROM creature LEFT OUTER JOIN game_event_creature ON creature.guid = game_event_creature.guid LEFT OUTER JOIN pool_creature ON creature.guid = pool_creature.guid");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 creatures. DB table `creature` is empty.");
                return;
            }

            Dictionary<uint, uint> spawnMasks = new Dictionary<uint, uint>();
            foreach (var mapDifficultyPair in Global.DB2Mgr.GetMapDifficulties())
            {
                foreach (var difficultyPair in mapDifficultyPair.Value)
                {
                    if (!spawnMasks.ContainsKey(mapDifficultyPair.Key))
                        spawnMasks[mapDifficultyPair.Key] = 0;

                    spawnMasks[mapDifficultyPair.Key] |= (uint)(1 << (int)difficultyPair.Key);
                }
            }

            uint count = 0;
            do
            {
                ulong guid = result.Read<ulong>(0);
                uint entry = result.Read<uint>(1);

                CreatureTemplate cInfo = GetCreatureTemplate(entry);
                if (cInfo == null)
                {
                    Log.outError(LogFilter.Sql, "Table `creature` has creature (GUID: {0}) with non existing creature entry {1}, skipped.", guid, entry);
                    continue;
                }

                CreatureData data = new CreatureData();
                data.id = entry;
                data.mapid = result.Read<ushort>(2);
                data.displayid = result.Read<uint>(3);
                data.equipmentId = result.Read<int>(4);
                data.posX = result.Read<float>(5);
                data.posY = result.Read<float>(6);
                data.posZ = result.Read<float>(7);
                data.orientation = result.Read<float>(8);
                data.spawntimesecs = result.Read<uint>(9);
                data.spawndist = result.Read<float>(10);
                data.currentwaypoint = result.Read<uint>(11);
                data.curhealth = result.Read<uint>(12);
                data.curmana = result.Read<uint>(13);
                data.movementType = result.Read<byte>(14);
                data.spawnMask = result.Read<uint>(15);
                short gameEvent = result.Read<short>(16);
                uint PoolId = result.Read<uint>(17);
                data.npcflag = result.Read<ulong>(18);
                data.unit_flags = result.Read<uint>(19);
                data.unit_flags2 = result.Read<uint>(20);
                data.unit_flags3 = result.Read<uint>(21);
                data.dynamicflags = result.Read<uint>(22);
                data.phaseid = result.Read<uint>(23);
                data.phaseGroup = result.Read<uint>(24);
                data.ScriptId = GetScriptId(result.Read<string>(25));
                if (data.ScriptId == 0)
                    data.ScriptId = cInfo.ScriptID;

                var mapEntry = CliDB.MapStorage.LookupByKey(data.mapid);
                if (mapEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0}) that spawned at not existed map (Id: {1}), skipped.", guid, data.mapid);
                    continue;
                }

                //if (!_transportMaps.Contains(data.mapid) && (spawnMasks.ContainsKey(data.mapid) && Convert.ToBoolean(data.spawnMask & ~spawnMasks[data.mapid])))
                //Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0}) that have wrong spawn mask {1} including not supported difficulty modes for map (Id: {2}) spawnMasks[data.mapid]: {3}.", guid, data.spawnMask, data.mapid, spawnMasks[data.mapid]);

                bool ok = true;
                for (uint diff = 0; diff < SharedConst.MaxCreatureDifficulties && ok; ++diff)
                {
                    if (_difficultyEntries[diff].Contains(data.id))
                    {
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0}) that listed as difficulty {1} template (entry: {2}) in `creaturetemplate`, skipped.", guid, diff + 1, data.id);
                        ok = false;
                    }
                }
                if (!ok)
                    continue;

                // -1 random, 0 no equipment,
                if (data.equipmentId != 0)
                {
                    if (GetEquipmentInfo(data.id, data.equipmentId) == null)
                    {
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (Entry: {0}) with equipmentid {1} not found in table `creatureequiptemplate`, set to no equipment.", data.id, data.equipmentId);
                        data.equipmentId = 0;
                    }
                }

                if (cInfo.FlagsExtra.HasAnyFlag(CreatureFlagsExtra.InstanceBind))
                {
                    if (!mapEntry.IsDungeon())
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with `creature_template`.`flagsextra` including CREATUREFLAGEXTRAINSTANCEBIND " +
                            "but creature are not in instance.", guid, data.id);
                }

                if (data.spawndist < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with `spawndist`< 0, set to 0.", guid, data.id);
                    data.spawndist = 0.0f;
                }
                else if (data.movementType == (byte)MovementGeneratorType.Random)
                {
                    if (MathFunctions.fuzzyEq(data.spawndist, 0.0f))
                    {
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with `MovementType`=1 (random movement) but with `spawndist`=0, replace by idle movement type (0).", guid, data.id);
                        data.movementType = (byte)MovementGeneratorType.Idle;
                    }
                }
                else if (data.movementType == (byte)MovementGeneratorType.Idle)
                {
                    if (data.spawndist != 0.0f)
                    {
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with `MovementType`=0 (idle) have `spawndist`<>0, set to 0.", guid, data.id);
                        data.spawndist = 0.0f;
                    }
                }

                if (Math.Abs(data.orientation) > 2 * MathFunctions.PI)
                {
                    Log.outError(LogFilter.Sql, "Table `creature` has creature (GUID: {0} Entry: {1}) with abs(`orientation`) > 2*PI (orientation is expressed in radians), normalized.", guid, data.id);
                    data.orientation = Position.NormalizeOrientation(data.orientation);
                }

                data.phaseMask = 1;
                if (data.phaseGroup != 0 && data.phaseid != 0)
                {
                    Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with both `phaseid` and `phasegroup` set, `phasegroup` set to 0", guid, data.id);
                    data.phaseGroup = 0;
                }

                if (data.phaseid != 0)
                {
                    if (!CliDB.PhaseStorage.ContainsKey(data.phaseid))
                    {
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with `phaseid` {2} does not exist, set to 0", guid, data.id, data.phaseid);
                        data.phaseid = 0;
                    }
                }

                if (data.phaseGroup != 0)
                {
                    if (Global.DB2Mgr.GetPhasesForGroup(data.phaseGroup).Empty())
                    {
                        Log.outError(LogFilter.Sql, "Table `creature` have creature (GUID: {0} Entry: {1}) with `phasegroup` {2} does not exist, set to 0", guid, data.id, data.phaseGroup);
                        data.phaseGroup = 0;
                    }
                }

                if (WorldConfig.GetBoolValue(WorldCfg.CalculateCreatureZoneAreaData))
                {
                    uint zoneId = 0;
                    uint areaId = 0;
                    Global.MapMgr.GetZoneAndAreaId(out zoneId, out areaId, data.mapid, data.posX, data.posY, data.posZ);

                    PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.UPD_CREATURE_ZONE_AREA_DATA);
                    stmt.AddValue(0, zoneId);
                    stmt.AddValue(1, areaId);
                    stmt.AddValue(2, guid);

                    DB.World.Execute(stmt);
                }

                // Add to grid if not managed by the game event or pool system
                if (gameEvent == 0 && PoolId == 0)
                    AddCreatureToGrid(guid, data);

                creatureDataStorage[guid] = data;
                count++;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creatures in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void AddCreatureToGrid(ulong guid, CreatureData data)
        {
            uint mask = data.spawnMask;
            for (byte i = 0; mask != 0; i++, mask >>= 1)
            {
                if (Convert.ToBoolean(mask & 1))
                {
                    CellCoord cellCoord = GridDefines.ComputeCellCoord(data.posX, data.posY);
                    var cellguids = CreateCellObjectGuids(data.mapid, i, cellCoord);
                    cellguids.creatures.Add(guid);
                }
            }
        }
        public void RemoveCreatureFromGrid(ulong guid, CreatureData data)
        {
            uint mask = data.spawnMask;
            for (byte i = 0; mask != 0; i++, mask >>= 1)
            {
                if (Convert.ToBoolean(mask & 1))
                {
                    CellCoord cellCoord = GridDefines.ComputeCellCoord(data.posX, data.posY);
                    CellObjectGuids cellguids = GetCellObjectGuids(data.mapid, i, cellCoord.GetId());
                    if (cellguids == null)
                        return;

                    cellguids.creatures.Remove(guid);
                }
            }
        }
        public ulong AddCreatureData(uint entry, uint team, uint mapId, float x, float y, float z, float o, uint spawntimedelay)
        {
            CreatureTemplate cInfo = GetCreatureTemplate(entry);
            if (cInfo == null)
                return 0;

            uint level = cInfo.Minlevel == cInfo.Maxlevel ? (uint)cInfo.Minlevel : RandomHelper.URand(cInfo.Minlevel, cInfo.Maxlevel); // Only used for extracting creature base stats
            CreatureBaseStats stats = GetCreatureBaseStats(level, cInfo.UnitClass);
            Map map = Global.MapMgr.CreateBaseMap(mapId);
            if (!map)
                return 0;

            ulong guid = GenerateCreatureSpawnId();
            CreatureData data = NewOrExistCreatureData(guid);
            data.id = entry;
            data.mapid = (ushort)mapId;
            data.displayid = 0;
            data.equipmentId = 0;
            data.posX = x;
            data.posY = y;
            data.posZ = z;
            data.orientation = o;
            data.spawntimesecs = spawntimedelay;
            data.spawndist = 0;
            data.currentwaypoint = 0;
            data.curhealth = stats.GenerateHealth(cInfo);
            data.curmana = stats.GenerateMana(cInfo);
            data.movementType = (byte)cInfo.MovementType;
            data.spawnMask = 1;
            data.phaseMask = PhaseMasks.Normal;
            data.dbData = false;
            data.npcflag = (uint)cInfo.Npcflag;
            data.unit_flags = (uint)cInfo.UnitFlags;
            data.dynamicflags = cInfo.DynamicFlags;

            AddCreatureToGrid(guid, data);

            // We use spawn coords to spawn
            if (!map.Instanceable() && !map.IsRemovalGrid(x, y))
            {
                Creature creature = new Creature();
                if (!creature.LoadCreatureFromDB(guid, map))
                {
                    Log.outError(LogFilter.Server, "AddCreature: Cannot add creature entry {0} to map", entry);
                    return 0;
                }
            }

            return guid;
        }
        public List<uint> GetCreatureQuestItemList(uint id)
        {
            return _creatureQuestItemStorage.LookupByKey(id);
        }
        public CreatureAddon GetCreatureAddon(ulong lowguid)
        {
            return creatureAddonStorage.LookupByKey(lowguid);
        }
        public CreatureTemplate GetCreatureTemplate(uint entry)
        {
            return creatureTemplateStorage.LookupByKey(entry);
        }
        public CreatureAddon GetCreatureTemplateAddon(uint entry)
        {
            return creatureTemplateAddonStorage.LookupByKey(entry);
        }

        public Dictionary<uint, CreatureTemplate> GetCreatureTemplates()
        {
            return creatureTemplateStorage;
        }
        public CreatureData GetCreatureData(ulong guid)
        {
            return creatureDataStorage.LookupByKey(guid);
        }
        public ObjectGuid GetLinkedRespawnGuid(ObjectGuid guid)
        {
            var retGuid = linkedRespawnStorage.LookupByKey(guid);
            if (retGuid.IsEmpty())
                return ObjectGuid.Empty;
            return retGuid;
        }
        public bool SetCreatureLinkedRespawn(ulong guidLow, ulong linkedGuidLow)
        {
            if (guidLow == 0)
                return false;

            CreatureData master = GetCreatureData(guidLow);
            ObjectGuid guid = ObjectGuid.Create(HighGuid.Creature, master.mapid, master.id, guidLow);
            PreparedStatement stmt;

            if (linkedGuidLow == 0) // we're removing the linking
            {
                linkedRespawnStorage.Remove(guid);
                stmt = DB.World.GetPreparedStatement(WorldStatements.DEL_CRELINKED_RESPAWN);
                stmt.AddValue(0, guidLow);
                DB.World.Execute(stmt);
                return true;
            }

            CreatureData slave = GetCreatureData(linkedGuidLow);
            if (slave == null)
            {
                Log.outError(LogFilter.Sql, "Creature '{0}' linking to non-existent creature '{1}'.", guidLow, linkedGuidLow);
                return false;
            }

            MapRecord map = CliDB.MapStorage.LookupByKey(master.mapid);
            if (map == null || !map.Instanceable() || (master.mapid != slave.mapid))
            {
                Log.outError(LogFilter.Sql, "Creature '{0}' linking to '{1}' on an unpermitted map.", guidLow, linkedGuidLow);
                return false;
            }

            if (!Convert.ToBoolean(master.spawnMask & slave.spawnMask))  // they must have a possibility to meet (normal/heroic difficulty)
            {
                Log.outError(LogFilter.Sql, "LinkedRespawn: Creature '{0}' linking to '{1}' with not corresponding spawnMask", guidLow, linkedGuidLow);
                return false;
            }

            ObjectGuid linkedGuid = ObjectGuid.Create(HighGuid.Creature, slave.mapid, slave.id, linkedGuidLow);

            linkedRespawnStorage[guid] = linkedGuid;
            stmt = DB.World.GetPreparedStatement(WorldStatements.REP_CREATURE_LINKED_RESPAWN);
            stmt.AddValue(0, guidLow);
            stmt.AddValue(1, linkedGuidLow);
            DB.World.Execute(stmt);
            return true;
        }
        public CreatureData NewOrExistCreatureData(ulong guid)
        {
            if (!creatureDataStorage.ContainsKey(guid))
                creatureDataStorage[guid] = new CreatureData();
            return creatureDataStorage[guid];
        }
        public void DeleteCreatureData(ulong guid)
        {
            CreatureData data = GetCreatureData(guid);
            if (data != null)
                RemoveCreatureFromGrid(guid, data);

            creatureDataStorage.Remove(guid);
        }
        public CreatureBaseStats GetCreatureBaseStats(uint level, uint unitClass)
        {
            var stats = creatureBaseStatsStorage.LookupByKey(MathFunctions.MakePair16(level, unitClass));
            if (stats != null)
                return stats;

            return new DefaultCreatureBaseStats();
        }
        public CreatureModelInfo GetCreatureModelRandomGender(ref uint displayID)
        {
            CreatureModelInfo modelInfo = GetCreatureModelInfo(displayID);
            if (modelInfo == null)
                return null;

            // If a model for another gender exists, 50% chance to use it
            if (modelInfo.DisplayIdOtherGender != 0 && RandomHelper.NextDouble() == 0)
            {
                CreatureModelInfo minfotmp = GetCreatureModelInfo(modelInfo.DisplayIdOtherGender);
                if (minfotmp == null)
                    Log.outError(LogFilter.Sql, "Model (Entry: {0}) has modelidothergender {1} not found in table `creaturemodelinfo`. ", displayID, modelInfo.DisplayIdOtherGender);
                else
                {
                    // DisplayID changed
                    displayID = modelInfo.DisplayIdOtherGender;
                    return minfotmp;
                }
            }

            return modelInfo;
        }
        public CreatureModelInfo GetCreatureModelInfo(uint modelId)
        {
            return creatureModelStorage.LookupByKey(modelId);
        }
        public TrainerSpellData GetNpcTrainerSpells(uint entry)
        {
            return cacheTrainerSpellStorage.LookupByKey(entry);
        }
        public NpcText GetNpcText(uint textId)
        {
            return _npcTextStorage.LookupByKey(textId);
        }

        //GameObjects
        public void LoadGameObjectTemplate()
        {
            var time = Time.GetMSTime();

            foreach (GameObjectsRecord db2go in CliDB.GameObjectsStorage.Values)
            {
                GameObjectTemplate go = new GameObjectTemplate();
                go.entry = db2go.Id;
                go.type = db2go.Type;
                go.displayId = db2go.DisplayID;
                go.name = db2go.Name[Global.WorldMgr.GetDefaultDbcLocale()];
                go.size = db2go.Size;

                unsafe
                {
                    fixed (int* b = go.Raw.data)
                    {
                        for (byte x = 0; x < db2go.Data.Length; ++x)
                            b[x] = db2go.Data[x];
                    }
                }

                go.RequiredLevel = 0;
                go.ScriptId = 0;

                gameObjectTemplateStorage[db2go.Id] = go;
            }

            //                                          0      1     2          3     4         5               6     7
            SQLResult result = DB.World.Query("SELECT entry, type, displayId, name, IconName, castBarCaption, unk1, size, " +
                //8      9      10     11     12     13     14     15     16     17     18      19      20
                "Data0, Data1, Data2, Data3, Data4, Data5, Data6, Data7, Data8, Data9, Data10, Data11, Data12, " +
                //21      22      23      24      25      26      27      28      29      30      31      32      33      34      35      36
                "Data13, Data14, Data15, Data16, Data17, Data18, Data19, Data20, Data21, Data22, Data23, Data24, Data25, Data26, Data27, Data28, " +
                //37      38       39     40      41             42      43
                "Data29, Data30, Data31, Data32, RequiredLevel, AIName, ScriptName FROM gameobject_template");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 gameobject definitions. DB table `gameobject_template` is empty.");
            }
            else
            {
                uint count = 0;
                do
                {
                    uint entry = result.Read<uint>(0);

                    GameObjectTemplate got = new GameObjectTemplate();

                    got.entry = entry;
                    got.type = (GameObjectTypes)result.Read<uint>(1);
                    got.displayId = result.Read<uint>(2);
                    got.name = result.Read<string>(3);
                    got.IconName = result.Read<string>(4);
                    got.castBarCaption = result.Read<string>(5);
                    got.unk1 = result.Read<string>(6);
                    got.size = result.Read<float>(7);

                    unsafe
                    {
                        fixed (int* b = got.Raw.data)
                        {
                            for (byte x = 0; x < SharedConst.MaxGOData; ++x)
                                b[x] = result.Read<int>(8 + x);
                        }
                    }

                    got.RequiredLevel = result.Read<int>(41);
                    got.AIName = result.Read<string>(42);
                    got.ScriptId = Global.ObjectMgr.GetScriptId(result.Read<string>(43));

                    switch (got.type)
                    {
                        case GameObjectTypes.Door:                      //0
                            if (got.Door.open != 0)
                                CheckGOLockId(got, got.Door.open, 1);
                            CheckGONoDamageImmuneId(got, got.Door.noDamageImmune, 3);
                            break;
                        case GameObjectTypes.Button:                    //1
                            if (got.Button.open != 0)
                                CheckGOLockId(got, got.Button.open, 1);
                            CheckGONoDamageImmuneId(got, got.Button.noDamageImmune, 4);
                            break;
                        case GameObjectTypes.QuestGiver:                //2
                            if (got.QuestGiver.open != 0)
                                CheckGOLockId(got, got.QuestGiver.open, 0);
                            CheckGONoDamageImmuneId(got, got.QuestGiver.noDamageImmune, 5);
                            break;
                        case GameObjectTypes.Chest:                     //3
                            if (got.Chest.open != 0)
                                CheckGOLockId(got, got.Chest.open, 0);

                            CheckGOConsumable(got, got.Chest.consumable, 3);

                            if (got.Chest.linkedTrap != 0)              // linked trap
                                CheckGOLinkedTrapId(got, got.Chest.linkedTrap, 7);
                            break;
                        case GameObjectTypes.Trap:                      //6
                            if (got.Trap.open != 0)
                                CheckGOLockId(got, got.Trap.open, 0);
                            break;
                        case GameObjectTypes.Chair:                     //7
                            CheckAndFixGOChairHeightId(got, ref got.Chair.chairheight, 1);
                            break;
                        case GameObjectTypes.SpellFocus:               //8
                            if (got.SpellFocus.spellFocusType != 0)
                            {
                                if (!CliDB.SpellFocusObjectStorage.ContainsKey(got.SpellFocus.spellFocusType))
                                    Log.outError(LogFilter.Sql, "GameObject (Entry: {0} GoType: {1}) have data0={2} but SpellFocus (Id: {3}) not exist.",
                                        entry, got.type, got.SpellFocus.spellFocusType, got.SpellFocus.spellFocusType);
                            }

                            if (got.SpellFocus.linkedTrap != 0)        // linked trap
                                CheckGOLinkedTrapId(got, got.SpellFocus.linkedTrap, 2);
                            break;
                        case GameObjectTypes.Goober:                    //10
                            if (got.Goober.open != 0)
                                CheckGOLockId(got, got.Goober.open, 0);

                            CheckGOConsumable(got, got.Goober.consumable, 3);

                            if (got.Goober.pageID != 0)                  // pageId
                            {
                                if (GetPageText(got.Goober.pageID) == null)
                                    Log.outError(LogFilter.Sql, "GameObject (Entry: {0} GoType: {1}) have data7={2} but PageText (Entry {3}) not exist.", entry, got.type, got.Goober.pageID, got.Goober.pageID);
                            }
                            CheckGONoDamageImmuneId(got, got.Goober.noDamageImmune, 11);
                            if (got.Goober.linkedTrap != 0)            // linked trap
                                CheckGOLinkedTrapId(got, got.Goober.linkedTrap, 12);
                            break;
                        case GameObjectTypes.AreaDamage:                //12
                            if (got.AreaDamage.open != 0)
                                CheckGOLockId(got, got.AreaDamage.open, 0);
                            break;
                        case GameObjectTypes.Camera:                    //13
                            if (got.Camera.open != 0)
                                CheckGOLockId(got, got.Camera.open, 0);
                            break;
                        case GameObjectTypes.MapObjTransport:              //15
                            {
                                if (got.MoTransport.taxiPathID != 0)
                                {
                                    if (got.MoTransport.taxiPathID >= CliDB.TaxiPathNodesByPath.Count || CliDB.TaxiPathNodesByPath[got.MoTransport.taxiPathID].Empty())
                                        Log.outError(LogFilter.Sql, "GameObject (Entry: {0} GoType: {1}) have data0={2} but TaxiPath (Id: {3}) not exist.",
                                        entry, got.type, got.MoTransport.taxiPathID, got.MoTransport.taxiPathID);
                                }
                                int transportMap = got.MoTransport.SpawnMap;
                                if (transportMap != 0)
                                    _transportMaps.Add((ushort)transportMap);
                                break;
                            }
                        case GameObjectTypes.SpellCaster:               //22
                                                                        // always must have spell
                            CheckGOSpellId(got, got.SpellCaster.spell, 0);
                            break;
                        case GameObjectTypes.FlagStand:                 //24
                            if (got.FlagStand.open != 0)
                                CheckGOLockId(got, got.FlagStand.open, 0);
                            CheckGONoDamageImmuneId(got, got.FlagStand.noDamageImmune, 5);
                            break;
                        case GameObjectTypes.FishingHole:               //25
                            if (got.FishingHole.open != 0)
                                CheckGOLockId(got, got.FishingHole.open, 4);
                            break;
                        case GameObjectTypes.FlagDrop:                  //26
                            if (got.FlagDrop.open != 0)
                                CheckGOLockId(got, got.FlagDrop.open, 0);
                            CheckGONoDamageImmuneId(got, got.FlagDrop.noDamageImmune, 3);
                            break;
                        case GameObjectTypes.BarberChair:              //32
                            CheckAndFixGOChairHeightId(got, ref got.BarberChair.chairheight, 0);
                            if (got.BarberChair.SitAnimKit != 0 && !CliDB.AnimKitStorage.ContainsKey(got.BarberChair.SitAnimKit))
                            {
                                Log.outError(LogFilter.Sql, "GameObject (Entry: {0} GoType: {1}) have data2 = {2} but AnimKit.dbc (Id: {3}) not exist, set to 0.",
                                   entry, got.type, got.BarberChair.SitAnimKit, got.BarberChair.SitAnimKit);
                                got.BarberChair.SitAnimKit = 0;
                            }
                            break;
                        case GameObjectTypes.GarrisonBuilding:
                            {
                                int transportMap = got.garrisonBuilding.SpawnMap;
                                if (transportMap != 0)
                                    _transportMaps.Add((ushort)transportMap);
                            }
                            break;
                        case GameObjectTypes.GatheringNode:
                            if (got.GatheringNode.open != 0)
                                CheckGOLockId(got, got.GatheringNode.open, 0);
                            if (got.GatheringNode.linkedTrap != 0)
                                CheckGOLinkedTrapId(got, got.GatheringNode.linkedTrap, 20);
                            break;
                    }

                    gameObjectTemplateStorage[entry] = got;
                    ++count;
                }
                while (result.NextRow());

                Log.outInfo(LogFilter.ServerLoading, "Loaded {0} game object templates in {1} ms", count, Time.GetMSTimeDiffToNow(time));
            }
        }
        public void LoadGameObjectTemplateAddons()
        {
            uint oldMSTime = Time.GetMSTime();

            //                                         0       1       2      3        4
            SQLResult result = DB.World.Query("SELECT entry, faction, flags, mingold, maxgold FROM gameobject_template_addon");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 gameobject template addon definitions. DB table `gameobject_template_addon` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);

                GameObjectTemplate got = Global.ObjectMgr.GetGameObjectTemplate(entry);
                if (got == null)
                {
                    Log.outError(LogFilter.Sql, "GameObject template (Entry: {0}) does not exist but has a record in `gameobject_template_addon`", entry);
                    continue;
                }

                GameObjectTemplateAddon gameObjectAddon = new GameObjectTemplateAddon();
                gameObjectAddon.faction = result.Read<ushort>(1);
                gameObjectAddon.flags = result.Read<uint>(2);
                gameObjectAddon.mingold = result.Read<uint>(3);
                gameObjectAddon.maxgold = result.Read<uint>(4);

                // checks
                if (gameObjectAddon.faction != 0 && !CliDB.FactionTemplateStorage.ContainsKey(gameObjectAddon.faction))
                    Log.outError(LogFilter.Sql, "GameObject (Entry: {0}) has invalid faction ({1}) defined in `gameobject_template_addon`.", entry, gameObjectAddon.faction);

                if (gameObjectAddon.maxgold > 0)
                {
                    switch (got.type)
                    {
                        case GameObjectTypes.Chest:
                        case GameObjectTypes.FishingHole:
                            break;
                        default:
                            Log.outError(LogFilter.Sql, "GameObject (Entry {0} GoType: {1}) cannot be looted but has maxgold set in `gameobject_template_addon`.", entry, got.type);
                            break;
                    }
                }

                _gameObjectTemplateAddonStore[entry] = gameObjectAddon;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} game object template addons in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadGameobjects()
        {
            var time = Time.GetMSTime();
            //                                              0            1   2    3           4           5           6
            SQLResult result = DB.World.Query("SELECT gameobject.guid, id, map, position_x, position_y, position_z, orientation, " +
                //   7          8          9          10         11             12            13     14         15          16
                "rotation0, rotation1, rotation2, rotation3, spawntimesecs, animprogress, state, spawnMask, eventEntry, pool_entry, " +
                //   17       18          19
                "phaseid, phasegroup, ScriptName " +
                "FROM gameobject LEFT OUTER JOIN game_event_gameobject ON gameobject.guid = game_event_gameobject.guid " +
                "LEFT OUTER JOIN pool_gameobject ON gameobject.guid = pool_gameobject.guid");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 gameobjects. DB table `gameobject` is empty.");

                return;
            }
            uint count = 0;

            // build single time for check spawnmask
            Dictionary<uint, uint> spawnMasks = new Dictionary<uint, uint>();
            foreach (var mapDifficultyPair in Global.DB2Mgr.GetMapDifficulties())
            {
                foreach (var difficultyPair in mapDifficultyPair.Value)
                {
                    if (!spawnMasks.ContainsKey(mapDifficultyPair.Key))
                        spawnMasks[mapDifficultyPair.Key] = 0;
                    spawnMasks[mapDifficultyPair.Key] |= (uint)(1 << (int)difficultyPair.Key);
                }
            }

            do
            {
                ulong guid = result.Read<ulong>(0);
                uint entry = result.Read<uint>(1);

                GameObjectTemplate gInfo = GetGameObjectTemplate(entry);
                if (gInfo == null)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0}) with non existing gameobject entry {1}, skipped.", guid, entry);
                    continue;
                }

                if (gInfo.displayId == 0)
                {
                    switch (gInfo.type)
                    {
                        case GameObjectTypes.Trap:
                        case GameObjectTypes.SpellFocus:
                            break;
                        default:
                            Log.outError(LogFilter.Sql, "Gameobject (GUID: {0} Entry {1} GoType: {2}) doesn't have a displayId ({3}), not loaded.", guid, entry, gInfo.type, gInfo.displayId);
                            break;
                    }
                }

                if (gInfo.displayId != 0 && !CliDB.GameObjectDisplayInfoStorage.ContainsKey(gInfo.displayId))
                {
                    Log.outError(LogFilter.Sql, "Gameobject (GUID: {0} Entry {1} GoType: {2}) has an invalid displayId ({3}), not loaded.", guid, entry, gInfo.type, gInfo.displayId);
                    continue;
                }

                GameObjectData data = new GameObjectData();
                data.id = entry;
                data.mapid = result.Read<ushort>(2);
                data.posX = result.Read<float>(3);
                data.posY = result.Read<float>(4);
                data.posZ = result.Read<float>(5);
                data.orientation = result.Read<float>(6);
                data.rotation.X = result.Read<float>(7);
                data.rotation.Y = result.Read<float>(8);
                data.rotation.Z = result.Read<float>(9);
                data.rotation.W = result.Read<float>(10);
                data.spawntimesecs = result.Read<int>(11);

                var mapEntry = CliDB.MapStorage.LookupByKey(data.mapid);
                if (mapEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) spawned on a non-existed map (Id: {2}), skip", guid, data.id, data.mapid);
                    continue;
                }

                if (data.spawntimesecs == 0 && gInfo.IsDespawnAtAction())
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with `spawntimesecs` (0) value, but the gameobejct is marked as despawnable at action.", guid, data.id);
                }

                data.animprogress = result.Read<uint>(12);
                data.artKit = 0;

                uint gostate = result.Read<uint>(13);
                if (gostate >= (uint)GameObjectState.Max)
                {
                    if (gInfo.type != GameObjectTypes.Transport || gostate > (int)GameObjectState.TransportActive + SharedConst.MaxTransportStopFrames)
                    {
                        Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with invalid `state` ({2}) value, skip", guid, data.id, gostate);
                        continue;
                    }
                }
                data.go_state = (GameObjectState)gostate;

                data.spawnMask = result.Read<uint>(14);

                //if (!_transportMaps.Contains(data.mapid) && (spawnMasks.ContainsKey(data.mapid) && Convert.ToBoolean(data.spawnMask & ~spawnMasks[data.mapid])))
                //Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) that has wrong spawn mask {2} including not supported difficulty modes for map (Id: {3}), skip",
                //guid, data.id, data.spawnMask, data.mapid);

                short gameEvent = result.Read<sbyte>(15);
                uint PoolId = result.Read<uint>(16);
                data.phaseid = result.Read<uint>(17);
                data.phaseGroup = result.Read<uint>(18);

                if (data.phaseGroup != 0 && data.phaseid != 0)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` have gameobject (GUID: {0} Entry: {1}) with both `phaseid` and `phasegroup` set, `phasegroup` set to 0", guid, data.id);
                    data.phaseGroup = 0;
                }

                if (data.phaseid != 0)
                {
                    if (!CliDB.PhaseStorage.ContainsKey(data.phaseid))
                    {
                        Log.outError(LogFilter.Sql, "Table `gameobject` have gameobject (GUID: {0} Entry: {1}) with `phaseid` {2} does not exist, set to 0", guid, data.id, data.phaseid);
                        data.phaseid = 0;
                    }
                }

                if (data.phaseGroup != 0)
                {
                    if (Global.DB2Mgr.GetPhasesForGroup(data.phaseGroup).Empty())
                    {
                        Log.outError(LogFilter.Sql, "Table `gameobject` have gameobject (GUID: {0} Entry: {1}) with `phaseGroup` {2} does not exist, set to 0", guid, data.id, data.phaseGroup);
                        data.phaseGroup = 0;
                    }
                }

                data.ScriptId = GetScriptId(result.Read<string>(19));
                if (data.ScriptId == 0)
                    data.ScriptId = gInfo.ScriptId;

                if (Math.Abs(data.orientation) > 2 * MathFunctions.PI)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with abs(`orientation`) > 2*PI (orientation is expressed in radians), normalized.", guid, data.id);
                    data.orientation = Position.NormalizeOrientation(data.orientation);
                }

                if (data.rotation.X < -1.0f || data.rotation.X > 1.0f)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with invalid rotationX ({1}) value, skip", guid, data.id, data.rotation.X);
                    continue;
                }

                if (data.rotation.Y < -1.0f || data.rotation.Y > 1.0f)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with invalid rotationY ({2}) value, skip", guid, data.id, data.rotation.Y);
                    continue;
                }

                if (data.rotation.Z < -1.0f || data.rotation.Z > 1.0f)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with invalid rotationZ ({3}) value, skip", guid, data.id, data.rotation.Z);
                    continue;
                }

                if (data.rotation.W < -1.0f || data.rotation.W > 1.0f)
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with invalid rotationW ({4}) value, skip", guid, data.id, data.rotation.W);
                    continue;
                }

                if (!GridDefines.IsValidMapCoord(data.mapid, data.posX, data.posY, data.posZ, data.orientation))
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject` has gameobject (GUID: {0} Entry: {1}) with invalid coordinates, skip", guid, data.id);
                    continue;
                }

                data.phaseMask = 1;

                if (WorldConfig.GetBoolValue(WorldCfg.CalculateGameobjectZoneAreaData))
                {
                    uint zoneId = 0;
                    uint areaId = 0;
                    Global.MapMgr.GetZoneAndAreaId(out zoneId, out areaId, data.mapid, data.posX, data.posY, data.posZ);

                    PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.UPD_GAMEOBJECT_ZONE_AREA_DATA);
                    stmt.AddValue(0, zoneId);
                    stmt.AddValue(1, areaId);
                    stmt.AddValue(2, guid);
                    DB.World.Execute(stmt);
                }

                if (gameEvent == 0 && PoolId == 0)                      // if not this is to be managed by GameEvent System or Pool system
                    AddGameObjectToGrid(guid, data);

                gameObjectDataStorage[guid] = data;
                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gameobjects in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadGameObjectAddons()
        {
            uint oldMSTime = Time.GetMSTime();

            _gameObjectAddonStorage.Clear();

            //                                         0     1                 2                 3                 4                 5                 6
            SQLResult result = DB.World.Query("SELECT guid, parent_rotation0, parent_rotation1, parent_rotation2, parent_rotation3, invisibilityType, invisibilityValue FROM gameobject_addon");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 gameobject addon definitions. DB table `gameobject_addon` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                ulong guid = result.Read<ulong>(0);

                GameObjectData goData = GetGOData(guid);
                if (goData == null)
                {
                    Log.outError(LogFilter.Sql, "GameObject (GUID: {0}) does not exist but has a record in `gameobject_addon`", guid);
                    continue;
                }

                GameObjectAddon gameObjectAddon = new GameObjectAddon();
                gameObjectAddon.ParentRotation = new Quaternion(result.Read<float>(1), result.Read<float>(2), result.Read<float>(3), result.Read<float>(4));
                gameObjectAddon.invisibilityType = (InvisibilityType)result.Read<byte>(5);
                gameObjectAddon.invisibilityValue = result.Read<uint>(6);

                if (gameObjectAddon.invisibilityType >= InvisibilityType.Max)
                {
                    Log.outError(LogFilter.Sql, "GameObject (GUID: {0}) has invalid InvisibilityType in `gameobject_addon`, disabled invisibility", guid);
                    gameObjectAddon.invisibilityType = InvisibilityType.General;
                    gameObjectAddon.invisibilityValue = 0;
                }

                if (gameObjectAddon.invisibilityType != 0 && gameObjectAddon.invisibilityValue == 0)
                {
                    Log.outError(LogFilter.Sql, "GameObject (GUID: {0}) has InvisibilityType set but has no InvisibilityValue in `gameobject_addon`, set to 1", guid);
                    gameObjectAddon.invisibilityValue = 1;
                }

                if (!gameObjectAddon.ParentRotation.isUnit())
                {
                    Log.outError(LogFilter.Sql, "GameObject (GUID: {0}) has invalid parent rotation in `gameobject_addon`, set to default", guid);
                    gameObjectAddon.ParentRotation = Quaternion.WAxis;
                }

                _gameObjectAddonStorage[guid] = gameObjectAddon;
                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gameobject addons in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadGameObjectQuestItems()
        {
            uint oldMSTime = Time.GetMSTime();

            //                                           0                1
            SQLResult result = DB.World.Query("SELECT GameObjectEntry, ItemId, Idx FROM gameobject_questitem ORDER BY Idx ASC");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 gameobject quest items. DB table `gameobject_questitem` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);
                uint item = result.Read<uint>(1);
                uint idx = result.Read<uint>(2);

                if (!gameObjectTemplateStorage.ContainsKey(entry))
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject_questitem` has data for nonexistent gameobject (entry: {0}, idx: {1}), skipped", entry, idx);
                    continue;
                }

                if (!CliDB.ItemStorage.ContainsKey(item))
                {
                    Log.outError(LogFilter.Sql, "Table `gameobject_questitem` has nonexistent item (ID: {0}) in gameobject (entry: {1}, idx: {2}), skipped", item, entry, idx);
                    continue;
                }

                _gameObjectQuestItemStorage.Add(entry, item);

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gameobject quest items in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadGameObjectForQuests()
        {
            uint oldMSTime = Time.GetMSTime();

            _gameObjectForQuestStorage.Clear();                         // need for reload case

            if (GetGameObjectTemplates().Empty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 GameObjects for quests");
                return;
            }

            uint count = 0;

            // collect GO entries for GO that must activated
            foreach (var go in GetGameObjectTemplates())
            {
                switch (go.Value.type)
                {
                    case GameObjectTypes.QuestGiver:
                        _gameObjectForQuestStorage.Add(go.Value.entry);
                        ++count;
                        break;
                    case GameObjectTypes.Chest:
                        {
                            // scan GO chest with loot including quest items
                            uint loot_id = (go.Value.GetLootId());

                            // find quest loot for GO
                            if (go.Value.Chest.questID != 0 || Loots.LootStorage.Gameobject.HaveQuestLootFor(loot_id))
                            {
                                _gameObjectForQuestStorage.Add(go.Value.entry);
                                ++count;
                            }
                            break;
                        }
                    case GameObjectTypes.Generic:
                        {
                            if (go.Value.Generic.questID > 0)            //quests objects
                            {
                                _gameObjectForQuestStorage.Add(go.Value.entry);
                                ++count;
                            }
                            break;
                        }
                    case GameObjectTypes.Goober:
                        {
                            if (go.Value.Goober.questID > 0)              //quests objects
                            {
                                _gameObjectForQuestStorage.Add(go.Value.entry);
                                ++count;
                            }
                            break;
                        }
                    default:
                        break;
                }
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} GameObjects for quests in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void AddGameObjectToGrid(ulong guid, GameObjectData data)
        {
            uint mask = data.spawnMask;
            for (byte i = 0; mask != 0; i++, mask >>= 1)
            {
                if (Convert.ToBoolean(mask & 1))
                {
                    CellCoord cellCoord = GridDefines.ComputeCellCoord(data.posX, data.posY);
                    var cellguids = CreateCellObjectGuids(data.mapid, i, cellCoord);
                    cellguids.gameobjects.Add(guid);
                }
            }
        }
        public void RemoveGameObjectFromGrid(ulong guid, GameObjectData data)
        {
            uint mask = data.spawnMask;
            for (byte i = 0; mask != 0; i++, mask >>= 1)
            {
                if (Convert.ToBoolean(mask & 1))
                {
                    CellCoord cellCoord = GridDefines.ComputeCellCoord(data.posX, data.posY);
                    CellObjectGuids cellguids = GetCellObjectGuids(data.mapid, i, cellCoord.GetId());
                    if (cellguids == null)
                        return;

                    cellguids.gameobjects.Remove(guid);
                }
            }
        }
        public ulong AddGOData(uint entry, uint mapId, float x, float y, float z, float o, uint spawntimedelay, float rotation0, float rotation1, float rotation2, float rotation3)
        {
            GameObjectTemplate goinfo = GetGameObjectTemplate(entry);
            if (goinfo == null)
                return 0;

            Map map = Global.MapMgr.CreateBaseMap(mapId);
            if (map == null)
                return 0;

            ulong guid = GenerateGameObjectSpawnId();
            GameObjectData data = new GameObjectData();
            data.id = entry;
            data.mapid = (ushort)mapId;
            data.posX = x;
            data.posY = y;
            data.posZ = z;
            data.orientation = o;
            data.rotation.X = rotation0;
            data.rotation.Y = rotation1;
            data.rotation.Z = rotation2;
            data.rotation.W = rotation3;
            data.spawntimesecs = (int)spawntimedelay;
            data.animprogress = 100;
            data.spawnMask = 1;
            data.go_state = GameObjectState.Ready;
            data.phaseMask = (ushort)PhaseMasks.Normal;
            data.artKit = (byte)(goinfo.type == GameObjectTypes.ControlZone ? 21 : 0);
            data.dbData = false;

            NewGOData(guid, data);
            AddGameObjectToGrid(guid, data);

            // Spawn if necessary (loaded grids only)
            // We use spawn coords to spawn
            if (!map.Instanceable() && map.IsGridLoaded(x, y))
            {
                GameObject go = new GameObject();
                if (!go.LoadGameObjectFromDB(guid, map))
                {
                    Log.outError(LogFilter.Server, "AddGOData: cannot add gameobject entry {0} to map", entry);
                    return 0;
                }
            }

            Log.outDebug(LogFilter.Maps, "AddGOData: dbguid:{0} entry:{1} map:{2} x:{3} y:{4} z:{5} o:{6}", guid, entry, mapId, x, y, z, o);

            return guid;
        }

        public GameObjectAddon GetGameObjectAddon(ulong lowguid)
        {
            return _gameObjectAddonStorage.LookupByKey(lowguid);
        }
        public List<uint> GetGameObjectQuestItemList(uint id)
        {
            return _gameObjectQuestItemStorage.LookupByKey(id);
        }
        MultiMap<uint, uint> GetGameObjectQuestItemMap() { return _gameObjectQuestItemStorage; }
        public GameObjectData GetGOData(ulong guid)
        {
            return gameObjectDataStorage.LookupByKey(guid);
        }
        public void DeleteGOData(ulong guid)
        {
            GameObjectData data = GetGOData(guid);
            if (data != null)
                RemoveGameObjectFromGrid(guid, data);

            gameObjectDataStorage.Remove(guid);
        }
        public void NewGOData(ulong guid, GameObjectData data)
        {
            gameObjectDataStorage.Add(guid, data);
        }
        public GameObjectTemplate GetGameObjectTemplate(uint entry)
        {
            return gameObjectTemplateStorage.LookupByKey(entry);
        }
        public GameObjectTemplateAddon GetGameObjectTemplateAddon(uint entry)
        {
            return _gameObjectTemplateAddonStore.LookupByKey(entry);
        }
        public Dictionary<uint, GameObjectTemplate> GetGameObjectTemplates()
        {
            return gameObjectTemplateStorage;
        }
        public bool IsGameObjectForQuests(uint entry)
        {
            return _gameObjectForQuestStorage.Contains(entry);
        }
        void CheckGOLockId(GameObjectTemplate goInfo, uint dataN, uint N)
        {
            if (CliDB.LockStorage.ContainsKey(dataN))
                return;

            Log.outError(LogFilter.Sql, "Gameobject (Entry: {0} GoType: {1}) have data{2}={3} but lock (Id: {4}) not found.", goInfo.entry, goInfo.type, N, goInfo.Door.open, goInfo.Door.open);
        }
        void CheckGOLinkedTrapId(GameObjectTemplate goInfo, uint dataN, uint N)
        {
            GameObjectTemplate trapInfo = Global.ObjectMgr.GetGameObjectTemplate(dataN);
            if (trapInfo != null)
            {
                if (trapInfo.type != GameObjectTypes.Trap)
                    Log.outError(LogFilter.Sql, "Gameobject (Entry: {0} GoType: {1}) have data{2}={3} but GO (Entry {4}) have not GAMEOBJECT_TYPE_TRAP type.", goInfo.entry, goInfo.type, N, dataN, dataN);
            }
        }
        void CheckGOSpellId(GameObjectTemplate goInfo, uint dataN, uint N)
        {
            if (Global.SpellMgr.HasSpellInfo(dataN))
                return;

            Log.outError(LogFilter.Sql, "Gameobject (Entry: {0} GoType: {1}) have data{2}={3}  but Spell (Entry {4}) not exist.", goInfo.entry, goInfo.type, N, dataN, dataN);
        }
        void CheckAndFixGOChairHeightId(GameObjectTemplate goInfo, ref uint dataN, uint N)
        {
            if (dataN <= (UnitStandStateType.SitHighChair - UnitStandStateType.SitLowChair))
                return;

            Log.outError(LogFilter.Sql, "Gameobject (Entry: {0} GoType: {1}) have data{2}={3}  but correct chair height in range 0..{4}.", goInfo.entry, goInfo.type, N, dataN, UnitStandStateType.SitHighChair - UnitStandStateType.SitLowChair);

            // prevent client and server unexpected work
            dataN = 0;
        }
        void CheckGONoDamageImmuneId(GameObjectTemplate goTemplate, uint dataN, uint N)
        {
            // 0/1 correct values
            if (dataN <= 1)
                return;

            Log.outError(LogFilter.Sql, "Gameobject (Entry: {0} GoType: {1}) have data{2}={3} but expected boolean (0/1) noDamageImmune field value.", goTemplate.entry, goTemplate.type, N, dataN);
        }
        void CheckGOConsumable(GameObjectTemplate goInfo, uint dataN, uint N)
        {
            // 0/1 correct values
            if (dataN <= 1)
                return;

            Log.outError(LogFilter.Sql, "Gameobject (Entry: {0} GoType: {1}) have data{2}={3}  but expected boolean (0/1) consumable field value.",
                goInfo.entry, goInfo.type, N, dataN);
        }

        //Items
        public void LoadItemTemplates()
        {
            var oldMSTime = Time.GetMSTime();
            uint sparseCount = 0;

            foreach (var sparse in CliDB.ItemSparseStorage.Values)
            {
                ItemRecord db2Data = CliDB.ItemStorage.LookupByKey(sparse.Id);
                if (db2Data == null)
                    continue;

                var itemTemplate = new ItemTemplate(db2Data, sparse);

                itemTemplate.MaxDurability = FillMaxDurability(db2Data.Class, db2Data.SubClass, sparse.inventoryType, (ItemQuality)sparse.Quality, sparse.ItemLevel);
                FillDisenchantFields(out itemTemplate.DisenchantID, out itemTemplate.RequiredDisenchantSkill, itemTemplate);

                var itemSpecOverrides = Global.DB2Mgr.GetItemSpecOverrides(sparse.Id);
                if (itemSpecOverrides != null)
                {
                    foreach (ItemSpecOverrideRecord itemSpecOverride in itemSpecOverrides)
                    {
                        ChrSpecializationRecord specialization = CliDB.ChrSpecializationStorage.LookupByKey(itemSpecOverride.SpecID);
                        if (specialization != null)
                        {
                            itemTemplate.ItemSpecClassMask |= 1u << (specialization.ClassID - 1);
                            itemTemplate.Specializations[0].Set(ItemTemplate.CalculateItemSpecBit(specialization), true);

                            itemTemplate.Specializations[1] = itemTemplate.Specializations[1].Or(itemTemplate.Specializations[0]);
                            itemTemplate.Specializations[2] = itemTemplate.Specializations[2].Or(itemTemplate.Specializations[0]);
                        }
                    }
                }
                else
                {
                    ItemSpecStats itemSpecStats = new ItemSpecStats(db2Data, sparse);

                    foreach (ItemSpecRecord itemSpec in CliDB.ItemSpecStorage.Values)
                    {
                        if (itemSpecStats.ItemType != itemSpec.ItemType)
                            continue;

                        bool hasPrimary = itemSpec.PrimaryStat == ItemSpecStat.None;
                        bool hasSecondary = itemSpec.SecondaryStat == ItemSpecStat.None;
                        for (uint i = 0; i < itemSpecStats.ItemSpecStatCount; ++i)
                        {
                            if (itemSpecStats.ItemSpecStatTypes[i] == itemSpec.PrimaryStat)
                                hasPrimary = true;
                            if (itemSpecStats.ItemSpecStatTypes[i] == itemSpec.SecondaryStat)
                                hasSecondary = true;
                        }

                        if (!hasPrimary || !hasSecondary)
                            continue;

                        ChrSpecializationRecord specialization = CliDB.ChrSpecializationStorage.LookupByKey(itemSpec.SpecID);
                        if (specialization != null)
                        {
                            if (Convert.ToBoolean((1 << (specialization.ClassID - 1)) & sparse.AllowableClass))
                            {
                                itemTemplate.ItemSpecClassMask |= 1u << (specialization.ClassID - 1);
                                int specBit = ItemTemplate.CalculateItemSpecBit(specialization);
                                itemTemplate.Specializations[0].Set(specBit, true);
                                if (itemSpec.MaxLevel > 40)
                                    itemTemplate.Specializations[1].Set(specBit, true);
                                if (itemSpec.MaxLevel >= 110)
                                    itemTemplate.Specializations[2].Set(specBit, true);
                            }
                        }
                    }
                }

                ++sparseCount;
                ItemTemplateStorage.Add(sparse.Id, itemTemplate);
            }

            foreach (var effectEntry in CliDB.ItemEffectStorage.Values)
            {
                var itemTemplate = ItemTemplateStorage.LookupByKey(effectEntry.ItemID);
                if (itemTemplate == null)
                    continue;

                itemTemplate.Effects.Add(effectEntry);
            }

            // Check if item templates for DBC referenced character start outfit are present
            List<uint> notFoundOutfit = new List<uint>();
            foreach (var entry in CliDB.CharStartOutfitStorage.Values)
            {
                for (int j = 0; j < ItemConst.MaxOutfitItems; ++j)
                {
                    if (entry.ItemID[j] <= 0)
                        continue;

                    uint item_id = (uint)entry.ItemID[j];

                    if (GetItemTemplate(item_id) == null)
                        notFoundOutfit.Add(item_id);
                }
            }

            foreach (var id in notFoundOutfit)
                Log.outError(LogFilter.Sql, "Item (Entry: {0}) does not exist in `item_template` but is referenced in `CharStartOutfit.dbc`", id);

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} item templates in {1} ms", sparseCount, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        static float[] qualityMultipliers = new float[]
        {
            0.92f, 0.92f, 0.92f, 1.11f, 1.32f, 1.61f, 0.0f, 0.0f
        };

        static float[] armorMultipliers = new float[]
        {
            0.00f, // INVTYPE_NON_EQUIP
            0.60f, // INVTYPE_HEAD
            0.00f, // INVTYPE_NECK
            0.60f, // INVTYPE_SHOULDERS
            0.00f, // INVTYPE_BODY
            1.00f, // INVTYPE_CHEST
            0.33f, // INVTYPE_WAIST
            0.72f, // INVTYPE_LEGS
            0.48f, // INVTYPE_FEET
            0.33f, // INVTYPE_WRISTS
            0.33f, // INVTYPE_HANDS
            0.00f, // INVTYPE_FINGER
            0.00f, // INVTYPE_TRINKET
            0.00f, // INVTYPE_WEAPON
            0.72f, // INVTYPE_SHIELD
            0.00f, // INVTYPE_RANGED
            0.00f, // INVTYPE_CLOAK
            0.00f, // INVTYPE_2HWEAPON
            0.00f, // INVTYPE_BAG
            0.00f, // INVTYPE_TABARD
            1.00f, // INVTYPE_ROBE
            0.00f, // INVTYPE_WEAPONMAINHAND
            0.00f, // INVTYPE_WEAPONOFFHAND
            0.00f, // INVTYPE_HOLDABLE
            0.00f, // INVTYPE_AMMO
            0.00f, // INVTYPE_THROWN
            0.00f, // INVTYPE_RANGEDRIGHT
            0.00f, // INVTYPE_QUIVER
            0.00f, // INVTYPE_RELIC
        };

        static float[] weaponMultipliers = new float[]
        {
            0.91f, // ITEM_SUBCLASS_WEAPON_AXE
            1.00f, // ITEM_SUBCLASS_WEAPON_AXE2
            1.00f, // ITEM_SUBCLASS_WEAPON_BOW
            1.00f, // ITEM_SUBCLASS_WEAPON_GUN
            0.91f, // ITEM_SUBCLASS_WEAPON_MACE
            1.00f, // ITEM_SUBCLASS_WEAPON_MACE2
            1.00f, // ITEM_SUBCLASS_WEAPON_POLEARM
            0.91f, // ITEM_SUBCLASS_WEAPON_SWORD
            1.00f, // ITEM_SUBCLASS_WEAPON_SWORD2
            1.00f, // ITEM_SUBCLASS_WEAPON_WARGLAIVES
            1.00f, // ITEM_SUBCLASS_WEAPON_STAFF
            0.00f, // ITEM_SUBCLASS_WEAPON_EXOTIC
            0.00f, // ITEM_SUBCLASS_WEAPON_EXOTIC2
            0.66f, // ITEM_SUBCLASS_WEAPON_FIST_WEAPON
            0.00f, // ITEM_SUBCLASS_WEAPON_MISCELLANEOUS
            0.66f, // ITEM_SUBCLASS_WEAPON_DAGGER
            0.00f, // ITEM_SUBCLASS_WEAPON_THROWN
            0.00f, // ITEM_SUBCLASS_WEAPON_SPEAR
            1.00f, // ITEM_SUBCLASS_WEAPON_CROSSBOW
            0.66f, // ITEM_SUBCLASS_WEAPON_WAND
            0.66f, // ITEM_SUBCLASS_WEAPON_FISHING_POLE
        };

        uint FillMaxDurability(ItemClass itemClass, uint itemSubClass, InventoryType inventoryType, ItemQuality quality, uint itemLevel)
        {
            if (itemClass != ItemClass.Armor && itemClass != ItemClass.Weapon)
                return 0;

            float levelPenalty = 1.0f;
            if (itemLevel <= 28)
                levelPenalty = 0.966f - (28u - itemLevel) / 54.0f;

            if (itemClass == ItemClass.Armor)
            {
                if (inventoryType > InventoryType.Robe)
                    return 0;

                return 5 * (uint)(Math.Round(25.0f * qualityMultipliers[(int)quality] * armorMultipliers[(int)inventoryType] * levelPenalty));
            }

            return 5 * (uint)(Math.Round(18.0f * qualityMultipliers[(int)quality] * weaponMultipliers[itemSubClass] * levelPenalty));
        }
        void FillDisenchantFields(out uint disenchantID, out uint requiredDisenchantSkill, ItemTemplate itemTemplate)
        {
            disenchantID = 0;
            requiredDisenchantSkill = 0xFFFFFFFF;
            if (itemTemplate.GetFlags().HasAnyFlag(ItemFlags.Conjured | ItemFlags.NoDisenchant) ||
                itemTemplate.GetBonding() == ItemBondingType.Quest || itemTemplate.GetArea() != 0 || itemTemplate.GetMap() != 0 ||
                itemTemplate.GetMaxStackSize() > 1 ||
                itemTemplate.GetQuality() < ItemQuality.Uncommon || itemTemplate.GetQuality() > ItemQuality.Epic ||
                !(itemTemplate.GetClass() == ItemClass.Armor || itemTemplate.GetClass() == ItemClass.Weapon) ||
                !(Item.GetSpecialPrice(itemTemplate) != 0 || Global.DB2Mgr.HasItemCurrencyCost(itemTemplate.GetId())))
                return;

            foreach (var disenchant in CliDB.ItemDisenchantLootStorage.Values)
            {
                if (disenchant.ItemClass == (uint)itemTemplate.GetClass() && disenchant.ItemQuality == (uint)itemTemplate.GetQuality() &&
                    disenchant.MinItemLevel <= itemTemplate.GetBaseItemLevel() && disenchant.MaxItemLevel >= itemTemplate.GetBaseItemLevel())
                {
                    if (disenchant.Id == 60 || disenchant.Id == 61)   // epic item disenchant ilvl range 66-99 (classic)
                    {
                        if (itemTemplate.GetBaseRequiredLevel() > 60 || itemTemplate.GetRequiredSkillRank() > 300)
                            continue;                                   // skip to epic item disenchant ilvl range 90-199 (TBC)
                    }
                    else if (disenchant.Id == 66 || disenchant.Id == 67)  // epic item disenchant ilvl range 90-199 (TBC)
                    {
                        if (itemTemplate.GetBaseRequiredLevel() <= 60 || (itemTemplate.GetRequiredSkill() != 0 && itemTemplate.GetRequiredSkillRank() <= 300))
                            continue;
                    }

                    disenchantID = disenchant.Id;
                    requiredDisenchantSkill = disenchant.RequiredDisenchantSkill;
                    return;
                }
            }
        }
        public void LoadItemTemplateAddon()
        {
            var time = Time.GetMSTime();

            uint count = 0;
            SQLResult result = DB.World.Query("SELECT Id, FlagsCu, FoodType, MinMoneyLoot, MaxMoneyLoot, SpellPPMChance FROM item_template_addon");
            if (!result.IsEmpty())
            {
                do
                {
                    uint itemId = result.Read<uint>(0);
                    ItemTemplate itemTemplate = GetItemTemplate(itemId);
                    if (itemTemplate == null)
                    {
                        Log.outError(LogFilter.Sql, "Item {0} specified in `itemtemplateaddon` does not exist, skipped.", itemId);
                        continue;
                    }

                    uint minMoneyLoot = result.Read<uint>(3);
                    uint maxMoneyLoot = result.Read<uint>(4);
                    if (minMoneyLoot > maxMoneyLoot)
                    {
                        Log.outError(LogFilter.Sql, "Minimum money loot specified in `itemtemplateaddon` for item {0} was greater than maximum amount, swapping.", itemId);
                        uint temp = minMoneyLoot;
                        minMoneyLoot = maxMoneyLoot;
                        maxMoneyLoot = temp;
                    }
                    itemTemplate.FlagsCu = (ItemFlagsCustom)result.Read<uint>(1);
                    itemTemplate.FoodType = result.Read<uint>(2);
                    itemTemplate.MinMoneyLoot = minMoneyLoot;
                    itemTemplate.MaxMoneyLoot = maxMoneyLoot;
                    itemTemplate.SpellPPMRate = result.Read<float>(5);
                    ++count;
                } while (result.NextRow());
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} item addon templates in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadItemScriptNames()
        {
            uint oldMSTime = Time.GetMSTime();
            uint count = 0;

            SQLResult result = DB.World.Query("SELECT Id, ScriptName FROM item_script_names");
            if (!result.IsEmpty())
            {
                do
                {
                    uint itemId = result.Read<uint>(0);
                    if (GetItemTemplate(itemId) == null)
                    {
                        Log.outError(LogFilter.Sql, "Item {0} specified in `item_script_names` does not exist, skipped.", itemId);
                        continue;
                    }

                    ItemTemplateStorage[itemId].ScriptId = GetScriptId(result.Read<string>(1));
                    ++count;
                } while (result.NextRow());
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} item script names in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public ItemTemplate GetItemTemplate(uint ItemId)
        {
            return ItemTemplateStorage.LookupByKey(ItemId);
        }
        public Dictionary<uint, ItemTemplate> GetItemTemplates()
        {
            return ItemTemplateStorage;
        }
        public void AddVendorItem(uint entry, uint item, int maxcount, uint incrtime, uint extendedCost, ItemVendorType type, bool persist = true)
        {
            VendorItemData vList = cacheVendorItemStorage[entry];
            vList.AddItem(item, maxcount, incrtime, extendedCost, type);

            if (persist)
            {
                PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.INS_NPC_VENDOR);

                stmt.AddValue(0, entry);
                stmt.AddValue(1, item);
                stmt.AddValue(2, maxcount);
                stmt.AddValue(3, incrtime);
                stmt.AddValue(4, extendedCost);
                stmt.AddValue(5, (byte)type);

                DB.World.Execute(stmt);
            }
        }
        public bool RemoveVendorItem(uint entry, uint item, ItemVendorType type, bool persist = true)
        {
            var iter = cacheVendorItemStorage.LookupByKey(entry);
            if (iter == null)
                return false;

            if (!iter.RemoveItem(item, type))
                return false;

            if (persist)
            {
                PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.DEL_NPC_VENDOR);

                stmt.AddValue(0, entry);
                stmt.AddValue(1, item);
                stmt.AddValue(2, (byte)type);

                DB.World.Execute(stmt);
            }

            return true;
        }
        public bool IsVendorItemValid(uint vendorentry, uint id, int maxcount, uint incrtime, uint ExtendedCost, ItemVendorType type, Player player = null, List<uint> skipvendors = null, ulong ORnpcflag = 0)
        {
            CreatureTemplate cInfo = GetCreatureTemplate(vendorentry);
            if (cInfo == null)
            {
                if (player != null)
                    player.SendSysMessage(CypherStrings.CommandVendorselection);
                else
                    Log.outError(LogFilter.Sql, "Table `(gameevent)npcvendor` have data for not existed creature template (Entry: {0}), ignore", vendorentry);
                return false;
            }

            if (!Convert.ToBoolean(((ulong)cInfo.Npcflag | ORnpcflag) & (ulong)NPCFlags.Vendor))
            {
                if (skipvendors == null || skipvendors.Count() == 0)
                {
                    if (player != null)
                        player.SendSysMessage(CypherStrings.CommandVendorselection);
                    else
                        Log.outError(LogFilter.Sql, "Table `(gameevent)npcvendor` have data for not creature template (Entry: {0}) without vendor flag, ignore", vendorentry);

                    if (skipvendors != null)
                        skipvendors.Add(vendorentry);
                }
                return false;
            }

            if ((type == ItemVendorType.Item && GetItemTemplate(id) == null) ||
                (type == ItemVendorType.Currency && CliDB.CurrencyTypesStorage.LookupByKey(id) == null))
            {
                if (player != null)
                    player.SendSysMessage(CypherStrings.ItemNotFound, id, type);
                else
                    Log.outError(LogFilter.Sql, "Table `(gameevent)npcvendor` for Vendor (Entry: {0}) have in item list non-existed item ({1}, type {2}), ignore", vendorentry, id, type);
                return false;
            }

            if (ExtendedCost != 0 && !CliDB.ItemExtendedCostStorage.ContainsKey(ExtendedCost))
            {
                if (player != null)
                    player.SendSysMessage(CypherStrings.ExtendedCostNotExist, ExtendedCost);
                else
                    Log.outError(LogFilter.Sql, "Table `(gameevent)npcvendor` have Item (Entry: {0}) with wrong ExtendedCost ({1}) for vendor ({2}), ignore", id, ExtendedCost, vendorentry);
                return false;
            }

            if (type == ItemVendorType.Item) // not applicable to currencies
            {
                if (maxcount > 0 && incrtime == 0)
                {
                    if (player != null)
                        player.SendSysMessage("MaxCount != 0 ({0}) but IncrTime == 0", maxcount);
                    else
                        Log.outError(LogFilter.Sql, "Table `(gameevent)npcvendor` has `maxcount` ({0}) for item {1} of vendor (Entry: {2}) but `incrtime`=0, ignore", maxcount, id, vendorentry);
                    return false;
                }
                else if (maxcount == 0 && incrtime > 0)
                {
                    if (player != null)
                        player.SendSysMessage("MaxCount == 0 but IncrTime<>= 0");
                    else
                        Log.outError(LogFilter.Sql, "Table `(gameevent)npcvendor` has `maxcount`=0 for item {0} of vendor (Entry: {0}) but `incrtime`<>0, ignore", id, vendorentry);
                    return false;
                }
            }

            VendorItemData vItems = GetNpcVendorItemList(vendorentry);
            if (vItems == null)
                return true;                                        // later checks for non-empty lists

            if (vItems.FindItemCostPair(id, ExtendedCost, type) != null)
            {
                if (player != null)
                    player.SendSysMessage(CypherStrings.ItemAlreadyInList, id, ExtendedCost, type);
                else
                    Log.outError(LogFilter.Sql, "Table `npcvendor` has duplicate items {0} (with extended cost {1}, type {2}) for vendor (Entry: {3}), ignoring", id, ExtendedCost, type, vendorentry);
                return false;
            }

            return true;
        }
        public VendorItemData GetNpcVendorItemList(uint entry)
        {
            return cacheVendorItemStorage.LookupByKey(entry);
        }
        public EquipmentInfo GetEquipmentInfo(uint entry, int id)
        {
            var equip = equipmentInfoStorage.LookupByKey(entry);
            if (equip.Empty())
                return null;

            if (id == -1)
                return equip[RandomHelper.IRand(0, equip.Count - 1)].Item2;
            else
                return equip.Find(p => p.Item1 == id).Item2;
        }

        //Maps
        public void LoadInstanceTemplate()
        {
            var time = Time.GetMSTime();
            //                                          0     1       2        4
            SQLResult result = DB.World.Query("SELECT map, parent, script, allowMount FROM instance_template");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 instance templates. DB table `instance_template` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                uint mapID = result.Read<uint>(0);

                if (!Global.MapMgr.IsValidMAP(mapID, true))
                {
                    Log.outError(LogFilter.Sql, "ObjectMgr.LoadInstanceTemplate: bad mapid {0} for template!", mapID);
                    continue;
                }

                var instanceTemplate = new InstanceTemplate();
                instanceTemplate.AllowMount = result.Read<bool>(3);
                instanceTemplate.Parent = result.Read<uint>(1);
                instanceTemplate.ScriptId = Global.ObjectMgr.GetScriptId(result.Read<string>(2));

                instanceTemplateStorage.Add(mapID, instanceTemplate);

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} instance templates in {1} ms", count, Time.GetMSTimeDiffToNow(time));
        }
        public void LoadGameTele()
        {
            uint oldMSTime = Time.GetMSTime();

            gameTeleStorage.Clear();

            //                                          0       1           2           3           4        5     6
            SQLResult result = DB.World.Query("SELECT id, position_x, position_y, position_z, orientation, map, name FROM game_tele");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 GameTeleports. DB table `game_tele` is empty!");
                return;
            }

            uint count = 0;

            do
            {
                uint id = result.Read<uint>(0);

                GameTele gt = new GameTele();

                gt.posX = result.Read<float>(1);
                gt.posY = result.Read<float>(2);
                gt.posZ = result.Read<float>(3);
                gt.orientation = result.Read<float>(4);
                gt.mapId = result.Read<uint>(5);
                gt.name = result.Read<string>(6);

                if (!GridDefines.IsValidMapCoord(gt.mapId, gt.posX, gt.posY, gt.posZ, gt.orientation))
                {
                    Log.outError(LogFilter.Sql, "Wrong position for id {0} (name: {1}) in `game_tele` table, ignoring.", id, gt.name);
                    continue;
                }
                gameTeleStorage.Add(id, gt);
                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} GameTeleports in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadAreaTriggerTeleports()
        {
            uint oldMSTime = Time.GetMSTime();

            _areaTriggerStorage.Clear();                                  // need for reload case

            //                                         0   1
            SQLResult result = DB.World.Query("SELECT ID, PortLocID FROM areatrigger_teleport");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 area trigger teleport definitions. DB table `areatrigger_teleport` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                ++count;

                uint Trigger_ID = result.Read<uint>(0);
                uint PortLocID = result.Read<uint>(1);

                WorldSafeLocsRecord portLoc = CliDB.WorldSafeLocsStorage.LookupByKey(PortLocID);
                if (portLoc == null)
                {
                    Log.outError(LogFilter.Sql, "Area Trigger (ID: {0}) has a non-existing Port Loc (ID: {1}) in WorldSafeLocs.dbc, skipped", Trigger_ID, PortLocID);
                    continue;
                }

                AreaTriggerStruct at = new AreaTriggerStruct();
                at.target_mapId = portLoc.MapID;
                at.target_X = portLoc.Loc.X;
                at.target_Y = portLoc.Loc.Y;
                at.target_Z = portLoc.Loc.Z;
                at.target_Orientation = (portLoc.Facing * MathFunctions.PI) / 180; // Orientation is initially in degrees
                at.PortLocId = portLoc.Id;

                AreaTriggerRecord atEntry = CliDB.AreaTriggerStorage.LookupByKey(Trigger_ID);
                if (atEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Area trigger (ID: {0}) does not exist in `AreaTrigger.dbc`.", Trigger_ID);
                    continue;
                }

                _areaTriggerStorage[Trigger_ID] = at;

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} area trigger teleport definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadAccessRequirements()
        {
            uint oldMSTime = Time.GetMSTime();

            _accessRequirementStorage.Clear();

            //                                          0      1           2          3          4           5      6             7             8                      9     
            SQLResult result = DB.World.Query("SELECT mapid, difficulty, level_min, level_max, item, item2, quest_done_A, quest_done_H, completed_achievement, quest_failed_text FROM access_requirement");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 access requirement definitions. DB table `access_requirement` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint mapid = result.Read<uint>(0);
                if (!CliDB.MapStorage.ContainsKey(mapid))
                {
                    Log.outError(LogFilter.Sql, "Map {0} referenced in `access_requirement` does not exist, skipped.", mapid);
                    continue;
                }

                uint difficulty = result.Read<uint>(1);
                if (Global.DB2Mgr.GetMapDifficultyData(mapid, (Difficulty)difficulty) == null)
                {
                    Log.outError(LogFilter.Sql, "Map {0} referenced in `access_requirement` does not have difficulty {1}, skipped", mapid, difficulty);
                    continue;
                }

                ulong requirementId = MathFunctions.MakePair64(mapid, difficulty);

                AccessRequirement ar = new AccessRequirement();
                ar.levelMin = result.Read<byte>(2);
                ar.levelMax = result.Read<byte>(3);
                ar.item = result.Read<uint>(4);
                ar.item2 = result.Read<uint>(5);
                ar.quest_A = result.Read<uint>(6);
                ar.quest_H = result.Read<uint>(7);
                ar.achievement = result.Read<uint>(8);
                ar.questFailedText = result.Read<string>(9);

                if (ar.item != 0)
                {
                    ItemTemplate pProto = GetItemTemplate(ar.item);
                    if (pProto == null)
                    {
                        Log.outError(LogFilter.Sql, "Key item {0} does not exist for map {1} difficulty {2}, removing key requirement.", ar.item, mapid, difficulty);
                        ar.item = 0;
                    }
                }

                if (ar.item2 != 0)
                {
                    ItemTemplate pProto = GetItemTemplate(ar.item2);
                    if (pProto == null)
                    {
                        Log.outError(LogFilter.Sql, "Second item {0} does not exist for map {1} difficulty {2}, removing key requirement.", ar.item2, mapid, difficulty);
                        ar.item2 = 0;
                    }
                }

                if (ar.quest_A != 0)
                {
                    if (GetQuestTemplate(ar.quest_A) == null)
                    {
                        Log.outError(LogFilter.Sql, "Required Alliance Quest {0} not exist for map {1} difficulty {2}, remove quest done requirement.", ar.quest_A, mapid, difficulty);
                        ar.quest_A = 0;
                    }
                }

                if (ar.quest_H != 0)
                {
                    if (GetQuestTemplate(ar.quest_H) == null)
                    {
                        Log.outError(LogFilter.Sql, "Required Horde Quest {0} not exist for map {1} difficulty {2}, remove quest done requirement.", ar.quest_H, mapid, difficulty);
                        ar.quest_H = 0;
                    }
                }

                if (ar.achievement != 0)
                {
                    if (!CliDB.AchievementStorage.ContainsKey(ar.achievement))
                    {
                        Log.outError(LogFilter.Sql, "Required Achievement {0} not exist for map {1} difficulty {2}, remove quest done requirement.", ar.achievement, mapid, difficulty);
                        ar.achievement = 0;
                    }
                }

                _accessRequirementStorage[requirementId] = ar;
                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} access requirement definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadInstanceEncounters()
        {
            uint oldMSTime = Time.GetMSTime();

            //                                           0         1            2                3
            SQLResult result = DB.World.Query("SELECT entry, creditType, creditEntry, lastEncounterDungeon FROM instance_encounters");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 instance encounters, table is empty!");
                return;
            }

            uint count = 0;
            Dictionary<uint, Tuple<uint, DungeonEncounterRecord>> dungeonLastBosses = new Dictionary<uint, Tuple<uint, DungeonEncounterRecord>>();
            do
            {
                uint entry = result.Read<uint>(0);
                EncounterCreditType creditType = (EncounterCreditType)result.Read<byte>(1);
                uint creditEntry = result.Read<uint>(2);
                uint lastEncounterDungeon = result.Read<uint>(3);
                DungeonEncounterRecord dungeonEncounter = CliDB.DungeonEncounterStorage.LookupByKey(entry);
                if (dungeonEncounter == null)
                {
                    Log.outError(LogFilter.Sql, "Table `instance_encounters` has an invalid encounter id {0}, skipped!", entry);
                    continue;
                }

                if (lastEncounterDungeon != 0 && Global.LFGMgr.GetLFGDungeonEntry(lastEncounterDungeon) == 0)
                {
                    Log.outError(LogFilter.Sql, "Table `instance_encounters` has an encounter {0} ({1}) marked as final for invalid dungeon id {2}, skipped!",
                        entry, dungeonEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()], lastEncounterDungeon);
                    continue;
                }

                var pair = dungeonLastBosses.LookupByKey(lastEncounterDungeon);
                if (lastEncounterDungeon != 0)
                {
                    if (pair != null)
                    {
                        Log.outError(LogFilter.Sql, "Table `instance_encounters` specified encounter {0} ({1}) as last encounter but {2} ({3}) is already marked as one, skipped!",
                            entry, dungeonEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()], pair.Item1, pair.Item2.Name[Global.WorldMgr.GetDefaultDbcLocale()]);
                        continue;
                    }

                    dungeonLastBosses[lastEncounterDungeon] = Tuple.Create(entry, dungeonEncounter);
                }

                switch (creditType)
                {
                    case EncounterCreditType.KillCreature:
                        {
                            CreatureTemplate creatureInfo = GetCreatureTemplate(creditEntry);
                            if (creatureInfo == null)
                            {
                                Log.outError(LogFilter.Sql, "Table `instance_encounters` has an invalid creature (entry {0}) linked to the encounter {1} ({2}), skipped!",
                                    creditEntry, entry, dungeonEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()]);
                                continue;
                            }
                            creatureInfo.FlagsExtra |= CreatureFlagsExtra.DungeonBoss;
                            break;
                        }
                    case EncounterCreditType.CastSpell:
                        if (!Global.SpellMgr.HasSpellInfo(creditEntry))
                        {
                            Log.outError(LogFilter.Sql, "Table `instance_encounters` has an invalid spell (entry {0}) linked to the encounter {1} ({2}), skipped!",
                                creditEntry, entry, dungeonEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()]);
                            continue;
                        }
                        break;
                    default:
                        Log.outError(LogFilter.Sql, "Table `instance_encounters` has an invalid credit type ({0}) for encounter {1} ({2}), skipped!",
                            creditType, entry, dungeonEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()]);
                        continue;
                }

                if (dungeonEncounter.DifficultyID == 0)
                {
                    for (uint i = 0; i < (int)Difficulty.Max; ++i)
                    {
                        if (Global.DB2Mgr.GetMapDifficultyData(dungeonEncounter.MapID, (Difficulty)i) != null)
                            _dungeonEncounterStorage.Add(MathFunctions.MakePair64(dungeonEncounter.MapID, i), new DungeonEncounter(dungeonEncounter, creditType, creditEntry, lastEncounterDungeon));
                    }
                }
                else
                    _dungeonEncounterStorage.Add(MathFunctions.MakePair64(dungeonEncounter.MapID, (uint)dungeonEncounter.DifficultyID), new DungeonEncounter(dungeonEncounter, creditType, creditEntry, lastEncounterDungeon));

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} instance encounters in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public InstanceTemplate GetInstanceTemplate(uint mapID)
        {
            return instanceTemplateStorage.LookupByKey(mapID);
        }
        public GameTele GetGameTele(uint id)
        {
            return gameTeleStorage.LookupByKey(id);
        }
        public GameTele GetGameTele(string name)
        {
            return gameTeleStorage.Values.FirstOrDefault(p => p.name.ToLower() == name.ToLower());
        }
        public List<DungeonEncounter> GetDungeonEncounterList(uint mapId, Difficulty difficulty)
        {
            return _dungeonEncounterStorage.LookupByKey(MathFunctions.MakePair64(mapId, (uint)difficulty));
        }
        public bool IsTransportMap(uint mapId) { return _transportMaps.Contains((ushort)mapId); }

        //Player
        public void LoadPlayerInfo()
        {
            for (uint race = 0; race < (int)Race.Max; ++race)
                _playerInfo[race] = new PlayerInfo[(int)Class.Max];

            var time = Time.GetMSTime();
            // Load playercreate
            {
                //                                         0     1      2    3     4           5           6           7
                SQLResult result = DB.World.Query("SELECT race, class, map, zone, position_x, position_y, position_z, orientation FROM playercreateinfo");

                if (result.IsEmpty())
                {
                    Log.outError(LogFilter.ServerLoading, "Loaded 0 player create definitions. DB table `playercreateinfo` is empty.");
                    return;
                }

                uint count = 0;
                do
                {
                    uint currentrace = result.Read<uint>(0);
                    uint currentclass = result.Read<uint>(1);
                    uint mapId = result.Read<uint>(2);
                    uint zoneId = result.Read<uint>(3);
                    float positionX = result.Read<float>(4);
                    float positionY = result.Read<float>(5);
                    float positionZ = result.Read<float>(6);
                    float orientation = result.Read<float>(7);

                    if (currentrace >= (int)Race.Max)
                    {
                        Log.outError(LogFilter.Sql, "Wrong race {0} in `playercreateinfo` table, ignoring.", currentrace);
                        continue;
                    }

                    var rEntry = CliDB.ChrRacesStorage.LookupByKey(currentrace);
                    if (rEntry == null)
                    {
                        Log.outError(LogFilter.Sql, "Wrong race {0} in `playercreateinfo` table, ignoring.", currentrace);
                        continue;
                    }

                    if (currentclass >= (int)Class.Max)
                    {
                        Log.outError(LogFilter.Sql, "Wrong class {0} in `playercreateinfo` table, ignoring.", currentclass);
                        continue;
                    }

                    if (CliDB.ChrClassesStorage.LookupByKey(currentclass) == null)
                    {
                        Log.outError(LogFilter.Sql, "Wrong class {0} in `playercreateinfo` table, ignoring.", currentclass);
                        continue;
                    }

                    PlayerInfo pInfo = new PlayerInfo();
                    pInfo.MapId = mapId;
                    pInfo.ZoneId = zoneId;
                    pInfo.PositionX = positionX;
                    pInfo.PositionY = positionY;
                    pInfo.PositionZ = positionZ;
                    pInfo.Orientation = orientation;

                    pInfo.DisplayId_m = rEntry.MaleDisplayID;
                    pInfo.DisplayId_f = rEntry.FemaleDisplayID;

                    _playerInfo[currentrace][currentclass] = pInfo;

                    ++count;
                } while (result.NextRow());

                Log.outInfo(LogFilter.ServerLoading, "Loaded {0} player create definitions in {1} ms", count, Time.GetMSTimeDiffToNow(time));
            }
            time = Time.GetMSTime();
            // Load playercreate items
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create Items Data...");
            {
                //                                         0     1      2       3
                SQLResult result = DB.World.Query("SELECT race, class, itemid, amount FROM playercreateinfo_item");

                if (result.IsEmpty())
                    Log.outError(LogFilter.ServerLoading, "Loaded 0 custom player create items. DB table `playercreateinfo_item` is empty.");
                else
                {
                    uint count = 0;
                    do
                    {
                        uint currentrace = result.Read<uint>(0);
                        if (currentrace >= (int)Race.Max)
                        {
                            Log.outError(LogFilter.Sql, "Wrong race {0} in `playercreateinfo_item` table, ignoring.", currentrace);
                            continue;
                        }

                        uint currentclass = result.Read<uint>(1);
                        if (currentclass >= (int)Class.Max)
                        {
                            Log.outError(LogFilter.Sql, "Wrong class {0} in `playercreateinfo_item` table, ignoring.", currentclass);
                            continue;
                        }

                        uint itemid = result.Read<uint>(2);
                        if (GetItemTemplate(itemid).GetId() == 0)
                        {
                            Log.outError(LogFilter.Sql, "Item id {0} (race {1} class {2}) in `playercreateinfo_item` table but not listed in `itemtemplate`, ignoring.", itemid, currentrace, currentclass);
                            continue;
                        }

                        int amount = result.Read<int>(3);

                        if (amount == 0)
                        {
                            Log.outError(LogFilter.Sql, "Item id {0} (class {1} race {2}) have amount == 0 in `playercreateinfo_item` table, ignoring.", itemid, currentrace, currentclass);
                            continue;
                        }

                        if (currentrace == 0 || currentclass == 0)
                        {
                            uint minrace = currentrace != 0 ? currentrace : 1;
                            uint maxrace = currentrace != 0 ? currentrace + 1 : (int)Race.Max;
                            uint minclass = currentclass != 0 ? currentclass : 1;
                            uint maxclass = currentclass != 0 ? currentclass + 1 : (int)Class.Max;
                            for (var r = minrace; r < maxrace; ++r)
                                for (var c = minclass; c < maxclass; ++c)
                                    PlayerCreateInfoAddItemHelper(r, c, itemid, amount);

                        }
                        else
                            PlayerCreateInfoAddItemHelper(currentrace, currentclass, itemid, amount);

                        ++count;
                    } while (result.NextRow());

                    Log.outInfo(LogFilter.ServerLoading, "Loaded {0} custom player create items in {1} ms", count, Time.GetMSTimeDiffToNow(time));
                }
            }

            // Load playercreate skills
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create Skill Data...");
            {
                uint oldMSTime = Time.GetMSTime();

                foreach (SkillRaceClassInfoRecord rcInfo in CliDB.SkillRaceClassInfoStorage.Values)
                {
                    if (rcInfo.Availability == 1)
                    {
                        for (int raceIndex = (int)Race.Human; raceIndex < (int)Race.Max; ++raceIndex)
                        {
                            if (rcInfo.RaceMask == -1 || Convert.ToBoolean((1 << (raceIndex - 1)) & rcInfo.RaceMask))
                            {
                                for (int classIndex = (int)Class.Warrior; classIndex < (int)Class.Max; ++classIndex)
                                {
                                    if (rcInfo.ClassMask == -1 || Convert.ToBoolean((1 << (classIndex - 1)) & rcInfo.ClassMask))
                                    {
                                        PlayerInfo info = _playerInfo[raceIndex][classIndex];
                                        if (info != null)
                                            info.skills.Add(rcInfo);
                                    }
                                }
                            }
                        }
                    }
                }
                Log.outInfo(LogFilter.ServerLoading, "Loaded player create skills in {0} ms", Time.GetMSTimeDiffToNow(oldMSTime));
            }

            // Load playercreate custom spells
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create Custom Spell Data...");
            {
                uint oldMSTime = Time.GetMSTime();

                SQLResult result = DB.World.Query("SELECT racemask, classmask, Spell FROM playercreateinfo_spell_custom");
                if (result.IsEmpty())
                {
                    Log.outError(LogFilter.ServerLoading, "Loaded 0 player create custom spells. DB table `playercreateinfo_spell_custom` is empty.");
                }
                else
                {
                    uint count = 0;
                    do
                    {
                        uint raceMask = result.Read<uint>(0);
                        uint classMask = result.Read<uint>(1);
                        uint spellId = result.Read<uint>(2);

                        if (raceMask != 0 && !Convert.ToBoolean(raceMask & (int)Race.RaceMaskAllPlayable))
                        {
                            Log.outError(LogFilter.Sql, "Wrong race mask {0} in `playercreateinfo_spell_custom` table, ignoring.", raceMask);
                            continue;
                        }

                        if (classMask != 0 && !Convert.ToBoolean(classMask & (int)Class.ClassMaskAllPlayable))
                        {
                            Log.outError(LogFilter.Sql, "Wrong class mask {0} in `playercreateinfo_spell_custom` table, ignoring.", classMask);
                            continue;
                        }

                        for (int raceIndex = (int)Race.Human; raceIndex < (int)Race.Max; ++raceIndex)
                        {
                            if (raceMask == 0 || Convert.ToBoolean((1 << (raceIndex - 1)) & raceMask))
                            {
                                for (int classIndex = (int)Class.Warrior; classIndex < (int)Class.Max; ++classIndex)
                                {
                                    if (classMask == 0 || Convert.ToBoolean((1 << (classIndex - 1)) & classMask))
                                    {
                                        PlayerInfo info = _playerInfo[raceIndex][classIndex];
                                        if (info != null)
                                        {
                                            info.customSpells.Add(spellId);
                                            ++count;
                                        }
                                        // We need something better here, the check is not accounting for spells used by multiple races/classes but not all of them.
                                        // Either split the masks per class, or per race, which kind of kills the point yet.
                                    }
                                }
                            }
                        }
                    }
                    while (result.NextRow());

                    Log.outInfo(LogFilter.ServerLoading, "Loaded {0} custom player create spells in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
                }
            }

            // Load playercreate cast spell
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create Cast Spell Data...");
            {
                uint oldMSTime = Time.GetMSTime();

                SQLResult result = DB.World.Query("SELECT raceMask, classMask, spell FROM playercreateinfo_cast_spell");

                if (result.IsEmpty())
                    Log.outError(LogFilter.ServerLoading, "Loaded 0 player create cast spells. DB table `playercreateinfo_cast_spell` is empty.");
                else
                {
                    uint count = 0;

                    do
                    {
                        uint raceMask = result.Read<uint>(0);
                        uint classMask = result.Read<uint>(1);
                        uint spellId = result.Read<uint>(2);

                        if (raceMask != 0 && !raceMask.HasAnyFlag<uint>((uint)Race.RaceMaskAllPlayable))
                        {
                            Log.outError(LogFilter.Sql, "Wrong race mask {0} in `playercreateinfo_cast_spell` table, ignoring.", raceMask);
                            continue;
                        }

                        if (classMask != 0 && !classMask.HasAnyFlag<uint>((uint)Class.ClassMaskAllPlayable))
                        {
                            Log.outError(LogFilter.Sql, "Wrong class mask {0} in `playercreateinfo_cast_spell` table, ignoring.", classMask);
                            continue;
                        }

                        for (int raceIndex = (int)Race.Human; raceIndex < (int)Race.Max; ++raceIndex)
                        {
                            if (raceMask == 0 || Convert.ToBoolean((1 << (raceIndex - 1)) & raceMask))
                            {
                                for (int classIndex = (int)Class.Warrior; classIndex < (int)Class.Max; ++classIndex)
                                {
                                    if (classMask == 0 || Convert.ToBoolean((1 << (classIndex - 1)) & classMask))
                                    {
                                        PlayerInfo info = _playerInfo[raceIndex][classIndex];
                                        if (info != null)
                                        {
                                            info.castSpells.Add(spellId);
                                            ++count;
                                        }
                                    }
                                }
                            }
                        }
                    } while (result.NextRow());

                    Log.outInfo(LogFilter.ServerLoading, "Loaded {0} player create cast spells in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
                }
            }

            // Load playercreate actions
            time = Time.GetMSTime();
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create Action Data...");
            {
                //                                         0     1      2       3       4
                SQLResult result = DB.World.Query("SELECT race, class, button, action, type FROM playercreateinfo_action");

                if (result.IsEmpty())
                    Log.outError(LogFilter.ServerLoading, "Loaded 0 player create actions. DB table `playercreateinfo_action` is empty.");
                else
                {
                    uint count = 0;
                    do
                    {
                        uint currentrace = result.Read<uint>(0);
                        if (currentrace >= (int)Race.Max)
                        {
                            Log.outError(LogFilter.Sql, "Wrong race {0} in `playercreateinfo_action` table, ignoring.", currentrace);
                            continue;
                        }

                        uint currentclass = result.Read<uint>(1);
                        if (currentclass >= (int)Class.Max)
                        {
                            Log.outError(LogFilter.Sql, "Wrong class {0} in `playercreateinfo_action` table, ignoring.", currentclass);
                            continue;
                        }
                        PlayerInfo info = _playerInfo[currentrace][currentclass];
                        if (info != null)
                            info.action.Add(new PlayerCreateInfoAction(result.Read<byte>(2), result.Read<uint>(3), result.Read<byte>(4)));

                        ++count;
                    } while (result.NextRow());

                    Log.outInfo(LogFilter.ServerLoading, "Loaded {0} player create actions in {1} ms", count, Time.GetMSTimeDiffToNow(time));
                }
            }
            time = Time.GetMSTime();
            // Loading levels data (class/race dependent)
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create Level Stats Data...");
            {
                //                                         0     1      2      3    4    5    6  
                SQLResult result = DB.World.Query("SELECT race, class, level, str, agi, sta, inte FROM player_levelstats");

                if (result.IsEmpty())
                {
                    Log.outError(LogFilter.ServerLoading, "Loaded 0 level stats definitions. DB table `player_levelstats` is empty.");
                    Global.WorldMgr.StopNow();
                    return;
                }

                uint count = 0;
                do
                {
                    uint currentrace = result.Read<uint>(0);
                    if (currentrace >= (int)Race.Max)
                    {
                        Log.outError(LogFilter.Sql, "Wrong race {0} in `player_levelstats` table, ignoring.", currentrace);
                        continue;
                    }

                    uint currentclass = result.Read<uint>(1);
                    if (currentclass >= (int)Class.Max)
                    {
                        Log.outError(LogFilter.Sql, "Wrong class {0} in `player_levelstats` table, ignoring.", currentclass);
                        continue;
                    }

                    uint currentlevel = result.Read<uint>(2);
                    if (currentlevel > WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                    {
                        if (currentlevel > 255)        // hardcoded level maximum
                            Log.outError(LogFilter.Sql, "Wrong (> {0}) level {1} in `player_levelstats` table, ignoring.", 255, currentlevel);
                        else
                        {
                            Log.outError(LogFilter.Sql, "Unused (> MaxPlayerLevel in worldserver.conf) level {0} in `player_levelstats` table, ignoring.", currentlevel);
                            ++count;                                // make result loading percent "expected" correct in case disabled detail mode for example.
                        }
                        continue;
                    }

                    var pInfo = _playerInfo[currentrace][currentclass];
                    if (pInfo == null)
                        continue;

                    var levelinfo = new PlayerLevelInfo();

                    for (var i = 0; i < (int)Stats.Max; i++)
                        levelinfo.stats[i] = result.Read<ushort>(i + 3);

                    pInfo.levelInfo[currentlevel - 1] = levelinfo;
                    ++count;
                } while (result.NextRow());

                // Fill gaps and check integrity
                for (uint race = 0; race < (int)Race.Max; ++race)
                {
                    // skip non existed races
                    if (!CliDB.ChrRacesStorage.ContainsKey(race) || _playerInfo[race][0] == null)
                        continue;

                    for (uint _class = 0; _class < (int)Class.Max; ++_class)
                    {
                        // skip non existed classes
                        if (CliDB.ChrClassesStorage.LookupByKey(_class) == null || _playerInfo[race][_class] == null)
                            continue;

                        PlayerInfo pInfo = _playerInfo[race][_class];
                        if (pInfo == null)
                            continue;

                        // skip non loaded combinations
                        if (pInfo.DisplayId_m == 0 || pInfo.DisplayId_f == 0)
                            continue;

                        // skip expansion races if not playing with expansion
                        if (WorldConfig.GetIntValue(WorldCfg.Expansion) < (int)Expansion.BurningCrusade && (race == (int)Race.BloodElf || race == (int)Race.Draenei))
                            continue;

                        // skip expansion classes if not playing with expansion
                        if (WorldConfig.GetIntValue(WorldCfg.Expansion) < (int)Expansion.WrathOfTheLichKing && _class == (int)Class.Deathknight)
                            continue;

                        if (WorldConfig.GetIntValue(WorldCfg.Expansion) < (int)Expansion.MistsOfPandaria && (race == (int)Race.PandarenNeutral || race == (int)Race.PandarenHorde || race == (int)Race.PandarenAlliance))
                            continue;

                        if (WorldConfig.GetIntValue(WorldCfg.Expansion) < (int)Expansion.Legion && _class == (int)Class.DemonHunter)
                            continue;

                        // fatal error if no level 1 data
                        if (pInfo.levelInfo == null || pInfo.levelInfo[0] == null)
                        {
                            Log.outError(LogFilter.Sql, "Race {0} Class {1} Level 1 does not have stats data!", race, _class);
                            Global.WorldMgr.StopNow();
                            return;
                        }

                        // fill level gaps
                        for (var level = 1; level < WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel); ++level)
                        {
                            if (pInfo.levelInfo[level] == null)
                            {
                                Log.outError(LogFilter.Sql, "Race {0} Class {1} Level {2} does not have stats data. Using stats data of level {3}.", race, _class, level + 1, level);
                                pInfo.levelInfo[level] = pInfo.levelInfo[level - 1];
                            }
                        }
                    }
                }

                Log.outInfo(LogFilter.ServerLoading, "Loaded {0} level stats definitions in {1} ms", count, Time.GetMSTimeDiffToNow(time));
            }
            time = Time.GetMSTime();
            // Loading xp per level data
            Log.outInfo(LogFilter.ServerLoading, "Loading Player Create XP Data...");
            {
                _playerXPperLevel = new uint[CliDB.XpGameTable.GetTableRowCount() + 1];

                //                                          0      1
                SQLResult result = DB.World.Query("SELECT Level, Experience FROM player_xp_for_level");

                // load the DBC's levels at first...
                for (uint level = 1; level < CliDB.XpGameTable.GetTableRowCount(); ++level)
                    _playerXPperLevel[level] = (uint)CliDB.XpGameTable.GetRow(level).Total;

                uint count = 0;
                // ...overwrite if needed (custom values)
                if (!result.IsEmpty())
                {
                    do
                    {
                        uint currentlevel = result.Read<byte>(0);
                        uint currentxp = result.Read<uint>(1);

                        if (currentlevel >= WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                        {
                            if (currentlevel > SharedConst.StrongMaxLevel)        // hardcoded level maximum
                                Log.outError(LogFilter.Sql, "Wrong (> {0}) level {1} in `player_xp_for_level` table, ignoring.", 255, currentlevel);
                            else
                            {
                                Log.outError(LogFilter.Sql, "Unused (> MaxPlayerLevel in worldserver.conf) level {0} in `player_xp_for_levels` table, ignoring.", currentlevel);
                                ++count;                                // make result loading percent "expected" correct in case disabled detail mode for example.
                            }
                            continue;
                        }
                        //PlayerXPperLevel
                        _playerXPperLevel[currentlevel] = currentxp;
                        ++count;
                    } while (result.NextRow());

                    // fill level gaps
                    for (var level = 1; level < WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel); ++level)
                    {
                        if (_playerXPperLevel[level] == 0)
                        {
                            Log.outError(LogFilter.Sql, "Level {0} does not have XP for level data. Using data of level [{1}] + 12000.", level + 1, level);
                            _playerXPperLevel[level] = _playerXPperLevel[level - 1] + 12000;
                        }
                    }
                }

                Log.outInfo(LogFilter.ServerLoading, "Loaded {0} xp for level definition(s) from database in {1} ms", count, Time.GetMSTimeDiffToNow(time));
            }
        }
        void PlayerCreateInfoAddItemHelper(uint race, uint _class, uint itemId, int count)
        {
            if (_playerInfo[race][_class] == null)
                return;

            if (count > 0)
                _playerInfo[race][_class].item.Add(new PlayerCreateInfoItem(itemId, (uint)count));
            else
            {
                if (count < -1)
                    Log.outError(LogFilter.Sql, "Invalid count {0} specified on item {1} be removed from original player create info (use -1)!", count, itemId);

                for (byte gender = 0; gender < (int)Gender.None; ++gender)
                {
                    CharStartOutfitRecord entry = Global.DB2Mgr.GetCharStartOutfitEntry(race, _class, gender);
                    if (entry != null)
                    {
                        bool found = false;
                        for (var x = 0; x < ItemConst.MaxOutfitItems; ++x)
                        {
                            if (entry.ItemID[x] > 0 && entry.ItemID[x] == itemId)
                            {
                                found = true;
                                entry.ItemID[x] = 0;
                                break;
                            }
                        }

                        if (!found)
                            Log.outError(LogFilter.Sql, "Item {0} specified to be removed from original create info not found in dbc!", itemId);
                    }
                }
            }
        }

        public PlayerInfo GetPlayerInfo(Race raceId, Class classId)
        {
            if (raceId >= Race.Max)
                return null;

            if (classId >= Class.Max)
                return null;

            var info = _playerInfo[(int)raceId][(int)classId];
            if (info == null)
                return null;

            return info;
        }
        public void GetPlayerClassLevelInfo(Class _class, uint level, out uint baseMana)
        {
            baseMana = 0;
            if (level < 1 || _class >= Class.Max)
                return;

            if (level > WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                level = (byte)WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel);

            GtBaseMPRecord mp = CliDB.BaseMPGameTable.GetRow(level);
            if (mp == null)
            {
                Log.outError(LogFilter.Sql, "Tried to get non-existant Class-Level combination data for base mp. Class {0} Level {1}", _class, level);
                return;
            }

            baseMana = (uint)CliDB.GetGameTableColumnForClass(mp, _class);
        }
        public PlayerLevelInfo GetPlayerLevelInfo(Race race, Class _class, uint level)
        {
            if (level < 1 || race >= Race.Max || _class >= Class.Max)
                return null;

            PlayerInfo pInfo = _playerInfo[(int)race][(int)_class];
            if (pInfo.DisplayId_m == 0 || pInfo.DisplayId_f == 0)
                return null;

            if (level <= WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                return pInfo.levelInfo[level - 1];
            //else
            //return BuildPlayerLevelInfo(race, _class, level, info);
            return null;
        }
        /*
        void BuildPlayerLevelInfo(byte race, Class _class, byte level, PlayerLevelInfo info)
        {
            // base data (last known level)
            info = _playerInfo[race][_class].levelInfo[WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel) - 1];

            // if conversion from uint32 to uint8 causes unexpected behaviour, change lvl to uint32
            for (int lvl = WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel) - 1; lvl < level; ++lvl)
            {
                switch (_class)
                {
                    case Class.Warrior:
                        info.stats[STAT_STRENGTH] += (lvl > 23 ? 2 : (lvl > 1 ? 1 : 0));
                        info.stats[STAT_STAMINA] += (lvl > 23 ? 2 : (lvl > 1 ? 1 : 0));
                        info.stats[STAT_AGILITY] += (lvl > 36 ? 1 : (lvl > 6 && (lvl % 2) ? 1 : 0));
                        info.stats[STAT_INTELLECT] += (lvl > 9 && !(lvl % 2) ? 1 : 0);
                        break;
                    case CLASS_PALADIN:
                        info.stats[STAT_STRENGTH] += (lvl > 3 ? 1 : 0);
                        info.stats[STAT_STAMINA] += (lvl > 33 ? 2 : (lvl > 1 ? 1 : 0));
                        info.stats[STAT_AGILITY] += (lvl > 38 ? 1 : (lvl > 7 && !(lvl % 2) ? 1 : 0));
                        info.stats[STAT_INTELLECT] += (lvl > 6 && (lvl % 2) ? 1 : 0);
                        break;
                    case CLASS_HUNTER:
                        info.stats[STAT_STRENGTH] += (lvl > 4 ? 1 : 0);
                        info.stats[STAT_STAMINA] += (lvl > 4 ? 1 : 0);
                        info.stats[STAT_AGILITY] += (lvl > 33 ? 2 : (lvl > 1 ? 1 : 0));
                        info.stats[STAT_INTELLECT] += (lvl > 8 && (lvl % 2) ? 1 : 0);
                        break;
                    case CLASS_ROGUE:
                        info.stats[STAT_STRENGTH] += (lvl > 5 ? 1 : 0);
                        info.stats[STAT_STAMINA] += (lvl > 4 ? 1 : 0);
                        info.stats[STAT_AGILITY] += (lvl > 16 ? 2 : (lvl > 1 ? 1 : 0));
                        info.stats[STAT_INTELLECT] += (lvl > 8 && !(lvl % 2) ? 1 : 0);
                        break;
                    case CLASS_PRIEST:
                        info.stats[STAT_STRENGTH] += (lvl > 9 && !(lvl % 2) ? 1 : 0);
                        info.stats[STAT_STAMINA] += (lvl > 5 ? 1 : 0);
                        info.stats[STAT_AGILITY] += (lvl > 38 ? 1 : (lvl > 8 && (lvl % 2) ? 1 : 0));
                        info.stats[STAT_INTELLECT] += (lvl > 22 ? 2 : (lvl > 1 ? 1 : 0));
                        break;
                    case CLASS_SHAMAN:
                        info.stats[STAT_STRENGTH] += (lvl > 34 ? 1 : (lvl > 6 && (lvl % 2) ? 1 : 0));
                        info.stats[STAT_STAMINA] += (lvl > 4 ? 1 : 0);
                        info.stats[STAT_AGILITY] += (lvl > 7 && !(lvl % 2) ? 1 : 0);
                        info.stats[STAT_INTELLECT] += (lvl > 5 ? 1 : 0);
                        break;
                    case CLASS_MAGE:
                        info.stats[STAT_STRENGTH] += (lvl > 9 && !(lvl % 2) ? 1 : 0);
                        info.stats[STAT_STAMINA] += (lvl > 5 ? 1 : 0);
                        info.stats[STAT_AGILITY] += (lvl > 9 && !(lvl % 2) ? 1 : 0);
                        info.stats[STAT_INTELLECT] += (lvl > 24 ? 2 : (lvl > 1 ? 1 : 0));
                        break;
                    case CLASS_WARLOCK:
                        info.stats[STAT_STRENGTH] += (lvl > 9 && !(lvl % 2) ? 1 : 0);
                        info.stats[STAT_STAMINA] += (lvl > 38 ? 2 : (lvl > 3 ? 1 : 0));
                        info.stats[STAT_AGILITY] += (lvl > 9 && !(lvl % 2) ? 1 : 0);
                        info.stats[STAT_INTELLECT] += (lvl > 33 ? 2 : (lvl > 2 ? 1 : 0));
                        break;
                    case CLASS_DRUID:
                        info.stats[STAT_STRENGTH] += (lvl > 38 ? 2 : (lvl > 6 && (lvl % 2) ? 1 : 0));
                        info.stats[STAT_STAMINA] += (lvl > 32 ? 2 : (lvl > 4 ? 1 : 0));
                        info.stats[STAT_AGILITY] += (lvl > 38 ? 2 : (lvl > 8 && (lvl % 2) ? 1 : 0));
                        info.stats[STAT_INTELLECT] += (lvl > 38 ? 3 : (lvl > 4 ? 1 : 0));
                }
            }
        }
        */

        //Pets
        public void LoadPetLevelInfo()
        {
            uint oldMSTime = Time.GetMSTime();

            //                                         0               1      2   3     4    5    6    7     8    9
            SQLResult result = DB.World.Query("SELECT creature_entry, level, hp, mana, str, agi, sta, inte, spi, armor FROM pet_levelstats");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 level pet stats definitions. DB table `pet_levelstats` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint creatureid = result.Read<uint>(0);
                if (GetCreatureTemplate(creatureid) == null)
                {
                    Log.outError(LogFilter.Sql, "Wrong creature id {0} in `pet_levelstats` table, ignoring.", creatureid);
                    continue;
                }

                uint currentlevel = result.Read<uint>(1);
                if (currentlevel > WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                {
                    if (currentlevel > SharedConst.StrongMaxLevel)        // hardcoded level maximum
                        Log.outError(LogFilter.Sql, "Wrong (> {0}) level {1} in `pet_levelstats` table, ignoring.", SharedConst.StrongMaxLevel, currentlevel);
                    else
                    {
                        Log.outInfo(LogFilter.Server, "Unused (> MaxPlayerLevel in worldserver.conf) level {0} in `pet_levelstats` table, ignoring.", currentlevel);
                        ++count;                                // make result loading percent "expected" correct in case disabled detail mode for example.
                    }
                    continue;
                }
                else if (currentlevel < 1)
                {
                    Log.outError(LogFilter.Sql, "Wrong (<1) level {0} in `pet_levelstats` table, ignoring.", currentlevel);
                    continue;
                }

                var pInfoMapEntry = petInfoStore.LookupByKey(creatureid);

                if (pInfoMapEntry == null)
                    pInfoMapEntry = new PetLevelInfo[WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel)];

                PetLevelInfo pLevelInfo = new PetLevelInfo();
                pLevelInfo.health = result.Read<uint>(2);
                pLevelInfo.mana = result.Read<uint>(3);
                pLevelInfo.armor = result.Read<uint>(9);

                for (int i = 0; i < (int)Stats.Max; i++)
                {
                    pLevelInfo.stats[i] = result.Read<uint>(i + 4);
                }

                pInfoMapEntry[currentlevel - 1] = pLevelInfo;

                ++count;
            }
            while (result.NextRow());

            // Fill gaps and check integrity
            foreach (var map in petInfoStore)
            {
                var pInfo = map.Value;

                // fatal error if no level 1 data
                if (pInfo == null || pInfo[0].health == 0)
                {
                    Log.outError(LogFilter.Sql, "Creature {0} does not have pet stats data for Level 1!", map.Key);
                    Global.WorldMgr.StopNow();
                }

                // fill level gaps
                for (byte level = 1; level < WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel); ++level)
                {
                    if (pInfo[level].health == 0)
                    {
                        Log.outError(LogFilter.Sql, "Creature {0} has no data for Level {1} pet stats data, using data of Level {2}.", map.Key, level + 1, level);
                        pInfo[level] = pInfo[level - 1];
                    }
                }
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} level pet stats definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadPetNames()
        {
            uint oldMSTime = Time.GetMSTime();
            //                                          0     1      2
            SQLResult result = DB.World.Query("SELECT word, entry, half FROM pet_name_generation");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 pet name parts. DB table `pet_name_generation` is empty!");
                return;
            }

            uint count = 0;

            do
            {
                string word = result.Read<string>(0);
                uint entry = result.Read<uint>(1);
                bool half = result.Read<bool>(2);
                if (half)
                    _petHalfName1.Add(entry, word);
                else
                    _petHalfName0.Add(entry, word);
                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} pet name parts in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadPetNumber()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.Characters.Query("SELECT MAX(id) FROM character_pet");
            if (!result.IsEmpty())
                _hiPetNumber = result.Read<uint>(0) + 1;

            Log.outInfo(LogFilter.ServerLoading, "Loaded the max pet number: {0} in {1} ms", _hiPetNumber - 1, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public PetLevelInfo GetPetLevelInfo(uint creatureid, uint level)
        {
            if (level > WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                level = WorldConfig.GetUIntValue(WorldCfg.MaxPlayerLevel);

            var petinfo = petInfoStore.LookupByKey(creatureid);

            if (petinfo == null)
                return null;

            return petinfo[level - 1];                           // data for level 1 stored in [0] array element, ...
        }
        public string GeneratePetName(uint entry)
        {
            var list0 = _petHalfName0[entry];
            var list1 = _petHalfName1[entry];

            if (list0.Empty() || list1.Empty())
            {
                CreatureTemplate cinfo = GetCreatureTemplate(entry);
                if (cinfo == null)
                    return "";

                string petname = Global.DB2Mgr.GetCreatureFamilyPetName(cinfo.Family, Global.WorldMgr.GetDefaultDbcLocale());
                if (!string.IsNullOrEmpty(petname))
                    return petname;
                else
                    return cinfo.Name;
            }

            return list0[RandomHelper.IRand(0, list0.Count - 1)] + list1[RandomHelper.IRand(0, list1.Count - 1)];
        }
        public uint GeneratePetNumber()
        {
            if (_hiPetNumber >= 0xFFFFFFFE)
            {
                Log.outError(LogFilter.Misc, "_hiPetNumber Id overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow(ShutdownExitCode.Error);
            }
            return _hiPetNumber++;
        }

        //Faction Change
        public void LoadFactionChangeAchievements()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT alliance_id, horde_id FROM player_factionchange_achievement");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 faction change achievement pairs. DB table `player_factionchange_achievement` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint alliance = result.Read<uint>(0);
                uint horde = result.Read<uint>(1);

                if (!CliDB.AchievementStorage.ContainsKey(alliance))
                    Log.outError(LogFilter.Sql, "Achievement {0} (alliance_id) referenced in `player_factionchange_achievement` does not exist, pair skipped!", alliance);
                else if (!CliDB.AchievementStorage.ContainsKey(horde))
                    Log.outError(LogFilter.Sql, "Achievement {0} (horde_id) referenced in `player_factionchange_achievement` does not exist, pair skipped!", horde);
                else
                    FactionChangeAchievements[alliance] = horde;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} faction change achievement pairs in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadFactionChangeItems()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT alliance_id, horde_id FROM player_factionchange_items");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 faction change item pairs. DB table `player_factionchange_items` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint alliance = result.Read<uint>(0);
                uint horde = result.Read<uint>(1);

                if (GetItemTemplate(alliance) == null)
                    Log.outError(LogFilter.Sql, "Item {0} (alliance_id) referenced in `player_factionchange_items` does not exist, pair skipped!", alliance);
                else if (GetItemTemplate(horde) == null)
                    Log.outError(LogFilter.Sql, "Item {0} (horde_id) referenced in `player_factionchange_items` does not exist, pair skipped!", horde);
                else
                    FactionChangeItems[alliance] = horde;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} faction change item pairs in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadFactionChangeQuests()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT alliance_id, horde_id FROM player_factionchange_quests");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 faction change quest pairs. DB table `player_factionchange_quests` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint alliance = result.Read<uint>(0);
                uint horde = result.Read<uint>(1);

                if (Global.ObjectMgr.GetQuestTemplate(alliance) == null)
                    Log.outError(LogFilter.Sql, "Quest {0} (alliance_id) referenced in `player_factionchange_quests` does not exist, pair skipped!", alliance);
                else if (Global.ObjectMgr.GetQuestTemplate(horde) == null)
                    Log.outError(LogFilter.Sql, "Quest {0} (horde_id) referenced in `player_factionchange_quests` does not exist, pair skipped!", horde);
                else
                    FactionChangeQuests[alliance] = horde;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} faction change quest pairs in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadFactionChangeReputations()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT alliance_id, horde_id FROM player_factionchange_reputations");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 faction change reputation pairs. DB table `player_factionchange_reputations` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint alliance = result.Read<uint>(0);
                uint horde = result.Read<uint>(1);

                if (!CliDB.FactionStorage.ContainsKey(alliance))
                    Log.outError(LogFilter.Sql, "Reputation {0} (alliance_id) referenced in `player_factionchange_reputations` does not exist, pair skipped!", alliance);
                else if (!CliDB.FactionStorage.ContainsKey(horde))
                    Log.outError(LogFilter.Sql, "Reputation {0} (horde_id) referenced in `player_factionchange_reputations` does not exist, pair skipped!", horde);
                else
                    FactionChangeReputation[alliance] = horde;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} faction change reputation pairs in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadFactionChangeSpells()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT alliance_id, horde_id FROM player_factionchange_spells");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 faction change spell pairs. DB table `player_factionchange_spells` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint alliance = result.Read<uint>(0);
                uint horde = result.Read<uint>(1);

                if (!Global.SpellMgr.HasSpellInfo(alliance))
                    Log.outError(LogFilter.Sql, "Spell {0} (alliance_id) referenced in `player_factionchange_spells` does not exist, pair skipped!", alliance);
                else if (!Global.SpellMgr.HasSpellInfo(horde))
                    Log.outError(LogFilter.Sql, "Spell {0} (horde_id) referenced in `player_factionchange_spells` does not exist, pair skipped!", horde);
                else
                    FactionChangeSpells[alliance] = horde;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} faction change spell pairs in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadFactionChangeTitles()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT alliance_id, horde_id FROM player_factionchange_titles");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 faction change title pairs. DB table `player_factionchange_title` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint alliance = result.Read<uint>(0);
                uint horde = result.Read<uint>(1);

                if (!CliDB.CharTitlesStorage.ContainsKey(alliance))
                    Log.outError(LogFilter.Sql, "Title {0} (alliance_id) referenced in `player_factionchange_title` does not exist, pair skipped!", alliance);
                else if (!CliDB.CharTitlesStorage.ContainsKey(horde))
                    Log.outError(LogFilter.Sql, "Title {0} (horde_id) referenced in `player_factionchange_title` does not exist, pair skipped!", horde);
                else
                    FactionChangeTitles[alliance] = horde;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} faction change title pairs in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        //Quests
        public void LoadQuests()
        {
            uint oldMSTime = Time.GetMSTime();

            // For reload case
            _questTemplates.Clear();
            _questObjectives.Clear();
            _exclusiveQuestGroups.Clear();

            SQLResult result = DB.World.Query("SELECT " +
                //0  1          2           3               4         5            6            7                  8                9                   10
                "ID, QuestType, QuestLevel, QuestPackageID, MinLevel, QuestSortID, QuestInfoID, SuggestedGroupNum, RewardNextQuest, RewardXPDifficulty, RewardXPMultiplier, " +
                //11          12                     13                     14                15                   16                   17                   18           19           20               21
                "RewardMoney, RewardMoneyDifficulty, RewardMoneyMultiplier, RewardBonusMoney, RewardDisplaySpell1, RewardDisplaySpell2, RewardDisplaySpell3, RewardSpell, RewardHonor, RewardKillHonor, StartItem, " +
                //22                         23                          24                        25     26
                "RewardArtifactXPDifficulty, RewardArtifactXPMultiplier, RewardArtifactCategoryID, Flags, FlagsEx, " +
                //27          28             29         30                 31           32             33         34
                "RewardItem1, RewardAmount1, ItemDrop1, ItemDropQuantity1, RewardItem2, RewardAmount2, ItemDrop2, ItemDropQuantity2, " +
                //35          36             37         38                 39           40             41         42
                "RewardItem3, RewardAmount3, ItemDrop3, ItemDropQuantity3, RewardItem4, RewardAmount4, ItemDrop4, ItemDropQuantity4, " +
                //43                  44                         45                          46                   47                         48
                "RewardChoiceItemID1, RewardChoiceItemQuantity1, RewardChoiceItemDisplayID1, RewardChoiceItemID2, RewardChoiceItemQuantity2, RewardChoiceItemDisplayID2, " +
                //49                  50                         51                          52                   53                         54
                "RewardChoiceItemID3, RewardChoiceItemQuantity3, RewardChoiceItemDisplayID3, RewardChoiceItemID4, RewardChoiceItemQuantity4, RewardChoiceItemDisplayID4, " +
                //55                  56                         57                          58                   59                         60
                "RewardChoiceItemID5, RewardChoiceItemQuantity5, RewardChoiceItemDisplayID5, RewardChoiceItemID6, RewardChoiceItemQuantity6, RewardChoiceItemDisplayID6, " +
                //61           62    63    64           65           66                 67                 68                 69             70
                "POIContinent, POIx, POIy, POIPriority, RewardTitle, RewardArenaPoints, RewardSkillLineID, RewardNumSkillUps, PortraitGiver, PortraitTurnIn, " +
                //71               72                   73                      74                75                   76                   77                      78
                "RewardFactionID1, RewardFactionValue1, RewardFactionOverride1, RewardFactionCapIn1, RewardFactionID2, RewardFactionValue2, RewardFactionOverride2, RewardFactionCapIn2, " +
                //79               80                   81                      82                   83                84                   85                      86
                "RewardFactionID3, RewardFactionValue3, RewardFactionOverride3, RewardFactionCapIn3, RewardFactionID4, RewardFactionValue4, RewardFactionOverride4, RewardFactionCapIn4, " +
                //87               88                   89                      90                   91
                "RewardFactionID5, RewardFactionValue5, RewardFactionOverride5, RewardFactionCapIn5, RewardFactionFlags, " +
                //92                93                  94                 95                  96                 97                  98                 99
                "RewardCurrencyID1, RewardCurrencyQty1, RewardCurrencyID2, RewardCurrencyQty2, RewardCurrencyID3, RewardCurrencyQty3, RewardCurrencyID4, RewardCurrencyQty4, " +
                //100                101                 102          103          104             105              106
                "AcceptedSoundKitID, CompleteSoundKitID, AreaGroupID, TimeAllowed, AllowableRaces, QuestRewardID, Expansion, " +
                //107      108             109               110              111                112                113                 114                 115
                "LogTitle, LogDescription, QuestDescription, AreaDescription, PortraitGiverText, PortraitGiverName, PortraitTurnInText, PortraitTurnInName, QuestCompletionLog" +
                " FROM quest_template");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quests definitions. DB table `quest_template` is empty.");
                return;
            }

            // create multimap previous quest for each existed quest
            // some quests can have many previous maps set by NextQuestId in previous quest
            // for example set of race quests can lead to single not race specific quest
            do
            {
                Quest newQuest = new Quest(result.GetFields());
                _questTemplates[newQuest.Id] = newQuest;
            }
            while (result.NextRow());

            // Load `quest_details`
            //                               0   1       2       3       4       5            6            7            8
            result = DB.World.Query("SELECT ID, Emote1, Emote2, Emote3, Emote4, EmoteDelay1, EmoteDelay2, EmoteDelay3, EmoteDelay4 FROM quest_details");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest details. DB table `quest_details` is empty.");
            }
            else
            {
                do
                {
                    uint questId = result.Read<uint>(0);

                    var quest = _questTemplates.LookupByKey(questId);
                    if (quest != null)
                        quest.LoadQuestDetails(result.GetFields());
                    else
                        Log.outError(LogFilter.Sql, "Table `quest_details` has data for quest {0} but such quest does not exist", questId);
                } while (result.NextRow());
            }

            // Load `quest_request_items`
            //                               0   1                2                  3                     4                       5
            result = DB.World.Query("SELECT ID, EmoteOnComplete, EmoteOnIncomplete, EmoteOnCompleteDelay, EmoteOnIncompleteDelay, CompletionText FROM quest_request_items");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest request items. DB table `quest_request_items` is empty.");
            }
            else
            {
                do
                {
                    uint questId = result.Read<uint>(0);

                    var quest = _questTemplates.LookupByKey(questId);
                    if (quest != null)
                        quest.LoadQuestRequestItems(result.GetFields());
                    else
                        Log.outError(LogFilter.Sql, "Table `quest_request_items` has data for quest {0} but such quest does not exist", questId);
                } while (result.NextRow());
            }

            // Load `quest_offer_reward`
            //                               0   1       2       3       4       5            6            7            8            9
            result = DB.World.Query("SELECT ID, Emote1, Emote2, Emote3, Emote4, EmoteDelay1, EmoteDelay2, EmoteDelay3, EmoteDelay4, RewardText FROM quest_offer_reward");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest reward emotes. DB table `quest_offer_reward` is empty.");
            }
            else
            {
                do
                {
                    uint questId = result.Read<uint>(0);

                    var quest = _questTemplates.LookupByKey(questId);
                    if (quest != null)
                        quest.LoadQuestOfferReward(result.GetFields());
                    else
                        Log.outError(LogFilter.Sql, "Table `quest_offer_reward` has data for quest {0} but such quest does not exist", questId);
                } while (result.NextRow());
            }

            // Load `quest_template_addon`
            //                               0   1         2                 3              4            5            6               7                     8
            result = DB.World.Query("SELECT ID, MaxLevel, AllowableClasses, SourceSpellID, PrevQuestID, NextQuestID, ExclusiveGroup, RewardMailTemplateID, RewardMailDelay, " +
                //9               10                   11                     12                     13                   14                   15                 16                     17
                "RequiredSkillID, RequiredSkillPoints, RequiredMinRepFaction, RequiredMaxRepFaction, RequiredMinRepValue, RequiredMaxRepValue, ProvidedItemCount, RewardMailSenderEntry, SpecialFlags FROM quest_template_addon LEFT JOIN quest_mail_sender ON Id=QuestId");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest template addons. DB table `quest_template_addon` is empty.");
            }
            else
            {
                do
                {
                    uint questId = result.Read<uint>(0);

                    var quest = _questTemplates.LookupByKey(questId);
                    if (quest != null)
                        quest.LoadQuestTemplateAddon(result.GetFields());
                    else
                        Log.outError(LogFilter.Sql, "Table `quest_template_addon` has data for quest {0} but such quest does not exist", questId);
                } while (result.NextRow());
            }

            // Load `quest_objectives`
            //                               0   1        2     3             4         5       6      7      8                  9
            result = DB.World.Query("SELECT ID, QuestID, Type, StorageIndex, ObjectID, Amount, Flags, Flags, ProgressBarWeight, Description FROM quest_objectives ORDER BY StorageIndex ASC");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest objectives. DB table `quest_objectives` is empty.");
            }
            else
            {
                do
                {
                    uint questId = result.Read<uint>(1);

                    var quest = _questTemplates.LookupByKey(questId);
                    if (quest != null)
                        quest.LoadQuestObjective(result.GetFields());
                    else
                        Log.outError(LogFilter.Sql, "Table `quest_objectives` has objective for quest {0} but such quest does not exist", questId);
                } while (result.NextRow());
            }

            // Load `quest_visual_effect` join table with quest_objectives because visual effects are based on objective ID (core stores objectives by their index in quest)
            //                                 0     1     2          3        4
            result = DB.World.Query("SELECT v.ID, o.ID, o.QuestID, v.Index, v.VisualEffect FROM quest_visual_effect AS v LEFT JOIN quest_objectives AS o ON v.ID = o.ID ORDER BY v.Index DESC");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest visual effects. DB table `quest_visual_effect` is empty.");
            }
            else
            {
                do
                {
                    uint vID = result.Read<uint>(0);
                    uint oID = result.Read<uint>(1);

                    if (vID == 0)
                    {
                        Log.outError(LogFilter.Sql, "Table `quest_visual_effect` has visual effect for null objective id");
                        continue;
                    }

                    // objID will be null if match for table join is not found
                    if (vID != oID)
                    {
                        Log.outError(LogFilter.Sql, "Table `quest_visual_effect` has visual effect for objective {0} but such objective does not exist.", vID);
                        continue;
                    }

                    uint questId = result.Read<uint>(2);

                    // Do not throw error here because error for non existing quest is thrown while loading quest objectives. we do not need duplication
                    var quest = _questTemplates.LookupByKey(questId);
                    if (quest != null)
                        quest.LoadQuestObjectiveVisualEffect(result.GetFields());
                } while (result.NextRow());
            }

            Dictionary<uint, uint> usedMailTemplates = new Dictionary<uint, uint>();

            // Post processing
            foreach (var qinfo in _questTemplates.Values)
            {
                // skip post-loading checks for disabled quests
                if (Global.DisableMgr.IsDisabledFor(DisableType.Quest, qinfo.Id, null))
                    continue;

                // additional quest integrity checks (GO, creaturetemplate and itemtemplate must be loaded already)

                if (qinfo.Type >= QuestType.Max)
                    Log.outError(LogFilter.Sql, "Quest {0} has `Method` = {1}, expected values are 0, 1 or 2.", qinfo.Id, qinfo.Type);

                if (Convert.ToBoolean(qinfo.SpecialFlags & ~QuestSpecialFlags.DbAllowed))
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `SpecialFlags` = {1} > max allowed value. Correct `SpecialFlags` to value <= {2}",
                        qinfo.Id, qinfo.SpecialFlags, QuestSpecialFlags.DbAllowed);
                    qinfo.SpecialFlags &= QuestSpecialFlags.DbAllowed;
                }

                if (qinfo.Flags.HasAnyFlag(QuestFlags.Daily) && qinfo.Flags.HasAnyFlag(QuestFlags.Weekly))
                {
                    Log.outError(LogFilter.Sql, "Weekly Quest {0} is marked as daily quest in `Flags`, removed daily flag.", qinfo.Id);
                    qinfo.Flags &= ~QuestFlags.Daily;
                }

                if (qinfo.Flags.HasAnyFlag(QuestFlags.Daily))
                {
                    if (!qinfo.SpecialFlags.HasAnyFlag(QuestSpecialFlags.Repeatable))
                    {
                        Log.outError(LogFilter.Sql, "Daily Quest {0} not marked as repeatable in `SpecialFlags`, added.", qinfo.Id);
                        qinfo.SpecialFlags |= QuestSpecialFlags.Repeatable;
                    }
                }

                if (qinfo.Flags.HasAnyFlag(QuestFlags.Weekly))
                {
                    if (!qinfo.SpecialFlags.HasAnyFlag(QuestSpecialFlags.Repeatable))
                    {
                        Log.outError(LogFilter.Sql, "Weekly Quest {0} not marked as repeatable in `SpecialFlags`, added.", qinfo.Id);
                        qinfo.SpecialFlags |= QuestSpecialFlags.Repeatable;
                    }
                }

                if (qinfo.SpecialFlags.HasAnyFlag(QuestSpecialFlags.Monthly))
                {
                    if (!qinfo.SpecialFlags.HasAnyFlag(QuestSpecialFlags.Repeatable))
                    {
                        Log.outError(LogFilter.Sql, "Monthly quest {0} not marked as repeatable in `SpecialFlags`, added.", qinfo.Id);
                        qinfo.SpecialFlags |= QuestSpecialFlags.Repeatable;
                    }
                }

                if (Convert.ToBoolean(qinfo.Flags & QuestFlags.Tracking))
                {
                    // at auto-reward can be rewarded only RewardChoiceItemId[0]
                    for (int j = 1; j < qinfo.RewardChoiceItemId.Length; ++j)
                    {
                        var id = qinfo.RewardChoiceItemId[j];
                        if (id != 0)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardChoiceItemId{1}` = {2} but item from `RewardChoiceItemId{3}` can't be rewarded with quest flag QUESTFLAGSTRACKING.",
                                qinfo.Id, j + 1, id, j + 1);
                            // no changes, quest ignore this data
                        }
                    }
                }

                if (qinfo.MinLevel == -1 || qinfo.MinLevel > SharedConst.DefaultMaxLevel)
                {
                    Log.outError(LogFilter.Sql, "Quest {0} should be disabled because `MinLevel` = {1}", qinfo.Id, qinfo.MinLevel);
                    // no changes needed, sending -1 in SMSGQUESTQUERYRESPONSE is valid
                }

                // client quest log visual (area case)
                if (qinfo.QuestSortID > 0)
                {
                    if (!CliDB.AreaTableStorage.ContainsKey(qinfo.QuestSortID))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `ZoneOrSort` = {1} (zone case) but zone with this id does not exist.",
                            qinfo.Id, qinfo.QuestSortID);
                        // no changes, quest not dependent from this value but can have problems at client
                    }
                }
                // client quest log visual (sort case)
                if (qinfo.QuestSortID < 0)
                {
                    var qSort = CliDB.QuestSortStorage.LookupByKey((uint)-qinfo.QuestSortID);
                    if (qSort == null)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `ZoneOrSort` = {1} (sort case) but quest sort with this id does not exist.",
                            qinfo.Id, qinfo.QuestSortID);
                        // no changes, quest not dependent from this value but can have problems at client (note some may be 0, we must allow this so no check)
                    }
                    //check for proper RequiredSkillId value (skill case)
                    var skillid = SharedConst.SkillByQuestSort(-qinfo.QuestSortID);
                    if (skillid != SkillType.None)
                    {
                        if (qinfo.RequiredSkillId != (uint)skillid)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `ZoneOrSort` = {1} but `RequiredSkillId` does not have a corresponding value ({2}).",
                                qinfo.Id, qinfo.QuestSortID, skillid);
                            //override, and force proper value here?
                        }
                    }
                }

                // AllowableClasses, can be 0/CLASSMASK_ALL_PLAYABLE to allow any class
                if (qinfo.AllowableClasses != 0)
                {
                    if (!Convert.ToBoolean(qinfo.AllowableClasses & (uint)Class.ClassMaskAllPlayable))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} does not contain any playable classes in `RequiredClasses` ({1}), value set to 0 (all classes).", qinfo.Id, qinfo.AllowableClasses);
                        qinfo.AllowableClasses = 0;
                    }
                }
                // AllowableRaces, can be -1/RACEMASK_ALL_PLAYABLE to allow any race
                if (qinfo.AllowableRaces != -1)
                {
                    if (!Convert.ToBoolean(qinfo.AllowableRaces & (uint)Race.RaceMaskAllPlayable))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} does not contain any playable races in `RequiredRaces` ({1}), value set to 0 (all races).", qinfo.Id, qinfo.AllowableRaces);
                        qinfo.AllowableRaces = -1;
                    }
                }
                // RequiredSkillId, can be 0
                if (qinfo.RequiredSkillId != 0)
                {
                    if (!CliDB.SkillLineStorage.ContainsKey(qinfo.RequiredSkillId))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RequiredSkillId` = {1} but this skill does not exist",
                            qinfo.Id, qinfo.RequiredSkillId);
                    }
                }

                if (qinfo.RequiredSkillPoints != 0)
                {
                    if (qinfo.RequiredSkillPoints > Global.WorldMgr.GetConfigMaxSkillValue())
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RequiredSkillPoints` = {1} but max possible skill is {2}, quest can't be done.",
                            qinfo.Id, qinfo.RequiredSkillPoints, Global.WorldMgr.GetConfigMaxSkillValue());
                        // no changes, quest can't be done for this requirement
                    }
                }
                // else Skill quests can have 0 skill level, this is ok

                if (qinfo.RequiredMinRepFaction != 0 && !CliDB.FactionStorage.ContainsKey(qinfo.RequiredMinRepFaction))
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RequiredMinRepFaction` = {1} but faction template {2} does not exist, quest can't be done.",
                        qinfo.Id, qinfo.RequiredMinRepFaction, qinfo.RequiredMinRepFaction);
                    // no changes, quest can't be done for this requirement
                }

                if (qinfo.RequiredMaxRepFaction != 0 && !CliDB.FactionStorage.ContainsKey(qinfo.RequiredMaxRepFaction))
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RequiredMaxRepFaction` = {1} but faction template {2} does not exist, quest can't be done.",
                        qinfo.Id, qinfo.RequiredMaxRepFaction, qinfo.RequiredMaxRepFaction);
                    // no changes, quest can't be done for this requirement
                }

                if (qinfo.RequiredMinRepValue != 0 && qinfo.RequiredMinRepValue > SharedConst.ReputationCap)
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RequiredMinRepValue` = {1} but max reputation is {2}, quest can't be done.",
                        qinfo.Id, qinfo.RequiredMinRepValue, SharedConst.ReputationCap);
                    // no changes, quest can't be done for this requirement
                }

                if (qinfo.RequiredMinRepValue != 0 && qinfo.RequiredMaxRepValue != 0 && qinfo.RequiredMaxRepValue <= qinfo.RequiredMinRepValue)
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RequiredMaxRepValue` = {1} and `RequiredMinRepValue` = {2}, quest can't be done.",
                        qinfo.Id, qinfo.RequiredMaxRepValue, qinfo.RequiredMinRepValue);
                    // no changes, quest can't be done for this requirement
                }

                if (qinfo.RequiredMinRepFaction == 0 && qinfo.RequiredMinRepValue != 0)
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RequiredMinRepValue` = {1} but `RequiredMinRepFaction` is 0, value has no effect",
                        qinfo.Id, qinfo.RequiredMinRepValue);
                    // warning
                }

                if (qinfo.RequiredMaxRepFaction == 0 && qinfo.RequiredMaxRepValue != 0)
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RequiredMaxRepValue` = {1} but `RequiredMaxRepFaction` is 0, value has no effect",
                        qinfo.Id, qinfo.RequiredMaxRepValue);
                    // warning
                }

                if (qinfo.RewardTitleId != 0 && !CliDB.CharTitlesStorage.ContainsKey(qinfo.RewardTitleId))
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `RewardTitleId` = {1} but CharTitle Id {1} does not exist, quest can't be rewarded with title.",
                        qinfo.Id, qinfo.RewardTitleId);
                    qinfo.RewardTitleId = 0;
                    // quest can't reward this title
                }

                if (qinfo.SourceItemId != 0)
                {
                    if (Global.ObjectMgr.GetItemTemplate(qinfo.SourceItemId) == null)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `SourceItemId` = {1} but item with entry {2} does not exist, quest can't be done.",
                            qinfo.Id, qinfo.SourceItemId, qinfo.SourceItemId);
                        qinfo.SourceItemId = 0;                       // quest can't be done for this requirement
                    }
                    else if (qinfo.SourceItemIdCount == 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `StartItem` = {1} but `ProvidedItemCount` = 0, set to 1 but need fix in DB.",
                            qinfo.Id, qinfo.SourceItemId);
                        qinfo.SourceItemIdCount = 1;                    // update to 1 for allow quest work for backward compatibility with DB
                    }
                }
                else if (qinfo.SourceItemIdCount > 0)
                {
                    Log.outError(LogFilter.Sql, "Quest {0} has `SourceItemId` = 0 but `SourceItemIdCount` = {1}, useless value.",
                        qinfo.Id, qinfo.SourceItemIdCount);
                    qinfo.SourceItemIdCount = 0;                          // no quest work changes in fact
                }

                if (qinfo.SourceSpellID != 0)
                {
                    SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(qinfo.SourceSpellID);
                    if (spellInfo == null)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `SourceSpellid` = {1} but spell {1} doesn't exist, quest can't be done.",
                            qinfo.Id, qinfo.SourceSpellID);
                        qinfo.SourceSpellID = 0;                        // quest can't be done for this requirement
                    }
                    else if (!Global.SpellMgr.IsSpellValid(spellInfo))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `SourceSpellid` = {1} but spell {1} is broken, quest can't be done.",
                            qinfo.Id, qinfo.SourceSpellID);
                        qinfo.SourceSpellID = 0;                        // quest can't be done for this requirement
                    }
                }

                foreach (QuestObjective obj in qinfo.Objectives)
                {
                    // Store objective for lookup by id
                    _questObjectives[obj.ID] = obj;

                    // Check storage index for objectives which store data
                    if (obj.StorageIndex < 0)
                    {
                        switch (obj.Type)
                        {
                            case QuestObjectiveType.Monster:
                            case QuestObjectiveType.Item:
                            case QuestObjectiveType.GameObject:
                            case QuestObjectiveType.TalkTo:
                            case QuestObjectiveType.PlayerKills:
                            case QuestObjectiveType.AreaTrigger:
                            case QuestObjectiveType.WinPetBattleAgainstNpc:
                            case QuestObjectiveType.ObtainCurrency:
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has invalid StorageIndex = {2} for objective type {3}", qinfo.Id, obj.ID, obj.StorageIndex, obj.Type);
                                break;
                            default:
                                break;
                        }
                    }

                    switch (obj.Type)
                    {
                        case QuestObjectiveType.Item:
                            qinfo.SetSpecialFlag(QuestSpecialFlags.Deliver);
                            if (GetItemTemplate((uint)obj.ObjectID) == null)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing item entry {2}, quest can't be done.", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.Monster:
                            qinfo.SetSpecialFlag(QuestSpecialFlags.Kill | QuestSpecialFlags.Cast);
                            if (GetCreatureTemplate((uint)obj.ObjectID) == null)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing creature entry {2}, quest can't be done.", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.GameObject:
                            qinfo.SetSpecialFlag(QuestSpecialFlags.Kill | QuestSpecialFlags.Cast);
                            if (GetGameObjectTemplate((uint)obj.ObjectID) == null)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing gameobject entry {2}, quest can't be done.", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.TalkTo:
                            // Need checks (is it creature only?)
                            qinfo.SetSpecialFlag(QuestSpecialFlags.Cast | QuestSpecialFlags.Speakto);
                            break;
                        case QuestObjectiveType.MinReputation:
                        case QuestObjectiveType.MaxReputation:
                            if (!CliDB.FactionStorage.ContainsKey((uint)obj.ObjectID))
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing faction id {2}", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.PlayerKills:
                            qinfo.SetSpecialFlag(QuestSpecialFlags.Kill);
                            if (obj.Amount <= 0)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has invalid player kills count {2}", qinfo.Id, obj.ID, obj.Amount);
                            break;
                        case QuestObjectiveType.Currency:
                        case QuestObjectiveType.HaveCurrency:
                        case QuestObjectiveType.ObtainCurrency:
                            if (!CliDB.CurrencyTypesStorage.ContainsKey((uint)obj.ObjectID))
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing currency {2}", qinfo.Id, obj.ID, obj.ObjectID);
                            if (obj.Amount <= 0)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has invalid currency amount {2}", qinfo.Id, obj.ID, obj.Amount);
                            break;
                        case QuestObjectiveType.LearnSpell:
                            if (!Global.SpellMgr.HasSpellInfo((uint)obj.ObjectID))
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing spell id {2}", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.WinPetBattleAgainstNpc:
                            if (obj.ObjectID != 0 && Global.ObjectMgr.GetCreatureTemplate((uint)obj.ObjectID) == null)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing creature entry {2}, quest can't be done.", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.DefeatBattlePet:
                            if (!CliDB.BattlePetSpeciesStorage.ContainsKey((uint)obj.ObjectID))
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing battlepet species id {2}", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.CriteriaTree:
                            if (!CliDB.CriteriaTreeStorage.ContainsKey((uint)obj.ObjectID))
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing criteria tree id {2}", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.AreaTrigger:
                            if (!CliDB.AreaTriggerStorage.ContainsKey((uint)obj.ObjectID) && obj.ObjectID != -1)
                                Log.outError(LogFilter.Sql, "Quest {0} objective {1} has non existing areatrigger id {2}", qinfo.Id, obj.ID, obj.ObjectID);
                            break;
                        case QuestObjectiveType.Money:
                        case QuestObjectiveType.WinPvpPetBattles:
                            break;
                        default:
                            Log.outError(LogFilter.Sql, "Quest {0} objective {1} has unhandled type {2}", qinfo.Id, obj.ID, obj.Type);
                            break;
                    }
                }

                for (var j = 0; j < SharedConst.QuestItemDropCount; j++)
                {
                    var id = qinfo.ItemDrop[j];
                    if (id != 0)
                    {
                        if (GetItemTemplate(id) == null)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RequiredSourceItemId{1}` = {2} but item with entry {2} does not exist, quest can't be done.",
                                qinfo.Id, j + 1, id);
                            // no changes, quest can't be done for this requirement
                        }
                    }
                    else
                    {
                        if (qinfo.ItemDropQuantity[j] > 0)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RequiredSourceItemId{1}` = 0 but `RequiredSourceItemCount{1}` = {2}.",
                                qinfo.Id, j + 1, qinfo.ItemDropQuantity[j]);
                            // no changes, quest ignore this data
                        }
                    }
                }

                for (var j = 0; j < SharedConst.QuestRewardChoicesCount; ++j)
                {
                    var id = qinfo.RewardChoiceItemId[j];
                    if (id != 0)
                    {
                        if (Global.ObjectMgr.GetItemTemplate(id) == null)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardChoiceItemId{1}` = {2} but item with entry {2} does not exist, quest will not reward this item.",
                                qinfo.Id, j + 1, id);
                            qinfo.RewardChoiceItemId[j] = 0;          // no changes, quest will not reward this
                        }

                        if (qinfo.RewardChoiceItemCount[j] == 0)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardChoiceItemId{1}` = {2} but `RewardChoiceItemCount{1}` = 0, quest can't be done.",
                                qinfo.Id, j + 1, id);
                            // no changes, quest can't be done
                        }
                    }
                    else if (qinfo.RewardChoiceItemCount[j] > 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardChoiceItemId{1}` = 0 but `RewardChoiceItemCount{1}` = {3}.",
                            qinfo.Id, j + 1, qinfo.RewardChoiceItemCount[j]);
                        // no changes, quest ignore this data
                    }
                }

                for (var j = 0; j < SharedConst.QuestRewardItemCount; ++j)
                {
                    var id = qinfo.RewardItemId[j];
                    if (id != 0)
                    {
                        if (Global.ObjectMgr.GetItemTemplate(id) == null)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardItemId{1}` = {2} but item with entry {3} does not exist, quest will not reward this item.",
                                qinfo.Id, j + 1, id, id);
                            qinfo.RewardItemId[j] = 0;                // no changes, quest will not reward this item
                        }

                        if (qinfo.RewardItemCount[j] == 0)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardItemId{1}` = {2} but `RewardItemIdCount{3}` = 0, quest will not reward this item.",
                                qinfo.Id, j + 1, id, j + 1);
                            // no changes
                        }
                    }
                    else if (qinfo.RewardItemCount[j] > 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardItemId{1}` = 0 but `RewardItemIdCount{2}` = {3}.",
                            qinfo.Id, j + 1, j + 1, qinfo.RewardItemCount[j]);
                        // no changes, quest ignore this data
                    }
                }

                for (var j = 0; j < SharedConst.QuestRewardReputationsCount; ++j)
                {
                    if (qinfo.RewardFactionId[j] != 0)
                    {
                        if (Math.Abs(qinfo.RewardFactionValue[j]) > 9)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has RewardFactionValueId{1} = {2}. That is outside the range of valid values (-9 to 9).", qinfo.Id, j + 1, qinfo.RewardFactionValue[j]);
                        }
                        if (!CliDB.FactionStorage.ContainsKey(qinfo.RewardFactionId[j]))
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardFactionId{1}` = {2} but raw faction (faction.dbc) {3} does not exist, quest will not reward reputation for this faction.",
                                qinfo.Id, j + 1, qinfo.RewardFactionId[j], qinfo.RewardFactionId[j]);
                            qinfo.RewardFactionId[j] = 0;            // quest will not reward this
                        }
                    }

                    else if (qinfo.RewardFactionOverride[j] != 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardFactionId{1}` = 0 but `RewardFactionValueIdOverride{2}` = {3}.",
                            qinfo.Id, j + 1, j + 1, qinfo.RewardFactionOverride[j]);
                        // no changes, quest ignore this data
                    }
                }

                for (uint i = 0; i < SharedConst.QuestRewardDisplaySpellCount; ++i)
                {
                    if (qinfo.RewardDisplaySpell[i] != 0)
                    {
                        SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(qinfo.RewardSpell);
                        if (spellInfo == null)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardSpell` = {1} but spell {2} does not exist, spell removed as display reward.",
                                qinfo.Id, qinfo.RewardSpell, qinfo.RewardSpell);
                            qinfo.RewardDisplaySpell[i] = 0;                        // no spell reward will display for this quest
                        }
                        else if (!Global.SpellMgr.IsSpellValid(spellInfo))
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardSpell` = {1} but spell {2} is broken, quest will not have a spell reward.",
                                qinfo.Id, qinfo.RewardSpell, qinfo.RewardSpell);
                            qinfo.RewardDisplaySpell[i] = 0;                        // no spell reward will display for this quest
                        }
                    }
                }

                if (qinfo.RewardSpell > 0)
                {
                    SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(qinfo.RewardSpell);
                    if (spellInfo == null)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardSpellCast` = {1} but spell {2} does not exist, quest will not have a spell reward.",
                            qinfo.Id, qinfo.RewardSpell, qinfo.RewardSpell);
                        qinfo.RewardSpell = 0;                    // no spell will be casted on player
                    }

                    else if (!Global.SpellMgr.IsSpellValid(spellInfo))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardSpellCast` = {1} but spell {2} is broken, quest will not have a spell reward.",
                            qinfo.Id, qinfo.RewardSpell, qinfo.RewardSpell);
                        qinfo.RewardSpell = 0;                    // no spell will be casted on player
                    }
                }

                if (qinfo.RewardMailTemplateId != 0)
                {
                    if (!CliDB.MailTemplateStorage.ContainsKey(qinfo.RewardMailTemplateId))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardMailTemplateId` = {1} but mail template {2} does not exist, quest will not have a mail reward.",
                            qinfo.Id, qinfo.RewardMailTemplateId, qinfo.RewardMailTemplateId);
                        qinfo.RewardMailTemplateId = 0;               // no mail will send to player
                        qinfo.RewardMailDelay = 0;                // no mail will send to player
                        qinfo.RewardMailSenderEntry = 0;
                    }
                    else if (usedMailTemplates.ContainsKey(qinfo.RewardMailTemplateId))
                    {
                        var usedId = usedMailTemplates.LookupByKey(qinfo.RewardMailTemplateId);
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardMailTemplateId` = {1} but mail template  {2} already used for quest {3}, quest will not have a mail reward.",
                            qinfo.Id, qinfo.RewardMailTemplateId, qinfo.RewardMailTemplateId, usedId);
                        qinfo.RewardMailTemplateId = 0;               // no mail will send to player
                        qinfo.RewardMailDelay = 0;                // no mail will send to player
                        qinfo.RewardMailSenderEntry = 0;
                    }
                    else
                        usedMailTemplates[qinfo.RewardMailTemplateId] = qinfo.Id;
                }

                if (qinfo.NextQuestInChain != 0)
                {
                    var qNext = _questTemplates.LookupByKey(qinfo.NextQuestInChain);
                    if (qNext == null)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `NextQuestIdChain` = {1} but quest {2} does not exist, quest chain will not work.",
                            qinfo.Id, qinfo.NextQuestInChain, qinfo.NextQuestInChain);
                        qinfo.NextQuestInChain = 0;
                    }
                    else
                        qNext.prevChainQuests.Add(qinfo.Id);
                }

                for (var j = 0; j < SharedConst.QuestRewardCurrencyCount; ++j)
                {
                    if (qinfo.RewardCurrencyId[j] != 0)
                    {
                        if (qinfo.RewardCurrencyCount[j] == 0)
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardCurrencyId{1}` = {2} but `RewardCurrencyCount{3}` = 0, quest can't be done.",
                                qinfo.Id, j + 1, qinfo.RewardCurrencyId[j], j + 1);
                            // no changes, quest can't be done for this requirement
                        }

                        if (!CliDB.CurrencyTypesStorage.ContainsKey(qinfo.RewardCurrencyId[j]))
                        {
                            Log.outError(LogFilter.Sql, "Quest {0} has `RewardCurrencyId{1}` = {2} but currency with entry {3} does not exist, quest can't be done.",
                                qinfo.Id, j + 1, qinfo.RewardCurrencyId[j], qinfo.RewardCurrencyId[j]);
                            qinfo.RewardCurrencyCount[j] = 0;             // prevent incorrect work of quest
                        }
                    }
                    else if (qinfo.RewardCurrencyCount[j] > 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardCurrencyId{1}` = 0 but `RewardCurrencyCount{2}` = {3}, quest can't be done.",
                            qinfo.Id, j + 1, j + 1, qinfo.RewardCurrencyCount[j]);
                        qinfo.RewardCurrencyCount[j] = 0;                 // prevent incorrect work of quest
                    }
                }

                if (qinfo.SoundAccept != 0)
                {
                    if (!CliDB.SoundKitStorage.ContainsKey(qinfo.SoundAccept))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `SoundAccept` = {1} but sound {2} does not exist, set to 0.",
                            qinfo.Id, qinfo.SoundAccept, qinfo.SoundAccept);
                        qinfo.SoundAccept = 0;                        // no sound will be played
                    }
                }

                if (qinfo.SoundTurnIn != 0)
                {
                    if (!CliDB.SoundKitStorage.ContainsKey(qinfo.SoundTurnIn))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `SoundTurnIn` = {1} but sound {2} does not exist, set to 0.",
                            qinfo.Id, qinfo.SoundTurnIn, qinfo.SoundTurnIn);
                        qinfo.SoundTurnIn = 0;                        // no sound will be played
                    }
                }

                if (qinfo.RewardSkillId > 0)
                {
                    if (!CliDB.SkillLineStorage.ContainsKey(qinfo.RewardSkillId))
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardSkillId` = {1} but this skill does not exist",
                            qinfo.Id, qinfo.RewardSkillId);
                    }
                    if (qinfo.RewardSkillPoints == 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardSkillId` = {1} but `RewardSkillPoints` is 0",
                            qinfo.Id, qinfo.RewardSkillId);
                    }
                }

                if (qinfo.RewardSkillPoints != 0)
                {
                    if (qinfo.RewardSkillPoints > Global.WorldMgr.GetConfigMaxSkillValue())
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardSkillPoints` = {1} but max possible skill is {2}, quest can't be done.",
                            qinfo.Id, qinfo.RewardSkillPoints, Global.WorldMgr.GetConfigMaxSkillValue());
                        // no changes, quest can't be done for this requirement
                    }
                    if (qinfo.RewardSkillId == 0)
                    {
                        Log.outError(LogFilter.Sql, "Quest {0} has `RewardSkillPoints` = {1} but `RewardSkillId` is 0",
                            qinfo.Id, qinfo.RewardSkillPoints);
                    }
                }

                // fill additional data stores
                if (qinfo.PrevQuestId != 0)
                {
                    if (!_questTemplates.ContainsKey((uint)Math.Abs(qinfo.PrevQuestId)))
                        Log.outError(LogFilter.Sql, "Quest {0} has PrevQuestId {1}, but no such quest", qinfo.Id, qinfo.PrevQuestId);
                    else
                        qinfo.prevQuests.Add(qinfo.PrevQuestId);
                }

                if (qinfo.NextQuestId != 0)
                {
                    var nextquest = _questTemplates.LookupByKey((uint)Math.Abs(qinfo.NextQuestId));
                    if (nextquest == null)
                        Log.outError(LogFilter.Sql, "Quest {0} has NextQuestId {1}, but no such quest", qinfo.Id, qinfo.NextQuestId);
                    else
                    {
                        int signedQuestId = qinfo.NextQuestId < 0 ? -(int)qinfo.Id : (int)qinfo.Id;
                        nextquest.prevQuests.Add(signedQuestId);
                    }
                }

                if (qinfo.ExclusiveGroup != 0)
                    _exclusiveQuestGroups.Add(qinfo.ExclusiveGroup, qinfo.Id);
                if (qinfo.LimitTime != 0)
                    qinfo.SetSpecialFlag(QuestSpecialFlags.Timed);
            }

            // check QUEST_SPECIAL_FLAGS_EXPLORATION_OR_EVENT for spell with SPELL_EFFECT_QUEST_COMPLETE
            foreach (var spellInfo in Global.SpellMgr.GetSpellInfoStorage().Values)
            {
                foreach (SpellEffectInfo effect in spellInfo.GetEffectsForDifficulty(Difficulty.None))
                {
                    if (effect == null || effect.Effect != SpellEffectName.QuestComplete)
                        continue;

                    uint questId = (uint)effect.MiscValue;
                    Quest quest = GetQuestTemplate(questId);

                    // some quest referenced in spells not exist (outdated spells)
                    if (quest == null)
                        continue;

                    if (!quest.HasSpecialFlag(QuestSpecialFlags.ExplorationOrEvent))
                    {
                        Log.outError(LogFilter.Sql, "Spell (id: {0}) have SPELL_EFFECT_QUEST_COMPLETE for quest {1}, but quest not have flag QUEST_SPECIAL_FLAGS_EXPLORATION_OR_EVENT. " +
                            "Quest flags must be fixed, quest modified to enable objective.", spellInfo.Id, questId);

                        // this will prevent quest completing without objective
                        quest.SetSpecialFlag(QuestSpecialFlags.ExplorationOrEvent);
                    }
                }
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quests definitions in {1} ms", _questTemplates.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestStartersAndEnders()
        {
            Log.outInfo(LogFilter.ServerLoading, "Loading GO Start Quest Data...");
            LoadGameobjectQuestStarters();
            Log.outInfo(LogFilter.ServerLoading, "Loading GO End Quest Data...");
            LoadGameobjectQuestEnders();
            Log.outInfo(LogFilter.ServerLoading, "Loading Creature Start Quest Data...");
            LoadCreatureQuestStarters();
            Log.outInfo(LogFilter.ServerLoading, "Loading Creature End Quest Data...");
            LoadCreatureQuestEnders();
        }
        public void LoadGameobjectQuestStarters()
        {
            LoadQuestRelationsHelper(_goQuestRelations, null, "gameobject_queststarter", true, true);

            foreach (var pair in _goQuestRelations)
            {
                GameObjectTemplate goInfo = GetGameObjectTemplate(pair.Key);
                if (goInfo == null)
                    Log.outError(LogFilter.Sql, "Table `gameobject_queststarter` have data for not existed gameobject entry ({0}) and existed quest {1}", pair.Key, pair.Value);
                else if (goInfo.type != GameObjectTypes.QuestGiver)
                    Log.outError(LogFilter.Sql, "Table `gameobject_queststarter` have data gameobject entry ({0}) for quest {1}, but GO is not GAMEOBJECT_TYPE_QUESTGIVER", pair.Key, pair.Value);
            }
        }
        public void LoadGameobjectQuestEnders()
        {
            LoadQuestRelationsHelper(_goQuestInvolvedRelations, _goQuestInvolvedRelationsReverse, "gameobject_questender", false, true);

            foreach (var pair in _goQuestInvolvedRelations)
            {
                GameObjectTemplate goInfo = GetGameObjectTemplate(pair.Key);
                if (goInfo == null)
                    Log.outError(LogFilter.Sql, "Table `gameobject_questender` have data for not existed gameobject entry ({0}) and existed quest {1}", pair.Key, pair.Value);
                else if (goInfo.type != GameObjectTypes.QuestGiver)
                    Log.outError(LogFilter.Sql, "Table `gameobject_questender` have data gameobject entry ({0}) for quest {1}, but GO is not GAMEOBJECT_TYPE_QUESTGIVER", pair.Key, pair.Value);
            }
        }
        public void LoadCreatureQuestStarters()
        {
            LoadQuestRelationsHelper(_creatureQuestRelations, null, "creature_queststarter", true, false);

            foreach (var pair in _creatureQuestRelations)
            {
                CreatureTemplate cInfo = GetCreatureTemplate(pair.Key);
                if (cInfo == null)
                    Log.outError(LogFilter.Sql, "Table `creature_queststarter` have data for not existed creature entry ({0}) and existed quest {1}", pair.Key, pair.Value);
                else if (!Convert.ToBoolean(cInfo.Npcflag & NPCFlags.QuestGiver))
                    Log.outError(LogFilter.Sql, "Table `creature_queststarter` has creature entry ({0}) for quest {1}, but npcflag does not include UNIT_NPC_FLAG_QUESTGIVER", pair.Key, pair.Value);
            }
        }
        public void LoadCreatureQuestEnders()
        {
            LoadQuestRelationsHelper(_creatureQuestInvolvedRelations, _creatureQuestInvolvedRelationsReverse, "creature_questender", false, false);

            foreach (var pair in _creatureQuestInvolvedRelations)
            {
                CreatureTemplate cInfo = GetCreatureTemplate(pair.Key);
                if (cInfo == null)
                    Log.outError(LogFilter.Sql, "Table `creature_questender` have data for not existed creature entry ({0}) and existed quest {1}", pair.Key, pair.Value);
                else if (!Convert.ToBoolean(cInfo.Npcflag & NPCFlags.QuestGiver))
                    Log.outError(LogFilter.Sql, "Table `creature_questender` has creature entry ({0}) for quest {1}, but npcflag does not include UNIT_NPC_FLAG_QUESTGIVER", pair.Key, pair.Value);
            }
        }
        void LoadQuestRelationsHelper(MultiMap<uint, uint> map, MultiMap<uint, uint> reverseMap, string table, bool starter, bool go)
        {
            uint oldMSTime = Time.GetMSTime();

            map.Clear();                                            // need for reload case

            uint count = 0;

            SQLResult result = DB.World.Query("SELECT id, quest, pool_entry FROM {0} qr LEFT JOIN pool_quest pq ON qr.quest = pq.entry", table);

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest relations from `{0}`, table is empty.", table);
                return;
            }

            var poolRelationMap = go ? Global.PoolMgr.mQuestGORelation : Global.PoolMgr.mQuestCreatureRelation;
            if (starter)
                poolRelationMap.Clear();

            do
            {
                uint id = result.Read<uint>(0);
                uint quest = result.Read<uint>(1);
                uint poolId = result.Read<uint>(2);

                if (!_questTemplates.ContainsKey(quest))
                {
                    Log.outError(LogFilter.Sql, "Table `{0}`: Quest {1} listed for entry {2} does not exist.", table, quest, id);
                    continue;
                }

                if (poolId == 0 || !starter)
                {
                    map.Add(id, quest);
                    if (reverseMap != null)
                        reverseMap.Add(quest, id);
                }
                else if (starter)
                    poolRelationMap.Add(quest, id);

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quest relations from {1} in {2} ms", count, table, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestPOI()
        {
            uint oldMSTime = Time.GetMSTime();

            _questPOIStorage.Clear();                              // need for reload case

            uint count = 0;

            //                                            0        1        2            3           4                 5           6         7            8       9       10         11              12             13
            SQLResult result = DB.World.Query("SELECT QuestID, BlobIndex, Idx1, ObjectiveIndex, QuestObjectiveID, QuestObjectID, MapID, WorldMapAreaId, Floor, Priority, Flags, WorldEffectID, PlayerConditionID, WoDUnk1 FROM quest_poi order by QuestID, Idx1");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 quest POI definitions. DB table `quest_poi` is empty.");
                return;
            }

            //                                         0        1    2  3 
            SQLResult points = DB.World.Query("SELECT QuestID, Idx1, X, Y FROM quest_poi_points ORDER BY QuestID DESC, Idx1, Idx2");
            Dictionary<uint, MultiMap<int, QuestPOIPoint>> POIs = new Dictionary<uint, MultiMap<int, QuestPOIPoint>>();

            if (!points.IsEmpty())
            {
                do
                {
                    uint questId = points.Read<uint>(0);
                    int Idx1 = points.Read<int>(1);
                    int x = points.Read<int>(2);
                    int y = points.Read<int>(3);

                    if (!POIs.ContainsKey(questId))
                        POIs[questId] = new MultiMap<int, QuestPOIPoint>();

                    QuestPOIPoint point = new QuestPOIPoint(x, y);
                    POIs[questId].Add(Idx1, point);
                } while (points.NextRow());
            }

            do
            {
                uint QuestID = Convert.ToUInt32(result.Read<int>(0));
                int BlobIndex = result.Read<int>(1);
                int Idx1 = result.Read<int>(2);
                int ObjectiveIndex = result.Read<int>(3);
                int QuestObjectiveID = result.Read<int>(4);
                int QuestObjectID = result.Read<int>(5);
                int MapID = result.Read<int>(6);
                int WorldMapAreaId = result.Read<int>(7);
                int Floor = result.Read<int>(8);
                int Priority = result.Read<int>(9);
                int Flags = result.Read<int>(10);
                int WorldEffectID = result.Read<int>(11);
                int PlayerConditionID = result.Read<int>(12);
                int WoDUnk1 = result.Read<int>(13);

                if (Global.ObjectMgr.GetQuestTemplate(QuestID) == null)
                    Log.outError(LogFilter.Sql, "`quest_poi` quest id ({0}) Idx1 ({1}) does not exist in `quest_template`", QuestID, Idx1);

                QuestPOI POI = new QuestPOI(BlobIndex, ObjectiveIndex, QuestObjectiveID, QuestObjectID, MapID, WorldMapAreaId, Floor, Priority, Flags, WorldEffectID, PlayerConditionID, WoDUnk1);
                if (!POIs.ContainsKey(QuestID) || !POIs[QuestID].ContainsKey(Idx1))
                {
                    Log.outError(LogFilter.Sql, "Table quest_poi references unknown quest points for quest {0} POI id {1}", QuestID, BlobIndex);
                    continue;
                }
                POI.points = POIs[QuestID][Idx1];
                _questPOIStorage.Add(QuestID, POI);

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quest POI definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestAreaTriggers()
        {
            uint oldMSTime = Time.GetMSTime();

            _questAreaTriggerStorage.Clear();                           // need for reload case

            SQLResult result = DB.World.Query("SELECT id, quest FROM areatrigger_involvedrelation");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 quest trigger points. DB table `areatrigger_involvedrelation` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                ++count;

                uint trigger_ID = result.Read<uint>(0);
                uint quest_ID = result.Read<uint>(1);

                AreaTriggerRecord atEntry = CliDB.AreaTriggerStorage.LookupByKey(trigger_ID);
                if (atEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Area trigger (ID:{0}) does not exist in `AreaTrigger.dbc`.", trigger_ID);
                    continue;
                }

                Quest quest = GetQuestTemplate(quest_ID);

                if (quest == null)
                {
                    Log.outError(LogFilter.Sql, "Table `areatrigger_involvedrelation` has record (id: {0}) for not existing quest {1}", trigger_ID, quest_ID);
                    continue;
                }

                if (!quest.HasSpecialFlag(QuestSpecialFlags.ExplorationOrEvent))
                {
                    Log.outError(LogFilter.Sql, "Table `areatrigger_involvedrelation` has record (id: {0}) for not quest {1}, but quest not have flag QUEST_SPECIAL_FLAGS_EXPLORATION_OR_EVENT. Trigger or quest flags must be fixed, quest modified to require objective.", trigger_ID, quest_ID);

                    // this will prevent quest completing without objective
                    quest.SetSpecialFlag(QuestSpecialFlags.ExplorationOrEvent);

                    // continue; - quest modified to required objective and trigger can be allowed.
                }

                _questAreaTriggerStorage.Add(trigger_ID, quest_ID);

            } while (result.NextRow());

            foreach (var pair in _questObjectives)
            {
                QuestObjective objective = pair.Value;
                if (objective.Type == QuestObjectiveType.AreaTrigger)
                    _questAreaTriggerStorage.Add((uint)objective.ObjectID, objective.QuestID);
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quest trigger points in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestGreetings()
        {
            uint oldMSTime = Time.GetMSTime();

            _questGreetingStorage.Clear(); // need for reload case

            //                                                0   1          2                3     
            SQLResult result = DB.World.Query("SELECT ID, type, GreetEmoteType, GreetEmoteDelay, Greeting FROM quest_greeting");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 npc texts, table is empty!");
                return;
            }

            do
            {
                uint id = result.Read<uint>(0);
                byte type = result.Read<byte>(1);
                // overwrite
                switch (type)
                {
                    case 0: // Creature
                        type = (byte)TypeId.Unit;
                        if (Global.ObjectMgr.GetCreatureTemplate(id) == null)
                        {
                            Log.outError(LogFilter.Sql, "Table `quest_greeting`: creature template entry {0} does not exist.", id);
                            continue;
                        }
                        break;
                    case 1: // GameObject
                        type = (byte)TypeId.GameObject;
                        if (Global.ObjectMgr.GetGameObjectTemplate(id) == null)
                        {
                            Log.outError(LogFilter.Sql, "Table `quest_greeting`: gameobject template entry {0} does not exist.", id);
                            continue;
                        }
                        break;
                    default:
                        continue;
                }

                ushort greetEmoteType = result.Read<ushort>(2);
                uint greetEmoteDelay = result.Read<uint>(3);
                string greeting = result.Read<string>(4);

                if (!_questGreetingStorage.ContainsKey(type))
                    _questGreetingStorage[type] = new Dictionary<uint, QuestGreeting>();

                _questGreetingStorage[type][id] = new QuestGreeting(greetEmoteType, greetEmoteDelay, greeting);
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quest_greeting in {1} ms", _questGreetingStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public Quest GetQuestTemplate(uint questId)
        {
            return _questTemplates.LookupByKey(questId);
        }
        public Dictionary<uint, Quest> GetQuestTemplates()
        {
            return _questTemplates;
        }
        public Dictionary<uint, Quest> GetQuestStorage()
        {
            return _questTemplates;
        }
        public MultiMap<uint, uint> GetGOQuestRelationMap()
        {
            return _goQuestRelations;
        }
        public List<uint> GetGOQuestRelationBounds(uint go_entry)
        {
            return _goQuestRelations.LookupByKey(go_entry);
        }
        public List<uint> GetGOQuestInvolvedRelationBounds(uint go_entry)
        {
            return _goQuestInvolvedRelations.LookupByKey(go_entry);
        }
        public List<uint> GetGOQuestInvolvedRelationReverseBounds(uint questId)
        {
            return _goQuestInvolvedRelationsReverse.LookupByKey(questId);
        }
        public MultiMap<uint, uint> GetCreatureQuestRelationMap()
        {
            return _creatureQuestRelations;
        }
        public List<uint> GetCreatureQuestRelationBounds(uint creature_entry)
        {
            return _creatureQuestRelations.LookupByKey(creature_entry);
        }
        public List<uint> GetCreatureQuestInvolvedRelationBounds(uint creature_entry)
        {
            return _creatureQuestInvolvedRelations.LookupByKey(creature_entry);
        }
        public List<uint> GetCreatureQuestInvolvedRelationReverseBounds(uint questId)
        {
            return _creatureQuestInvolvedRelationsReverse.LookupByKey(questId);
        }
        public List<QuestPOI> GetQuestPOIList(uint questId)
        {
            return _questPOIStorage.LookupByKey(questId);
        }
        public QuestObjective GetQuestObjective(uint questObjectiveId)
        {
            return _questObjectives.LookupByKey(questObjectiveId);
        }
        public List<uint> GetQuestsForAreaTrigger(uint triggerId)
        {
            return _questAreaTriggerStorage.LookupByKey(triggerId);
        }
        public QuestGreeting GetQuestGreeting(ObjectGuid guid)
        {
            var dic = _questGreetingStorage.LookupByKey(guid.GetTypeId());
            if (dic.Empty())
                return null;

            var greeting = dic.LookupByKey(guid.GetEntry());
            if (greeting == null)
                return null;

            return greeting;
        }

        //Spells /Skills / Phases
        public void LoadTerrainSwapDefaults()
        {
            _terrainMapDefaultStore.Clear();

            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT MapId, TerrainSwapMap FROM `terrain_swap_defaults`");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 terrain swap defaults. DB table `terrain_swap_defaults` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint mapId = result.Read<uint>(0);
                if (!CliDB.MapStorage.ContainsKey(mapId))
                {
                    Log.outError(LogFilter.Sql, "Map {0} defined in `terrain_swap_defaults` does not exist, skipped.", mapId);
                    continue;
                }

                uint terrainSwap = result.Read<uint>(1);
                if (!CliDB.MapStorage.ContainsKey(terrainSwap))
                {
                    Log.outError(LogFilter.Sql, "TerrainSwapMap {0} defined in `terrain_swap_defaults` does not exist, skipped.", terrainSwap);
                    continue;
                }

                _terrainMapDefaultStore.Add(mapId, terrainSwap);

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} terrain swap defaults in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadTerrainPhaseInfo()
        {
            _terrainPhaseInfoStore.Clear();

            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT Id, TerrainSwapMap FROM `terrain_phase_info`");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 terrain phase infos. DB table `terrain_phase_info` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint phaseId = result.Read<uint>(0);
                if (!CliDB.PhaseStorage.ContainsKey(phaseId))
                {
                    Log.outError(LogFilter.Sql, "Phase {0} defined in `terrain_phase_info` does not exist, skipped.", phaseId);
                    continue;
                }

                uint terrainSwap = result.Read<uint>(1);

                _terrainPhaseInfoStore.Add(phaseId, terrainSwap);

                ++count;
            }
            while (result.NextRow());
            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} phase infos in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadTerrainWorldMaps()
        {
            _terrainWorldMapStore.Clear();

            uint oldMSTime = Time.GetMSTime();

            //                                               0               1
            SQLResult result = DB.World.Query("SELECT TerrainSwapMap, WorldMapArea FROM `terrain_worldmap`");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 terrain world maps. DB table `terrain_worldmap` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint mapId = result.Read<uint>(0);

                if (!CliDB.MapStorage.ContainsKey(mapId))
                {
                    Log.outError(LogFilter.Sql, "TerrainSwapMap {0} defined in `terrain_worldmap` does not exist, skipped.", mapId);
                    continue;
                }

                uint worldMapArea = result.Read<uint>(1);

                _terrainWorldMapStore.Add(mapId, worldMapArea);

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} terrain world maps in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadAreaPhases()
        {
            _phases.Clear();

            uint oldMSTime = Time.GetMSTime();

            //                                               0       1
            SQLResult result = DB.World.Query("SELECT AreaId, PhaseId FROM `phase_area`");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 phase areas. DB table `phase_area` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                PhaseInfoStruct phase = new PhaseInfoStruct();
                uint area = result.Read<uint>(0);
                phase.Id = result.Read<uint>(1);

                _phases.Add(area, phase);

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} phase areas in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadNPCSpellClickSpells()
        {
            uint oldMSTime = Time.GetMSTime();

            _spellClickInfoStorage.Clear();
            //                                           0          1         2            3
            SQLResult result = DB.World.Query("SELECT npc_entry, spell_id, cast_flags, user_type FROM npc_spellclick_spells");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 spellclick spells. DB table `npc_spellclick_spells` is empty.");
                return;
            }

            uint count = 0;

            do
            {
                uint npc_entry = result.Read<uint>(0);
                CreatureTemplate cInfo = GetCreatureTemplate(npc_entry);
                if (cInfo == null)
                {
                    Log.outError(LogFilter.Sql, "Table npc_spellclick_spells references unknown creature_template {0}. Skipping entry.", npc_entry);
                    continue;
                }

                uint spellid = result.Read<uint>(1);
                SpellInfo spellinfo = Global.SpellMgr.GetSpellInfo(spellid);
                if (spellinfo == null)
                {
                    Log.outError(LogFilter.Sql, "Table npc_spellclick_spells creature: {0} references unknown spellid {1}. Skipping entry.", npc_entry, spellid);
                    continue;
                }

                SpellClickUserTypes userType = (SpellClickUserTypes)result.Read<byte>(3);
                if (userType >= SpellClickUserTypes.Max)
                    Log.outError(LogFilter.Sql, "Table npc_spellclick_spells creature: {0} references unknown user type {1}. Skipping entry.", npc_entry, userType);

                byte castFlags = result.Read<byte>(2);
                SpellClickInfo info = new SpellClickInfo();
                info.spellId = spellid;
                info.castFlags = castFlags;
                info.userType = userType;
                _spellClickInfoStorage.Add(npc_entry, info);

                ++count;
            }
            while (result.NextRow());

            // all spellclick data loaded, now we check if there are creatures with NPC_FLAG_SPELLCLICK but with no data
            // NOTE: It *CAN* be the other way around: no spellclick flag but with spellclick data, in case of creature-only vehicle accessories
            var ctc = GetCreatureTemplates();
            foreach (var creature in ctc.Values)
            {
                if (creature.Npcflag.HasAnyFlag(NPCFlags.SpellClick) && !_spellClickInfoStorage.ContainsKey(creature.Entry))
                {
                    Log.outError(LogFilter.Sql, "npc_spellclick_spells: Creature template {0} has UNIT_NPC_FLAG_SPELLCLICK but no data in spellclick table! Removing flag", creature.Entry);
                    creature.Npcflag &= ~NPCFlags.SpellClick;
                }
            }

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} spellclick definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadFishingBaseSkillLevel()
        {
            uint oldMSTime = Time.GetMSTime();

            _fishingBaseForAreaStorage.Clear();                            // for reload case

            SQLResult result = DB.World.Query("SELECT entry, skill FROM skill_fishing_base_level");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 areas for fishing base skill level. DB table `skill_fishing_base_level` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint entry = result.Read<uint>(0);
                int skill = result.Read<int>(1);

                AreaTableRecord fArea = CliDB.AreaTableStorage.LookupByKey(entry);
                if (fArea == null)
                {
                    Log.outError(LogFilter.Sql, "AreaId {0} defined in `skill_fishing_base_level` does not exist", entry);
                    continue;
                }

                _fishingBaseForAreaStorage[entry] = skill;
                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} areas for fishing base skill level in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadSkillTiers()
        {
            uint oldMSTime = Time.GetMSTime();

            _skillTiers.Clear();

            SQLResult result = DB.World.Query("SELECT ID, Value1, Value2, Value3, Value4, Value5, Value6, Value7, Value8, Value9, Value10, " +
                " Value11, Value12, Value13, Value14, Value15, Value16 FROM skill_tiers");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 skill max values. DB table `skill_tiers` is empty.");
                return;
            }

            do
            {
                uint id = result.Read<uint>(0);
                SkillTiersEntry tier = new SkillTiersEntry();
                for (int i = 0; i < SkillConst.MaxSkillStep; ++i)
                    tier.Value[i] = result.Read<uint>(1 + i);

                _skillTiers[id] = tier;

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} skill max values in {1} ms", _skillTiers.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public List<uint> GetPhaseTerrainSwaps(uint phaseid) { return _terrainPhaseInfoStore[phaseid]; }
        public List<uint> GetDefaultTerrainSwaps(uint mapid) { return _terrainMapDefaultStore[mapid]; }
        public List<uint> GetTerrainWorldMaps(uint terrainId) { return _terrainWorldMapStore[terrainId]; }
        public MultiMap<uint, uint> GetDefaultTerrainSwapStore() { return _terrainMapDefaultStore; }
        public List<PhaseInfoStruct> GetPhasesForArea(uint area) { return _phases[area]; }
        public MultiMap<uint, PhaseInfoStruct> GetAreaAndZonePhases() { return _phases; }
        public List<PhaseInfoStruct> GetPhasesForAreaOrZoneForLoading(uint areaOrZone)
        {
            return _phases.LookupByKey(areaOrZone);
        }
        public List<SpellClickInfo> GetSpellClickInfoMapBounds(uint creature_id)
        {
            return _spellClickInfoStorage.LookupByKey(creature_id);
        }
        public int GetFishingBaseSkillLevel(uint entry)
        {
            return _fishingBaseForAreaStorage.LookupByKey(entry);
        }
        public SkillTiersEntry GetSkillTier(uint skillTierId)
        {
            return _skillTiers.LookupByKey(skillTierId);
        }

        //Locales
        public void LoadCreatureLocales()
        {
            uint oldMSTime = Time.GetMSTime();

            _creatureLocaleStorage.Clear(); // need for reload case

            //                                         0      1       2     3        4      5
            SQLResult result = DB.World.Query("SELECT entry, locale, Name, NameAlt, Title, TitleAlt FROM creature_template_locale");

            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);
                string name = result.Read<string>(2);
                string nameAlt = result.Read<string>(3);
                string title = result.Read<string>(4);
                string titleAlt = result.Read<string>(5);

                if (!_creatureLocaleStorage.ContainsKey(id))
                    _creatureLocaleStorage[id] = new CreatureLocale();

                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                CreatureLocale data = _creatureLocaleStorage[id];
                AddLocaleString(name, locale, data.Name);
                AddLocaleString(nameAlt, locale, data.NameAlt);
                AddLocaleString(title, locale, data.Title);
                AddLocaleString(titleAlt, locale, data.TitleAlt);

            } while (result.NextRow());
        }
        public void LoadGameObjectLocales()
        {
            uint oldMSTime = Time.GetMSTime();

            _gameObjectLocaleStorage.Clear(); // need for reload case

            //                                               0      1       2     3               4
            SQLResult result = DB.World.Query("SELECT entry, locale, name, castBarCaption, unk1 FROM gameobject_template_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);

                string name = result.Read<string>(2);
                string castBarCaption = result.Read<string>(3);
                string unk1 = result.Read<string>(4);

                if (!_gameObjectLocaleStorage.ContainsKey(id))
                    _gameObjectLocaleStorage[id] = new GameObjectLocale();

                GameObjectLocale data = _gameObjectLocaleStorage[id];
                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(name, locale, data.Name);
                AddLocaleString(castBarCaption, locale, data.CastBarCaption);
                AddLocaleString(unk1, locale, data.Unk1);

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gameobject_template_locale locale strings in {1} ms", _gameObjectLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestTemplateLocale()
        {
            uint oldMSTime = Time.GetMSTime();

            _questObjectivesLocaleStorage.Clear(); // need for reload case
            //                                         0     1     2           3                 4                5                 6                  7                   8                   9                  10
            SQLResult result = DB.World.Query("SELECT Id, locale, LogTitle, LogDescription, QuestDescription, AreaDescription, PortraitGiverText, PortraitGiverName, PortraitTurnInText, PortraitTurnInName, QuestCompletionLog" +
                " FROM quest_template_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);

                string logTitle = result.Read<string>(2);
                string logDescription = result.Read<string>(3);
                string questDescription = result.Read<string>(4);
                string areaDescription = result.Read<string>(5);
                string portraitGiverText = result.Read<string>(6);
                string portraitGiverName = result.Read<string>(7);
                string portraitTurnInText = result.Read<string>(8);
                string portraitTurnInName = result.Read<string>(9);
                string questCompletionLog = result.Read<string>(10);

                if (!_questTemplateLocaleStorage.ContainsKey(id))
                    _questTemplateLocaleStorage[id] = new QuestTemplateLocale();

                QuestTemplateLocale data = _questTemplateLocaleStorage[id];
                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(logTitle, locale, data.LogTitle);
                AddLocaleString(logDescription, locale, data.LogDescription);
                AddLocaleString(questDescription, locale, data.QuestDescription);
                AddLocaleString(areaDescription, locale, data.AreaDescription);
                AddLocaleString(portraitGiverText, locale, data.PortraitGiverText);
                AddLocaleString(portraitGiverName, locale, data.PortraitGiverName);
                AddLocaleString(portraitTurnInText, locale, data.PortraitTurnInText);
                AddLocaleString(portraitTurnInName, locale, data.PortraitTurnInName);
                AddLocaleString(questCompletionLog, locale, data.QuestCompletionLog);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Quest Tempalate locale strings in {1} ms", _questTemplateLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestObjectivesLocale()
        {
            uint oldMSTime = Time.GetMSTime();

            _questObjectivesLocaleStorage.Clear(); // need for reload case
            //                                        0     1          2
            SQLResult result = DB.World.Query("SELECT Id, locale, Description FROM quest_objectives_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);

                string Description = result.Read<string>(2);

                if (!_questObjectivesLocaleStorage.ContainsKey(id))
                    _questObjectivesLocaleStorage[id] = new QuestObjectivesLocale();

                QuestObjectivesLocale data = _questObjectivesLocaleStorage[id];
                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(Description, locale, data.Description);
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Quest Objectives locale strings in {1} ms", _questObjectivesLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestOfferRewardLocale()
        {
            uint oldMSTime = Time.GetMSTime();

            _questOfferRewardLocaleStorage.Clear(); // need for reload case
                                                    //                                               0     1          2
            SQLResult result = DB.World.Query("SELECT Id, locale, RewardText FROM quest_offer_reward_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);

                string RewardText = result.Read<string>(2);

                if (!_questOfferRewardLocaleStorage.ContainsKey(id))
                    _questOfferRewardLocaleStorage[id] = new QuestOfferRewardLocale();

                QuestOfferRewardLocale data = _questOfferRewardLocaleStorage[id];
                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(RewardText, locale, data.RewardText);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Quest Offer Reward locale strings in {1} ms", _questOfferRewardLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadQuestRequestItemsLocale()
        {
            uint oldMSTime = Time.GetMSTime();

            _questRequestItemsLocaleStorage.Clear(); // need for reload case
                                                   //                                               0     1          2
            SQLResult result = DB.World.Query("SELECT Id, locale, CompletionText FROM quest_request_items_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);
                string completionText = result.Read<string>(2);

                if (!_questRequestItemsLocaleStorage.ContainsKey(id))
                    _questRequestItemsLocaleStorage[id] = new QuestRequestItemsLocale();

                QuestRequestItemsLocale data = _questRequestItemsLocaleStorage[id];
                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(completionText, locale, data.CompletionText);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Quest Request Items locale strings in {1} ms", _questRequestItemsLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadGossipMenuItemsLocales()
        {
            uint oldMSTime = Time.GetMSTime();

            _gossipMenuItemsLocaleStorage.Clear();                              // need for reload case

            SQLResult result = DB.World.Query("SELECT menu_id, id, option_text_loc1, box_text_loc1, option_text_loc2, box_text_loc2, " +
                "option_text_loc3, box_text_loc3, option_text_loc4, box_text_loc4, option_text_loc5, box_text_loc5, option_text_loc6, box_text_loc6, " +
                "option_text_loc7, box_text_loc7, option_text_loc8, box_text_loc8 FROM locales_gossip_menu_option");

            if (result.IsEmpty())
                return;

            do
            {
                uint menuId = result.Read<ushort>(0);
                uint id = result.Read<ushort>(1);

                GossipMenuItemsLocale data = new GossipMenuItemsLocale();
                for (byte i = 1; i < (int)LocaleConstant.OldTotal; ++i)
                {
                    LocaleConstant locale = (LocaleConstant)i;
                    AddLocaleString(result.Read<string>(2 + 2 * (i - 1)), locale, data.OptionText);
                    AddLocaleString(result.Read<string>(2 + 2 * (i - 1) + 1), locale, data.BoxText);
                }

                _gossipMenuItemsLocaleStorage[MathFunctions.MakePair32(menuId, id)] = data;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} gossip_menu_option locale strings in {1} ms", _gossipMenuItemsLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadPageTextLocales()
        {
            uint oldMSTime = Time.GetMSTime();

            _pageTextLocaleStorage.Clear(); // needed for reload case

            //                                               0      1     2
            SQLResult result = DB.World.Query("SELECT ID, locale, Text FROM page_text_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);
                string text = result.Read<string>(2);

                if (!_pageTextLocaleStorage.ContainsKey(id))
                    _pageTextLocaleStorage[id] = new PageTextLocale();

                PageTextLocale data = _pageTextLocaleStorage[id];
                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(text, locale, data.Text);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} PageText locale strings in {1} ms", _pageTextLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadPointOfInterestLocales()
        {
            uint oldMSTime = Time.GetMSTime();

            _pointOfInterestLocaleStorage.Clear(); // need for reload case

            //                                        0      1      2
            SQLResult result = DB.World.Query("SELECT ID, locale, Name FROM points_of_interest_locale");
            if (result.IsEmpty())
                return;

            do
            {
                uint id = result.Read<uint>(0);
                string localeName = result.Read<string>(1);
                string name = result.Read<string>(2);

                if (!_pointOfInterestLocaleStorage.ContainsKey(id))
                    _pointOfInterestLocaleStorage[id] = new PointOfInterestLocale();

                PointOfInterestLocale data = _pointOfInterestLocaleStorage[id];

                LocaleConstant locale = localeName.ToEnum<LocaleConstant>();
                if (locale == LocaleConstant.enUS)
                    continue;

                AddLocaleString(name, locale, data.Name);
            }
            while (result.NextRow());
        }

        public CreatureLocale GetCreatureLocale(uint entry)
        {
            return _creatureLocaleStorage.LookupByKey(entry);
        }
        public GameObjectLocale GetGameObjectLocale(uint entry)
        {
            return _gameObjectLocaleStorage.LookupByKey(entry);
        }
        public QuestTemplateLocale GetQuestLocale(uint entry)
        {
            return _questTemplateLocaleStorage.LookupByKey(entry);
        }
        public QuestOfferRewardLocale GetQuestOfferRewardLocale(uint entry)
        {
            return _questOfferRewardLocaleStorage.LookupByKey(entry);
        }
        public QuestRequestItemsLocale GetQuestRequestItemsLocale(uint entry)
        {
            return _questRequestItemsLocaleStorage.LookupByKey(entry);
        }
        public QuestObjectivesLocale GetQuestObjectivesLocale(uint entry)
        {
            return _questObjectivesLocaleStorage.LookupByKey(entry);
        }
        public GossipMenuItemsLocale GetGossipMenuItemsLocale(uint menuId, uint menuItemId)
        {
            return _gossipMenuItemsLocaleStorage.LookupByKey(MathFunctions.MakePair32(menuId, menuItemId));
        }
        public PageTextLocale GetPageTextLocale(uint entry)
        {
            return _pageTextLocaleStorage.LookupByKey(entry);
        }
        public PointOfInterestLocale GetPointOfInterestLocale(uint id)
        {
            return _pointOfInterestLocaleStorage.LookupByKey(id);
        }

        //General
        public void LoadReputationRewardRate()
        {
            uint oldMSTime = Time.GetMSTime();

            _repRewardRateStorage.Clear();                             // for reload case

            //                                          0          1             2                  3                  4                 5                      6             7
            SQLResult result = DB.World.Query("SELECT faction, quest_rate, quest_daily_rate, quest_weekly_rate, quest_monthly_rate, quest_repeatable_rate, creature_rate, spell_rate FROM reputation_reward_rate");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded `reputation_reward_rate`, table is empty!");
                return;
            }
            uint count = 0;
            do
            {
                uint factionId = result.Read<uint>(0);

                RepRewardRate repRate = new RepRewardRate();

                repRate.questRate = result.Read<float>(1);
                repRate.questDailyRate = result.Read<float>(2);
                repRate.questWeeklyRate = result.Read<float>(3);
                repRate.questMonthlyRate = result.Read<float>(4);
                repRate.questRepeatableRate = result.Read<float>(5);
                repRate.creatureRate = result.Read<float>(6);
                repRate.spellRate = result.Read<float>(7);

                FactionRecord factionEntry = CliDB.FactionStorage.LookupByKey(factionId);
                if (factionEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Faction (faction.dbc) {0} does not exist but is used in `reputation_reward_rate`", factionId);
                    continue;
                }

                if (repRate.questRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has quest_rate with invalid rate {0}, skipping data for faction {1}", repRate.questRate, factionId);
                    continue;
                }

                if (repRate.questDailyRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has quest_daily_rate with invalid rate {0}, skipping data for faction {1}", repRate.questDailyRate, factionId);
                    continue;
                }

                if (repRate.questWeeklyRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has quest_weekly_rate with invalid rate {0}, skipping data for faction {1}", repRate.questWeeklyRate, factionId);
                    continue;
                }

                if (repRate.questMonthlyRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has quest_monthly_rate with invalid rate {0}, skipping data for faction {1}", repRate.questMonthlyRate, factionId);
                    continue;
                }

                if (repRate.questRepeatableRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has quest_repeatable_rate with invalid rate {0}, skipping data for faction {1}", repRate.questRepeatableRate, factionId);
                    continue;
                }

                if (repRate.creatureRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has creature_rate with invalid rate {0}, skipping data for faction {1}", repRate.creatureRate, factionId);
                    continue;
                }

                if (repRate.spellRate < 0.0f)
                {
                    Log.outError(LogFilter.Sql, "Table reputation_reward_rate has spell_rate with invalid rate {0}, skipping data for faction {1}", repRate.spellRate, factionId);
                    continue;
                }

                _repRewardRateStorage[factionId] = repRate;

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} reputation_reward_rate in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadReputationOnKill()
        {
            uint oldMSTime = Time.GetMSTime();

            // For reload case
            _repOnKillStorage.Clear();

            //                                                0            1                     2
            SQLResult result = DB.World.Query("SELECT creature_id, RewOnKillRepFaction1, RewOnKillRepFaction2, " +
                //   3             4             5                   6             7             8                   9
                "IsTeamAward1, MaxStanding1, RewOnKillRepValue1, IsTeamAward2, MaxStanding2, RewOnKillRepValue2, TeamDependent " +
                "FROM creature_onkill_reputation");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "oaded 0 creature award reputation definitions. DB table `creature_onkill_reputation` is empty.");
                return;
            }
            uint count = 0;
            do
            {
                uint creature_id = result.Read<uint>(0);

                ReputationOnKillEntry repOnKill = new ReputationOnKillEntry();
                repOnKill.RepFaction1 = result.Read<ushort>(1);
                repOnKill.RepFaction2 = result.Read<ushort>(2);
                repOnKill.IsTeamAward1 = result.Read<bool>(3);
                repOnKill.ReputationMaxCap1 = result.Read<byte>(4);
                repOnKill.RepValue1 = result.Read<int>(5);
                repOnKill.IsTeamAward2 = result.Read<bool>(6);
                repOnKill.ReputationMaxCap2 = result.Read<byte>(7);
                repOnKill.RepValue2 = result.Read<int>(8);
                repOnKill.TeamDependent = result.Read<bool>(9);

                if (GetCreatureTemplate(creature_id) == null)
                {
                    Log.outError(LogFilter.Sql, "Table `creature_onkill_reputation` have data for not existed creature entry ({0}), skipped", creature_id);
                    continue;
                }

                if (repOnKill.RepFaction1 != 0)
                {
                    FactionRecord factionEntry1 = CliDB.FactionStorage.LookupByKey(repOnKill.RepFaction1);
                    if (factionEntry1 == null)
                    {
                        Log.outError(LogFilter.Sql, "Faction (faction.dbc) {0} does not exist but is used in `creature_onkill_reputation`", repOnKill.RepFaction1);
                        continue;
                    }
                }

                if (repOnKill.RepFaction2 != 0)
                {
                    FactionRecord factionEntry2 = CliDB.FactionStorage.LookupByKey(repOnKill.RepFaction2);
                    if (factionEntry2 == null)
                    {
                        Log.outError(LogFilter.Sql, "Faction (faction.dbc) {0} does not exist but is used in `creature_onkill_reputation`", repOnKill.RepFaction2);
                        continue;
                    }
                }

                _repOnKillStorage[creature_id] = repOnKill;

                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature award reputation definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadReputationSpilloverTemplate()
        {
            var oldMSTime = Time.GetMSTime();

            _repSpilloverTemplateStorage.Clear();                      // for reload case

            //                                        0        1         2       3       4         5       6       7         8       9       10        11      12      13        14      15
            SQLResult result = DB.World.Query("SELECT faction, faction1, rate_1, rank_1, faction2, rate_2, rank_2, faction3, rate_3, rank_3, faction4, rate_4, rank_4, faction5, rate_5, rank_5 FROM " +
            "reputation_spillover_template");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded `reputation_spillover_template`, table is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint factionId = result.Read<uint>(0);

                RepSpilloverTemplate repTemplate = new RepSpilloverTemplate();
                repTemplate.faction[0] = result.Read<uint>(1);
                repTemplate.faction_rate[0] = result.Read<float>(2);
                repTemplate.faction_rank[0] = result.Read<uint>(3);
                repTemplate.faction[1] = result.Read<uint>(4);
                repTemplate.faction_rate[1] = result.Read<float>(5);
                repTemplate.faction_rank[1] = result.Read<uint>(6);
                repTemplate.faction[2] = result.Read<uint>(7);
                repTemplate.faction_rate[2] = result.Read<float>(8);
                repTemplate.faction_rank[2] = result.Read<uint>(9);
                repTemplate.faction[3] = result.Read<uint>(10);
                repTemplate.faction_rate[3] = result.Read<float>(11);
                repTemplate.faction_rank[3] = result.Read<uint>(12);
                repTemplate.faction[4] = result.Read<uint>(13);
                repTemplate.faction_rate[4] = result.Read<float>(14);
                repTemplate.faction_rank[4] = result.Read<uint>(15);

                var factionEntry = CliDB.FactionStorage.LookupByKey(factionId);
                if (factionEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Faction (faction.dbc) {0} does not exist but is used in `reputation_spillover_template`", factionId);
                    continue;
                }

                if (factionEntry.ParentFactionID == 0)
                {
                    Log.outError(LogFilter.Sql, "Faction (faction.dbc) {0} in `reputation_spillover_template` does not belong to any team, skipping", factionId);
                    continue;
                }

                bool invalidSpilloverFaction = false;
                for (var i = 0; i < 5; ++i)
                {
                    if (repTemplate.faction[i] != 0)
                    {
                        var factionSpillover = CliDB.FactionStorage.LookupByKey(repTemplate.faction[i]);
                        if (factionSpillover.Id == 0)
                        {
                            Log.outError(LogFilter.Sql, "Spillover faction (faction.dbc) {0} does not exist but is used in `reputation_spillover_template` for faction {1}, skipping", repTemplate.faction[i], factionId);
                            invalidSpilloverFaction = true;
                            break;
                        }

                        if (!factionSpillover.CanHaveReputation())
                        {
                            Log.outError(LogFilter.Sql, "Spillover faction (faction.dbc) {0} for faction {1} in `reputation_spillover_template` can not be listed for client, and then useless, skipping",
                                repTemplate.faction[i], factionId);
                            invalidSpilloverFaction = true;
                            break;
                        }

                        if (repTemplate.faction_rank[i] >= (uint)ReputationRank.Max)
                        {
                            Log.outError(LogFilter.Sql, "Rank {0} used in `reputation_spillover_template` for spillover faction {1} is not valid, skipping", repTemplate.faction_rank[i], repTemplate.faction[i]);
                            invalidSpilloverFaction = true;
                            break;
                        }
                    }
                }

                if (invalidSpilloverFaction)
                    continue;

                _repSpilloverTemplateStorage[factionId] = repTemplate;
                ++count;
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} reputation_spillover_template in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadTavernAreaTriggers()
        {
            uint oldMSTime = Time.GetMSTime();

            _tavernAreaTriggerStorage.Clear();                          // need for reload case

            SQLResult result = DB.World.Query("SELECT id FROM areatrigger_tavern");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 tavern triggers. DB table `areatrigger_tavern` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                ++count;

                uint Trigger_ID = result.Read<uint>(0);

                AreaTriggerRecord atEntry = CliDB.AreaTriggerStorage.LookupByKey(Trigger_ID);
                if (atEntry == null)
                {
                    Log.outError(LogFilter.Sql, "Area trigger (ID:{0}) does not exist in `AreaTrigger.dbc`.", Trigger_ID);
                    continue;
                }

                _tavernAreaTriggerStorage.Add(Trigger_ID);
            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} tavern triggers in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadMailLevelRewards()
        {
            uint oldMSTime = Time.GetMSTime();

            _mailLevelRewardStorage.Clear();                           // for reload case

            //                                           0        1             2            3
            SQLResult result = DB.World.Query("SELECT level, raceMask, mailTemplateId, senderEntry FROM mail_level_reward");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 level dependent mail rewards. DB table `mail_level_reward` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                byte level = result.Read<byte>(0);
                uint raceMask = result.Read<uint>(1);
                uint mailTemplateId = result.Read<uint>(2);
                uint senderEntry = result.Read<uint>(3);

                if (level > WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel))
                {
                    Log.outError(LogFilter.Sql, "Table `mail_level_reward` have data for level {0} that more supported by client ({1}), ignoring.", level, WorldConfig.GetIntValue(WorldCfg.MaxPlayerLevel));
                    continue;
                }

                if (!Convert.ToBoolean(raceMask & (uint)Race.RaceMaskAllPlayable))
                {
                    Log.outError(LogFilter.Sql, "Table `mail_level_reward` have raceMask ({0}) for level {1} that not include any player races, ignoring.", raceMask, level);
                    continue;
                }

                if (!CliDB.MailTemplateStorage.ContainsKey(mailTemplateId))
                {
                    Log.outError(LogFilter.Sql, "Table `mail_level_reward` have invalid mailTemplateId ({0}) for level {1} that invalid not include any player races, ignoring.", mailTemplateId, level);
                    continue;
                }

                if (GetCreatureTemplate(senderEntry) == null)
                {
                    Log.outError(LogFilter.Sql, "Table `mail_level_reward` have not existed sender creature entry ({0}) for level {1} that invalid not include any player races, ignoring.", senderEntry, level);
                    continue;
                }

                _mailLevelRewardStorage.Add(level, new MailLevelReward(raceMask, mailTemplateId, senderEntry));

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} level dependent mail rewards in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadExplorationBaseXP()
        {
            uint oldMSTime = Time.GetMSTime();

            SQLResult result = DB.World.Query("SELECT level, basexp FROM exploration_basexp");

            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 BaseXP definitions. DB table `exploration_basexp` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                byte level = result.Read<byte>(0);
                uint basexp = result.Read<uint>(1);
                _baseXPTable[level] = basexp;
                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} BaseXP definitions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadTempSummons()
        {
            uint oldMSTime = Time.GetMSTime();

            _tempSummonDataStorage.Clear();   // needed for reload case

            //                                             0           1             2        3      4           5           6           7            8           9
            SQLResult result = DB.World.Query("SELECT summonerId, summonerType, groupId, entry, position_x, position_y, position_z, orientation, summonType, summonTime FROM creature_summon_groups");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 temp summons. DB table `creature_summon_groups` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint summonerId = result.Read<uint>(0);
                SummonerType summonerType = (SummonerType)result.Read<byte>(1);
                byte group = result.Read<byte>(2);

                switch (summonerType)
                {
                    case SummonerType.Creature:
                        if (GetCreatureTemplate(summonerId) == null)
                        {
                            Log.outError(LogFilter.Sql, "Table `creature_summon_groups` has summoner with non existing entry {0} for creature summoner type, skipped.", summonerId);
                            continue;
                        }
                        break;
                    case SummonerType.GameObject:
                        if (GetGameObjectTemplate(summonerId) == null)
                        {
                            Log.outError(LogFilter.Sql, "Table `creature_summon_groups` has summoner with non existing entry {0} for gameobject summoner type, skipped.", summonerId);
                            continue;
                        }
                        break;
                    case SummonerType.Map:
                        if (!CliDB.MapStorage.ContainsKey(summonerId))
                        {
                            Log.outError(LogFilter.Sql, "Table `creature_summon_groups` has summoner with non existing entry {0} for map summoner type, skipped.", summonerId);
                            continue;
                        }
                        break;
                    default:
                        Log.outError(LogFilter.Sql, "Table `creature_summon_groups` has unhandled summoner type {0} for summoner {1}, skipped.", summonerType, summonerId);
                        continue;
                }

                TempSummonData data = new TempSummonData();
                data.entry = result.Read<uint>(3);

                if (GetCreatureTemplate(data.entry) == null)
                {
                    Log.outError(LogFilter.Sql, "Table `creature_summon_groups` has creature in group [Summoner ID: {0}, Summoner Type: {1}, Group ID: {2}] with non existing creature entry {3}, skipped.",
                        summonerId, summonerType, group, data.entry);
                    continue;
                }

                float posX = result.Read<float>(4);
                float posY = result.Read<float>(5);
                float posZ = result.Read<float>(6);
                float orientation = result.Read<float>(7);

                data.pos = new Position(posX, posY, posZ, orientation);

                data.type = (TempSummonType)result.Read<byte>(8);

                if (data.type > TempSummonType.ManualDespawn)
                {
                    Log.outError(LogFilter.Sql, "Table `creature_summon_groups` has unhandled temp summon type {0} in group [Summoner ID: {1}, Summoner Type: {2}, Group ID: {3}] for creature entry {4}, skipped.",
                        data.type, summonerId, summonerType, group, data.entry);
                    continue;
                }

                data.time = result.Read<uint>(9);

                Tuple<uint, SummonerType, byte> key = Tuple.Create(summonerId, summonerType, group);
                _tempSummonDataStorage.Add(key, data);

                ++count;

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} temp summons in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadPageTexts()
        {
            uint oldMSTime = Time.GetMSTime();

            //                                         0   1     2           3                 4
            SQLResult result = DB.World.Query("SELECT ID, text, NextPageID, PlayerConditionID, Flags FROM page_text");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 page texts. DB table `page_text` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                uint id = result.Read<uint>(0);

                PageText pageText = new PageText();
                pageText.Text = result.Read<string>(1);
                pageText.NextPageID = result.Read<uint>(2);
                pageText.PlayerConditionID = result.Read<int>(3);
                pageText.Flags = result.Read<byte>(4);

                _pageTextStorage[id] = pageText;
                ++count;
            }
            while (result.NextRow());

            foreach (var pair in _pageTextStorage)
            {
                if (pair.Value.NextPageID != 0)
                {
                    if (!_pageTextStorage.ContainsKey(pair.Value.NextPageID))
                        Log.outError(LogFilter.Sql, "Page text (ID: {0}) has non-existing `NextPageID` ({1})", pair.Key, pair.Value.NextPageID);

                }
            }
            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} page texts in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadReservedPlayersNames()
        {
            uint oldMSTime = Time.GetMSTime();

            _reservedNamesStorage.Clear();                                // need for reload case

            SQLResult result = DB.Characters.Query("SELECT name FROM reserved_name");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 reserved player names. DB table `reserved_name` is empty!");
                return;
            }

            uint count = 0;
            do
            {
                string name = result.Read<string>(0);

                _reservedNamesStorage.Add(name.ToLower());
                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} reserved player names in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        //not very fast function but it is called only once a day, or on starting-up
        public void ReturnOrDeleteOldMails(bool serverUp)
        {
            uint oldMSTime = Time.GetMSTime();

            long curTime = Time.UnixTime;
            DateTime lt = Time.UnixTimeToDateTime(curTime).ToLocalTime();
            long basetime = curTime;
            Log.outInfo(LogFilter.Server, "Returning mails current time: hour: {0}, minute: {1}, second: {2} ", lt.Hour, lt.Minute, lt.Second);

            PreparedStatement stmt;
            // Delete all old mails without item and without body immediately, if starting server
            if (!serverUp)
            {
                stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_EMPTY_EXPIRED_MAIL);
                stmt.AddValue(0, basetime);
                DB.Characters.Execute(stmt);
            }
            stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_EXPIRED_MAIL);
            stmt.AddValue(0, basetime);
            SQLResult result = DB.Characters.Query(stmt);
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "No expired mails found.");
                return;                                             // any mails need to be returned or deleted
            }

            MultiMap<uint, MailItemInfo> itemsCache = new MultiMap<uint, MailItemInfo>();
            stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_EXPIRED_MAIL_ITEMS);
            stmt.AddValue(0, (uint)basetime);
            SQLResult items = DB.Characters.Query(stmt);
            if (!items.IsEmpty())
            {
                MailItemInfo item = new MailItemInfo();
                do
                {
                    item.item_guid = result.Read<uint>(0);
                    item.item_template = result.Read<uint>(1);
                    uint mailId = result.Read<uint>(2);
                    itemsCache.Add(mailId, item);
                } while (items.NextRow());
            }

            uint deletedCount = 0;
            uint returnedCount = 0;
            do
            {
                Mail m = new Mail();
                m.messageID = result.Read<uint>(0);
                m.messageType = (MailMessageType)result.Read<byte>(1);
                m.sender = result.Read<uint>(2);
                m.receiver = result.Read<ulong>(3);
                bool has_items = result.Read<bool>(4);
                m.expire_time = result.Read<uint>(5);
                m.deliver_time = 0;
                m.COD = result.Read<ulong>(6);
                m.checkMask = (MailCheckMask)result.Read<byte>(7);
                m.mailTemplateId = result.Read<ushort>(8);

                Player player = null;
                if (serverUp)
                    player = Global.ObjAccessor.FindPlayer(ObjectGuid.Create(HighGuid.Player, m.receiver));

                if (player && player.m_mailsLoaded)
                {                                                   // this code will run very improbably (the time is between 4 and 5 am, in game is online a player, who has old mail
                    // his in mailbox and he has already listed his mails)
                    continue;
                }

                // Delete or return mail
                if (has_items)
                {
                    // read items from cache
                    List<MailItemInfo> temp = itemsCache[m.messageID];
                    Extensions.Swap(ref m.items, ref temp);

                    // if it is mail from non-player, or if it's already return mail, it shouldn't be returned, but deleted
                    if (m.messageType != MailMessageType.Normal || (m.checkMask.HasAnyFlag(MailCheckMask.CodPayment | MailCheckMask.Returned)))
                    {
                        // mail open and then not returned
                        foreach (var itemInfo in m.items)
                        {
                            stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_ITEM_INSTANCE);
                            stmt.AddValue(0, itemInfo.item_guid);
                            DB.Characters.Execute(stmt);
                        }
                    }
                    else
                    {
                        // Mail will be returned
                        stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_MAIL_RETURNED);
                        stmt.AddValue(0, m.receiver);
                        stmt.AddValue(1, m.sender);
                        stmt.AddValue(2, basetime + 30 * Time.Day);
                        stmt.AddValue(3, basetime);
                        stmt.AddValue(4, MailCheckMask.Returned);
                        stmt.AddValue(5, m.messageID);
                        DB.Characters.Execute(stmt);
                        foreach (var itemInfo in m.items)
                        {
                            // Update receiver in mail items for its proper delivery, and in instance_item for avoid lost item at sender delete
                            stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_MAIL_ITEM_RECEIVER);
                            stmt.AddValue(0, m.sender);
                            stmt.AddValue(1, itemInfo.item_guid);
                            DB.Characters.Execute(stmt);

                            stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_ITEM_OWNER);
                            stmt.AddValue(0, m.sender);
                            stmt.AddValue(1, itemInfo.item_guid);
                            DB.Characters.Execute(stmt);
                        }
                        ++returnedCount;
                        continue;
                    }
                }

                stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_MAIL_BY_ID);
                stmt.AddValue(0, m.messageID);
                DB.Characters.Execute(stmt);
                ++deletedCount;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Processed {0} expired mails: {1} deleted and {2} returned in {3} ms", deletedCount + returnedCount, deletedCount, returnedCount, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadSceneTemplates()
        {
            uint oldMSTime = Time.GetMSTime();
            _sceneTemplateStorage.Clear();

            SQLResult result = DB.World.Query("SELECT SceneId, Flags, ScriptPackageID, ScriptName FROM scene_template");
            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 scene templates. DB table `scene_template` is empty.");
                return;
            }

            uint count = 0;
            do
            {
                uint sceneId = result.Read<uint>(0);
                SceneTemplate sceneTemplate = new SceneTemplate();
                sceneTemplate.SceneId = sceneId;
                sceneTemplate.PlaybackFlags = (SceneFlags)result.Read<uint>(1);
                sceneTemplate.ScenePackageId = result.Read<uint>(2);
                sceneTemplate.ScriptId = GetScriptId(result.Read<string>(3));

                _sceneTemplateStorage[sceneId] = sceneTemplate;

            } while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} scene templates in {1} ms.", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public MailLevelReward GetMailLevelReward(uint level, uint raceMask)
        {
            var mailList = _mailLevelRewardStorage.LookupByKey((byte)level);
            if (mailList.Empty())
                return null;

            foreach (var mailReward in mailList)
                if (Convert.ToBoolean(mailReward.raceMask & raceMask))
                    return mailReward;

            return null;
        }
        public RepRewardRate GetRepRewardRate(uint factionId)
        {
            return _repRewardRateStorage.LookupByKey(factionId);
        }
        public RepSpilloverTemplate GetRepSpillover(uint factionId)
        {
            return _repSpilloverTemplateStorage.LookupByKey(factionId);
        }
        public ReputationOnKillEntry GetReputationOnKilEntry(uint id)
        {
            return _repOnKillStorage.LookupByKey(id);
        }
        public void SetHighestGuids()
        {
            SQLResult result = DB.Characters.Query("SELECT MAX(guid) FROM characters");
            if (!result.IsEmpty())
                GetGuidSequenceGenerator(HighGuid.Player).Set(result.Read<ulong>(0) + 1);

            result = DB.Characters.Query("SELECT MAX(guid) FROM item_instance");
            if (!result.IsEmpty())
                GetGuidSequenceGenerator(HighGuid.Item).Set(result.Read<ulong>(0) + 1);

            // Cleanup other tables from not existed guids ( >= hiItemGuid)
            DB.Characters.Execute("DELETE FROM character_inventory WHERE item >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed());      // One-time query
            DB.Characters.Execute("DELETE FROM mail_items WHERE item_guid >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed());          // One-time query
            DB.Characters.Execute("DELETE FROM auctionhouse WHERE itemguid >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed());         // One-time query
            DB.Characters.Execute("DELETE FROM guild_bank_item WHERE item_guid >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed());     // One-time query

            result = DB.World.Query("SELECT MAX(guid) FROM transports");
            if (!result.IsEmpty())
                GetGuidSequenceGenerator(HighGuid.Transport).Set(result.Read<ulong>(0) + 1);

            result = DB.Characters.Query("SELECT MAX(id) FROM auctionhouse");
            if (!result.IsEmpty())
                _auctionId = result.Read<uint>(0) + 1;

            result = DB.Characters.Query("SELECT MAX(id) FROM mail");
            if (!result.IsEmpty())
                _mailId = result.Read<uint>(0) + 1;

            result = DB.Characters.Query("SELECT MAX(arenateamid) FROM arena_team");
            if (!result.IsEmpty())
                Global.ArenaTeamMgr.SetNextArenaTeamId(result.Read<uint>(0) + 1);

            result = DB.Characters.Query("SELECT MAX(setguid) FROM character_equipmentsets");
            if (!result.IsEmpty())
                _equipmentSetGuid = result.Read<ulong>(0) + 1;

            result = DB.Characters.Query("SELECT MAX(guildId) FROM guild");
            if (result.GetRowCount() == 1)
                Global.GuildMgr.SetNextGuildId(result.Read<uint>(0) + 1);

            result = DB.Characters.Query("SELECT MAX(itemId) from character_void_storage");
            if (!result.IsEmpty())
                _voidItemId = result.Read<ulong>(0) + 1;

            result = DB.World.Query("SELECT MAX(guid) FROM creature");
            if (!result.IsEmpty())
                _creatureSpawnId = result.Read<ulong>(0) + 1;

            result = DB.World.Query("SELECT MAX(guid) FROM gameobject");
            if (!result.IsEmpty())
                _gameObjectSpawnId = result.Read<ulong>(0) + 1;
        }
        public uint GenerateAuctionID()
        {
            if (_auctionId >= 0xFFFFFFFE)
            {
                Log.outError(LogFilter.Server, "Auctions ids overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow();
            }
            return _auctionId++;
        }
        public ulong GenerateEquipmentSetGuid()
        {
            if (_equipmentSetGuid >= 0xFFFFFFFFFFFFFFFE)
            {
                Log.outError(LogFilter.Server, "EquipmentSet guid overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow();
            }
            return _equipmentSetGuid++;
        }
        public uint GenerateMailID()
        {
            if (_mailId >= 0xFFFFFFFE)
            {
                Log.outError(LogFilter.Server, "Mail ids overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow();
            }
            return _mailId++;
        }
        public ulong GenerateVoidStorageItemId()
        {
            if (_voidItemId >= 0xFFFFFFFFFFFFFFFE)
            {
                Log.outError(LogFilter.Misc, "_voidItemId overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow(ShutdownExitCode.Error);
            }
            return _voidItemId++;
        }
        public ulong GenerateCreatureSpawnId()
        {
            if (_creatureSpawnId >= 0xFFFFFFFFFFFFFFFE)
            {
                Log.outFatal(LogFilter.Server, "Creature spawn id overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow();
            }
            return _creatureSpawnId++;
        }
        public ulong GenerateGameObjectSpawnId()
        {
            if (_gameObjectSpawnId >= 0xFFFFFFFFFFFFFFFE)
            {
                Log.outFatal(LogFilter.Server, "Gameobject spawn id overflow!! Can't continue, shutting down server. ");
                Global.WorldMgr.StopNow();
            }
            return _gameObjectSpawnId++;
        }
        public ObjectGuidGenerator GetGenerator(HighGuid high)
        {
            Contract.Assert(ObjectGuid.IsGlobal(high) || ObjectGuid.IsRealmSpecific(high), "Only global guid can be generated in ObjectMgr context");

            return GetGuidSequenceGenerator(high);
        }
        ObjectGuidGenerator GetGuidSequenceGenerator(HighGuid high)
        {
            if (!_guidGenerators.ContainsKey(high))
                _guidGenerators[high] = new ObjectGuidGenerator(high);

            return _guidGenerators[high];
        }

        public uint GetBaseXP(uint level)
        {
            return _baseXPTable.ContainsKey(level) ? _baseXPTable[level] : 0;
        }
        public uint GetXPForLevel(uint level)
        {
            if (level < _playerXPperLevel.Length)
                return _playerXPperLevel[level];
            return 0;
        }
        public uint GetMaxLevelForExpansion(Expansion expansion)
        {
            switch (expansion)
            {
                case Expansion.Classic:
                    return 60;
                case Expansion.BurningCrusade:
                    return 70;
                case Expansion.WrathOfTheLichKing:
                    return 80;
                case Expansion.Cataclysm:
                    return 85;
                case Expansion.MistsOfPandaria:
                    return 90;
                case Expansion.WarlordsOfDraenor:
                    return 100;
                case Expansion.Legion:
                    return 110;
                default:
                    break;
            }
            return 0;
        }
        public CellObjectGuids CreateCellObjectGuids(uint mapid, byte spawnMask, CellCoord cellCoord)
        {
            return CreateCellObjectGuids(mapid, spawnMask, cellCoord.GetId());
        }
        CellObjectGuids CreateCellObjectGuids(uint mapid, byte spawnMask, uint cellid)
        {
            uint newid = MathFunctions.MakePair32(mapid, spawnMask);

            if (!mapObjectGuidsStore.ContainsKey(newid))
                mapObjectGuidsStore.Add(newid, new Dictionary<uint, CellObjectGuids>());

            if (!mapObjectGuidsStore[newid].ContainsKey(cellid))
                mapObjectGuidsStore[newid].Add(cellid, new CellObjectGuids());

            return mapObjectGuidsStore[newid][cellid];
        }
        public CellObjectGuids GetCellObjectGuids(uint mapid, byte spawnMask, uint cellid)
        {
            uint newid = MathFunctions.MakePair32(mapid, spawnMask);

            if (mapObjectGuidsStore.ContainsKey(newid) && mapObjectGuidsStore[newid].ContainsKey(cellid))
                return mapObjectGuidsStore[newid][cellid];

            return null;
        }
        public Dictionary<uint, CellObjectGuids> GetMapObjectGuids(uint mapid, byte spawnMode)
        {
            var pair = MathFunctions.MakePair32(mapid, spawnMode);
            return mapObjectGuidsStore.LookupByKey(pair);
        }
        public PageText GetPageText(uint pageEntry)
        {
            return _pageTextStorage.LookupByKey(pageEntry);
        }

        public uint GetNearestTaxiNode(float x, float y, float z, uint mapid, Team team)
        {
            bool found = false;
            float dist = 10000;
            uint id = 0;

            TaxiNodeFlags requireFlag = (team == Team.Alliance) ? TaxiNodeFlags.Alliance : TaxiNodeFlags.Horde;
            foreach (var node in CliDB.TaxiNodesStorage.Values)
            {
                var i = node.Id;
                if (node.MapID != mapid || !node.Flags.HasAnyFlag(requireFlag))
                    continue;

                byte field = (byte)((i - 1) / 8);
                byte submask = (byte)(1 << (int)((i - 1) % 8));

                // skip not taxi network nodes
                if ((CliDB.TaxiNodesMask[field] & submask) == 0)
                    continue;

                float dist2 = (node.Pos.X - x) * (node.Pos.X - x) + (node.Pos.Y - y) * (node.Pos.Y - y) + (node.Pos.Z - z) * (node.Pos.Z - z);
                if (found)
                {
                    if (dist2 < dist)
                    {
                        dist = dist2;
                        id = i;
                    }
                }
                else
                {
                    found = true;
                    dist = dist2;
                    id = i;
                }
            }

            return id;
        }
        public void GetTaxiPath(uint source, uint destination, out uint path, out uint cost)
        {
            var pathSet = CliDB.TaxiPathSetBySource.LookupByKey(source);
            if (pathSet == null)
            {
                path = 0;
                cost = 0;
                return;
            }

            var dest_i = pathSet.LookupByKey(destination);
            if (dest_i == null)
            {
                path = 0;
                cost = 0;
                return;
            }

            cost = dest_i.price;
            path = dest_i.ID;
        }
        public uint GetTaxiMountDisplayId(uint id, Team team, bool allowed_alt_team = false)
        {
            uint mount_id = 0;

            // select mount creature id
            TaxiNodesRecord node = CliDB.TaxiNodesStorage.LookupByKey(id);
            if (node != null)
            {
                uint mount_entry = 0;
                if (team == Team.Alliance)
                    mount_entry = node.MountCreatureID[1];
                else
                    mount_entry = node.MountCreatureID[0];

                // Fix for Alliance not being able to use Acherus taxi
                // only one mount type for both sides
                if (mount_entry == 0 && allowed_alt_team)
                {
                    // Simply reverse the selection. At least one team in theory should have a valid mount ID to choose.
                    mount_entry = team == Team.Alliance ? node.MountCreatureID[0] : node.MountCreatureID[1];
                }

                CreatureTemplate mount_info = GetCreatureTemplate(mount_entry);
                if (mount_info != null)
                {
                    mount_id = mount_info.GetRandomValidModelId();
                    if (mount_id == 0)
                    {
                        Log.outError(LogFilter.Sql, "No displayid found for the taxi mount with the entry {0}! Can't load it!", mount_entry);
                        return 0;
                    }
                }
            }

            // minfo is not actually used but the mount_id was updated
            GetCreatureModelRandomGender(ref mount_id);

            return mount_id;
        }

        public AreaTriggerStruct GetAreaTrigger(uint trigger)
        {
            return _areaTriggerStorage.LookupByKey(trigger);
        }
        public AccessRequirement GetAccessRequirement(uint mapid, Difficulty difficulty)
        {
            return _accessRequirementStorage.LookupByKey(MathFunctions.MakePair64(mapid, (uint)difficulty));
        }
        public bool IsTavernAreaTrigger(uint Trigger_ID)
        {
            return _tavernAreaTriggerStorage.Contains(Trigger_ID);
        }
        public AreaTriggerStruct GetGoBackTrigger(uint Map)
        {
            bool useParentDbValue = false;
            uint parentId = 0;
            MapRecord mapEntry = CliDB.MapStorage.LookupByKey(Map);
            if (mapEntry == null || mapEntry.CorpseMapID < 0)
                return null;

            if (mapEntry.IsDungeon())
            {
                InstanceTemplate iTemplate = GetInstanceTemplate(Map);

                if (iTemplate == null)
                    return null;

                parentId = iTemplate.Parent;
                useParentDbValue = true;
            }

            uint entrance_map = (uint)mapEntry.CorpseMapID;
            foreach (var pair in _areaTriggerStorage)
            {
                if ((!useParentDbValue && pair.Value.target_mapId == entrance_map) || (useParentDbValue && pair.Value.target_mapId == parentId))
                {
                    AreaTriggerRecord atEntry = CliDB.AreaTriggerStorage.LookupByKey(pair.Key);
                    if (atEntry != null && atEntry.MapID == Map)
                        return pair.Value;
                }
            }
            return null;
        }
        public AreaTriggerStruct GetMapEntranceTrigger(uint Map)
        {
            foreach (var pair in _areaTriggerStorage)
            {
                if (pair.Value.target_mapId == Map)
                {
                    AreaTriggerRecord atEntry = CliDB.AreaTriggerStorage.LookupByKey(pair.Key);
                    if (atEntry != null)
                        return pair.Value;
                }
            }
            return null;
        }

        public SceneTemplate GetSceneTemplate(uint sceneId)
        {
            return _sceneTemplateStorage.LookupByKey(sceneId);
        }

        public List<TempSummonData> GetSummonGroup(uint summonerId, SummonerType summonerType, byte group)
        {
            Tuple<uint, SummonerType,byte> key = Tuple.Create(summonerId, summonerType, group);
            return _tempSummonDataStorage.LookupByKey(key);
        }

        public bool IsReservedName(string name)
        {
            return _reservedNamesStorage.Contains(name.ToLower());
        }

        //Vehicles
        public void LoadVehicleTemplateAccessories()
        {
            uint oldMSTime = Time.GetMSTime();

            _vehicleTemplateAccessoryStore.Clear();                           // needed for reload case

            uint count = 0;

            //                                          0             1              2          3           4             5
            SQLResult result = DB.World.Query("SELECT `entry`, `accessory_entry`, `seat_id`, `minion`, `summontype`, `summontimer` FROM `vehicle_template_accessory`");
            if (result.IsEmpty())
            {
                Log.outError(LogFilter.ServerLoading, "Loaded 0 vehicle template accessories. DB table `vehicle_template_accessory` is empty.");
                return;
            }

            do
            {
                uint entry = result.Read<uint>(0);
                uint accessory = result.Read<uint>(1);
                sbyte seatId = result.Read<sbyte>(2);
                bool isMinion = result.Read<bool>(3);
                byte summonType = result.Read<byte>(4);
                uint summonTimer = result.Read<uint>(5);

                if (GetCreatureTemplate(entry) == null)
                {
                    Log.outError(LogFilter.Sql, "Table `vehicle_template_accessory`: creature template entry {0} does not exist.", entry);
                    continue;
                }

                if (GetCreatureTemplate(accessory) == null)
                {
                    Log.outError(LogFilter.Sql, "Table `vehicle_template_accessory`: Accessory {0} does not exist.", accessory);
                    continue;
                }

                if (!_spellClickInfoStorage.ContainsKey(entry))
                {
                    Log.outError(LogFilter.Sql, "Table `vehicle_template_accessory`: creature template entry {0} has no data in npc_spellclick_spells", entry);
                    continue;
                }

                _vehicleTemplateAccessoryStore.Add(entry, new VehicleAccessory(accessory, seatId, isMinion, summonType, summonTimer));

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Vehicle Template Accessories in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }
        public void LoadVehicleAccessories()
        {
            uint oldMSTime = Time.GetMSTime();

            _vehicleAccessoryStore.Clear();                           // needed for reload case

            uint count = 0;

            //                                          0             1             2          3           4             5
            SQLResult result = DB.World.Query("SELECT `guid`, `accessory_entry`, `seat_id`, `minion`, `summontype`, `summontimer` FROM `vehicle_accessory`");

            if (result.IsEmpty())
            {
                Log.outInfo(LogFilter.ServerLoading, "Loaded 0 Vehicle Accessories in {0} ms", Time.GetMSTimeDiffToNow(oldMSTime));
                return;
            }

            do
            {
                uint uiGUID = result.Read<uint>(0);
                uint uiAccessory = result.Read<uint>(1);
                sbyte uiSeat = result.Read<sbyte>(2);
                bool bMinion = result.Read<bool>(3);
                byte uiSummonType = result.Read<byte>(4);
                uint uiSummonTimer = result.Read<uint>(5);

                if (GetCreatureTemplate(uiAccessory) == null)
                {
                    Log.outError(LogFilter.Sql, "Table `vehicle_accessory`: Accessory {0} does not exist.", uiAccessory);
                    continue;
                }

                _vehicleAccessoryStore.Add(uiGUID, new VehicleAccessory(uiAccessory, uiSeat, bMinion, uiSummonType, uiSummonTimer));

                ++count;
            }
            while (result.NextRow());

            Log.outInfo(LogFilter.ServerLoading, "Loaded {0} Vehicle Accessories in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime));
        }

        public List<VehicleAccessory> GetVehicleAccessoryList(Vehicle veh)
        {
            Creature cre = veh.GetBase().ToCreature();
            if (cre != null)
            {
                // Give preference to GUID-based accessories
                var list = _vehicleAccessoryStore.LookupByKey(cre.GetSpawnId());
                if (!list.Empty())
                    return list;
            }

            // Otherwise return entry-based
            return _vehicleTemplateAccessoryStore.LookupByKey(veh.GetCreatureEntry());
        }

        #region Fields
        public static LanguageDesc[] lang_description;
        //General
        Dictionary<uint, StringArray> CypherStringStorage = new Dictionary<uint, StringArray>();
        Dictionary<uint, RepRewardRate> _repRewardRateStorage = new Dictionary<uint, RepRewardRate>();
        Dictionary<uint, ReputationOnKillEntry> _repOnKillStorage = new Dictionary<uint, ReputationOnKillEntry>();
        Dictionary<uint, RepSpilloverTemplate> _repSpilloverTemplateStorage = new Dictionary<uint, RepSpilloverTemplate>();
        MultiMap<byte, MailLevelReward> _mailLevelRewardStorage = new MultiMap<byte, MailLevelReward>();
        MultiMap<Tuple<uint, SummonerType, byte>, TempSummonData> _tempSummonDataStorage = new MultiMap<Tuple<uint, SummonerType, byte>, TempSummonData>();
        Dictionary<uint, PageText> _pageTextStorage = new Dictionary<uint, PageText>();
        List<string> _reservedNamesStorage = new List<string>();
        Dictionary<uint, SceneTemplate> _sceneTemplateStorage = new Dictionary<uint, SceneTemplate>();

        Dictionary<byte, byte> _raceExpansionRequirementStorage = new Dictionary<byte, byte>();
        Dictionary<byte, byte> _classExpansionRequirementStorage = new Dictionary<byte, byte>();
        Dictionary<uint, string> _realmNameStorage = new Dictionary<uint, string>();

        //Quest
        Dictionary<uint, Quest> _questTemplates = new Dictionary<uint, Quest>();
        MultiMap<uint, uint> _goQuestRelations = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _goQuestInvolvedRelations = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _goQuestInvolvedRelationsReverse = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _creatureQuestRelations = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _creatureQuestInvolvedRelations = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _creatureQuestInvolvedRelationsReverse = new MultiMap<uint, uint>();
        public MultiMap<int, uint> _exclusiveQuestGroups = new MultiMap<int, uint>();
        MultiMap<uint, QuestPOI> _questPOIStorage = new MultiMap<uint, QuestPOI>();
        MultiMap<uint, uint> _questAreaTriggerStorage = new MultiMap<uint, uint>();
        Dictionary<uint, QuestObjective> _questObjectives = new Dictionary<uint, QuestObjective>();
        Dictionary<byte, Dictionary<uint, QuestGreeting>> _questGreetingStorage = new Dictionary<byte, Dictionary<uint, QuestGreeting>>();

        //Scripts
        List<string> scriptNamesStorage = new List<string>();
        MultiMap<uint, uint> spellScriptsStorage = new MultiMap<uint, uint>();
        public Dictionary<uint, MultiMap<uint, ScriptInfo>> sSpellScripts = new Dictionary<uint, MultiMap<uint, ScriptInfo>>();
        public Dictionary<uint, MultiMap<uint, ScriptInfo>> sEventScripts = new Dictionary<uint, MultiMap<uint, ScriptInfo>>();
        public Dictionary<uint, MultiMap<uint, ScriptInfo>> sWaypointScripts = new Dictionary<uint, MultiMap<uint, ScriptInfo>>();
        Dictionary<uint, uint> areaTriggerScriptStorage = new Dictionary<uint, uint>();

        //Maps
        public Dictionary<uint, GameTele> gameTeleStorage = new Dictionary<uint, GameTele>();
        Dictionary<uint, Dictionary<uint, CellObjectGuids>> mapObjectGuidsStore = new Dictionary<uint, Dictionary<uint, CellObjectGuids>>();
        Dictionary<uint, InstanceTemplate> instanceTemplateStorage = new Dictionary<uint, InstanceTemplate>();
        public MultiMap<uint, GraveYardData> GraveYardStorage = new MultiMap<uint, GraveYardData>();
        List<ushort> _transportMaps = new List<ushort>();

        //Spells /Skills / Phases
        MultiMap<uint, uint> _terrainPhaseInfoStore = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _terrainMapDefaultStore = new MultiMap<uint, uint>();
        MultiMap<uint, uint> _terrainWorldMapStore = new MultiMap<uint, uint>();
        MultiMap<uint, PhaseInfoStruct> _phases = new MultiMap<uint, PhaseInfoStruct>();
        MultiMap<uint, SpellClickInfo> _spellClickInfoStorage = new MultiMap<uint, SpellClickInfo>();
        Dictionary<uint, int> _fishingBaseForAreaStorage = new Dictionary<uint, int>();
        Dictionary<uint, SkillTiersEntry> _skillTiers = new Dictionary<uint, SkillTiersEntry>();

        //Gossip
        MultiMap<uint, GossipMenuItems> gossipMenuItemsStorage = new MultiMap<uint, GossipMenuItems>();
        MultiMap<uint, GossipMenus> gossipMenusStorage = new MultiMap<uint, GossipMenus>();
        Dictionary<uint, PointOfInterest> pointsOfInterestStorage = new Dictionary<uint, PointOfInterest>();

        //Creature
        Dictionary<uint, CreatureTemplate> creatureTemplateStorage = new Dictionary<uint, CreatureTemplate>();
        Dictionary<uint, CreatureModelInfo> creatureModelStorage = new Dictionary<uint, CreatureModelInfo>();
        Dictionary<ulong, CreatureData> creatureDataStorage = new Dictionary<ulong, CreatureData>();
        Dictionary<ulong, CreatureAddon> creatureAddonStorage = new Dictionary<ulong, CreatureAddon>();
        MultiMap<uint, uint> _creatureQuestItemStorage = new MultiMap<uint, uint>();
        Dictionary<uint, CreatureAddon> creatureTemplateAddonStorage = new Dictionary<uint, CreatureAddon>();
        MultiMap<uint, Tuple<uint, EquipmentInfo>> equipmentInfoStorage = new MultiMap<uint, Tuple<uint, EquipmentInfo>>();
        Dictionary<ObjectGuid, ObjectGuid> linkedRespawnStorage = new Dictionary<ObjectGuid, ObjectGuid>();
        Dictionary<uint, CreatureBaseStats> creatureBaseStatsStorage = new Dictionary<uint, CreatureBaseStats>();
        Dictionary<uint, VendorItemData> cacheVendorItemStorage = new Dictionary<uint, VendorItemData>();
        Dictionary<uint, TrainerSpellData> cacheTrainerSpellStorage = new Dictionary<uint, TrainerSpellData>();
        List<uint>[] _difficultyEntries = new List<uint>[SharedConst.MaxCreatureDifficulties]; // already loaded difficulty 1 value in creatures, used in CheckCreatureTemplate
        List<uint>[] _hasDifficultyEntries = new List<uint>[SharedConst.MaxCreatureDifficulties]; // already loaded creatures with difficulty 1 values, used in CheckCreatureTemplate
        Dictionary<uint, NpcText> _npcTextStorage = new Dictionary<uint, NpcText>();

        //GameObject
        Dictionary<uint, GameObjectTemplate> gameObjectTemplateStorage = new Dictionary<uint, GameObjectTemplate>();
        Dictionary<ulong, GameObjectData> gameObjectDataStorage = new Dictionary<ulong, GameObjectData>();
        Dictionary<ulong, GameObjectTemplateAddon> _gameObjectTemplateAddonStore = new Dictionary<ulong, GameObjectTemplateAddon>();
        Dictionary<ulong, GameObjectAddon> _gameObjectAddonStorage = new Dictionary<ulong, GameObjectAddon>();
        MultiMap<uint, uint> _gameObjectQuestItemStorage = new MultiMap<uint, uint>();
        List<uint> _gameObjectForQuestStorage = new List<uint>();

        //Item
        Dictionary<uint, ItemTemplate> ItemTemplateStorage = new Dictionary<uint, ItemTemplate>();

        //Player
        PlayerInfo[][] _playerInfo = new PlayerInfo[(int)Race.Max][];

        //Faction Change
        public Dictionary<uint, uint> FactionChangeAchievements = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> FactionChangeItems = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> FactionChangeQuests = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> FactionChangeReputation = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> FactionChangeSpells = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> FactionChangeTitles = new Dictionary<uint, uint>();

        //Pets
        Dictionary<uint, PetLevelInfo[]> petInfoStore = new Dictionary<uint, PetLevelInfo[]>();
        MultiMap<uint, string> _petHalfName0 = new MultiMap<uint, string>();
        MultiMap<uint, string> _petHalfName1 = new MultiMap<uint, string>();

        //Vehicles
        MultiMap<uint, VehicleAccessory> _vehicleTemplateAccessoryStore = new MultiMap<uint, VehicleAccessory>();
        MultiMap<ulong, VehicleAccessory> _vehicleAccessoryStore = new MultiMap<ulong, VehicleAccessory>();

        //Locales
        Dictionary<uint, CreatureLocale> _creatureLocaleStorage = new Dictionary<uint, CreatureLocale>();
        Dictionary<uint, GameObjectLocale> _gameObjectLocaleStorage = new Dictionary<uint, GameObjectLocale>();
        Dictionary<uint, QuestTemplateLocale> _questTemplateLocaleStorage = new Dictionary<uint, QuestTemplateLocale>();
        Dictionary<uint, QuestObjectivesLocale> _questObjectivesLocaleStorage = new Dictionary<uint, QuestObjectivesLocale>();
        Dictionary<uint, QuestOfferRewardLocale> _questOfferRewardLocaleStorage = new Dictionary<uint, QuestOfferRewardLocale>();
        Dictionary<uint, QuestRequestItemsLocale> _questRequestItemsLocaleStorage = new Dictionary<uint, QuestRequestItemsLocale>();
        Dictionary<uint, GossipMenuItemsLocale> _gossipMenuItemsLocaleStorage = new Dictionary<uint, GossipMenuItemsLocale>();
        Dictionary<uint, PageTextLocale> _pageTextLocaleStorage = new Dictionary<uint, PageTextLocale>();
        Dictionary<uint, PointOfInterestLocale> _pointOfInterestLocaleStorage = new Dictionary<uint, PointOfInterestLocale>();

        List<uint> _tavernAreaTriggerStorage = new List<uint>();
        Dictionary<uint, AreaTriggerStruct> _areaTriggerStorage = new Dictionary<uint, AreaTriggerStruct>();
        Dictionary<ulong, AccessRequirement> _accessRequirementStorage = new Dictionary<ulong, AccessRequirement>();
        MultiMap<ulong, DungeonEncounter> _dungeonEncounterStorage = new MultiMap<ulong, DungeonEncounter>();

        Dictionary<HighGuid, ObjectGuidGenerator> _guidGenerators = new Dictionary<HighGuid, ObjectGuidGenerator>();
        // first free id for selected id type
        uint _auctionId;
        ulong _equipmentSetGuid;
        uint _mailId;
        uint _hiPetNumber;
        ulong _voidItemId;
        ulong _creatureSpawnId;
        ulong _gameObjectSpawnId;
        uint[] _playerXPperLevel;
        Dictionary<uint, uint> _baseXPTable = new Dictionary<uint, uint>();
        #endregion
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct ScriptInfo
    {
        [FieldOffset(0)]
        public ScriptsType type;

        [FieldOffset(4)]
        public uint id;

        [FieldOffset(8)]
        public uint delay;

        [FieldOffset(12)]
        public ScriptCommands command;

        [FieldOffset(16)]
        public raw Raw;

        [FieldOffset(16)]
        public talk Talk;

        [FieldOffset(16)]
        public emote Emote;

        [FieldOffset(16)]
        public fieldset FieldSet;

        [FieldOffset(16)]
        public moveto MoveTo;

        [FieldOffset(16)]
        public flagtoggle FlagToggle;

        [FieldOffset(16)]
        public teleportto TeleportTo;

        [FieldOffset(16)]
        public questexplored QuestExplored;

        [FieldOffset(16)]
        public killcredit KillCredit;

        [FieldOffset(16)]
        public respawngameobject RespawnGameObject;

        [FieldOffset(16)]
        public tempsummoncreature TempSummonCreature;

        [FieldOffset(16)]
        public toggledoor ToggleDoor;

        [FieldOffset(16)]
        public removeaura RemoveAura;

        [FieldOffset(16)]
        public castspell CastSpell;

        [FieldOffset(16)]
        public playsound PlaySound;

        [FieldOffset(16)]
        public createitem CreateItem;

        [FieldOffset(16)]
        public despawnself DespawnSelf;

        [FieldOffset(16)]
        public loadpath LoadPath;

        [FieldOffset(16)]
        public callscript CallScript;

        [FieldOffset(16)]
        public kill Kill;

        [FieldOffset(16)]
        public orientation Orientation;

        [FieldOffset(16)]
        public equip Equip;

        [FieldOffset(16)]
        public model Model;

        [FieldOffset(16)]
        public playmovie PlayMovie;

        [FieldOffset(16)]
        public movement Movement;

        [FieldOffset(16)]
        public playanimkit PlayAnimKit;

        public string GetDebugInfo()
        {
            return string.Format("{0} ('{1}' script id: {2})", command, Global.ObjectMgr.GetScriptsTableNameByType(type), id);
        }

        #region Structs
        public unsafe struct raw
        {
            public fixed uint nData[3];
            public fixed float fData[4];
        }

        public struct talk                   // TALK (0)
        {
            public ChatMsg ChatType;        // datalong
            public eScriptFlags Flags;           // datalong2
            public int TextID;          // dataint
        }

        public struct emote                   // EMOTE (1)
        {
            public uint EmoteID;         // datalong
            public eScriptFlags Flags;           // datalong2
        }

        public struct fieldset                    // FIELDSET (2)
        {
            public uint FieldID;         // datalong
            public uint FieldValue;      // datalong2
        }

        public struct moveto                   // MOVETO (3)
        {
            public uint Unused1;         // datalong
            public uint TravelTime;      // datalong2
            public int Unused2;         // dataint

            public float DestX;
            public float DestY;
            public float DestZ;
        }

        public struct flagtoggle                   // FLAGSET (4)
        // FLAGREMOVE (5)
        {
            public uint FieldID;         // datalong
            public uint FieldValue;      // datalong2
        }

        public struct teleportto                 // TELEPORTTO (6)
        {
            public uint MapID;           // datalong
            public eScriptFlags Flags;           // datalong2
            public int Unused1;         // dataint

            public float DestX;
            public float DestY;
            public float DestZ;
            public float Orientation;
        }

        public struct questexplored                  // QUESTEXPLORED (7)
        {
            public uint QuestID;         // datalong
            public uint Distance;        // datalong2
        }

        public struct killcredit                    // KILLCREDIT (8)
        {
            public uint CreatureEntry;   // datalong
            public eScriptFlags Flags;           // datalong2
        }

        public struct respawngameobject                 // RESPAWNGAMEOBJECT (9)
        {
            public uint GOGuid;          // datalong
            public uint DespawnDelay;    // datalong2
        }

        public struct tempsummoncreature                // TEMPSUMMONCREATURE (10)
        {
            public uint CreatureEntry;   // datalong
            public uint DespawnDelay;    // datalong2
            public int Unused1;         // dataint

            public float PosX;
            public float PosY;
            public float PosZ;
            public float Orientation;
        }

        public struct toggledoor                  // CLOSEDOOR (12)
        // OPENDOOR (11)
        {
            public uint GOGuid;          // datalong
            public uint ResetDelay;      // datalong2
        }

        // ACTIVATEOBJECT (13)

        public struct removeaura                   // REMOVEAURA (14)
        {
            public uint SpellID;         // datalong
            public eScriptFlags Flags;           // datalong2
        }

        public struct castspell                  // CASTSPELL (15)
        {
            public uint SpellID;         // datalong
            public eScriptFlags Flags;           // datalong2
            public int CreatureEntry;   // dataint

            public float SearchRadius;
        }

        public struct playsound                     // PLAYSOUND (16)
        {
            public uint SoundID;         // datalong
            public eScriptFlags Flags;           // datalong2
        }

        public struct createitem                   // CREATEITEM (17)
        {
            public uint ItemEntry;       // datalong
            public uint Amount;          // datalong2
        }

        public struct despawnself                 // DESPAWNSELF (18)
        {
            public uint DespawnDelay;    // datalong
        }

        public struct loadpath                    // LOADPATH (20)
        {
            public uint PathID;          // datalong
            public uint IsRepeatable;    // datalong2
        }

        public struct callscript                   // CALLSCRIPTTOUNIT (21)
        {
            public uint CreatureEntry;   // datalong
            public uint ScriptID;        // datalong2
            public uint ScriptType;      // dataint
        }

        public struct kill                    // KILL (22)
        {
            public uint Unused1;         // datalong
            public uint Unused2;         // datalong2
            public int RemoveCorpse;    // dataint
        }

        public struct orientation                    // ORIENTATION (30)
        {
            public eScriptFlags Flags;           // datalong
            public uint Unused1;         // datalong2
            public int Unused2;         // dataint

            public float Unused3;
            public float Unused4;
            public float Unused5;
            public float _Orientation;
        }

        public struct equip                  // EQUIP (31)
        {
            public uint EquipmentID;     // datalong
        }

        public struct model                    // MODEL (32)
        {
            public uint ModelID;         // datalong
        }

        // CLOSEGOSSIP (33)

        public struct playmovie                    // PLAYMOVIE (34)
        {
            public uint MovieID;         // datalong
        }

        public struct movement                      // SCRIPT_COMMAND_MOVEMENT (35)
        {
            public uint MovementType;     // datalong
            public uint MovementDistance; // datalong2
            public int Path;             // dataint
        }

        public struct playanimkit                  // SCRIPT_COMMAND_PLAY_ANIMKIT (36)
        {
            public uint AnimKitID;       // datalong
        }
        #endregion
    }

    public class CellObjectGuids
    {
        public SortedSet<ulong> creatures = new SortedSet<ulong>();
        public SortedSet<ulong> gameobjects = new SortedSet<ulong>();
    }

    public class GameTele
    {
        public float posX;
        public float posY;
        public float posZ;
        public float orientation;
        public uint mapId;
        public string name;
        public string nameLow;
    }

    public class PetLevelInfo
    {
        public PetLevelInfo()
        {
            health = 0;
            mana = 0;
        }

        public uint[] stats = new uint[(int)Stats.Max];
        public uint health;
        public uint mana;
        public uint armor;
    }

    public class SpellClickInfo
    {
        public uint spellId;
        public byte castFlags;
        public SpellClickUserTypes userType;

        // helpers
        public bool IsFitToRequirements(Unit clicker, Unit clickee)
        {
            Player playerClicker = clicker.ToPlayer();
            if (playerClicker == null)
                return true;

            Unit summoner = null;
            // Check summoners for party
            if (clickee.IsSummon())
                summoner = clickee.ToTempSummon().GetSummoner();
            if (summoner == null)
                summoner = clickee;

            // This only applies to players
            switch (userType)
            {
                case SpellClickUserTypes.Friend:
                    if (!playerClicker.IsFriendlyTo(summoner))
                        return false;
                    break;
                case SpellClickUserTypes.Raid:
                    if (!playerClicker.IsInRaidWith(summoner))
                        return false;
                    break;
                case SpellClickUserTypes.Party:
                    if (!playerClicker.IsInPartyWith(summoner))
                        return false;
                    break;
                default:
                    break;
            }

            return true;
        }
    }

    public class GraveYardData
    {
        public uint safeLocId;
        public uint team;
    }

    public class QuestPOI
    {
        public QuestPOI(int _BlobIndex, int _ObjectiveIndex, int _QuestObjectiveID, int _QuestObjectID, int _MapID, int _WorldMapAreaID, int _Foor, int _Priority, int _Flags, 
            int _WorldEffectID, int _PlayerConditionID, int _UnkWoD1)
        {
            BlobIndex = _BlobIndex;
            ObjectiveIndex = _ObjectiveIndex;
            QuestObjectiveID = _QuestObjectiveID;
            QuestObjectID = _QuestObjectID;
            MapID = _MapID;
            WorldMapAreaID = _WorldMapAreaID;
            Floor = _Foor;
            Priority = _Priority;
            Flags = _Flags;
            WorldEffectID = _WorldEffectID;
            PlayerConditionID = _PlayerConditionID;
            UnkWoD1 = _UnkWoD1;
        }

        public int BlobIndex;
        public int ObjectiveIndex;
        public int QuestObjectiveID;
        public int QuestObjectID;
        public int MapID;
        public int WorldMapAreaID;
        public int Floor;
        public int Priority;
        public int Flags;
        public int WorldEffectID;
        public int PlayerConditionID;
        public int UnkWoD1;
        public List<QuestPOIPoint> points = new List<QuestPOIPoint>();
    }

    public class QuestPOIPoint
    {
        public QuestPOIPoint(int _x, int _y)
        {
            X = _x;
            Y = _y;
        }

        public int X;
        public int Y;
    }

    public class QuestGreeting
    {
        public QuestGreeting()
        {
            greeting = "";
        }
        public QuestGreeting(ushort _greetEmoteType, uint _greetEmoteDelay, string _greeting)
        {
            greetEmoteType = _greetEmoteType;
            greetEmoteDelay = _greetEmoteDelay;
            greeting = _greeting;
        }

        public ushort greetEmoteType;
        public uint greetEmoteDelay;
        public string greeting;
    }

    public class AreaTriggerStruct
    {
        public uint target_mapId;
        public float target_X;
        public float target_Y;
        public float target_Z;
        public float target_Orientation;
        public uint PortLocId;
    }

    public class DungeonEncounter
    {
        public DungeonEncounter(DungeonEncounterRecord _dbcEntry, EncounterCreditType _creditType, uint _creditEntry, uint _lastEncounterDungeon)
        {
            dbcEntry = _dbcEntry;
            creditType = _creditType;
            creditEntry = _creditEntry;
            lastEncounterDungeon = _lastEncounterDungeon;
        }

        public DungeonEncounterRecord dbcEntry;
        public EncounterCreditType creditType;
        public uint creditEntry;
        public uint lastEncounterDungeon;
    }

    public class MailLevelReward
    {
        public MailLevelReward(uint _raceMask = 0, uint _mailTemplateId = 0, uint _senderEntry = 0)
        {
            raceMask = _raceMask;
            mailTemplateId = _mailTemplateId;
            senderEntry = _senderEntry;
        }

        public uint raceMask;
        public uint mailTemplateId;
        public uint senderEntry;
    }

    public class PageText
    {
        public string Text;
        public uint NextPageID;
        public int PlayerConditionID;
        public byte Flags;
    }

    public struct ExtendedPlayerName
    {
        public ExtendedPlayerName(string name, string realmName)
        {
            Name = name;
            Realm = realmName;
        }

        public string Name;
        public string Realm;
    }

    public class LanguageDesc
    {
        public LanguageDesc(Language langid, uint spellid, object skillid)
        {
            lang_id = langid;
            spell_id = spellid;
            skill_id = Convert.ToUInt32(skillid);
        }

        public Language lang_id;
        public uint spell_id;
        public uint skill_id;
    }

    class ItemSpecStats
    {
        public ItemSpecStats(ItemRecord item, ItemSparseRecord sparse)
        {
            if (item.Class == ItemClass.Weapon)
            {
                ItemType = 5;
                switch ((ItemSubClassWeapon)item.SubClass)
                {
                    case ItemSubClassWeapon.Axe:
                        AddStat(ItemSpecStat.OneHandedAxe);
                        break;
                    case ItemSubClassWeapon.Axe2:
                        AddStat(ItemSpecStat.TwoHandedAxe);
                        break;
                    case ItemSubClassWeapon.Bow:
                        AddStat(ItemSpecStat.Bow);
                        break;
                    case ItemSubClassWeapon.Gun:
                        AddStat(ItemSpecStat.Gun);
                        break;
                    case ItemSubClassWeapon.Mace:
                        AddStat(ItemSpecStat.OneHandedMace);
                        break;
                    case ItemSubClassWeapon.Mace2:
                        AddStat(ItemSpecStat.TwoHandedMace);
                        break;
                    case ItemSubClassWeapon.Polearm:
                        AddStat(ItemSpecStat.Polearm);
                        break;
                    case ItemSubClassWeapon.Sword:
                        AddStat(ItemSpecStat.OneHandedSword);
                        break;
                    case ItemSubClassWeapon.Sword2:
                        AddStat(ItemSpecStat.TwoHandedSword);
                        break;
                    case ItemSubClassWeapon.Warglaives:
                        AddStat(ItemSpecStat.Warglaives);
                        break;
                    case ItemSubClassWeapon.Staff:
                        AddStat(ItemSpecStat.Staff);
                        break;
                    case ItemSubClassWeapon.Fist:
                        AddStat(ItemSpecStat.FistWeapon);
                        break;
                    case ItemSubClassWeapon.Dagger:
                        AddStat(ItemSpecStat.Dagger);
                        break;
                    case ItemSubClassWeapon.Thrown:
                        AddStat(ItemSpecStat.Thrown);
                        break;
                    case ItemSubClassWeapon.Crossbow:
                        AddStat(ItemSpecStat.Crossbow);
                        break;
                    case ItemSubClassWeapon.Wand:
                        AddStat(ItemSpecStat.Wand);
                        break;
                    default:
                        break;
                }
            }
            else if (item.Class == ItemClass.Armor)
            {
                switch ((ItemSubClassArmor)item.SubClass)
                {
                    case ItemSubClassArmor.Cloth:
                        if (sparse.inventoryType != InventoryType.Cloak)
                        {
                            ItemType = 1;
                            break;
                        }

                        ItemType = 0;
                        AddStat(ItemSpecStat.Cloak);
                        break;
                    case ItemSubClassArmor.Leather:
                        ItemType = 2;
                        break;
                    case ItemSubClassArmor.Mail:
                        ItemType = 3;
                        break;
                    case ItemSubClassArmor.Plate:
                        ItemType = 4;
                        break;
                    default:
                        if (item.SubClass == (int)ItemSubClassArmor.Shield)
                        {
                            ItemType = 6;
                            AddStat(ItemSpecStat.Shield);
                        }
                        else if (item.SubClass > (int)ItemSubClassArmor.Shield && item.SubClass <= (int)ItemSubClassArmor.Relic)
                        {
                            ItemType = 6;
                            AddStat(ItemSpecStat.Relic);
                        }
                        else
                            ItemType = 0;
                        break;
                }
            }
            else if (item.Class == ItemClass.Gem)
            {
                ItemType = 7;
                GemPropertiesRecord gem = CliDB.GemPropertiesStorage.LookupByKey(sparse.GemProperties);
                if (gem != null)
                {
                    if (gem.Type.HasAnyFlag(SocketColor.RelicIron))
                        AddStat(ItemSpecStat.RelicIron);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicBlood))
                        AddStat(ItemSpecStat.RelicBlood);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicShadow))
                        AddStat(ItemSpecStat.RelicShadow);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicFel))
                        AddStat(ItemSpecStat.RelicFel);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicArcane))
                        AddStat(ItemSpecStat.RelicArcane);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicFrost))
                        AddStat(ItemSpecStat.RelicFrost);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicFire))
                        AddStat(ItemSpecStat.RelicFire);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicWater))
                        AddStat(ItemSpecStat.RelicWater);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicLife))
                        AddStat(ItemSpecStat.RelicLife);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicWind))
                        AddStat(ItemSpecStat.RelicWind);
                    if (gem.Type.HasAnyFlag(SocketColor.RelicHoly))
                        AddStat(ItemSpecStat.RelicHoly);
                }
            }
            else
                ItemType = 0;

            for (uint i = 0; i < ItemConst.MaxStats; ++i)
                if (sparse.ItemStatType[i] != -1)
                    AddModStat(sparse.ItemStatType[i]);
        }

        void AddStat(ItemSpecStat statType)
        {
            if (ItemSpecStatCount >= ItemConst.MaxStats)
                return;

            for (uint i = 0; i < ItemConst.MaxStats; ++i)
                if (ItemSpecStatTypes[i] == statType)
                    return;

            ItemSpecStatTypes[ItemSpecStatCount++] = statType;
        }

        void AddModStat(int itemStatType)
        {
            switch ((ItemModType)itemStatType)
            {
                case ItemModType.Agility:
                    AddStat(ItemSpecStat.Agility);
                    break;
                case ItemModType.Strength:
                    AddStat(ItemSpecStat.Strength);
                    break;
                case ItemModType.Intellect:
                    AddStat(ItemSpecStat.Intellect);
                    break;
                case ItemModType.DodgeRating:
                    AddStat(ItemSpecStat.Dodge);
                    break;
                case ItemModType.ParryRating:
                    AddStat(ItemSpecStat.Parry);
                    break;
                case ItemModType.CritMeleeRating:
                case ItemModType.CritRangedRating:
                case ItemModType.CritSpellRating:
                case ItemModType.CritRating:
                    AddStat(ItemSpecStat.Crit);
                    break;
                case ItemModType.HasteRating:
                    AddStat(ItemSpecStat.Haste);
                    break;
                case ItemModType.HitRating:
                    AddStat(ItemSpecStat.Hit);
                    break;
                case ItemModType.ExtraArmor:
                    AddStat(ItemSpecStat.BonusArmor);
                    break;
                case ItemModType.AgiStrInt:
                    AddStat(ItemSpecStat.Agility);
                    AddStat(ItemSpecStat.Strength);
                    AddStat(ItemSpecStat.Intellect);
                    break;
                case ItemModType.AgiStr:
                    AddStat(ItemSpecStat.Agility);
                    AddStat(ItemSpecStat.Strength);
                    break;
                case ItemModType.AgiInt:
                    AddStat(ItemSpecStat.Agility);
                    AddStat(ItemSpecStat.Intellect);
                    break;
                case ItemModType.StrInt:
                    AddStat(ItemSpecStat.Strength);
                    AddStat(ItemSpecStat.Intellect);
                    break;
            }
        }

        public uint ItemType;
        public ItemSpecStat[] ItemSpecStatTypes = new ItemSpecStat[ItemConst.MaxStats];
        public uint ItemSpecStatCount;
    }

    public class SkillTiersEntry
    {
        public uint Id;
        public uint[] Value = new uint[SkillConst.MaxSkillStep];
    }

    public class PhaseInfoStruct
    {
        public uint Id;
        public List<Condition> Conditions = new List<Condition>();
    }

    public class SceneTemplate
    {
        public uint SceneId;
        public SceneFlags PlaybackFlags;
        public uint ScenePackageId;
        public uint ScriptId;
    }

    public class DefaultCreatureBaseStats : CreatureBaseStats
    {
        public DefaultCreatureBaseStats()
        {
            BaseArmor = 1;
            for (byte j = 0; j < 4; ++j)
            {
                BaseHealth[j] = 1;
                BaseDamage[j] = 0.0f;
            }
            BaseMana = 0;
            AttackPower = 0;
            RangedAttackPower = 0;
        }
    }

    public class GossipMenuItemsLocale
    {
        public StringArray OptionText = new StringArray((int)LocaleConstant.Total);
        public StringArray BoxText = new StringArray((int)LocaleConstant.Total);
    }
}
