diff --git a/Source/Framework/Constants/MapConst.cs b/Source/Framework/Constants/MapConst.cs index 433c97500..859a210b8 100644 --- a/Source/Framework/Constants/MapConst.cs +++ b/Source/Framework/Constants/MapConst.cs @@ -92,12 +92,16 @@ namespace Framework.Constants public enum InstanceResetMethod { - All, - ChangeDifficulty, - Global, - GroupDisband, - GroupJoin, - RespawnDelay + Manual, + OnChangeDifficulty, + Expire, + } + + public enum InstanceResetResult + { + Success, + NotEmpty, + CannotReset } [Flags] @@ -188,6 +192,7 @@ namespace Framework.Constants CannotEnterTooManyInstances, // Player Has Entered Too Many Instances Recently CannotEnterMaxPlayers, // Target Map Already Has The Maximum Number Of Players Allowed CannotEnterZoneInCombat, // A Boss Encounter Is Currently In Progress On The Target Map + CannotEnterInstanceShuttingDown, CannotEnterUnspecifiedReason } diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index 9551600ae..96f687779 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -2892,6 +2892,11 @@ namespace Game.Entities // flight will started later } } + else if (mapEntry.IsDungeon() && instanceId != 0) + { + // try finding instance by id first + map = Global.MapMgr.FindMap(mapId, instanceId); + } // Map could be changed before mapEntry = CliDB.MapStorage.LookupByKey(mapId); @@ -2940,6 +2945,9 @@ namespace Game.Entities case EnterState.CannotEnterZoneInCombat: SendTransferAborted(map.GetId(), TransferAbortReason.ZoneInCombat); break; + case EnterState.CannotEnterInstanceShuttingDown: + SendTransferAborted(map.GetId(), TransferAbortReason.NotFound); + break; default: break; } diff --git a/Source/Game/Entities/Player/Player.Map.cs b/Source/Game/Entities/Player/Player.Map.cs index 062322ac2..ae67bbe28 100644 --- a/Source/Game/Entities/Player/Player.Map.cs +++ b/Source/Game/Entities/Player/Player.Map.cs @@ -16,7 +16,6 @@ */ using Framework.Constants; -using Framework.Database; using Game.DataStorage; using Game.Groups; using Game.Guilds; @@ -24,7 +23,7 @@ using Game.Maps; using Game.Networking.Packets; using System; using System.Collections.Generic; -using Framework.Dynamic; +using System.Linq; namespace Game.Entities { @@ -498,9 +497,40 @@ namespace Game.Entities } // Reset all solo instances and optionally send a message on success for each - public void ResetInstances(InstanceResetMethod method, bool isRaid, bool isLegacy) + public void ResetInstances(InstanceResetMethod method) { + foreach (var (mapId, instanceId) in m_recentInstances.ToList()) + { + Map map = Global.MapMgr.FindMap(mapId, instanceId); + bool forgetInstance = false; + if (map) + { + InstanceMap instance = map.ToInstanceMap(); + if (instance != null) + { + switch (instance.Reset(method)) + { + case InstanceResetResult.Success: + SendResetInstanceSuccess(map.GetId()); + forgetInstance = true; + break; + case InstanceResetResult.NotEmpty: + if (method == InstanceResetMethod.Manual) + SendResetInstanceFailed(ResetFailedReason.Failed, map.GetId()); + else if (method == InstanceResetMethod.OnChangeDifficulty) + forgetInstance = true; + break; + case InstanceResetResult.CannotReset: + break; + default: + break; + } + } + } + if (forgetInstance) + m_recentInstances.Remove(mapId); + } } public void SendResetInstanceSuccess(uint MapId) diff --git a/Source/Game/Groups/Group.cs b/Source/Game/Groups/Group.cs index 9bead0668..f5db44c19 100644 --- a/Source/Game/Groups/Group.cs +++ b/Source/Game/Groups/Group.cs @@ -463,12 +463,6 @@ namespace Game.Groups if (!IsLeader(player.GetGUID()) && !IsBGGroup() && !IsBFGroup()) { - // reset the new member's instances, unless he is currently in one of them - // including raid/heroic instances that they are not permanently bound to! - player.ResetInstances(InstanceResetMethod.GroupJoin, false, false); - player.ResetInstances(InstanceResetMethod.GroupJoin, true, false); - player.ResetInstances(InstanceResetMethod.GroupJoin, true, true); - if (player.GetDungeonDifficultyID() != GetDungeonDifficultyID()) { player.SetDungeonDifficultyID(GetDungeonDifficultyID()); @@ -754,15 +748,11 @@ namespace Game.Groups stmt.AddValue(0, m_dbStoreId); trans.Append(stmt); - DB.Characters.CommitTransaction(trans); - - ResetInstances(InstanceResetMethod.GroupDisband, false, false, null); - ResetInstances(InstanceResetMethod.GroupDisband, true, false, null); - ResetInstances(InstanceResetMethod.GroupDisband, true, true, null); - stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_LFG_DATA); stmt.AddValue(0, m_dbStoreId); - DB.Characters.Execute(stmt); + trans.Append(stmt); + + DB.Characters.CommitTransaction(trans); Global.GroupMgr.FreeGroupDbStoreId(this); } @@ -1359,9 +1349,30 @@ namespace Game.Groups public Difficulty GetRaidDifficultyID() { return m_raidDifficulty; } public Difficulty GetLegacyRaidDifficultyID() { return m_legacyRaidDifficulty; } - public void ResetInstances(InstanceResetMethod method, bool isRaid, bool isLegacy, Player SendMsgTo) + public void ResetInstances(InstanceResetMethod method, Player notifyPlayer) { - + for (GroupInstanceReference refe = m_ownedInstancesMgr.GetFirst(); refe != null; refe = refe.Next()) + { + InstanceMap map = refe.GetSource(); + switch (map.Reset(method)) + { + case InstanceResetResult.Success: + notifyPlayer.SendResetInstanceSuccess(map.GetId()); + m_recentInstances.Remove(map.GetId()); + break; + case InstanceResetResult.NotEmpty: + if (method == InstanceResetMethod.Manual) + notifyPlayer.SendResetInstanceFailed(ResetFailedReason.Failed, map.GetId()); + else if (method == InstanceResetMethod.OnChangeDifficulty) + m_recentInstances.Remove(map.GetId()); // map might not have been reset on difficulty change but we still don't want to zone in there again + break; + case InstanceResetResult.CannotReset: + m_recentInstances.Remove(map.GetId()); // forget the instance, allows retrying different lockout with a new leader + break; + default: + break; + } + } } public void LinkOwnedInstance(GroupInstanceReference refe) diff --git a/Source/Game/Handlers/MiscHandler.cs b/Source/Game/Handlers/MiscHandler.cs index 26bfe1c19..6594918ab 100644 --- a/Source/Game/Handlers/MiscHandler.cs +++ b/Source/Game/Handlers/MiscHandler.cs @@ -300,7 +300,7 @@ namespace Game reviveAtTrigger = true; break; case EnterState.CannotEnterCorpseInDifferentInstance: - player.SendPacket(new AreaTriggerNoCorpse()); + SendPacket(new AreaTriggerNoCorpse()); Log.outDebug(LogFilter.Maps, "MAP: Player '{0}' does not have a corpse in instance map {1} and cannot enter", player.GetName(), at.target_mapId); break; case EnterState.CannotEnterInstanceBindMismatch: @@ -326,6 +326,11 @@ namespace Game player.SendTransferAborted(at.target_mapId, TransferAbortReason.ZoneInCombat); reviveAtTrigger = true; break; + case EnterState.CannotEnterInstanceShuttingDown: + player.SendTransferAborted(at.target_mapId, TransferAbortReason.NotFound); + Log.outDebug(LogFilter.Maps, $"MAP: Player '{player.GetName()}' cannot enter instance map {at.target_mapId} because instance is resetting."); + reviveAtTrigger = true; + break; default: break; } @@ -594,14 +599,23 @@ namespace Game [WorldPacketHandler(ClientOpcodes.ResetInstances)] void HandleResetInstances(ResetInstances packet) { + Map map = _player.GetMap(); + if (map != null && map.Instanceable()) + return; + Group group = GetPlayer().GetGroup(); if (group) { - if (group.IsLeader(GetPlayer().GetGUID())) - group.ResetInstances(InstanceResetMethod.All, false, false, GetPlayer()); + if (!group.IsLeader(GetPlayer().GetGUID())) + return; + + if (group.IsLFGGroup()) + return; + + group.ResetInstances(InstanceResetMethod.Manual, _player); } else - GetPlayer().ResetInstances(InstanceResetMethod.All, false, false); + GetPlayer().ResetInstances(InstanceResetMethod.Manual); } [WorldPacketHandler(ClientOpcodes.SetDungeonDifficulty)] @@ -635,7 +649,7 @@ namespace Game // cannot reset while in an instance Map map = GetPlayer().GetMap(); - if (map && map.IsDungeon()) + if (map && map.Instanceable()) { Log.outDebug(LogFilter.Network, "WorldSession:HandleSetDungeonDifficulty: player (Name: {0}, {1}) tried to reset the instance while player is inside!", GetPlayer().GetName(), GetPlayer().GetGUID().ToString()); @@ -645,33 +659,19 @@ namespace Game Group group = GetPlayer().GetGroup(); if (group) { - if (group.IsLeader(GetPlayer().GetGUID())) - { - for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) - { - Player groupGuy = refe.GetSource(); - if (!groupGuy) - continue; + if (!group.IsLeader(_player.GetGUID())) + return; - if (!groupGuy.IsInWorld) - return; + if (group.IsLFGGroup()) + return; - if (groupGuy.GetMap().IsNonRaidDungeon()) - { - Log.outDebug(LogFilter.Network, "WorldSession:HandleSetDungeonDifficulty: {0} tried to reset the instance while group member (Name: {1}, {2}) is inside!", - GetPlayer().GetGUID().ToString(), groupGuy.GetName(), groupGuy.GetGUID().ToString()); - return; - } - } - // the difficulty is set even if the instances can't be reset - //_player.SendDungeonDifficulty(true); - group.ResetInstances(InstanceResetMethod.ChangeDifficulty, false, false, GetPlayer()); - group.SetDungeonDifficultyID(difficultyID); - } + // the difficulty is set even if the instances can't be reset + group.ResetInstances(InstanceResetMethod.OnChangeDifficulty, _player); + group.SetDungeonDifficultyID(difficultyID); } else { - GetPlayer().ResetInstances(InstanceResetMethod.ChangeDifficulty, false, false); + GetPlayer().ResetInstances(InstanceResetMethod.OnChangeDifficulty); GetPlayer().SetDungeonDifficultyID(difficultyID); GetPlayer().SendDungeonDifficulty(); } @@ -702,7 +702,7 @@ namespace Game return; } - if (((int)(difficultyEntry.Flags & DifficultyFlags.Legacy) >> 5) != setRaidDifficulty.Legacy) + if (((int)(difficultyEntry.Flags & DifficultyFlags.Legacy) != 0) != (setRaidDifficulty.Legacy != 0)) { Log.outDebug(LogFilter.Network, "WorldSession.HandleSetDungeonDifficulty: {0} sent not matching legacy difficulty {1}!", GetPlayer().GetGUID().ToString(), difficultyEntry.Id); @@ -715,7 +715,7 @@ namespace Game // cannot reset while in an instance Map map = GetPlayer().GetMap(); - if (map && map.IsDungeon()) + if (map && map.Instanceable()) { Log.outDebug(LogFilter.Network, "WorldSession:HandleSetRaidDifficulty: player (Name: {0}, {1} tried to reset the instance while inside!", GetPlayer().GetName(), GetPlayer().GetGUID().ToString()); @@ -725,34 +725,22 @@ namespace Game Group group = GetPlayer().GetGroup(); if (group) { - if (group.IsLeader(GetPlayer().GetGUID())) - { - for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) - { - Player groupGuy = refe.GetSource(); - if (!groupGuy) - continue; + if (!group.IsLeader(_player.GetGUID())) + return; - if (!groupGuy.IsInWorld) - return; + if (group.IsLFGGroup()) + return; - if (groupGuy.GetMap().IsRaid()) - { - Log.outDebug(LogFilter.Network, "WorldSession:HandleSetRaidDifficulty: player {0} tried to reset the instance while inside!", GetPlayer().GetGUID().ToString()); - return; - } - } - // the difficulty is set even if the instances can't be reset - group.ResetInstances(InstanceResetMethod.ChangeDifficulty, true, setRaidDifficulty.Legacy != 0, GetPlayer()); - if (setRaidDifficulty.Legacy != 0) - group.SetLegacyRaidDifficultyID(difficultyID); - else - group.SetRaidDifficultyID(difficultyID); - } + // the difficulty is set even if the instances can't be reset + group.ResetInstances(InstanceResetMethod.OnChangeDifficulty, _player); + if (setRaidDifficulty.Legacy != 0) + group.SetLegacyRaidDifficultyID(difficultyID); + else + group.SetRaidDifficultyID(difficultyID); } else { - GetPlayer().ResetInstances(InstanceResetMethod.ChangeDifficulty, true, setRaidDifficulty.Legacy != 0); + GetPlayer().ResetInstances(InstanceResetMethod.OnChangeDifficulty); if (setRaidDifficulty.Legacy != 0) GetPlayer().SetLegacyRaidDifficultyID(difficultyID); else diff --git a/Source/Game/Maps/Map.cs b/Source/Game/Maps/Map.cs index b8819e07c..a720db78a 100644 --- a/Source/Game/Maps/Map.cs +++ b/Source/Game/Maps/Map.cs @@ -4786,6 +4786,9 @@ namespace Game.Maps return EnterState.CannotEnterAlreadyInMap; } + if (m_shuttingDown) + return EnterState.CannotEnterInstanceShuttingDown; + // allow GM's to enter if (player.IsGameMaster()) return base.CannotEnter(player); @@ -4842,16 +4845,10 @@ namespace Game.Maps } } - // for normal instances cancel the reset schedule when the - // first player enters (no players yet) - SetResetSchedule(false); - Log.outInfo(LogFilter.Maps, "MAP: Player '{0}' entered instance '{1}' of map '{2}'", player.GetName(), GetInstanceId(), GetMapName()); // initialize unload state m_unloadTimer = 0; - m_resetAfterUnload = false; - m_unloadWhenEmpty = false; // this will acquire the same mutex so it cannot be in the previous block base.AddPlayerToMap(player, initPlayer); @@ -4888,15 +4885,12 @@ namespace Game.Maps // if last player set unload timer if (m_unloadTimer == 0 && GetPlayers().Count == 1) - m_unloadTimer = m_unloadWhenEmpty ? 1 : (uint)Math.Max(WorldConfig.GetIntValue(WorldCfg.InstanceUnloadDelay), 1); + m_unloadTimer = m_shuttingDown ? 1 : (uint)Math.Max(WorldConfig.GetIntValue(WorldCfg.InstanceUnloadDelay), 1); if (i_scenario != null) i_scenario.OnPlayerExit(player); base.RemovePlayerFromMap(player, remove); - - // for normal instances schedule the reset after all players have left - SetResetSchedule(true); } public void CreateInstanceData() @@ -4942,47 +4936,44 @@ namespace Game.Maps i_owningGroupRef.Link(group, this); } - public bool Reset(InstanceResetMethod method) + public InstanceResetResult Reset(InstanceResetMethod method) { - // note: since the map may not be loaded when the instance needs to be reset - // the instance must be deleted from the DB + // raids can be reset if no boss was killed + if (method != InstanceResetMethod.Expire && i_instanceLock != null && i_instanceLock.GetData().CompletedEncountersMask != 0) + return InstanceResetResult.CannotReset; if (HavePlayers()) { - // on manual reset, fail - if (method == InstanceResetMethod.All || method == InstanceResetMethod.ChangeDifficulty) + switch (method) { - // notify the players to leave the instance so it can be reset - foreach (Player player in GetPlayers()) - player.SendResetFailedNotify(GetId()); - } - else - { - // on lock expiration boot players (do we also care about extension state?) - if (method == InstanceResetMethod.Global) - { + case InstanceResetMethod.Manual: + // notify the players to leave the instance so it can be reset + foreach (var player in GetPlayers()) + player.SendResetFailedNotify(GetId()); + break; + case InstanceResetMethod.OnChangeDifficulty: + // no client notification + break; + case InstanceResetMethod.Expire: + // on lock expiration boot players (do we also care about extension state?) // set the homebind timer for players inside (1 minute) foreach (Player player in GetPlayers()) player.m_InstanceValid = false; - } - - if (!HasPermBoundPlayers()) - { - // the unload timer is not started - // instead the map will unload immediately after the players have left - m_unloadWhenEmpty = true; - m_resetAfterUnload = true; - } + m_shuttingDown = true; + break; + default: + break; } + + return InstanceResetResult.NotEmpty; } else { // unloaded at next update m_unloadTimer = 1; - m_resetAfterUnload = !(method == InstanceResetMethod.Global && HasPermBoundPlayers()); } - return GetPlayers().Empty(); + return InstanceResetResult.Success; } public string GetScriptName() @@ -5097,31 +5088,6 @@ namespace Game.Maps } } - public override void UnloadAll() - { - Cypher.Assert(!HavePlayers()); - - if (m_resetAfterUnload) - { - DeleteRespawnTimes(); - DeleteCorpseData(); - } - - base.UnloadAll(); - } - - public void SetResetSchedule(bool on) - { - - } - - bool HasPermBoundPlayers() - { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PERM_BIND_BY_INSTANCE); - stmt.AddValue(0, GetInstanceId()); - return !DB.Characters.Query(stmt).IsEmpty(); - } - public uint GetMaxPlayers() { MapDifficultyRecord mapDiff = GetMapDifficulty(); @@ -5168,8 +5134,7 @@ namespace Game.Maps InstanceScenario i_scenario; InstanceLock i_instanceLock; GroupInstanceReference i_owningGroupRef = new(); - bool m_resetAfterUnload; - bool m_unloadWhenEmpty; + bool m_shuttingDown; } public class BattlegroundMap : Map diff --git a/Source/Game/Maps/MapManager.cs b/Source/Game/Maps/MapManager.cs index 1bea95ec1..6f4abf312 100644 --- a/Source/Game/Maps/MapManager.cs +++ b/Source/Game/Maps/MapManager.cs @@ -326,8 +326,8 @@ namespace Game.Entities map.UnloadAll(); - // Free up the instance id and allow it to be reused for bgs and arenas (other instances are handled in the InstanceSaveMgr) - if (map.IsBattlegroundOrArena()) + // Free up the instance id and allow it to be reused for normal dungeons, bgs and arenas + if (map.IsBattlegroundOrArena() || (map.IsDungeon() && !map.GetMapDifficulty().HasResetSchedule())) FreeInstanceId(map.GetInstanceId()); // erase map