diff --git a/Source/Framework/Constants/SharedConst.cs b/Source/Framework/Constants/SharedConst.cs index 2c364244f..f043a3ad9 100644 --- a/Source/Framework/Constants/SharedConst.cs +++ b/Source/Framework/Constants/SharedConst.cs @@ -1734,6 +1734,7 @@ namespace Framework.Constants WardenNumMemChecks, WardenNumOtherChecks, Weather, + WeeklyQuestResetTimeWDay, WintergraspBattletime, WintergraspEnable, WintergraspNobattletime, diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index 24aee207a..3adef5c7d 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -29,8 +29,8 @@ namespace Framework.Database "ig.gemItemId1, ig.gemBonuses1, ig.gemContext1, ig.gemScalingLevel1, ig.gemItemId2, ig.gemBonuses2, ig.gemContext2, ig.gemScalingLevel2, ig.gemItemId3, ig.gemBonuses3, ig.gemContext3, ig.gemScalingLevel3, " + "im.fixedScalingLevel, im.artifactKnowledgeLevel"; - PrepareStatement(CharStatements.DEL_QUEST_POOL_SAVE, "DELETE FROM pool_quest_save WHERE pool_id = ?"); - PrepareStatement(CharStatements.INS_QUEST_POOL_SAVE, "INSERT INTO pool_quest_save (pool_id, quest_id) VALUES (?, ?)"); + PrepareStatement(CharStatements.DEL_POOL_QUEST_SAVE, "DELETE FROM pool_quest_save WHERE pool_id = ?"); + PrepareStatement(CharStatements.INS_POOL_QUEST_SAVE, "INSERT INTO pool_quest_save (pool_id, quest_id) VALUES (?, ?)"); PrepareStatement(CharStatements.DEL_NONEXISTENT_GUILD_BANK_ITEM, "DELETE FROM guild_bank_item WHERE guildid = ? AND TabId = ? AND SlotId = ?"); PrepareStatement(CharStatements.DEL_EXPIRED_BANS, "UPDATE character_banned SET active = 0 WHERE unbandate <= UNIX_TIMESTAMP() AND unbandate <> bandate"); PrepareStatement(CharStatements.SEL_CHECK_NAME, "SELECT 1 FROM characters WHERE name = ?"); @@ -537,7 +537,6 @@ namespace Framework.Database PrepareStatement(CharStatements.SEL_PINFO_XP, "SELECT a.xp, b.guid FROM characters a LEFT JOIN guild_member b ON a.guid = b.guid WHERE a.guid = ?"); PrepareStatement(CharStatements.SEL_CHAR_HOMEBIND, "SELECT mapId, zoneId, posX, posY, posZ, orientation FROM character_homebind WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHAR_GUID_NAME_BY_ACC, "SELECT guid, name, online FROM characters WHERE account = ?"); - PrepareStatement(CharStatements.SEL_POOL_QUEST_SAVE, "SELECT quest_id FROM pool_quest_save WHERE pool_id = ?"); PrepareStatement(CharStatements.SEL_CHAR_CUSTOMIZE_INFO, "SELECT name, race, class, gender, at_login FROM characters WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHAR_RACE_OR_FACTION_CHANGE_INFOS, "SELECT at_login, knownTitles FROM characters WHERE guid = ?"); PrepareStatement(CharStatements.SEL_INSTANCE, "SELECT data, completedEncounters, entranceId FROM instance WHERE map = ? AND id = ?"); @@ -791,8 +790,8 @@ namespace Framework.Database public enum CharStatements { - DEL_QUEST_POOL_SAVE, - INS_QUEST_POOL_SAVE, + DEL_POOL_QUEST_SAVE, + INS_POOL_QUEST_SAVE, DEL_NONEXISTENT_GUILD_BANK_ITEM, DEL_EXPIRED_BANS, SEL_CHECK_NAME, @@ -1199,7 +1198,6 @@ namespace Framework.Database SEL_PINFO_BANS, SEL_CHAR_HOMEBIND, SEL_CHAR_GUID_NAME_BY_ACC, - SEL_POOL_QUEST_SAVE, SEL_CHAR_CUSTOMIZE_INFO, SEL_CHAR_RACE_OR_FACTION_CHANGE_INFOS, SEL_INSTANCE, diff --git a/Source/Framework/Database/Databases/WorldDatabase.cs b/Source/Framework/Database/Databases/WorldDatabase.cs index fa9cf9e0e..c4cfb1e08 100644 --- a/Source/Framework/Database/Databases/WorldDatabase.cs +++ b/Source/Framework/Database/Databases/WorldDatabase.cs @@ -21,7 +21,6 @@ namespace Framework.Database { public override void PreparedStatements() { - PrepareStatement(WorldStatements.SEL_QUEST_POOLS, "SELECT entry, pool_entry FROM pool_quest"); PrepareStatement(WorldStatements.DEL_LINKED_RESPAWN, "DELETE FROM linked_respawn WHERE guid = ? AND linkType = ?"); PrepareStatement(WorldStatements.DEL_LINKED_RESPAWN_MASTER, "DELETE FROM linked_respawn WHERE linkedGuid = ? AND linkType = ?"); PrepareStatement(WorldStatements.REP_LINKED_RESPAWN, "REPLACE INTO linked_respawn (guid, linkedGuid, linkType) VALUES (?, ?, ?)"); @@ -98,7 +97,6 @@ namespace Framework.Database public enum WorldStatements { - SEL_QUEST_POOLS, DEL_LINKED_RESPAWN, DEL_LINKED_RESPAWN_MASTER, REP_LINKED_RESPAWN, diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 695f9a002..6e5b3ea32 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -537,7 +537,7 @@ namespace Game.Entities if (_spellFocusInfo.delay <= diff) ReacquireSpellFocusTarget(); else - _spellFocusInfo.delay -= 0; + _spellFocusInfo.delay -= diff; } // periodic check to see if the creature has passed an evade boundary @@ -1588,26 +1588,14 @@ namespace Game.Entities SetControlled(true, UnitState.Root); } - public override bool HasQuest(uint quest_id) + public override bool HasQuest(uint questId) { - var qr = Global.ObjectMgr.GetCreatureQuestRelationBounds(GetEntry()); - foreach (var id in qr) - { - if (id == quest_id) - return true; - } - return false; + return Global.ObjectMgr.GetCreatureQuestRelations(GetEntry()).HasQuest(questId); } - public override bool HasInvolvedQuest(uint quest_id) + public override bool HasInvolvedQuest(uint questId) { - var qir = Global.ObjectMgr.GetCreatureQuestInvolvedRelationBounds(GetEntry()); - foreach (var id in qir) - { - if (id == quest_id) - return true; - } - return false; + return Global.ObjectMgr.GetCreatureQuestInvolvedRelations(GetEntry()).HasQuest(questId); } public static bool DeleteFromDB(ulong spawnId) diff --git a/Source/Game/Entities/GameObject/GameObject.cs b/Source/Game/Entities/GameObject/GameObject.cs index 5eeecbb93..23937b082 100644 --- a/Source/Game/Entities/GameObject/GameObject.cs +++ b/Source/Game/Entities/GameObject/GameObject.cs @@ -1210,27 +1210,14 @@ namespace Game.Entities return true; } - public override bool HasQuest(uint quest_id) + public override bool HasQuest(uint questId) { - - var qr = Global.ObjectMgr.GetGOQuestRelationBounds(GetEntry()); - foreach (var id in qr) - { - if (id == quest_id) - return true; - } - return false; + return Global.ObjectMgr.GetGOQuestRelations(GetEntry()).HasQuest(questId); } - public override bool HasInvolvedQuest(uint quest_id) + public override bool HasInvolvedQuest(uint questId) { - var qir = Global.ObjectMgr.GetGOQuestInvolvedRelationBounds(GetEntry()); - foreach (var id in qir) - { - if (id == quest_id) - return true; - } - return false; + return Global.ObjectMgr.GetGOQuestInvolvedRelations(GetEntry()).HasQuest(questId); } public bool IsTransport() diff --git a/Source/Game/Entities/Player/Player.Quest.cs b/Source/Game/Entities/Player/Player.Quest.cs index a8b8cf1b2..64842c2c5 100644 --- a/Source/Game/Entities/Player/Player.Quest.cs +++ b/Source/Game/Entities/Player/Player.Quest.cs @@ -260,15 +260,15 @@ namespace Game.Entities public void PrepareQuestMenu(ObjectGuid guid) { - List objectQR; - List objectQIR; + QuestRelationResult questRelations; + QuestRelationResult questInvolvedRelations; // pets also can have quests Creature creature = ObjectAccessor.GetCreatureOrPetOrVehicle(this, guid); if (creature != null) { - objectQR = Global.ObjectMgr.GetCreatureQuestRelationBounds(creature.GetEntry()); - objectQIR = Global.ObjectMgr.GetCreatureQuestInvolvedRelationBounds(creature.GetEntry()); + questRelations = Global.ObjectMgr.GetCreatureQuestRelations(creature.GetEntry()); + questInvolvedRelations = Global.ObjectMgr.GetCreatureQuestInvolvedRelations(creature.GetEntry()); } else { @@ -279,8 +279,8 @@ namespace Game.Entities GameObject gameObject = _map.GetGameObject(guid); if (gameObject != null) { - objectQR = Global.ObjectMgr.GetGOQuestRelationBounds(gameObject.GetEntry()); - objectQIR = Global.ObjectMgr.GetGOQuestInvolvedRelationBounds(gameObject.GetEntry()); + questRelations = Global.ObjectMgr.GetGOQuestRelations(gameObject.GetEntry()); + questInvolvedRelations = Global.ObjectMgr.GetGOQuestInvolvedRelations(gameObject.GetEntry()); } else return; @@ -289,18 +289,18 @@ namespace Game.Entities QuestMenu qm = PlayerTalkClass.GetQuestMenu(); qm.ClearMenu(); - foreach (var quest_id in objectQIR) + foreach (var questId in questInvolvedRelations) { - QuestStatus status = GetQuestStatus(quest_id); + QuestStatus status = GetQuestStatus(questId); if (status == QuestStatus.Complete) - qm.AddMenuItem(quest_id, 4); + qm.AddMenuItem(questId, 4); else if (status == QuestStatus.Incomplete) - qm.AddMenuItem(quest_id, 4); + qm.AddMenuItem(questId, 4); } - foreach (var quest_id in objectQR) + foreach (var questId in questRelations) { - Quest quest = Global.ObjectMgr.GetQuestTemplate(quest_id); + Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; @@ -308,11 +308,11 @@ namespace Game.Entities continue; if (quest.IsAutoComplete() && (!quest.IsRepeatable() || quest.IsDaily() || quest.IsWeekly() || quest.IsMonthly())) - qm.AddMenuItem(quest_id, 0); + qm.AddMenuItem(questId, 0); else if (quest.IsAutoComplete()) - qm.AddMenuItem(quest_id, 4); - else if (GetQuestStatus(quest_id) == QuestStatus.None) - qm.AddMenuItem(quest_id, 2); + qm.AddMenuItem(questId, 4); + else if (GetQuestStatus(questId) == QuestStatus.None) + qm.AddMenuItem(questId, 2); } } @@ -365,7 +365,7 @@ namespace Game.Entities public Quest GetNextQuest(ObjectGuid guid, Quest quest) { - List objectQR; + QuestRelationResult quests; uint nextQuestID = quest.NextQuestInChain; switch (guid.GetHigh()) @@ -379,7 +379,7 @@ namespace Game.Entities { Creature creature = ObjectAccessor.GetCreatureOrPetOrVehicle(this, guid); if (creature != null) - objectQR = Global.ObjectMgr.GetCreatureQuestRelationBounds(creature.GetEntry()); + quests = Global.ObjectMgr.GetCreatureQuestRelations(creature.GetEntry()); else return null; break; @@ -392,7 +392,7 @@ namespace Game.Entities Cypher.Assert(_map != null); GameObject gameObject = _map.GetGameObject(guid); if (gameObject != null) - objectQR = Global.ObjectMgr.GetGOQuestRelationBounds(gameObject.GetEntry()); + quests = Global.ObjectMgr.GetGOQuestRelations(gameObject.GetEntry()); else return null; break; @@ -401,12 +401,9 @@ namespace Game.Entities return null; } - // for unit and go state - foreach (var id in objectQR) - { - if (id == nextQuestID) + if (nextQuestID != 0) + if (quests.HasQuest(nextQuestID)) return Global.ObjectMgr.GetQuestTemplate(nextQuestID); - } return null; } @@ -1898,8 +1895,8 @@ namespace Game.Entities public QuestGiverStatus GetQuestDialogStatus(WorldObject questgiver) { - List qr; - List qir; + QuestRelationResult questRelations; + QuestRelationResult questInvolvedRelations; switch (questgiver.GetTypeId()) { @@ -1913,8 +1910,8 @@ namespace Game.Entities return questStatus.Value; } - qr = Global.ObjectMgr.GetGOQuestRelationBounds(questgiver.GetEntry()); - qir = Global.ObjectMgr.GetGOQuestInvolvedRelationBounds(questgiver.GetEntry()); + questRelations = Global.ObjectMgr.GetGOQuestRelations(questgiver.GetEntry()); + questInvolvedRelations = Global.ObjectMgr.GetGOQuestInvolvedRelations(questgiver.GetEntry()); break; } case TypeId.Unit: @@ -1927,8 +1924,8 @@ namespace Game.Entities return questStatus.Value; } - qr = Global.ObjectMgr.GetCreatureQuestRelationBounds(questgiver.GetEntry()); - qir = Global.ObjectMgr.GetCreatureQuestInvolvedRelationBounds(questgiver.GetEntry()); + questRelations = Global.ObjectMgr.GetCreatureQuestRelations(questgiver.GetEntry()); + questInvolvedRelations = Global.ObjectMgr.GetCreatureQuestInvolvedRelations(questgiver.GetEntry()); break; } default: @@ -1939,7 +1936,7 @@ namespace Game.Entities QuestGiverStatus result = QuestGiverStatus.None; - foreach (var questId in qir) + foreach (var questId in questInvolvedRelations) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) @@ -1974,7 +1971,7 @@ namespace Game.Entities } } - foreach (var questId in qr) + foreach (var questId in questRelations) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) diff --git a/Source/Game/Events/GameEventManager.cs b/Source/Game/Events/GameEventManager.cs index 8bb0b70ab..810e463aa 100644 --- a/Source/Game/Events/GameEventManager.cs +++ b/Source/Game/Events/GameEventManager.cs @@ -1428,7 +1428,7 @@ namespace Game { foreach (var pair in mGameEventCreatureQuests[eventId]) { - var CreatureQuestMap = Global.ObjectMgr.GetCreatureQuestRelationMap(); + var CreatureQuestMap = Global.ObjectMgr.GetCreatureQuestRelationMapHACK(); if (activate) // Add the pair(id, quest) to the multimap CreatureQuestMap.Add(pair.Item1, pair.Item2); else @@ -1442,7 +1442,7 @@ namespace Game } foreach (var pair in mGameEventGameObjectQuests[eventId]) { - var GameObjectQuestMap = Global.ObjectMgr.GetGOQuestRelationMap(); + var GameObjectQuestMap = Global.ObjectMgr.GetGOQuestRelationMapHACK(); if (activate) // Add the pair(id, quest) to the multimap GameObjectQuestMap.Add(pair.Item1, pair.Item2); else diff --git a/Source/Game/Globals/Global.cs b/Source/Game/Globals/Global.cs index 906181811..ad7c40863 100644 --- a/Source/Game/Globals/Global.cs +++ b/Source/Game/Globals/Global.cs @@ -104,6 +104,7 @@ public static class Global public static DB2Manager DB2Mgr { get { return DB2Manager.Instance; } } public static DisableManager DisableMgr { get { return DisableManager.Instance; } } public static PoolManager PoolMgr { get { return PoolManager.Instance; } } + public static QuestPoolManager QuestPoolMgr { get { return QuestPoolManager.Instance; } } public static WeatherManager WeatherMgr { get { return WeatherManager.Instance; } } public static GameEventManager GameEventMgr { get { return GameEventManager.Instance; } } diff --git a/Source/Game/Globals/ObjectManager.cs b/Source/Game/Globals/ObjectManager.cs index 3b60be76e..d19110f3c 100644 --- a/Source/Game/Globals/ObjectManager.cs +++ b/Source/Game/Globals/ObjectManager.cs @@ -7751,7 +7751,7 @@ namespace Game } public void LoadGameobjectQuestStarters() { - LoadQuestRelationsHelper(_goQuestRelations, null, "gameobject_queststarter", true, true); + LoadQuestRelationsHelper(_goQuestRelations, null, "gameobject_queststarter"); foreach (var pair in _goQuestRelations) { @@ -7764,7 +7764,7 @@ namespace Game } public void LoadGameobjectQuestEnders() { - LoadQuestRelationsHelper(_goQuestInvolvedRelations, _goQuestInvolvedRelationsReverse, "gameobject_questender", false, true); + LoadQuestRelationsHelper(_goQuestInvolvedRelations, _goQuestInvolvedRelationsReverse, "gameobject_questender"); foreach (var pair in _goQuestInvolvedRelations) { @@ -7777,7 +7777,7 @@ namespace Game } public void LoadCreatureQuestStarters() { - LoadQuestRelationsHelper(_creatureQuestRelations, null, "creature_queststarter", true, false); + LoadQuestRelationsHelper(_creatureQuestRelations, null, "creature_queststarter"); foreach (var pair in _creatureQuestRelations) { @@ -7790,7 +7790,7 @@ namespace Game } public void LoadCreatureQuestEnders() { - LoadQuestRelationsHelper(_creatureQuestInvolvedRelations, _creatureQuestInvolvedRelationsReverse, "creature_questender", false, false); + LoadQuestRelationsHelper(_creatureQuestInvolvedRelations, _creatureQuestInvolvedRelationsReverse, "creature_questender"); foreach (var pair in _creatureQuestInvolvedRelations) { @@ -7801,7 +7801,7 @@ namespace Game 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 map, MultiMap reverseMap, string table, bool starter, bool go) + void LoadQuestRelationsHelper(MultiMap map, MultiMap reverseMap, string table) { uint oldMSTime = Time.GetMSTime(); @@ -7809,7 +7809,7 @@ namespace Game 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); + SQLResult result = DB.World.Query($"SELECT id, quest FROM {table} qr LEFT JOIN pool_quest pq ON qr.quest = pq.entry"); if (result.IsEmpty()) { @@ -7817,15 +7817,10 @@ namespace Game return; } - var poolRelationMap = go ? Global.PoolMgr.mQuestGORelation : Global.PoolMgr.mQuestCreatureRelation; - if (starter) - poolRelationMap.Clear(); - do { uint id = result.Read(0); uint quest = result.Read(1); - uint poolId = result.Read(2); if (!_questTemplates.ContainsKey(quest)) { @@ -7833,14 +7828,9 @@ namespace Game continue; } - if (poolId == 0 || !starter) - { - map.Add(id, quest); - if (reverseMap != null) - reverseMap.Add(quest, id); - } - else - poolRelationMap.Add(quest, id); + map.Add(id, quest); + if (reverseMap != null) + reverseMap.Add(quest, id); ++count; } while (result.NextRow()); @@ -8050,38 +8040,14 @@ namespace Game { return _questTemplatesAutoPush; } - public MultiMap GetGOQuestRelationMap() - { - return _goQuestRelations; - } - public List GetGOQuestRelationBounds(uint go_entry) - { - return _goQuestRelations.LookupByKey(go_entry); - } - public List GetGOQuestInvolvedRelationBounds(uint go_entry) - { - return _goQuestInvolvedRelations.LookupByKey(go_entry); - } - public List GetGOQuestInvolvedRelationReverseBounds(uint questId) - { - return _goQuestInvolvedRelationsReverse.LookupByKey(questId); - } - public MultiMap GetCreatureQuestRelationMap() - { - return _creatureQuestRelations; - } - public List GetCreatureQuestRelationBounds(uint creature_entry) - { - return _creatureQuestRelations.LookupByKey(creature_entry); - } - public List GetCreatureQuestInvolvedRelationBounds(uint creature_entry) - { - return _creatureQuestInvolvedRelations.LookupByKey(creature_entry); - } - public List GetCreatureQuestInvolvedRelationReverseBounds(uint questId) - { - return _creatureQuestInvolvedRelationsReverse.LookupByKey(questId); - } + public MultiMap GetGOQuestRelationMapHACK() { return _goQuestRelations; } + public QuestRelationResult GetGOQuestRelations(uint entry) { return GetQuestRelationsFrom(_goQuestRelations, entry, true); } + public QuestRelationResult GetGOQuestInvolvedRelations(uint entry) { return GetQuestRelationsFrom(_goQuestInvolvedRelations, entry, false);} + public List GetGOQuestInvolvedRelationReverseBounds(uint questId) { return _goQuestInvolvedRelationsReverse.LookupByKey(questId); } + public MultiMap GetCreatureQuestRelationMapHACK() { return _creatureQuestRelations; } + public QuestRelationResult GetCreatureQuestRelations(uint entry) { return GetQuestRelationsFrom(_creatureQuestRelations, entry, true); } + public QuestRelationResult GetCreatureQuestInvolvedRelations(uint entry) { return GetQuestRelationsFrom(_creatureQuestInvolvedRelations, entry, false); } + public List GetCreatureQuestInvolvedRelationReverseBounds(uint questId) { return _creatureQuestInvolvedRelationsReverse.LookupByKey(questId); } public QuestPOIData GetQuestPOIData(uint questId) { return _questPOIStorage.LookupByKey(questId); @@ -8122,7 +8088,8 @@ namespace Game { return _exclusiveQuestGroups.LookupByKey(exclusiveGroupId); } - + QuestRelationResult GetQuestRelationsFrom(MultiMap map, uint key, bool onlyActive) { return new QuestRelationResult(map.LookupByKey(key), onlyActive);} + //Spells /Skills / Phases public void LoadPhases() { @@ -10423,7 +10390,7 @@ namespace Game { return _phaseNameStorage.TryGetValue(phaseId, out string value) ? value : "Unknown Name"; } - + //Vehicles public void LoadVehicleTemplate() { @@ -11706,4 +11673,21 @@ namespace Game public byte Expansion; public uint AchievementId; } + + public class QuestRelationResult : List + { + bool _onlyActive; + + public QuestRelationResult() { } + + public QuestRelationResult(List range, bool onlyActive) : base(range) + { + _onlyActive = onlyActive; + } + + public bool HasQuest(uint questId) + { + return Contains(questId) && (!_onlyActive || Quest.IsTakingQuestEnabled(questId)); + } + } } diff --git a/Source/Game/Handlers/QueryHandler.cs b/Source/Game/Handlers/QueryHandler.cs index ff841ba01..0ef59aedf 100644 --- a/Source/Game/Handlers/QueryHandler.cs +++ b/Source/Game/Handlers/QueryHandler.cs @@ -292,12 +292,10 @@ namespace Game questCompletionNPC.QuestID = questID; - var creatures = Global.ObjectMgr.GetCreatureQuestInvolvedRelationReverseBounds(questID); - foreach (var id in creatures) + foreach (var id in Global.ObjectMgr.GetCreatureQuestInvolvedRelationReverseBounds(questID)) questCompletionNPC.NPCs.Add(id); - var gos = Global.ObjectMgr.GetGOQuestInvolvedRelationReverseBounds(questID); - foreach (var id in gos) + foreach (var id in Global.ObjectMgr.GetGOQuestInvolvedRelationReverseBounds(questID)) questCompletionNPC.NPCs.Add(id | 0x80000000); // GO mask response.QuestCompletionNPCs.Add(questCompletionNPC); diff --git a/Source/Game/Handlers/QuestHandler.cs b/Source/Game/Handlers/QuestHandler.cs index af431fe9f..53f37e317 100644 --- a/Source/Game/Handlers/QuestHandler.cs +++ b/Source/Game/Handlers/QuestHandler.cs @@ -605,7 +605,7 @@ namespace Game } // in pool and not currently available (wintergrasp weekly, dalaran weekly) - can't share - if (Global.PoolMgr.IsPartOfAPool(packet.QuestID) != 0 && !Global.PoolMgr.IsSpawnedObject(packet.QuestID)) + if (Global.QuestPoolMgr.IsQuestActive(packet.QuestID)) { sender.SendPushToPartyResponse(sender, QuestPushReason.NotDaily); return; diff --git a/Source/Game/Pools/PoolManager.cs b/Source/Game/Pools/PoolManager.cs index c0abb8052..06797d3c8 100644 --- a/Source/Game/Pools/PoolManager.cs +++ b/Source/Game/Pools/PoolManager.cs @@ -31,7 +31,6 @@ namespace Game public void Initialize() { - mQuestSearchMap.Clear(); mGameobjectSearchMap.Clear(); mCreatureSearchMap.Clear(); } @@ -253,91 +252,6 @@ namespace Game } } - Log.outInfo(LogFilter.ServerLoading, "Loading Quest Pooling Data..."); - { - uint oldMSTime = Time.GetMSTime(); - - PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.SEL_QUEST_POOLS); - SQLResult result = DB.World.Query(stmt); - - if (result.IsEmpty()) - { - Log.outInfo(LogFilter.ServerLoading, "Loaded 0 quests in pools"); - } - else - { - List creBounds; - List goBounds; - - Dictionary poolTypeMap = new(); - uint count = 0; - do - { - uint entry = result.Read(0); - uint pool_id = result.Read(1); - - if (!poolTypeMap.ContainsKey(pool_id)) - poolTypeMap[pool_id] = 0; - - Quest quest = Global.ObjectMgr.GetQuestTemplate(entry); - if (quest == null) - { - Log.outError(LogFilter.Sql, "`pool_quest` has a non existing quest template (Entry: {0}) defined for pool id ({1}), skipped.", entry, pool_id); - continue; - } - - if (!mPoolTemplate.ContainsKey(pool_id)) - { - Log.outError(LogFilter.Sql, "`pool_quest` pool id ({0}) is not in `pool_template`, skipped.", pool_id); - continue; - } - - if (!quest.IsDailyOrWeekly()) - { - Log.outError(LogFilter.Sql, "`pool_quest` has an quest ({0}) which is not daily or weekly in pool id ({1}), use ExclusiveGroup instead, skipped.", entry, pool_id); - continue; - } - - if (poolTypeMap[pool_id] == QuestTypes.None) - poolTypeMap[pool_id] = quest.IsDaily() ? QuestTypes.Daily : QuestTypes.Weekly; - - QuestTypes currType = quest.IsDaily() ? QuestTypes.Daily : QuestTypes.Weekly; - - if (poolTypeMap[pool_id] != currType) - { - Log.outError(LogFilter.Sql, "`pool_quest` quest {0} is {1} but pool ({2}) is specified for {3}, mixing not allowed, skipped.", - entry, currType, pool_id, poolTypeMap[pool_id]); - continue; - } - - creBounds = mQuestCreatureRelation.LookupByKey(entry); - goBounds = mQuestGORelation.LookupByKey(entry); - - if (creBounds.Empty() && goBounds.Empty()) - { - Log.outError(LogFilter.Sql, "`pool_quest` lists entry ({0}) as member of pool ({1}) but is not started anywhere, skipped.", entry, pool_id); - continue; - } - - PoolTemplateData pPoolTemplate = mPoolTemplate[pool_id]; - PoolObject plObject = new(entry, 0.0f); - - if (!mPoolQuestGroups.ContainsKey(pool_id)) - mPoolQuestGroups[pool_id] = new PoolGroup(); - - PoolGroup questgroup = mPoolQuestGroups[pool_id]; - questgroup.SetPoolId(pool_id); - questgroup.AddEntry(plObject, pPoolTemplate.MaxLimit); - - mQuestSearchMap.Add(entry, pool_id); - ++count; - } - while (result.NextRow()); - - Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quests in pools in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime)); - } - } - // The initialize method will spawn all pools not in an event and not in another pool, this is why there is 2 left joins with 2 null checks Log.outInfo(LogFilter.ServerLoading, "Starting objects pooling system..."); { @@ -385,67 +299,6 @@ namespace Game } } - public void SaveQuestsToDB() - { - SQLTransaction trans = new(); - - foreach (var questPoolGroup in mPoolQuestGroups.Values) - { - if (questPoolGroup.IsEmpty()) - continue; - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_QUEST_POOL_SAVE); - stmt.AddValue(0, questPoolGroup.GetPoolId()); - trans.Append(stmt); - } - - foreach (var pair in mQuestSearchMap) - { - if (IsSpawnedObject(pair.Key)) - { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_QUEST_POOL_SAVE); - stmt.AddValue(0, pair.Value); - stmt.AddValue(1, pair.Key); - trans.Append(stmt); - } - } - - DB.Characters.CommitTransaction(trans); - } - - public void ChangeDailyQuests() - { - foreach (var questPoolGroup in mPoolQuestGroups.Values) - { - Quest quest = Global.ObjectMgr.GetQuestTemplate((uint)questPoolGroup.GetFirstEqualChancedObjectId()); - if (quest != null) - { - if (quest.IsWeekly()) - continue; - - UpdatePool(questPoolGroup.GetPoolId(), 1); // anything non-zero means don't load from db - } - } - - SaveQuestsToDB(); - } - - public void ChangeWeeklyQuests() - { - foreach (var questPoolGroup in mPoolQuestGroups.Values) - { - Quest quest = Global.ObjectMgr.GetQuestTemplate((uint)questPoolGroup.GetFirstEqualChancedObjectId()); - if (quest != null) - { - if (quest.IsDaily()) - continue; - - UpdatePool(questPoolGroup.GetPoolId(), 1); - } - } - - SaveQuestsToDB(); - } - void SpawnPool(uint pool_id, ulong db_guid) { switch (typeof(T).Name) @@ -462,10 +315,6 @@ namespace Game if (mPoolPoolGroups.ContainsKey(pool_id) && !mPoolPoolGroups[pool_id].IsEmpty()) mPoolPoolGroups[pool_id].SpawnObject(mSpawnedData, mPoolTemplate[pool_id].MaxLimit, db_guid); break; - case "Quest": - if (mPoolQuestGroups.ContainsKey(pool_id) && !mPoolQuestGroups[pool_id].IsEmpty()) - mPoolQuestGroups[pool_id].SpawnObject(mSpawnedData, mPoolTemplate[pool_id].MaxLimit, db_guid); - break; } } @@ -474,7 +323,6 @@ namespace Game SpawnPool(pool_id, 0); SpawnPool(pool_id, 0); SpawnPool(pool_id, 0); - SpawnPool(pool_id, 0); } public void DespawnPool(uint pool_id) @@ -487,9 +335,6 @@ namespace Game if (mPoolPoolGroups.ContainsKey(pool_id) && !mPoolPoolGroups[pool_id].IsEmpty()) mPoolPoolGroups[pool_id].DespawnObject(mSpawnedData); - - if (mPoolQuestGroups.ContainsKey(pool_id) && !mPoolQuestGroups[pool_id].IsEmpty()) - mPoolQuestGroups[pool_id].DespawnObject(mSpawnedData); } public bool CheckPool(uint pool_id) @@ -503,9 +348,6 @@ namespace Game if (mPoolPoolGroups.ContainsKey(pool_id) && !mPoolPoolGroups[pool_id].CheckPool()) return false; - if (mPoolQuestGroups.ContainsKey(pool_id) && !mPoolQuestGroups[pool_id].CheckPool()) - return false; - return true; } @@ -528,8 +370,6 @@ namespace Game return mGameobjectSearchMap.LookupByKey(db_guid); case "Pool": return mPoolSearchMap.LookupByKey(db_guid); - case "Quest": - return mQuestSearchMap.LookupByKey(db_guid); } return 0; } @@ -566,11 +406,9 @@ namespace Game Dictionary> mPoolCreatureGroups = new(); Dictionary> mPoolGameobjectGroups = new(); Dictionary> mPoolPoolGroups = new(); - Dictionary> mPoolQuestGroups = new(); Dictionary mCreatureSearchMap = new(); Dictionary mGameobjectSearchMap = new(); Dictionary mPoolSearchMap = new(); - Dictionary mQuestSearchMap = new(); // dynamic data ActivePoolData mSpawnedData = new(); @@ -684,37 +522,6 @@ namespace Game case "Pool": Global.PoolMgr.DespawnPool((uint)guid); break; - case "Quest": - // Creatures - var questMap = Global.ObjectMgr.GetCreatureQuestRelationMap(); - var qr = Global.PoolMgr.mQuestCreatureRelation.LookupByKey(guid); - foreach (var creature in qr) - { - if (!questMap.ContainsKey(creature)) - continue; - - foreach (var quest in questMap[creature].ToList()) - { - if (quest == guid) - questMap.Remove(creature, quest); - } - } - - // Gameobjects - questMap = Global.ObjectMgr.GetGOQuestRelationMap(); - qr = Global.PoolMgr.mQuestGORelation.LookupByKey(guid); - foreach (var go in qr) - { - if (!questMap.ContainsKey(go)) - continue; - - foreach (var quest in questMap[go]) - { - if (quest == guid) - questMap.Remove(go, quest); - } - } - break; } } @@ -743,12 +550,6 @@ namespace Game public void SpawnObject(ActivePoolData spawns, uint limit, ulong triggerFrom) { - if (typeof(T).Name == "Quest") - { - SpawnQuestObject(spawns, limit, triggerFrom); - return; - } - int count = (int)(limit - spawns.GetActiveObjectCount(poolId)); // If triggered from some object respawn this object is still marked as spawned @@ -807,71 +608,6 @@ namespace Game DespawnObject(spawns, triggerFrom); } - void SpawnQuestObject(ActivePoolData spawns, uint limit, ulong triggerFrom) - { - Log.outDebug(LogFilter.Pool, "PoolGroup: Spawning pool {0}", poolId); - // load state from db - if (triggerFrom == 0) - { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_POOL_QUEST_SAVE); - stmt.AddValue(0, poolId); - SQLResult result = DB.Characters.Query(stmt); - - if (!result.IsEmpty()) - { - do - { - uint questId = result.Read(0); - spawns.ActivateObject(questId, poolId); - PoolObject tempObj = new(questId, 0.0f); - Spawn1Object(tempObj); - --limit; - } while (result.NextRow() && limit != 0); - return; - } - } - - List currentQuests = spawns.GetActiveQuests(); - List newQuests = new(); - - // always try to select different quests - foreach (var poolObject in EqualChanced) - { - if (spawns.IsActiveObject(poolObject.guid)) - continue; - newQuests.Add(poolObject.guid); - } - - // clear the pool - DespawnObject(spawns); - - // recycle minimal amount of quests if possible count is lower than limit - while (limit > newQuests.Count && !currentQuests.Empty()) - { - ulong questId = currentQuests.SelectRandom(); - newQuests.Add(questId); - currentQuests.Remove(questId); - } - - if (newQuests.Empty()) - return; - - // activate random quests - do - { - ulong questId = newQuests.SelectRandom(); - spawns.ActivateObject(questId, poolId); - PoolObject tempObj = new(questId, 0.0f); - Spawn1Object(tempObj); - newQuests.Remove(questId); - --limit; - } while (limit != 0 && !newQuests.Empty()); - - // if we are here it means the pool is initialized at startup and did not have previous saved state - if (triggerFrom == 0) - Global.PoolMgr.SaveQuestsToDB(); - } - void Spawn1Object(PoolObject obj) { switch (typeof(T).Name) @@ -916,25 +652,6 @@ namespace Game case "Pool": Global.PoolMgr.SpawnPool((uint)obj.guid); break; - case "Quest": - // Creatures - var questMap = Global.ObjectMgr.GetCreatureQuestRelationMap(); - var qr = Global.PoolMgr.mQuestCreatureRelation.LookupByKey(obj.guid); - foreach (var creature in qr) - { - Log.outDebug(LogFilter.Pool, "PoolGroup: Adding quest {0} to creature {1}", obj.guid, creature); - questMap.Add(creature, (uint)obj.guid); - } - - // Gameobjects - questMap = Global.ObjectMgr.GetGOQuestRelationMap(); - qr = Global.PoolMgr.mQuestGORelation.LookupByKey(obj.guid); - foreach (var go in qr) - { - Log.outDebug(LogFilter.Pool, "PoolGroup: Adding quest {0} to GO {1}", obj.guid, go); - questMap.Add(go, (uint)obj.guid); - } - break; } } @@ -977,8 +694,6 @@ namespace Game return mSpawnedGameobjects.Contains(db_guid); case "Pool": return mSpawnedPools.ContainsKey(db_guid); - case "Quest": - return mActiveQuests.Contains(db_guid); default: return false; } @@ -997,9 +712,6 @@ namespace Game case "Pool": mSpawnedPools[db_guid] = 0; break; - case "Quest": - mActiveQuests.Add(db_guid); - break; default: return; } @@ -1022,9 +734,6 @@ namespace Game case "Pool": mSpawnedPools.Remove(db_guid); break; - case "Quest": - mActiveQuests.Remove(db_guid); - break; default: return; } @@ -1033,11 +742,8 @@ namespace Game --mSpawnedPools[pool_id]; } - public List GetActiveQuests() { return mActiveQuests; } // a copy of the set - List mSpawnedCreatures = new(); List mSpawnedGameobjects = new(); - List mActiveQuests = new(); Dictionary mSpawnedPools = new(); } diff --git a/Source/Game/Pools/QuestPools.cs b/Source/Game/Pools/QuestPools.cs new file mode 100644 index 000000000..e2d50590d --- /dev/null +++ b/Source/Game/Pools/QuestPools.cs @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2012-2020 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 . + */ + +using Framework.Constants; +using Framework.Database; +using Game.Entities; +using Game.Maps; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Game +{ + public class QuestPoolManager : Singleton + { + List _dailyPools = new(); + List _weeklyPools = new(); + List _monthlyPools = new(); + Dictionary _poolLookup = new(); // questId -> pool + + QuestPoolManager() { } + + public static void RegeneratePool(QuestPool pool) + { + Cypher.Assert(!pool.members.Empty(), $"Quest pool {pool.poolId} is empty"); + Cypher.Assert(pool.numActive <= pool.members.Count, $"Quest Pool {pool.poolId} requests {pool.numActive} spawns, but has only {pool.members.Count} members."); + + int n = pool.members.Count - 1; + pool.activeQuests.Clear(); + for (uint i = 0; i < pool.numActive; ++i) + { + uint j = RandomHelper.URand(i, n); + if (i != j) + { + var leftList = pool.members[i]; + pool.members[i] = pool.members[j]; + pool.members[j] = leftList; + } + + foreach (uint quest in pool.members[i]) + pool.activeQuests.Add(quest); + } + } + + public static void SaveToDB(QuestPool pool, SQLTransaction trans) + { + PreparedStatement delStmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_POOL_QUEST_SAVE); + delStmt.AddValue(0, pool.poolId); + trans.Append(delStmt); + + foreach (uint questId in pool.activeQuests) + { + PreparedStatement insStmt = DB.Characters.GetPreparedStatement(CharStatements.INS_POOL_QUEST_SAVE); + insStmt.AddValue(0, pool.poolId); + insStmt.AddValue(1, questId); + trans.Append(insStmt); + } + } + + public void LoadFromDB() + { + uint oldMSTime = Time.GetMSTime(); + Dictionary, int>> lookup = new(); // poolId -> (list, index) + + _poolLookup.Clear(); + _dailyPools.Clear(); + _weeklyPools.Clear(); + _monthlyPools.Clear(); + + // load template data from world DB + { + SQLResult result = DB.World.Query("SELECT qpm.questId, qpm.poolId, qpm.poolIndex, qpt.numActive FROM quest_pool_members qpm LEFT JOIN quest_pool_template qpt ON qpm.poolId = qpt.poolId"); + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.ServerLoading, "Loaded 0 quest pools. DB table `quest_pool_members` is empty."); + return; + } + + do + { + if (result.IsNull(2)) + { + Log.outError(LogFilter.Sql, $"Table `quest_pool_members` contains reference to non-existing pool {result.Read(1)}. Skipped."); + continue; + } + + uint questId = result.Read(0); + uint poolId = result.Read(1); + uint poolIndex = result.Read(2); + uint numActive = result.Read(3); + + Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); + if (quest == null) + { + Log.outError(LogFilter.Sql, "Table `quest_pool_members` contains reference to non-existing quest %u. Skipped.", questId); + continue; + } + if (!quest.IsDailyOrWeekly() && !quest.IsMonthly()) + { + Log.outError(LogFilter.Sql, "Table `quest_pool_members` contains reference to quest %u, which is neither daily, weekly nor monthly. Skipped.", questId); + continue; + } + + if (!lookup.ContainsKey(poolId)) + { + var poolList = quest.IsDaily() ? _dailyPools : quest.IsWeekly() ? _weeklyPools : _monthlyPools; + poolList.Add(new QuestPool() { poolId = poolId, numActive = numActive }); + + lookup.Add(poolId, new Tuple, int>(poolList, poolList.Count - 1)); + } + + var pair = lookup[poolId]; + + var members = pair.Item1[pair.Item2].members; + members.Add(poolIndex, questId); + } while (result.NextRow()); + } + + // load saved spawns from character DB + { + SQLResult result = DB.Characters.Query("SELECT pool_id, quest_id FROM pool_quest_save"); + if (!result.IsEmpty()) + { + List unknownPoolIds = new(); + do + { + uint poolId = result.Read(0); + uint questId = result.Read(1); + + var it = lookup.LookupByKey(poolId); + if (it == null || it.Item1 == null) + { + Log.outError(LogFilter.Sql, "Table `pool_quest_save` contains reference to non-existant quest pool %u. Deleted.", poolId); + unknownPoolIds.Add(poolId); + continue; + } + + it.Item1[it.Item2].activeQuests.Add(questId); + } while (result.NextRow()); + + SQLTransaction trans0 = new SQLTransaction(); + foreach (uint poolId in unknownPoolIds) + { + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_POOL_QUEST_SAVE); + stmt.AddValue(0, poolId); + trans0.Append(stmt); + } + DB.Characters.CommitTransaction(trans0); + } + } + + // post-processing and sanity checks + SQLTransaction trans = new SQLTransaction(); + foreach (var pair in lookup) + { + if (pair.Value.Item1 == null) + continue; + + QuestPool pool = pair.Value.Item1[pair.Value.Item2]; + if (pool.members.Count < pool.numActive) + { + Log.outError(LogFilter.Sql, $"Table `quest_pool_template` contains quest pool {pool.poolId} requesting {pool.numActive} spawns, but only has {pool.members.Count} members. Requested spawns reduced."); + pool.numActive = (uint)pool.members.Count; + } + + bool doRegenerate = pool.activeQuests.Empty(); + if (!doRegenerate) + { + List accountedFor = new(); + uint activeCount = 0; + for (uint i = (uint)pool.members.Count; (i--) != 0;) + { + var member = pool.members[i]; + if (member.Empty()) + { + Log.outError(LogFilter.Sql, $"Table `quest_pool_members` contains no entries at index {i} for quest pool {pool.poolId}. Index removed."); + pool.members.Remove(i); + continue; + } + + // check if the first member is active + bool status = pool.activeQuests.Contains(member[0]); + // temporarily remove any spawns that are accounted for + if (status) + { + accountedFor.Add(member[0]); + pool.activeQuests.Remove(member[0]); + } + + // now check if all other members also have the same status, and warn if not + foreach (var id in member) + { + bool otherStatus = pool.activeQuests.Contains(id); + if (status != otherStatus) + Log.outWarn(LogFilter.Sql, $"Table `pool_quest_save` {(status ? "does not have" : "has")} quest {id} (in pool {pool.poolId}, index {i}) saved, but its index is{(status ? "" : " not")} " + + $"active (because quest {member[0]} is{(status ? "" : " not")} in the table). Set quest {id} to {(status ? "" : "in")}active."); + if (otherStatus) + pool.activeQuests.Remove(id); + if (status) + accountedFor.Add(id); + } + + if (status) + ++activeCount; + } + + // warn for any remaining active spawns (not part of the pool) + foreach (uint quest in pool.activeQuests) + Log.outWarn(LogFilter.Sql, $"Table `pool_quest_save` has saved quest {quest} for pool {pool.poolId}, but that quest is not part of the pool. Skipped."); + + // only the previously-found spawns should actually be active + pool.activeQuests = accountedFor; + + if (activeCount != pool.numActive) + { + doRegenerate = true; + Log.outError(LogFilter.Sql, "Table `pool_quest_save` has %u active members saved for pool %u, which requests %u active members. Pool spawns re-generated.", activeCount, pool.poolId, pool.numActive); + } + } + + if (doRegenerate) + { + RegeneratePool(pool); + SaveToDB(pool, trans); + } + + foreach (var memberKey in pool.members.Keys) + { + foreach (uint quest in pool.members[memberKey]) + { + QuestPool refe = _poolLookup[quest]; + if (refe != null) + { + Log.outError(LogFilter.Sql, "Table `quest_pool_members` lists quest %u as member of pool %u, but it is already a member of pool %u. Skipped.", quest, pool.poolId, refe.poolId); + continue; + } + refe = pool; + } + } + } + DB.Characters.CommitTransaction(trans); + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {_dailyPools.Count} daily, {_weeklyPools.Count} weekly and {_monthlyPools.Count} monthly quest pools in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + } + + void Regenerate(List pools) + { + SQLTransaction trans = new SQLTransaction(); + foreach (QuestPool pool in pools) + { + RegeneratePool(pool); + SaveToDB(pool, trans); + } + DB.Characters.CommitTransaction(trans); + } + + // the storage structure ends up making this kind of inefficient + // we don't use it in practice (only in debug commands), so that's fine + public QuestPool FindQuestPool(uint poolId) + { + bool lambda(QuestPool p) { return p.poolId == poolId; }; + + var questPool = _dailyPools.Find(lambda); + if (questPool != null) + return questPool; + + questPool = _weeklyPools.Find(lambda); + if (questPool != null) + return questPool; + + questPool = _monthlyPools.Find(lambda); + if (questPool != null) + return questPool; + + return null; + } + + public bool IsQuestActive(uint questId) + { + var it = _poolLookup.LookupByKey(questId); + if (it == null) // not pooled + return true; + + return it.activeQuests.Contains(questId); + } + + public void ChangeDailyQuests() { Regenerate(_dailyPools); } + public void ChangeWeeklyQuests() { Regenerate(_weeklyPools); } + public void ChangeMonthlyQuests() { Regenerate(_monthlyPools); } + + public bool IsQuestPooled(uint questId) { return _poolLookup.ContainsKey(questId); } + } + + public class QuestPool + { + public uint poolId; + public uint numActive; + public MultiMap members = new(); + public List activeQuests = new(); + } +} diff --git a/Source/Game/Quest/Quest.cs b/Source/Game/Quest/Quest.cs index 9c19c9406..09791a40a 100644 --- a/Source/Game/Quest/Quest.cs +++ b/Source/Game/Quest/Quest.cs @@ -312,6 +312,14 @@ namespace Game return 0; } + public static bool IsTakingQuestEnabled(uint questId) + { + if (!Global.QuestPoolMgr.IsQuestActive(questId)) + return false; + + return true; + } + public uint MoneyValue(Player player) { QuestMoneyRewardRecord money = CliDB.QuestMoneyRewardStorage.LookupByKey(player.GetQuestLevel(this)); diff --git a/Source/Game/Server/WorldConfig.cs b/Source/Game/Server/WorldConfig.cs index f64ea2a4f..543ea17a9 100644 --- a/Source/Game/Server/WorldConfig.cs +++ b/Source/Game/Server/WorldConfig.cs @@ -542,6 +542,18 @@ namespace Game Values[WorldCfg.InstanceResetTimeHour] = GetDefaultValue("Instance.ResetTimeHour", 4); Values[WorldCfg.InstanceUnloadDelay] = GetDefaultValue("Instance.UnloadDelay", 30 * Time.Minute * Time.InMilliseconds); Values[WorldCfg.DailyQuestResetTimeHour] = GetDefaultValue("Quests.DailyResetTime", 3); + if ((int)Values[WorldCfg.DailyQuestResetTimeHour] < 0 || (int)Values[WorldCfg.DailyQuestResetTimeHour] > 23) + { + Log.outError(LogFilter.ServerLoading, $"Quests.DailyResetTime ({Values[WorldCfg.DailyQuestResetTimeHour]}) must be in range 0..23. Set to 3."); + Values[WorldCfg.DailyQuestResetTimeHour] = 3; + } + + Values[WorldCfg.WeeklyQuestResetTimeWDay] = GetDefaultValue("Quests.WeeklyResetWDay", 3); + if ((int)Values[WorldCfg.WeeklyQuestResetTimeWDay] < 0 || (int)Values[WorldCfg.WeeklyQuestResetTimeWDay] > 6) + { + Log.outError(LogFilter.ServerLoading, $"Quests.WeeklyResetDay ({Values[WorldCfg.WeeklyQuestResetTimeWDay]}) must be in range 0..6. Set to 3 (Wednesday)."); + Values[WorldCfg.WeeklyQuestResetTimeWDay] = 3; + } Values[WorldCfg.MaxPrimaryTradeSkill] = GetDefaultValue("MaxPrimaryTradeSkill", 2); Values[WorldCfg.MinPetitionSigns] = GetDefaultValue("MinPetitionSigns", 4); diff --git a/Source/Game/Server/WorldManager.cs b/Source/Game/Server/WorldManager.cs index f9b031898..5e7c2a870 100644 --- a/Source/Game/Server/WorldManager.cs +++ b/Source/Game/Server/WorldManager.cs @@ -688,6 +688,9 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loading Objects Pooling Data..."); Global.PoolMgr.LoadFromDB(); + Log.outInfo(LogFilter.ServerLoading, "Loading Quest Pooling Data..."); + Global.QuestPoolMgr.LoadFromDB(); // must be after quest templates + Log.outInfo(LogFilter.ServerLoading, "Loading Game Event Data..."); // must be after loading pools fully Global.GameEventMgr.LoadFromDB(); @@ -1065,14 +1068,9 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Deleting expired bans..."); DB.Login.Execute("DELETE FROM ip_banned WHERE unbandate <= UNIX_TIMESTAMP() AND unbandate<>bandate"); // One-time query - Log.outInfo(LogFilter.ServerLoading, "Calculate next daily quest reset time..."); - InitDailyQuestResetTime(); - - Log.outInfo(LogFilter.ServerLoading, "Calculate next weekly quest reset time..."); - InitWeeklyQuestResetTime(); - - Log.outInfo(LogFilter.ServerLoading, "Calculate next monthly quest reset time..."); - InitMonthlyQuestResetTime(); + Log.outInfo(LogFilter.ServerLoading, "Initializing quest reset times..."); + InitQuestResetTimes(); + CheckScheduledResetTimes(); Log.outInfo(LogFilter.ServerLoading, "Calculate random Battlegroundreset time..."); InitRandomBGResetTime(); @@ -1307,20 +1305,7 @@ namespace Game } } - // Handle daily quests reset time - if (currentGameTime > m_NextDailyQuestReset) - { - DailyReset(); - InitDailyQuestResetTime(false); - } - - // Handle weekly quests reset time - if (currentGameTime > m_NextWeeklyQuestReset) - ResetWeeklyQuests(); - - // Handle monthly quests reset time - if (currentGameTime > m_NextMonthlyQuestReset) - ResetMonthlyQuests(); + CheckScheduledResetTimes(); if (currentGameTime > m_NextRandomBGReset) ResetRandomBG(); @@ -1979,50 +1964,136 @@ namespace Game } } - void InitWeeklyQuestResetTime() + void InitQuestResetTimes() { - long wstime = GetWorldState(WorldStates.WeeklyQuestResetTime); - long curtime = GameTime.GetGameTime(); - m_NextWeeklyQuestReset = wstime < curtime ? curtime : wstime; + m_NextDailyQuestReset = GetWorldState(WorldStates.DailyQuestResetTime); + m_NextWeeklyQuestReset = GetWorldState(WorldStates.WeeklyQuestResetTime); + m_NextMonthlyQuestReset = GetWorldState(WorldStates.MonthlyQuestResetTime); } - void InitDailyQuestResetTime(bool loading = true) + static long GetNextDailyResetTime(long t) { - long mostRecentQuestTime = 0; + return Time.GetLocalHourTimestamp(t, WorldConfig.GetUIntValue(WorldCfg.DailyQuestResetTimeHour), true); + } - if (loading) + void DailyReset() + { + // reset all saved quest status + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_DAILY); + DB.Characters.Execute(stmt); + + stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHARACTER_GARRISON_FOLLOWER_ACTIVATIONS); + stmt.AddValue(0, 1); + DB.Characters.Execute(stmt); + + // reset all quest status in memory + foreach (var itr in m_sessions) { - SQLResult result = DB.Characters.Query("SELECT MAX(time) FROM character_queststatus_daily"); - if (!result.IsEmpty()) - { - mostRecentQuestTime = result.Read(0); - } + Player player = itr.Value.GetPlayer(); + if (player != null) + player.DailyReset(); } + // reselect pools + Global.QuestPoolMgr.ChangeDailyQuests(); - // FIX ME: client not show day start time - long curTime = GameTime.GetGameTime(); + // store next reset time + long now = GameTime.GetGameTime(); + long next = GetNextDailyResetTime(now); + Cypher.Assert(now < next); - // current day reset time - long curDayResetTime = Time.GetNextResetUnixTime(WorldConfig.GetIntValue(WorldCfg.DailyQuestResetTimeHour)); + m_NextDailyQuestReset = next; + SetWorldState(WorldStates.DailyQuestResetTime, (ulong)next); - // last reset time before current moment - long resetTime = (curTime < curDayResetTime) ? curDayResetTime - Time.Day : curDayResetTime; - - // need reset (if we have quest time before last reset time (not processed by some reason) - if (mostRecentQuestTime != 0 && mostRecentQuestTime <= resetTime) - m_NextDailyQuestReset = mostRecentQuestTime; - else // plan next reset time - m_NextDailyQuestReset = (curTime >= curDayResetTime) ? curDayResetTime + Time.Day : curDayResetTime; - - SetWorldState(WorldStates.DailyQuestResetTime, (ulong)m_NextDailyQuestReset); + Log.outInfo(LogFilter.Misc, "Daily quests for all characters have been reset."); } - void InitMonthlyQuestResetTime() + static long GetNextWeeklyResetTime(long t) { - long wstime = GetWorldState(WorldStates.MonthlyQuestResetTime); - long curtime = GameTime.GetGameTime(); - m_NextMonthlyQuestReset = wstime < curtime ? curtime : wstime; + t = GetNextDailyResetTime(t); + DateTime time = Time.UnixTimeToDateTime(t); + int wday = (int)time.DayOfWeek; + int target = WorldConfig.GetIntValue(WorldCfg.WeeklyQuestResetTimeWDay); + if (target < wday) + wday -= 7; + t += (Time.Day * (target - wday)); + return t; + } + + void ResetWeeklyQuests() + { + // reset all saved quest status + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_WEEKLY); + DB.Characters.Execute(stmt); + // reset all quest status in memory + foreach (var itr in m_sessions) + { + Player player = itr.Value.GetPlayer(); + if (player != null) + player.ResetWeeklyQuestStatus(); + } + + // reselect pools + Global.QuestPoolMgr.ChangeWeeklyQuests(); + + // store next reset time + long now = GameTime.GetGameTime(); + long next = GetNextWeeklyResetTime(now); + Cypher.Assert(now < next); + + m_NextWeeklyQuestReset = next; + SetWorldState(WorldStates.WeeklyQuestResetTime, (ulong)next); + + Log.outInfo(LogFilter.Misc, "Weekly quests for all characters have been reset."); + } + + static long GetNextMonthlyResetTime(long t) + { + t = GetNextDailyResetTime(t); + DateTime time = Time.UnixTimeToDateTime(t); + if (time.Day == 1) + return t; + + var newDate = new DateTime(time.Year, time.Month + 1, 1, 0, 0, 0, time.Kind); + return Time.DateTimeToUnixTime(newDate); + } + + void ResetMonthlyQuests() + { + // reset all saved quest status + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_MONTHLY); + DB.Characters.Execute(stmt); + // reset all quest status in memory + foreach (var itr in m_sessions) + { + Player player = itr.Value.GetPlayer(); + if (player != null) + player.ResetMonthlyQuestStatus(); + } + + // reselect pools + Global.QuestPoolMgr.ChangeMonthlyQuests(); + + // store next reset time + long now = GameTime.GetGameTime(); + long next = GetNextMonthlyResetTime(now); + Cypher.Assert(now < next); + + m_NextMonthlyQuestReset = next; + SetWorldState(WorldStates.MonthlyQuestResetTime, (ulong)next); + + Log.outInfo(LogFilter.Misc, "Monthly quests for all characters have been reset."); + } + + void CheckScheduledResetTimes() + { + long now = GameTime.GetGameTime(); + if (m_NextDailyQuestReset <= now) + DailyReset(); + if (m_NextWeeklyQuestReset <= now) + ResetWeeklyQuests(); + if (m_NextMonthlyQuestReset <= now) + ResetMonthlyQuests(); } void InitRandomBGResetTime() @@ -2089,25 +2160,6 @@ namespace Game SetWorldState(WorldStates.CurrencyResetTime, (ulong)m_NextCurrencyReset); } - void DailyReset() - { - Log.outInfo(LogFilter.Server, "Daily quests reset for all characters."); - - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_DAILY); - DB.Characters.Execute(stmt); - - stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHARACTER_GARRISON_FOLLOWER_ACTIVATIONS); - stmt.AddValue(0, 1); - DB.Characters.Execute(stmt); - - foreach (var session in m_sessions.Values) - if (session.GetPlayer() != null) - session.GetPlayer().DailyReset(); - - // change available dailies - Global.PoolMgr.ChangeDailyQuests(); - } - void ResetCurrencyWeekCap() { DB.Characters.Execute("UPDATE `character_currency` SET `WeeklyQuantity` = 0"); @@ -2120,49 +2172,6 @@ namespace Game SetWorldState(WorldStates.CurrencyResetTime, (ulong)m_NextCurrencyReset); } - void ResetWeeklyQuests() - { - Log.outInfo(LogFilter.Server, "Weekly quests reset for all characters."); - - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_WEEKLY); - DB.Characters.Execute(stmt); - - foreach (var session in m_sessions.Values) - if (session.GetPlayer() != null) - session.GetPlayer().ResetWeeklyQuestStatus(); - - m_NextWeeklyQuestReset += Time.Week; - SetWorldState(WorldStates.WeeklyQuestResetTime, (ulong)m_NextWeeklyQuestReset); - - // change available weeklies - Global.PoolMgr.ChangeWeeklyQuests(); - } - - void ResetMonthlyQuests() - { - Log.outInfo(LogFilter.Server, "Monthly quests reset for all characters."); - - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_MONTHLY); - DB.Characters.Execute(stmt); - - foreach (var session in m_sessions.Values) - if (session.GetPlayer() != null) - session.GetPlayer().ResetMonthlyQuestStatus(); - - long curTime = GameTime.GetGameTime(); - - // current day reset time - long curDayResetTime = Time.GetNextResetUnixTime(30, 1, 0); - - // last reset time before current moment - long nextMonthResetTime = (curTime < curDayResetTime) ? curDayResetTime - Time.Day : curDayResetTime; - - // plan next reset time - m_NextMonthlyQuestReset = (curTime >= nextMonthResetTime) ? nextMonthResetTime + Time.Month : nextMonthResetTime; - - SetWorldState(WorldStates.MonthlyQuestResetTime, (ulong)m_NextMonthlyQuestReset); - } - public void ResetEventSeasonalQuests(ushort event_id) { PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_RESET_CHARACTER_QUESTSTATUS_SEASONAL_BY_EVENT); diff --git a/Source/WorldServer/WorldServer.conf.dist b/Source/WorldServer/WorldServer.conf.dist index cd1be7856..6f519c409 100644 --- a/Source/WorldServer/WorldServer.conf.dist +++ b/Source/WorldServer/WorldServer.conf.dist @@ -1025,6 +1025,15 @@ Quests.IgnoreAutoComplete = 0 Quests.DailyResetTime = 3 +# +# Quests.WeeklyResetWDay +# Description: Day of the week when weekly quest reset occurs. +# Range: 0 (Sunday) to 6 (Saturday) +# Default: 3 - (Wednesday) +# + +Quests.WeeklyResetWDay = 3 + # # Guild.EventLogRecordsCount # Description: Number of log entries for guild events that are stored per guild. Old entries diff --git a/sql/updates/world/master/2021_12_18_05_world_2019_08_04_00_world.sql b/sql/updates/world/master/2021_12_18_05_world_2019_08_04_00_world.sql new file mode 100644 index 000000000..2c82d1d24 --- /dev/null +++ b/sql/updates/world/master/2021_12_18_05_world_2019_08_04_00_world.sql @@ -0,0 +1,65 @@ +-- quest pools no longer support nesting, but they also don't need it +DROP TABLE IF EXISTS `quest_pool_members`; +CREATE TABLE `quest_pool_members` ( + `questId` int(10) unsigned not null, + `poolId` int(10) unsigned not null, + `poolIndex` tinyint(2) unsigned not null COMMENT 'Multiple quests with the same index will always spawn together!', + `description` varchar(255) default null, + PRIMARY KEY (`questId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +CREATE TEMPORARY TABLE `_temp_pool_quests` ( + `sortIndex` int auto_increment not null, + `questId` int(10) unsigned not null, + `subPool` int(10) unsigned, + `topPool` int(10) unsigned not null, + `description` varchar(255) default null, + PRIMARY KEY (`sortIndex`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- build a lookup table of questid <-> nested pool <-> toplevel pool +INSERT INTO `_temp_pool_quests` (`questId`,`subPool`,`topPool`,`description`) +SELECT pq.`entry` as `questId`, IF(pp.`poolSpawnId` is null, null, pq.`pool_entry`) as `subPool`, IFNULL(pp.`poolSpawnId`,pq.`pool_entry`) AS `topPool`, pq.`description` +FROM `pool_quest` as pq LEFT JOIN `pool_members` pp ON (pp.`type`=2) and (pp.`spawnId` = pq.`pool_entry`) +ORDER BY `topPool` ASC, `subPool` ASC; + +-- delete any nested pools whose members we'll remove +DELETE FROM `pool_template` WHERE `entry` IN (SELECT DISTINCT `subPool` FROM `_temp_pool_quests` WHERE `subPool` is not null); +DELETE FROM `pool_members` WHERE `type`=2 AND `spawnId` IN (SELECT DISTINCT `subPool` FROM `_temp_pool_quests` WHERE `subPool` is not null); + +-- ensure quests without a subPool have different subPool values +UPDATE `_temp_pool_quests` SET `subPool`=`sortIndex` WHERE `subPool` is null; + +SET @pool_index = 0; +SET @last_pool = 0; +SET @last_subpool = 0; + +-- poolIndex is chosen as follows: +-- *) if we're starting a new pool, the index is 0 +-- *) if the preceding element had the same subpool, the index remains the same +-- *) if the preceding element had a different subpool, the index increases by 1 +INSERT INTO `quest_pool_members` (`questId`, `poolId`, `poolIndex`, `description`) +SELECT + `questId`, + `topPool` as `poolId`, + (CASE WHEN @last_subpool = `subPool` THEN @pool_index ELSE (@pool_index := (((@last_subpool := `subPool`) and 0) + (CASE WHEN @last_pool = `topPool` THEN @pool_index+1 ELSE ((@last_pool := `topPool`) and 0) END))) END) as `poolIndex`, + `description` +FROM `_temp_pool_quests`; + +-- drop the old table +DROP TABLE `pool_quest`; + +DROP TABLE IF EXISTS `quest_pool_template`; +CREATE TABLE `quest_pool_template` ( + `poolId` mediumint(8) unsigned not null, + `numActive` int(10) unsigned not null COMMENT 'Number of indices to have active at any time', + `description` varchar(255) default null, + PRIMARY KEY (`poolId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- copy any quest pool templates over +INSERT INTO `quest_pool_template` (`poolId`, `numActive`, `description`) +SELECT DISTINCT pt.`entry`,pt.`max_limit`,pt.`description` FROM `quest_pool_members` qpm LEFT JOIN `pool_template` pt ON (qpm.`poolId` = pt.`entry`); + +-- and delete them from the original table +DELETE pt FROM `pool_template` pt LEFT JOIN `quest_pool_template` qpt ON qpt.`poolId`=pt.`entry` WHERE qpt.`poolId` is not null;