From d3dde761a2dc96411d3d6ea3218df0195afbf9f0 Mon Sep 17 00:00:00 2001 From: hondacrx Date: Tue, 4 Oct 2022 10:00:07 -0400 Subject: [PATCH] Core/Instances: Instance lock rewrite (WIP) Port From (https://github.com/TrinityCore/TrinityCore/commit/17665c929c3a9fb7fe75dd680648129bc1c1f874) --- Source/Framework/Constants/CliDBConst.cs | 15 +- Source/Framework/Constants/MapConst.cs | 3 + Source/Framework/Constants/SharedConst.cs | 5 +- Source/Framework/Database/MySqlBase.cs | 2 +- Source/Framework/Logging/Log.cs | 1 + Source/Game/Chat/Commands/MiscCommands.cs | 16 - Source/Game/Chat/Commands/TeleCommands.cs | 2 +- Source/Game/DataStorage/Structs/M_Records.cs | 14 +- Source/Game/Entities/Player/Player.DB.cs | 6 +- Source/Game/Entities/Player/Player.Fields.cs | 2 +- Source/Game/Entities/Player/Player.Map.cs | 76 ++- Source/Game/Entities/Player/Player.cs | 23 +- Source/Game/Entities/Unit/Unit.Combat.cs | 26 - Source/Game/Globals/Global.cs | 1 + Source/Game/Groups/Group.cs | 25 + Source/Game/Handlers/CalendarHandler.cs | 83 ++- Source/Game/Handlers/CharacterHandler.cs | 2 +- Source/Game/Handlers/MiscHandler.cs | 22 +- .../Maps/Instances/InstanceLockManager.cs | 513 ++++++++++++++++++ Source/Game/Maps/Instances/InstanceScript.cs | 86 ++- Source/Game/Maps/Map.cs | 289 ++++------ Source/Game/Maps/MapManager.cs | 112 ++-- .../Networking/Packets/CalendarPackets.cs | 4 +- .../Networking/Packets/InstancePackets.cs | 6 +- Source/Game/World/WorldManager.cs | 2 + Source/WorldServer/Server.cs | 1 + Source/WorldServer/WorldServer.conf.dist | 1 + 27 files changed, 907 insertions(+), 431 deletions(-) create mode 100644 Source/Game/Maps/Instances/InstanceLockManager.cs diff --git a/Source/Framework/Constants/CliDBConst.cs b/Source/Framework/Constants/CliDBConst.cs index bda6c1ac6..7e2e0c317 100644 --- a/Source/Framework/Constants/CliDBConst.cs +++ b/Source/Framework/Constants/CliDBConst.cs @@ -1628,9 +1628,20 @@ namespace Framework.Constants Scenario = 5 } - public enum MapDifficultyFlags : byte + public enum MapDifficultyFlags : int { - CannotExtend = 0x10 + LimitToPlayersFromOneRealm = 0x01, + UseLootBasedLockInsteadOfInstanceLock = 0x02, // Lock to single encounters + LockedToSoloOwner = 0x04, + ResumeDungeonProgressBasedOnLockout = 0x08, // Mythic dungeons with this flag zone into leaders instance instead of always using a fresh one (Return to Karazhan, Operation: Mechagon) + DisableLockExtension = 0x10, + } + + public enum MapDifficultyResetInterval : byte + { + Anytime = 0, + Daily = 1, + Weekly = 2 } public enum AreaMountFlags diff --git a/Source/Framework/Constants/MapConst.cs b/Source/Framework/Constants/MapConst.cs index 206b477a1..433c97500 100644 --- a/Source/Framework/Constants/MapConst.cs +++ b/Source/Framework/Constants/MapConst.cs @@ -71,6 +71,8 @@ namespace Framework.Constants public const string VMapMagic = "VMAP_4.B"; public const float VMAPInvalidHeightValue = -200000.0f; + + public const uint MaxDungeonEncountersPerBoss = 4; } public enum NewWorldReason @@ -182,6 +184,7 @@ namespace Framework.Constants CannotEnterNotInRaid, // Target Instance Is A Raid Instance And The Player Is Not In A Raid Group CannotEnterCorpseInDifferentInstance, // Player Is Dead And Their Corpse Is Not In Target Instance CannotEnterInstanceBindMismatch, // Player'S Permanent Instance Save Is Not Compatible With Their Group'S Current Instance Bind + CannotEnterAlreadyCompletedEncounter, // Player is locked to encounter that wasn't defeated in the instance yet 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 diff --git a/Source/Framework/Constants/SharedConst.cs b/Source/Framework/Constants/SharedConst.cs index 63c544fd0..159d6322a 100644 --- a/Source/Framework/Constants/SharedConst.cs +++ b/Source/Framework/Constants/SharedConst.cs @@ -943,9 +943,8 @@ namespace Framework.Constants public enum MapFlags { - CanToggleDifficulty = 0x0100, - FlexLocking = 0x8000, // All difficulties share completed encounters lock, not bound to a single instance id - // heroic difficulty flag overrides it and uses instance id bind + CanToggleDifficulty = 0x100, + FlexLocking = 0x8000, Garrison = 0x4000000 } diff --git a/Source/Framework/Database/MySqlBase.cs b/Source/Framework/Database/MySqlBase.cs index 0468c1249..6797b84c2 100644 --- a/Source/Framework/Database/MySqlBase.cs +++ b/Source/Framework/Database/MySqlBase.cs @@ -324,7 +324,7 @@ namespace Framework.Database } } - public void EscapeString(ref string str) + public static void EscapeString(ref string str) { str = MySqlHelper.EscapeString(str); } diff --git a/Source/Framework/Logging/Log.cs b/Source/Framework/Logging/Log.cs index 7d145a96a..4bbbda7ea 100644 --- a/Source/Framework/Logging/Log.cs +++ b/Source/Framework/Logging/Log.cs @@ -417,6 +417,7 @@ public enum LogFilter Garrison, Gameevent, Guild, + Instance, Lfg, Loot, MapsScript, diff --git a/Source/Game/Chat/Commands/MiscCommands.cs b/Source/Game/Chat/Commands/MiscCommands.cs index 056a1ebc6..8859dd185 100644 --- a/Source/Game/Chat/Commands/MiscCommands.cs +++ b/Source/Game/Chat/Commands/MiscCommands.cs @@ -103,22 +103,6 @@ namespace Game.Chat } } - // if the player or the player's group is bound to another instance - // the player will not be bound to another one - InstanceBind bind = _player.GetBoundInstance(target.GetMapId(), target.GetDifficultyID(map.GetEntry())); - if (bind == null) - { - Group group = _player.GetGroup(); - // if no bind exists, create a solo bind - InstanceBind gBind = group ? group.GetBoundInstance(target) : null; // if no bind exists, create a solo bind - if (gBind == null) - { - InstanceSave save = Global.InstanceSaveMgr.GetInstanceSave(target.GetInstanceId()); - if (save != null) - _player.BindToInstance(save, !save.CanReset()); - } - } - if (map.IsRaid()) { _player.SetRaidDifficultyID(target.GetRaidDifficultyID()); diff --git a/Source/Game/Chat/Commands/TeleCommands.cs b/Source/Game/Chat/Commands/TeleCommands.cs index f5525cf55..de90c4748 100644 --- a/Source/Game/Chat/Commands/TeleCommands.cs +++ b/Source/Game/Chat/Commands/TeleCommands.cs @@ -334,7 +334,7 @@ namespace Game.Chat if (player == null) return false; - DB.World.EscapeString(ref normalizedName); + WorldDatabase.EscapeString(ref normalizedName); SQLResult result = DB.World.Query($"SELECT c.position_x, c.position_y, c.position_z, c.orientation, c.map, ct.name FROM creature c INNER JOIN creature_template ct ON c.id = ct.entry WHERE ct.name LIKE '{normalizedName}'"); if (result.IsEmpty()) diff --git a/Source/Game/DataStorage/Structs/M_Records.cs b/Source/Game/DataStorage/Structs/M_Records.cs index 404b0bcf9..99e3dea9c 100644 --- a/Source/Game/DataStorage/Structs/M_Records.cs +++ b/Source/Game/DataStorage/Structs/M_Records.cs @@ -105,6 +105,7 @@ namespace Game.DataStorage } public bool IsDynamicDifficultyMap() { return (Flags[0] & MapFlags.CanToggleDifficulty) != 0; } + public bool IsFlexLocking() { return (Flags[0] & MapFlags.FlexLocking) != 0; } public bool IsGarrison() { return (Flags[0] & MapFlags.Garrison) != 0; } public bool IsSplitByFaction() { return Id == 609 || Id == 2175; } } @@ -126,19 +127,24 @@ namespace Game.DataStorage public LocalizedString Message; // m_message_lang (text showed when transfer to map failed) public uint DifficultyID; public int LockID; - public byte ResetInterval; + public MapDifficultyResetInterval ResetInterval; public uint MaxPlayers; public int ItemContext; public uint ItemContextPickerID; - public int Flags; + public MapDifficultyFlags Flags; public int ContentTuningID; public uint MapID; + public bool HasResetSchedule() { return ResetInterval != MapDifficultyResetInterval.Anytime; } + public bool IsUsingEncounterLocks() { return Flags.HasFlag(MapDifficultyFlags.UseLootBasedLockInsteadOfInstanceLock); } + public bool IsRestoringDungeonState() { return Flags.HasFlag(MapDifficultyFlags.ResumeDungeonProgressBasedOnLockout); } + public bool IsExtendable() { return !Flags.HasFlag(MapDifficultyFlags.DisableLockExtension); } + public uint GetRaidDuration() { - if (ResetInterval == 1) + if (ResetInterval == MapDifficultyResetInterval.Daily) return 86400; - if (ResetInterval == 2) + if (ResetInterval == MapDifficultyResetInterval.Weekly) return 604800; return 0; } diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index 242d16fd6..ae81b2042 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -2863,7 +2863,7 @@ namespace Game.Entities ObjectGuid transGUID = ObjectGuid.Create(HighGuid.Transport, transguid); Transport transport = null; - Map transportMap = Global.MapMgr.CreateMap(mapId, this, instanceId); + Map transportMap = Global.MapMgr.CreateMap(mapId, this); if (transportMap != null) { Transport transportOnMap = transportMap.GetTransport(transGUID); @@ -2873,7 +2873,7 @@ namespace Game.Entities { mapId = transportOnMap.GetExpectedMapId(); instanceId = 0; - transportMap = Global.MapMgr.CreateMap(mapId, this, instanceId); + transportMap = Global.MapMgr.CreateMap(mapId, this); if (transportMap) transport = transportMap.GetTransport(transGUID); } @@ -2990,7 +2990,7 @@ namespace Game.Entities // NOW player must have valid map // load the player's map here if it's not already loaded if (!map) - map = Global.MapMgr.CreateMap(mapId, this, instance_id); + map = Global.MapMgr.CreateMap(mapId, this); AreaTriggerStruct areaTrigger = null; bool check = false; diff --git a/Source/Game/Entities/Player/Player.Fields.cs b/Source/Game/Entities/Player/Player.Fields.cs index 6bb1ff6f3..c2f51038a 100644 --- a/Source/Game/Entities/Player/Player.Fields.cs +++ b/Source/Game/Entities/Player/Player.Fields.cs @@ -79,6 +79,7 @@ namespace Game.Entities GroupUpdateCounter[] m_groupUpdateSequences = new GroupUpdateCounter[2]; public Dictionary> m_boundInstances = new(); + Dictionary m_recentInstances = new(); Dictionary _instanceResetTimes = new(); uint _pendingBindId; uint _pendingBindTimer; @@ -87,7 +88,6 @@ namespace Game.Entities Difficulty m_dungeonDifficulty; Difficulty m_raidDifficulty; Difficulty m_legacyRaidDifficulty; - Difficulty m_prevMapDifficulty; //Movement public PlayerTaxi m_taxi = new(); diff --git a/Source/Game/Entities/Player/Player.Map.cs b/Source/Game/Entities/Player/Player.Map.cs index 7acd59c40..b0d0dc06e 100644 --- a/Source/Game/Entities/Player/Player.Map.cs +++ b/Source/Game/Entities/Player/Player.Map.cs @@ -223,7 +223,7 @@ namespace Game.Entities // call enter script hooks after everyting else has processed Global.ScriptMgr.OnPlayerUpdateZone(this, newZone, newArea); if (oldZone != newZone) - { + { Global.OutdoorPvPMgr.HandlePlayerEnterZone(this, newZone); Global.BattleFieldMgr.HandlePlayerEnterZone(this, newZone); SendInitWorldStates(newZone, newArea); // only if really enters to new zone, not just area change, works strange... @@ -288,8 +288,8 @@ namespace Game.Entities } public ZonePVPTypeOverride GetOverrideZonePVPType() { return (ZonePVPTypeOverride)(uint)m_activePlayerData.OverrideZonePVPType; } - public void SetOverrideZonePVPType(ZonePVPTypeOverride type) { SetUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.OverrideZonePVPType), (uint)type); } - + public void SetOverrideZonePVPType(ZonePVPTypeOverride type) { SetUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.OverrideZonePVPType), (uint)type); } + public InstanceBind GetBoundInstance(uint mapid, Difficulty difficulty, bool withExpired = false) { // some instances only have one difficulty @@ -355,7 +355,7 @@ namespace Game.Entities } if (pair.Value.perm) - GetSession().SendCalendarRaidLockout(pair.Value.save, false); + GetSession().SendCalendarRaidLockoutRemoved(pair.Value.save); pair.Value.save.RemovePlayer(this); // save can become invalid difficultyDic.Remove(pair.Key); @@ -436,20 +436,14 @@ namespace Game.Entities return null; } - public void BindToInstance() + public void ConfirmPendingBind() { - InstanceSave mapSave = Global.InstanceSaveMgr.GetInstanceSave(_pendingBindId); - if (mapSave == null) //it seems sometimes mapSave is NULL, but I did not check why + InstanceMap map = GetMap().ToInstanceMap(); + if (map == null || map.GetInstanceId() != _pendingBindId) return; - InstanceSaveCreated data = new(); - data.Gm = IsGameMaster(); - SendPacket(data); if (!IsGameMaster()) - { - BindToInstance(mapSave, true, BindExtensionState.Keep); - GetSession().SendCalendarRaidLockout(mapSave, true); - } + map.CreateInstanceLockForPlayer(this); } public void SetPendingBind(uint instanceId, uint bindTimer) @@ -460,41 +454,25 @@ namespace Game.Entities public void SendRaidInfo() { + DateTime now = GameTime.GetSystemTime(); + + var instanceLocks = Global.InstanceLockMgr.GetInstanceLocksForPlayer(GetGUID()); + InstanceInfoPkt instanceInfo = new(); - long now = GameTime.GetGameTime(); - foreach (var difficultyDic in m_boundInstances.Values) + foreach (InstanceLock instanceLock in instanceLocks) { - foreach (var instanceBind in difficultyDic.Values) - { - if (instanceBind.perm) - { - InstanceSave save = instanceBind.save; + InstanceLockPkt lockInfos = new(); + lockInfos.InstanceID = instanceLock.GetInstanceId(); + lockInfos.MapID = instanceLock.GetMapId(); + lockInfos.DifficultyID = (uint)instanceLock.GetDifficultyId(); + lockInfos.TimeRemaining = (int)(instanceLock.GetEffectiveExpiryTime() - now).TotalSeconds; + lockInfos.CompletedMask = instanceLock.GetData().CompletedEncountersMask; - InstanceLock lockInfos; - lockInfos.InstanceID = save.GetInstanceId(); - lockInfos.MapID = save.GetMapId(); - lockInfos.DifficultyID = (uint)save.GetDifficultyID(); - if (instanceBind.extendState != BindExtensionState.Extended) - lockInfos.TimeRemaining = (int)(save.GetResetTime() - now); - else - lockInfos.TimeRemaining = (int)(Global.InstanceSaveMgr.GetSubsequentResetTime(save.GetMapId(), save.GetDifficultyID(), save.GetResetTime()) - now); + lockInfos.Locked = !instanceLock.IsExpired(); + lockInfos.Extended = instanceLock.IsExtended(); - lockInfos.CompletedMask = 0; - Map map = Global.MapMgr.FindMap(save.GetMapId(), save.GetInstanceId()); - if (map != null) - { - InstanceScript instanceScript = ((InstanceMap)map).GetInstanceScript(); - if (instanceScript != null) - lockInfos.CompletedMask = instanceScript.GetCompletedEncounterMask(); - } - - lockInfos.Locked = instanceBind.extendState != BindExtensionState.Expired; - lockInfos.Extended = instanceBind.extendState == BindExtensionState.Extended; - - instanceInfo.LockList.Add(lockInfos); - } - } + instanceInfo.LockList.Add(lockInfos); } SendPacket(instanceInfo); @@ -820,5 +798,15 @@ namespace Game.Entities if (HasAuraType(AuraType.ForceBeathBar)) m_MirrorTimerFlags |= PlayerUnderwaterState.InWater; } + + public uint GetRecentInstanceId(uint mapId) + { + return m_recentInstances.LookupByKey(mapId); + } + + public void SetRecentInstance(uint mapId, uint instanceId) + { + m_recentInstances[mapId] = instanceId; + } } } diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index 42b6572bd..efa132724 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -78,7 +78,6 @@ namespace Game.Entities m_dungeonDifficulty = Difficulty.Normal; m_raidDifficulty = Difficulty.NormalRaid; m_legacyRaidDifficulty = Difficulty.Raid10N; - m_prevMapDifficulty = Difficulty.NormalRaid; m_InstanceValid = true; _specializationInfo = new SpecializationInfo(); @@ -580,7 +579,7 @@ namespace Game.Entities { // Player left the instance if (_pendingBindId == GetInstanceId()) - BindToInstance(); + ConfirmPendingBind(); SetPendingBind(0, 0); } else @@ -4051,8 +4050,8 @@ namespace Game.Entities corpse.UpdatePositionData(); corpse.SetZoneScript(); - // we do not need to save corpses for BG/arenas - if (!GetMap().IsBattlegroundOrArena()) + // we do not need to save corpses for instances + if (!GetMap().Instanceable()) corpse.SaveToDB(); return corpse; @@ -5304,20 +5303,12 @@ namespace Game.Entities // raid downscaling - send difficulty to player if (GetMap().IsRaid()) { - m_prevMapDifficulty = GetMap().GetDifficultyID(); - DifficultyRecord difficulty = CliDB.DifficultyStorage.LookupByKey(m_prevMapDifficulty); - SendRaidDifficulty(difficulty.Flags.HasAnyFlag(DifficultyFlags.Legacy), (int)m_prevMapDifficulty); + Difficulty mapDifficulty = GetMap().GetDifficultyID(); + var difficulty = CliDB.DifficultyStorage.LookupByKey(mapDifficulty); + SendRaidDifficulty((difficulty.Flags & DifficultyFlags.Legacy) != 0, (int)mapDifficulty); } else if (GetMap().IsNonRaidDungeon()) - { - m_prevMapDifficulty = GetMap().GetDifficultyID(); - SendDungeonDifficulty((int)m_prevMapDifficulty); - } - else if (!GetMap().Instanceable()) - { - DifficultyRecord difficulty = CliDB.DifficultyStorage.LookupByKey(m_prevMapDifficulty); - SendRaidDifficulty(difficulty.Flags.HasAnyFlag(DifficultyFlags.Legacy)); - } + SendDungeonDifficulty((int)GetMap().GetDifficultyID()); PhasingHandler.OnMapChange(this); diff --git a/Source/Game/Entities/Unit/Unit.Combat.cs b/Source/Game/Entities/Unit/Unit.Combat.cs index e09ce8bce..92328280a 100644 --- a/Source/Game/Entities/Unit/Unit.Combat.cs +++ b/Source/Game/Entities/Unit/Unit.Combat.cs @@ -917,32 +917,6 @@ namespace Game.Entities summoner.ToGameObject().GetAI()?.SummonedCreatureDies(creature, attacker); } } - - // Dungeon specific stuff, only applies to players killing creatures - if (creature.GetInstanceId() != 0) - { - Map instanceMap = creature.GetMap(); - - /// @todo do instance binding anyway if the charmer/owner is offline - if (instanceMap.IsDungeon() && ((attacker != null && attacker.GetCharmerOrOwnerPlayerOrPlayerItself() != null) || attacker == victim)) - { - if (instanceMap.IsRaidOrHeroicDungeon()) - { - if (creature.GetCreatureTemplate().FlagsExtra.HasAnyFlag(CreatureFlagsExtra.InstanceBind)) - instanceMap.ToInstanceMap().PermBindAllPlayers(); - } - else - { - // the reset time is set but not added to the scheduler - // until the players leave the instance - long resettime = GameTime.GetGameTime() + 2 * Time.Hour; - InstanceSave save = Global.InstanceSaveMgr.GetInstanceSave(creature.GetInstanceId()); - if (save != null) - if (save.GetResetTime() < resettime) - save.SetResetTime(resettime); - } - } - } } // outdoor pvp things, do these after setting the death state, else the player activity notify won't work... doh... diff --git a/Source/Game/Globals/Global.cs b/Source/Game/Globals/Global.cs index f8b235350..aac38c06c 100644 --- a/Source/Game/Globals/Global.cs +++ b/Source/Game/Globals/Global.cs @@ -74,6 +74,7 @@ public static class Global public static WaypointManager WaypointMgr { get { return WaypointManager.Instance; } } public static TransportManager TransportMgr { get { return TransportManager.Instance; } } public static InstanceSaveManager InstanceSaveMgr { get { return InstanceSaveManager.Instance; } } + public static InstanceLockManager InstanceLockMgr { get { return InstanceLockManager.Instance; } } public static ScenarioManager ScenarioMgr { get { return ScenarioManager.Instance; } } //PVP diff --git a/Source/Game/Groups/Group.cs b/Source/Game/Groups/Group.cs index 3a32810e6..aef36142c 100644 --- a/Source/Game/Groups/Group.cs +++ b/Source/Game/Groups/Group.cs @@ -2155,6 +2155,30 @@ namespace Game.Groups worker(refe.GetSource()); } + uint GetInstanceId(MapRecord mapEntry) { return 0; } + + public ObjectGuid GetRecentInstanceOwner(uint mapId) + { + + if (m_recentInstances.TryGetValue(mapId, out Tuple value)) + return value.Item1; + + return m_leaderGuid; + } + + public uint GetRecentInstanceId(uint mapId) + { + if (m_recentInstances.TryGetValue(mapId, out Tuple value)) + return value.Item2; + + return 0; + } + + public void SetRecentInstance(uint mapId, ObjectGuid instanceOwner, uint instanceId) + { + m_recentInstances[mapId] = Tuple.Create(instanceOwner, instanceId); + } + List m_memberSlots = new(); GroupRefManager m_memberMgr = new(); List m_invitees = new(); @@ -2174,6 +2198,7 @@ namespace Game.Groups ObjectGuid m_looterGuid; ObjectGuid m_masterLooterGuid; Dictionary> m_boundInstances = new(); + Dictionary> m_recentInstances = new(); byte[] m_subGroupsCounts; ObjectGuid m_guid; uint m_dbStoreId; diff --git a/Source/Game/Handlers/CalendarHandler.cs b/Source/Game/Handlers/CalendarHandler.cs index c8292febf..05a3820ab 100644 --- a/Source/Game/Handlers/CalendarHandler.cs +++ b/Source/Game/Handlers/CalendarHandler.cs @@ -72,27 +72,16 @@ namespace Game packet.Events.Add(eventInfo); } - foreach (var difficulty in CliDB.DifficultyStorage.Values) + foreach (InstanceLock instanceLock in Global.InstanceLockMgr.GetInstanceLocksForPlayer(_player.GetGUID())) { - var boundInstances = _player.GetBoundInstances((Difficulty)difficulty.Id); - if (boundInstances != null) - { - foreach (var boundInstance in boundInstances.Values) - { - if (boundInstance.perm) - { - CalendarSendCalendarRaidLockoutInfo lockoutInfo; + CalendarSendCalendarRaidLockoutInfo lockoutInfo = new(); - InstanceSave save = boundInstance.save; - lockoutInfo.MapID = (int)save.GetMapId(); - lockoutInfo.DifficultyID = (uint)save.GetDifficultyID(); - lockoutInfo.ExpireTime = save.GetResetTime() - currTime; - lockoutInfo.InstanceID = save.GetInstanceId(); // instance save id as unique instance copy id + lockoutInfo.MapID = (int)instanceLock.GetMapId(); + lockoutInfo.DifficultyID = (uint)instanceLock.GetDifficultyId(); + lockoutInfo.ExpireTime = (int)(instanceLock.GetEffectiveExpiryTime() - GameTime.GetSystemTime()).TotalSeconds; + lockoutInfo.InstanceID = instanceLock.GetInstanceId(); - packet.RaidLockouts.Add(lockoutInfo); - } - } - } + packet.RaidLockouts.Add(lockoutInfo); } SendPacket(packet); @@ -253,7 +242,7 @@ namespace Game Global.CalendarMgr.SendCalendarCommandResult(guid, CalendarError.EventPassed); return; } - + CalendarEvent oldEvent = Global.CalendarMgr.GetEvent(calendarCopyEvent.EventID); if (oldEvent != null) { @@ -564,39 +553,17 @@ namespace Game player.BindToInstance(instanceBind.save, true, newState, false); } - /* - InstancePlayerBind* instanceBind = GetPlayer().GetBoundInstance(setSavedInstanceExtend.MapID, Difficulty(setSavedInstanceExtend.DifficultyID); - if (!instanceBind || !instanceBind.save) - return; - - InstanceSave* save = instanceBind.save; - // http://www.wowwiki.com/Instance_Lock_Extension - // SendCalendarRaidLockoutUpdated(save); - */ } - public void SendCalendarRaidLockout(InstanceSave save, bool add) + public void SendCalendarRaidLockoutAdded(InstanceLock instanceLock) { - long currTime = GameTime.GetGameTime(); - - if (add) - { - CalendarRaidLockoutAdded calendarRaidLockoutAdded = new(); - calendarRaidLockoutAdded.InstanceID = save.GetInstanceId(); - calendarRaidLockoutAdded.ServerTime = (uint)currTime; - calendarRaidLockoutAdded.MapID = (int)save.GetMapId(); - calendarRaidLockoutAdded.DifficultyID = save.GetDifficultyID(); - calendarRaidLockoutAdded.TimeRemaining = (int)(save.GetResetTime() - currTime); - SendPacket(calendarRaidLockoutAdded); - } - else - { - CalendarRaidLockoutRemoved calendarRaidLockoutRemoved = new(); - calendarRaidLockoutRemoved.InstanceID = save.GetInstanceId(); - calendarRaidLockoutRemoved.MapID = (int)save.GetMapId(); - calendarRaidLockoutRemoved.DifficultyID = save.GetDifficultyID(); - SendPacket(calendarRaidLockoutRemoved); - } + CalendarRaidLockoutAdded calendarRaidLockoutAdded = new(); + calendarRaidLockoutAdded.InstanceID = instanceLock.GetInstanceId(); + calendarRaidLockoutAdded.ServerTime = (uint)GameTime.GetGameTime(); + calendarRaidLockoutAdded.MapID = (int)instanceLock.GetMapId(); + calendarRaidLockoutAdded.DifficultyID = instanceLock.GetDifficultyId(); + calendarRaidLockoutAdded.TimeRemaining = (int)(instanceLock.GetExpiryTime() - GameTime.GetSystemTime()).TotalSeconds; + SendPacket(calendarRaidLockoutAdded); } public void SendCalendarRaidLockoutUpdated(InstanceSave save) @@ -614,5 +581,23 @@ namespace Game SendPacket(packet); } + + public void SendCalendarRaidLockoutRemoved(InstanceSave save) + { + CalendarRaidLockoutRemoved calendarRaidLockoutRemoved = new(); + calendarRaidLockoutRemoved.InstanceID = save.GetInstanceId(); + calendarRaidLockoutRemoved.MapID = (int)save.GetMapId(); + calendarRaidLockoutRemoved.DifficultyID = save.GetDifficultyID(); + SendPacket(calendarRaidLockoutRemoved); + } + + void SendCalendarRaidLockoutRemoved(InstanceLock instanceLock) + { + CalendarRaidLockoutRemoved calendarRaidLockoutRemoved = new(); + calendarRaidLockoutRemoved.InstanceID = instanceLock.GetInstanceId(); + calendarRaidLockoutRemoved.MapID = (int)instanceLock.GetMapId(); + calendarRaidLockoutRemoved.DifficultyID = instanceLock.GetDifficultyId(); + SendPacket(calendarRaidLockoutRemoved); + } } } diff --git a/Source/Game/Handlers/CharacterHandler.cs b/Source/Game/Handlers/CharacterHandler.cs index 8055ef404..f9c6a0de1 100644 --- a/Source/Game/Handlers/CharacterHandler.cs +++ b/Source/Game/Handlers/CharacterHandler.cs @@ -1312,7 +1312,7 @@ namespace Game for (int i = 0; i < SharedConst.MaxDeclinedNameCases; ++i) { string declinedName = packet.DeclinedNames.name[i]; - DB.Characters.EscapeString(ref declinedName); + CharacterDatabase.EscapeString(ref declinedName); packet.DeclinedNames.name[i] = declinedName; } diff --git a/Source/Game/Handlers/MiscHandler.cs b/Source/Game/Handlers/MiscHandler.cs index 05c597db4..252090399 100644 --- a/Source/Game/Handlers/MiscHandler.cs +++ b/Source/Game/Handlers/MiscHandler.cs @@ -304,18 +304,14 @@ namespace Game 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: - { - MapRecord entry = CliDB.MapStorage.LookupByKey(at.target_mapId); - if (entry != null) - { - string mapName = entry.MapName[player.GetSession().GetSessionDbcLocale()]; - Log.outDebug(LogFilter.Maps, "MAP: Player '{0}' cannot enter instance map '{1}' because their permanent bind is incompatible with their group's", player.GetName(), mapName); - // is there a special opcode for this? - // @todo figure out how to get player localized difficulty string (e.g. "10 player", "Heroic" etc) - player.SendSysMessage(CypherStrings.InstanceBindMismatch, mapName); - } - reviveAtTrigger = true; - } + player.SendTransferAborted(at.target_mapId, TransferAbortReason.LockedToDifferentInstance); + Log.outDebug(LogFilter.Maps, $"MAP: Player '{player.GetName()}' cannot enter instance map {at.target_mapId} because their permanent bind is incompatible with their group's"); + reviveAtTrigger = true; + break; + case EnterState.CannotEnterAlreadyCompletedEncounter: + player.SendTransferAborted(at.target_mapId, TransferAbortReason.AlreadyCompletedEncounter); + Log.outDebug(LogFilter.Maps, $"MAP: Player '{player.GetName()}' cannot enter instance map {at.target_mapId} because their permanent bind is incompatible with their group's"); + reviveAtTrigger = true; break; case EnterState.CannotEnterTooManyInstances: player.SendTransferAborted(at.target_mapId, TransferAbortReason.TooManyInstances); @@ -783,7 +779,7 @@ namespace Game } if (packet.AcceptLock) - GetPlayer().BindToInstance(); + GetPlayer().ConfirmPendingBind(); else GetPlayer().RepopAtGraveyard(); diff --git a/Source/Game/Maps/Instances/InstanceLockManager.cs b/Source/Game/Maps/Instances/InstanceLockManager.cs new file mode 100644 index 000000000..4cf076c8e --- /dev/null +++ b/Source/Game/Maps/Instances/InstanceLockManager.cs @@ -0,0 +1,513 @@ +/* + * 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.Configuration; +using Framework.Constants; +using Framework.Database; +using Game.DataStorage; +using Game.Entities; +using System; +using System.Collections.Generic; + +namespace Game.Maps +{ + //#define INSTANCE_ID_HIGH_MASK 0x1F440000 + //#define INSTANCE_ID_LFG_MASK 0x00000001 + //#define INSTANCE_ID_NORMAL_MASK 0x00010000 + + using InstanceLockKey = Tuple; + + //using PlayerLockMap = std::unordered_map>; + //using LockMap = std::unordered_map; + + public class InstanceLockManager : Singleton + { + Dictionary> _temporaryInstanceLocksByPlayer = new(); // locks stored here before any boss gets killed + Dictionary> _instanceLocksByPlayer = new(); + Dictionary _instanceLockDataById = new(); + bool _unloading; + + InstanceLockManager() { } + + public void Load() + { + Dictionary instanceLockDataById = new(); + + // 0 1 2 + SQLResult result = DB.Characters.Query("SELECT instanceId, data, completedEncountersMask FROM instance"); + if (!result.IsEmpty()) + { + do + { + uint instanceId = result.Read(0); + + SharedInstanceLockData data = new(); + data.Data = result.Read(1); + data.CompletedEncountersMask = result.Read(2); + data.InstanceId = instanceId; + + instanceLockDataById[instanceId] = data; + + } while (result.NextRow()); + } + + // 0 1 2 3 4 5 6 7 8 + SQLResult lockResult = DB.Characters.Query("SELECT guid, mapId, lockId, instanceId, difficulty, data, completedEncountersMask, expiryTime, extended FROM character_instance_lock"); + if (!result.IsEmpty()) + { + do + { + ObjectGuid playerGuid = ObjectGuid.Create(HighGuid.Player, lockResult.Read(0)); + uint mapId = lockResult.Read(1); + uint lockId = lockResult.Read(2); + uint instanceId = lockResult.Read(3); + Difficulty difficulty = (Difficulty)lockResult.Read(4); + DateTime expiryTime = Time.UnixTimeToDateTime(lockResult.Read(7)); + + // Mark instance id as being used + Global.MapMgr.RegisterInstanceId(instanceId); + + InstanceLock instanceLock; + if (new MapDb2Entries(mapId, difficulty).IsInstanceIdBound()) + { + var sharedData = instanceLockDataById.LookupByKey(instanceId); + if (sharedData == null) + { + Log.outError(LogFilter.Instance, $"Missing instance data for instance id based lock (id {instanceId})"); + DB.Characters.Query($"DELETE FROM character_instance_lock WHERE instanceId = {instanceId}"); + continue; + } + + instanceLock = new SharedInstanceLock(mapId, difficulty, expiryTime, instanceId, sharedData); + _instanceLockDataById[instanceId] = sharedData; + } + else + instanceLock = new InstanceLock(mapId, difficulty, expiryTime, instanceId); + + instanceLock.GetData().Data = lockResult.Read(5); + instanceLock.GetData().CompletedEncountersMask = lockResult.Read(6); + instanceLock.SetExtended(lockResult.Read(8)); + + _instanceLocksByPlayer[playerGuid][Tuple.Create(mapId, lockId)] = instanceLock; + + } while (result.NextRow()); + } + } + + public void Unload() + { + _unloading = true; + _instanceLocksByPlayer.Clear(); + _instanceLockDataById.Clear(); + } + + public TransferAbortReason CanJoinInstanceLock(ObjectGuid playerGuid, MapDb2Entries entries, InstanceLock instanceLock) + { + if (!entries.MapDifficulty.HasResetSchedule()) + return TransferAbortReason.None; + + InstanceLock playerInstanceLock = FindActiveInstanceLock(playerGuid, entries); + if (playerInstanceLock == null) + return TransferAbortReason.None; + + if (entries.Map.IsFlexLocking()) + { + // compare completed encounters - if instance has any encounters unkilled in players lock then cannot enter + if ((playerInstanceLock.GetData().CompletedEncountersMask & ~instanceLock.GetData().CompletedEncountersMask) != 0) + return TransferAbortReason.AlreadyCompletedEncounter; + + return TransferAbortReason.None; + } + + if (!entries.MapDifficulty.IsUsingEncounterLocks() && playerInstanceLock.GetInstanceId() != 0 && playerInstanceLock.GetInstanceId() != instanceLock.GetInstanceId()) + return TransferAbortReason.LockedToDifferentInstance; + + return TransferAbortReason.None; + } + + public InstanceLock FindInstanceLock(Dictionary> locks, ObjectGuid playerGuid, MapDb2Entries entries) + { + var playerLocks = locks.LookupByKey(playerGuid); + if (playerLocks == null) + return null; + + return playerLocks.LookupByKey(entries.GetKey()); + } + + public InstanceLock FindActiveInstanceLock(ObjectGuid playerGuid, MapDb2Entries entries) + { + return FindActiveInstanceLock(playerGuid, entries, false, true); + } + + public InstanceLock FindActiveInstanceLock(ObjectGuid playerGuid, MapDb2Entries entries, bool ignoreTemporary, bool ignoreExpired) + { + InstanceLock instanceLock = FindInstanceLock(_instanceLocksByPlayer, playerGuid, entries); + + // Ignore expired and not extended locks + if (instanceLock != null && (!instanceLock.IsExpired() || instanceLock.IsExtended() || !ignoreExpired)) + return instanceLock; + + if (ignoreTemporary) + return null; + + return FindInstanceLock(_temporaryInstanceLocksByPlayer, playerGuid, entries); + } + + public ICollection GetInstanceLocksForPlayer(ObjectGuid playerGuid) + { + return _instanceLocksByPlayer.LookupByKey(playerGuid)?.Values; + } + + public InstanceLock CreateInstanceLockForNewInstance(ObjectGuid playerGuid, MapDb2Entries entries, uint instanceId) + { + if (!entries.MapDifficulty.HasResetSchedule()) + return null; + + InstanceLock instanceLock; + if (entries.IsInstanceIdBound()) + { + SharedInstanceLockData sharedData = new(); + _instanceLockDataById[instanceId] = sharedData; + instanceLock = new SharedInstanceLock(entries.MapDifficulty.MapID, (Difficulty)entries.MapDifficulty.DifficultyID, + GetNextResetTime(entries), 0, sharedData); + } + else + instanceLock = new InstanceLock(entries.MapDifficulty.MapID, (Difficulty)entries.MapDifficulty.DifficultyID, + GetNextResetTime(entries), 0); + + _temporaryInstanceLocksByPlayer[playerGuid][entries.GetKey()] = instanceLock; + Log.outDebug(LogFilter.Instance, $"[{entries.Map.Id}-{entries.Map.MapName[Global.WorldMgr.GetDefaultDbcLocale()]} | " + + $"{entries.MapDifficulty.DifficultyID}-{CliDB.DifficultyStorage.LookupByKey(entries.MapDifficulty.DifficultyID).Name}] Created new temporary instance lock for {playerGuid} in instance {instanceId}"); + return instanceLock; + } + + public InstanceLock UpdateInstanceLockForPlayer(SQLTransaction trans, ObjectGuid playerGuid, MapDb2Entries entries, InstanceLockUpdateEvent updateEvent) + { + InstanceLock instanceLock = FindActiveInstanceLock(playerGuid, entries, true, true); + if (instanceLock == null) + { + // Move lock from temporary storage if it exists there + // This is to avoid destroying expired locks before any boss is killed in a fresh lock + // player can still change his mind, exit instance and reactivate old lock + var playerLocks = _temporaryInstanceLocksByPlayer.LookupByKey(playerGuid); + if (playerLocks != null) + { + var playerInstanceLock = playerLocks.LookupByKey(entries.GetKey()); + if (playerInstanceLock != null) + { + instanceLock = playerInstanceLock; + _instanceLocksByPlayer[playerGuid][entries.GetKey()] = instanceLock; + + playerLocks.Remove(entries.GetKey()); + if (playerLocks.Empty()) + _temporaryInstanceLocksByPlayer.Remove(playerGuid); + + Log.outDebug(LogFilter.Instance, $"[{entries.Map.Id}-{entries.Map.MapName[Global.WorldMgr.GetDefaultDbcLocale()]} | " + + $"{entries.MapDifficulty.DifficultyID}-{CliDB.DifficultyStorage.LookupByKey(entries.MapDifficulty.DifficultyID).Name}] Promoting temporary lock to permanent for {playerGuid} in instance {updateEvent.InstanceId}"); + } + } + } + + if (instanceLock == null) + { + if (entries.IsInstanceIdBound()) + { + var sharedDataItr = _instanceLockDataById.LookupByKey(updateEvent.InstanceId); + Cypher.Assert(sharedDataItr != null); + + instanceLock = new SharedInstanceLock(entries.MapDifficulty.MapID, (Difficulty)entries.MapDifficulty.DifficultyID, + GetNextResetTime(entries), updateEvent.InstanceId, sharedDataItr); + Cypher.Assert((instanceLock as SharedInstanceLock).GetSharedData().InstanceId == updateEvent.InstanceId); + } + else + instanceLock = new InstanceLock(entries.MapDifficulty.MapID, (Difficulty)entries.MapDifficulty.DifficultyID, + GetNextResetTime(entries), updateEvent.InstanceId); + + _instanceLocksByPlayer[playerGuid][entries.GetKey()] = instanceLock; + Log.outDebug(LogFilter.Instance, $"[{entries.Map.Id}-{entries.Map.MapName[Global.WorldMgr.GetDefaultDbcLocale()]} | " + + $"{entries.MapDifficulty.DifficultyID}-{CliDB.DifficultyStorage.LookupByKey(entries.MapDifficulty.DifficultyID).Name}] Created new instance lock for {playerGuid} in instance {updateEvent.InstanceId}"); + } + else + { + if (entries.IsInstanceIdBound()) + { + Cypher.Assert(instanceLock.GetInstanceId() == 0 || instanceLock.GetInstanceId() == updateEvent.InstanceId); + var sharedDataItr = _instanceLockDataById.LookupByKey(updateEvent.InstanceId); + Cypher.Assert(sharedDataItr != null); + Cypher.Assert(sharedDataItr == (instanceLock as SharedInstanceLock).GetSharedData()); + } + + instanceLock.SetInstanceId(updateEvent.InstanceId); + } + + instanceLock.GetData().Data = updateEvent.NewData; + if (updateEvent.CompletedEncounter != null) + { + instanceLock.GetData().CompletedEncountersMask |= 1u << updateEvent.CompletedEncounter.Bit; + Log.outDebug(LogFilter.Instance, $"[{entries.Map.Id}-{entries.Map.MapName[Global.WorldMgr.GetDefaultDbcLocale()]} | " + + $"{entries.MapDifficulty.DifficultyID}-{CliDB.DifficultyStorage.LookupByKey(entries.MapDifficulty.DifficultyID).Name}] " + + $"Instance lock for {playerGuid} in instance {updateEvent.InstanceId} gains completed encounter [{updateEvent.CompletedEncounter.Id}-{updateEvent.CompletedEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()]}]"); + } + + // Synchronize map completed encounters into players completed encounters for UI + if (!entries.MapDifficulty.IsUsingEncounterLocks()) + instanceLock.GetData().CompletedEncountersMask |= updateEvent.InstanceCompletedEncountersMask; + + if (instanceLock.IsExpired()) + { + Cypher.Assert(instanceLock.IsExtended(), "Instance lock must have been extended to create instance map from it"); + instanceLock.SetExpiryTime(GetNextResetTime(entries)); + instanceLock.SetExtended(false); + Log.outDebug(LogFilter.Instance, $"[{entries.Map.Id}-{entries.Map.MapName[Global.WorldMgr.GetDefaultDbcLocale()]} | " + + $"{entries.MapDifficulty.DifficultyID}-{CliDB.DifficultyStorage.LookupByKey(entries.MapDifficulty.DifficultyID).Name}] Expired instance lock for {playerGuid} in instance {updateEvent.InstanceId} is now active"); + } + + // TODO: DB SAVE IN TRANSACTION + trans.Append($"DELETE FROM character_instance_lock WHERE guid={playerGuid.GetCounter()} AND mapId={entries.MapDifficulty.MapID} AND lockId={entries.MapDifficulty.LockID}"); + string escapedData = instanceLock.GetData().Data; + CharacterDatabase.EscapeString(ref escapedData); + trans.Append($"INSERT INTO character_instance_lock (guid, mapId, lockId, instanceId, difficulty, data, completedEncountersMask, entranceWorldSafeLocId, expiryTime, extended) " + + $"VALUES ({playerGuid.GetCounter()}, {entries.MapDifficulty.MapID}, {entries.MapDifficulty.LockID}, {instanceLock.GetInstanceId()}, {entries.MapDifficulty.DifficultyID}, \"{escapedData}\", " + + $"{instanceLock.GetData().CompletedEncountersMask}, {instanceLock.GetData().EntranceWorldSafeLocId}, {Time.DateTimeToUnixTime(instanceLock.GetExpiryTime())}, {(instanceLock.IsExtended() ? 1 : 0)}"); + + return instanceLock; + } + + public void UpdateSharedInstanceLock(SQLTransaction trans, InstanceLockUpdateEvent updateEvent) + { + var sharedData = _instanceLockDataById.LookupByKey(updateEvent.InstanceId); + Cypher.Assert(sharedData != null); + Cypher.Assert(sharedData.InstanceId == updateEvent.InstanceId); + sharedData.Data = updateEvent.NewData; + if (updateEvent.CompletedEncounter != null) + { + sharedData.CompletedEncountersMask |= 1u << updateEvent.CompletedEncounter.Bit; + Log.outDebug(LogFilter.Instance, $"Instance {updateEvent.InstanceId} gains completed encounter [{updateEvent.CompletedEncounter.Id}-{updateEvent.CompletedEncounter.Name[Global.WorldMgr.GetDefaultDbcLocale()]}]"); + } + + trans.Append($"DELETE FROM instance2 WHERE instanceId={sharedData.InstanceId}"); + string escapedData = sharedData.Data; + CharacterDatabase.EscapeString(ref escapedData); + trans.Append($"INSERT INTO instance2 (instanceId, data, completedEncountersMask, entranceWorldSafeLocId) VALUES ({sharedData.InstanceId}, \"{escapedData}\", {sharedData.CompletedEncountersMask}, {sharedData.EntranceWorldSafeLocId})"); + } + + public void OnSharedInstanceLockDataDelete(uint instanceId) + { + if (_unloading) + return; + + _instanceLockDataById.Remove(instanceId); + DB.Characters.Execute($"DELETE FROM instance2 WHERE instanceId={instanceId}"); + Log.outDebug(LogFilter.Instance, $"Deleting instance {instanceId} as it is no longer referenced by any player"); + } + + public void UpdateInstanceLockExtensionForPlayer(ObjectGuid playerGuid, MapDb2Entries entries, bool extended) + { + InstanceLock instanceLock = FindActiveInstanceLock(playerGuid, entries, true, false); + if (instanceLock != null) + { + instanceLock.SetExtended(extended); + DB.Characters.Execute($"UPDATE character_instance_lock SET extended = {(extended ? 1 : 0)} WHERE guid = {playerGuid.GetCounter()} AND mapId = {entries.MapDifficulty.MapID} AND lockId = {entries.MapDifficulty.LockID}"); + Log.outDebug(LogFilter.Instance, $"[{entries.Map.Id}-{entries.Map.MapName[Global.WorldMgr.GetDefaultDbcLocale()]} | " + + $"{entries.MapDifficulty.DifficultyID}-{CliDB.DifficultyStorage.LookupByKey(entries.MapDifficulty.DifficultyID).Name}] Instance lock for {playerGuid} is {(extended ? "now" : "no longer")} extended"); + } + } + + public DateTime GetNextResetTime(MapDb2Entries entries) + { + DateTime dateTime = GameTime.GetDateAndTime(); + int resetHour = ConfigMgr.GetDefaultValue("ResetSchedule.DailyHour", 9); + + int hour = 0; + int day = 0; + switch (entries.MapDifficulty.ResetInterval) + { + case MapDifficultyResetInterval.Daily: + { + if (dateTime.Hour >= resetHour) + day++; + + hour = resetHour; + break; + } + case MapDifficultyResetInterval.Weekly: + { + int resetDay = ConfigMgr.GetDefaultValue("ResetSchedule.WeeklyDay", 2); + int daysAdjust = resetDay - dateTime.Day; + if (dateTime.Day > resetDay || (dateTime.Day == resetDay && dateTime.Hour >= resetHour)) + daysAdjust += 7; // passed it for current week, grab time from next week + + hour = resetHour; + day += daysAdjust; + break; + } + default: + break; + } + + return new DateTime(dateTime.Year, dateTime.Month, day, hour, 0, 0); + } + } + + public class InstanceLockData + { + public string Data; + public uint CompletedEncountersMask; + public uint EntranceWorldSafeLocId; + } + + public class InstanceLock + { + uint _mapId; + Difficulty _difficultyId; + uint _instanceId; + DateTime _expiryTime; + bool _extended; + InstanceLockData _data; + + public InstanceLock(uint mapId, Difficulty difficultyId, DateTime expiryTime, uint instanceId) + { + _mapId = mapId; + _difficultyId = difficultyId; + _instanceId = instanceId; + _expiryTime = expiryTime; + _extended = false; + } + + public bool IsExpired() + { + return _expiryTime < GameTime.GetSystemTime(); + } + + public DateTime GetEffectiveExpiryTime() + { + if (!IsExtended()) + return GetExpiryTime(); + + MapDb2Entries entries = new(_mapId, _difficultyId); + + // return next reset time + if (IsExpired()) + return Global.InstanceLockMgr.GetNextResetTime(entries); + + // if not expired, return expiration time + 1 reset period + return GetExpiryTime() + TimeSpan.FromSeconds(entries.MapDifficulty.GetRaidDuration()); + } + + public uint GetMapId() { return _mapId; } + + public Difficulty GetDifficultyId() { return _difficultyId; } + + public uint GetInstanceId() { return _instanceId; } + + public virtual void SetInstanceId(uint instanceId) { _instanceId = instanceId; } + + public DateTime GetExpiryTime() { return _expiryTime; } + + public void SetExpiryTime(DateTime expiryTime) { _expiryTime = expiryTime; } + + public bool IsExtended() { return _extended; } + + public void SetExtended(bool extended) { _extended = extended; } + + public InstanceLockData GetData() { return _data; } + + public virtual InstanceLockData GetInstanceInitializationData() { return _data; } + } + + class SharedInstanceLockData : InstanceLockData + { + public uint InstanceId; + + ~SharedInstanceLockData() + { + // Cleanup database + if (InstanceId != 0) + Global.InstanceLockMgr.OnSharedInstanceLockDataDelete(InstanceId); + } + } + + class SharedInstanceLock : InstanceLock + { + + /// + /// Instance id based locks have two states + /// One shared by everyone, which is the real state used by instance + /// and one for each player that shows in UI that might have less encounters completed + /// + SharedInstanceLockData _sharedData; + + public SharedInstanceLock(uint mapId, Difficulty difficultyId, DateTime expiryTime, uint instanceId, SharedInstanceLockData sharedData) : base(mapId, difficultyId, expiryTime, instanceId) + { + _sharedData = sharedData; + } + + public void SetInstanceId(uint instanceId) + { + base.SetInstanceId(instanceId); + _sharedData.InstanceId = instanceId; + } + + public InstanceLockData GetInstanceInitializationData() { return _sharedData; } + + public SharedInstanceLockData GetSharedData() { return _sharedData; } + } + + public struct MapDb2Entries + { + public MapRecord Map; + public MapDifficultyRecord MapDifficulty; + + public MapDb2Entries(uint mapId, Difficulty difficulty) + { + Map = CliDB.MapStorage.LookupByKey(mapId); + MapDifficulty = Global.DB2Mgr.GetMapDifficultyData(mapId, difficulty); + } + + public MapDb2Entries(MapRecord map, MapDifficultyRecord mapDifficulty) + { + Map = map; + MapDifficulty = mapDifficulty; + } + + public InstanceLockKey GetKey() + { + return Tuple.Create(MapDifficulty.MapID, (uint)MapDifficulty.LockID); + } + + public bool IsInstanceIdBound() + { + return !Map.IsFlexLocking() && !MapDifficulty.IsUsingEncounterLocks(); + } + } + + public struct InstanceLockUpdateEvent + { + public uint InstanceId; + public string NewData; + public uint InstanceCompletedEncountersMask; + public DungeonEncounterRecord CompletedEncounter; + + public InstanceLockUpdateEvent(uint instanceId, string newData, uint instanceCompletedEncountersMask, DungeonEncounterRecord completedEncounter) + { + InstanceId = instanceId; + NewData = newData; + InstanceCompletedEncountersMask = instanceCompletedEncountersMask; + CompletedEncounter = completedEncounter; + } + } +} diff --git a/Source/Game/Maps/Instances/InstanceScript.cs b/Source/Game/Maps/Instances/InstanceScript.cs index d9741fc69..a94300bc6 100644 --- a/Source/Game/Maps/Instances/InstanceScript.cs +++ b/Source/Game/Maps/Instances/InstanceScript.cs @@ -18,6 +18,7 @@ using Framework.Constants; using Framework.Database; using Framework.IO; +using Game.DataStorage; using Game.Entities; using Game.Groups; using Game.Networking.Packets; @@ -25,6 +26,7 @@ using Game.Scenarios; using Game.Spells; using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace Game.Maps @@ -164,6 +166,19 @@ namespace Game.Maps } } + void LoadDungeonEncounterData(DungeonEncounterData[] encounters) + { + foreach (DungeonEncounterData encounter in encounters) + LoadDungeonEncounterData(encounter.BossId, encounter.DungeonEncounterId); + } + + void LoadDungeonEncounterData(uint bossId, uint[] dungeonEncounterIds) + { + if (bossId < bosses.Count) + for (int i = 0; i < MapConst.MaxDungeonEncountersPerBoss; ++i) + bosses[bossId].DungeonEncounters[i] = CliDB.DungeonEncounterStorage.LookupByKey(dungeonEncounterIds[i]); + } + public virtual void UpdateDoorState(GameObject door) { var range = doors.LookupByKey(door.GetEntry()); @@ -379,6 +394,7 @@ namespace Game.Maps } } + DungeonEncounterRecord dungeonEncounter = null; switch (state) { case EncounterState.InProgress: @@ -395,9 +411,18 @@ namespace Game.Maps break; } case EncounterState.Fail: + ResetCombatResurrections(); + SendEncounterEnd(); + break; case EncounterState.Done: ResetCombatResurrections(); SendEncounterEnd(); + dungeonEncounter = bossInfo.GetDungeonEncounterForDifficulty(instance.GetDifficultyID()); + if (dungeonEncounter != null) + { + DoUpdateCriteria(CriteriaType.DefeatDungeonEncounter, dungeonEncounter.Id); + SendBossKillCredit(dungeonEncounter.Id); + } break; default: break; @@ -405,6 +430,8 @@ namespace Game.Maps bossInfo.state = state; SaveToDB(); + if (state == EncounterState.Done) + instance.UpdateInstanceLock(dungeonEncounter, new(id, state)); } for (uint type = 0; type < (int)DoorType.Max; ++type) @@ -508,6 +535,32 @@ namespace Game.Maps return saveStream.ToString(); } + public string UpdateSaveData(string oldData, UpdateSaveDataEvent saveEvent) + { + if (!instance.GetMapDifficulty().IsUsingEncounterLocks()) + return GetSaveData(); + + int position = (int)(headers.Count + saveEvent.BossId) * 2; + StringBuilder newData = new(oldData); + if (position >= oldData.Length) + { + // Initialize blank data + StringBuilder saveStream = new(); + WriteSaveDataHeaders(saveStream); + for (var i = 0; i < bosses.Count; ++i) + saveStream.Append($"{(uint)EncounterState.NotStarted} "); + + WriteSaveDataMore(saveStream); + + newData = saveStream; + } + + newData.Remove(position, 2); + newData.Insert(position, $"{(uint)saveEvent.NewState}0"); + + return newData.ToString(); + } + void WriteSaveDataHeaders(StringBuilder data) { foreach (char header in headers) @@ -962,7 +1015,7 @@ namespace Game.Maps Dictionary _creatureInfo = new(); Dictionary _gameObjectInfo = new(); Dictionary _objectGuids = new(); - uint completedEncounters; + uint completedEncounters; // DEPRECATED, REMOVE List _instanceSpawnGroups = new(); List _activatedAreaTriggers = new(); uint _entranceId; @@ -972,6 +1025,11 @@ namespace Game.Maps bool _combatResurrectionTimerStarted; } + class DungeonEncounterData + { + public uint BossId; + public uint[] DungeonEncounterId = new uint[4]; + } public class DoorData { @@ -1025,6 +1083,12 @@ namespace Game.Maps public class BossInfo { + public EncounterState state; + public List[] door = new List[(int)DoorType.Max]; + public List minion = new(); + public List boundary = new(); + public DungeonEncounterRecord[] DungeonEncounters = new DungeonEncounterRecord[MapConst.MaxDungeonEncountersPerBoss]; + public BossInfo() { state = EncounterState.ToBeDecided; @@ -1032,10 +1096,10 @@ namespace Game.Maps door[i] = new List(); } - public EncounterState state; - public List[] door = new List[(int)DoorType.Max]; - public List minion = new(); - public List boundary = new(); + public DungeonEncounterRecord GetDungeonEncounterForDifficulty(Difficulty difficulty) + { + return DungeonEncounters.FirstOrDefault(dungeonEncounter => dungeonEncounter?.DifficultyID == 0 || (Difficulty)dungeonEncounter?.DifficultyID == difficulty); + } } class DoorInfo { @@ -1057,4 +1121,16 @@ namespace Game.Maps public BossInfo bossInfo; } + + public struct UpdateSaveDataEvent + { + public uint BossId; + public EncounterState NewState; + + public UpdateSaveDataEvent(uint bossId, EncounterState state) + { + BossId = bossId; + NewState = state; + } + } } diff --git a/Source/Game/Maps/Map.cs b/Source/Game/Maps/Map.cs index 5f1ab3c08..1c5f48648 100644 --- a/Source/Game/Maps/Map.cs +++ b/Source/Game/Maps/Map.cs @@ -1709,35 +1709,26 @@ namespace Game.Maps Log.outDebug(LogFilter.Maps, $"Map::CanPlayerEnter - player '{player.GetName()}' is dead but does not have a corpse!"); } - //Get instance where player's group is bound & its map - if (!loginCheck && group) + if (entry.Instanceable()) { - InstanceBind boundInstance = group.GetBoundInstance(entry); - if (boundInstance != null && boundInstance.save != null) + //Get instance where player's group is bound & its map + uint instanceIdToCheck = Global.MapMgr.FindInstanceIdForPlayer(mapid, player); + if (instanceIdToCheck != 0) { - Map boundMap = Global.MapMgr.FindMap(mapid, boundInstance.save.GetInstanceId()); + Map boundMap = Global.MapMgr.FindMap(mapid, instanceIdToCheck); if (boundMap != null) { EnterState denyReason = boundMap.CannotEnter(player); if (denyReason != 0) return denyReason; } + + // players are only allowed to enter 10 instances per hour + if (entry.IsDungeon() && !player.CheckInstanceCount(instanceIdToCheck) && !player.IsDead()) + return EnterState.CannotEnterTooManyInstances; } } - // players are only allowed to enter 5 instances per hour - if (entry.IsDungeon() && (!player.GetGroup() || (player.GetGroup() && !player.GetGroup().IsLFGGroup()))) - { - uint instanceIdToCheck = 0; - InstanceSave save = player.GetInstanceSave(mapid); - if (save != null) - instanceIdToCheck = save.GetInstanceId(); - - // instanceId can never be 0 - will not be found - if (!player.CheckInstanceCount(instanceIdToCheck) && !player.IsDead()) - return EnterState.CannotEnterTooManyInstances; - } - return EnterState.CanEnter; } @@ -3414,6 +3405,7 @@ namespace Game.Maps } public InstanceMap ToInstanceMap() { return IsDungeon() ? (this as InstanceMap) : null; } + public BattlegroundMap ToBattlegroundMap() { return IsBattlegroundOrArena() ? (this as BattlegroundMap) : null; } public void Balance() @@ -4759,8 +4751,10 @@ namespace Game.Maps public class InstanceMap : Map { - public InstanceMap(uint id, long expiry, uint InstanceId, Difficulty spawnMode, int instanceTeam) : base(id, expiry, InstanceId, spawnMode) + public InstanceMap(uint id, long expiry, uint InstanceId, Difficulty spawnMode, int instanceTeam, InstanceLock instanceLock) : base(id, expiry, InstanceId, spawnMode) { + i_instanceLock = instanceLock; + //lets initialize visibility distance for dungeons InitVisibilityDistance(); @@ -4804,121 +4798,43 @@ namespace Game.Maps if (!player.IsLoading() && IsRaid() && GetInstanceScript() != null && GetInstanceScript().IsEncounterInProgress()) return EnterState.CannotEnterZoneInCombat; - // cannot enter if player is permanent saved to a different instance id - InstanceBind playerBind = player.GetBoundInstance(GetId(), GetDifficultyID()); - if (playerBind != null) - if (playerBind.perm && playerBind.save != null) - if (playerBind.save.GetInstanceId() != GetInstanceId()) - return EnterState.CannotEnterInstanceBindMismatch; + if (i_instanceLock != null) + { + // cannot enter if player is permanent saved to a different instance id + TransferAbortReason lockError = Global.InstanceLockMgr.CanJoinInstanceLock(player.GetGUID(), new MapDb2Entries(GetEntry(), GetMapDifficulty()), i_instanceLock); + if (lockError == TransferAbortReason.LockedToDifferentInstance) + return EnterState.CannotEnterInstanceBindMismatch; + + if (lockError == TransferAbortReason.AlreadyCompletedEncounter) + return EnterState.CannotEnterAlreadyCompletedEncounter; + } return base.CannotEnter(player); } public override bool AddPlayerToMap(Player player, bool initPlayer = true) { - Group group = player.GetGroup(); - // increase current instances (hourly limit) - if (!group || !group.IsLFGGroup()) - player.AddInstanceEnterTime(GetInstanceId(), GameTime.GetGameTime()); + player.AddInstanceEnterTime(GetInstanceId(), GameTime.GetGameTime()); - // get or create an instance save for the map - InstanceSave mapSave = Global.InstanceSaveMgr.GetInstanceSave(GetInstanceId()); - if (mapSave == null) + MapDb2Entries entries = new(GetEntry(), GetMapDifficulty()); + if (entries.MapDifficulty.HasResetSchedule() && i_instanceLock != null && i_instanceLock.GetData().CompletedEncountersMask != 0) { - Log.outInfo(LogFilter.Maps, "InstanceMap.Add: creating instance save for map {0} spawnmode {1} with instance id {2}", GetId(), GetDifficultyID(), GetInstanceId()); - mapSave = Global.InstanceSaveMgr.AddInstanceSave(GetId(), GetInstanceId(), GetDifficultyID(), 0, 0, true); - } - - Cypher.Assert(mapSave != null); - - // check for existing instance binds - InstanceBind playerBind = player.GetBoundInstance(GetId(), GetDifficultyID()); - if (playerBind != null && playerBind.perm) - { - // cannot enter other instances if bound permanently - if (playerBind.save != mapSave) + if (!entries.MapDifficulty.IsUsingEncounterLocks()) { - Log.outError(LogFilter.Maps, "InstanceMap.Add: player {0}({1}) is permanently bound to instance {2} {3}, {4}, {5}, {6}, {7}, {8} but he is being put into instance {9} {10}, {11}, {12}, {13}, {14}, {15}", - player.GetName(), player.GetGUID().ToString(), GetMapName(), playerBind.save.GetMapId(), - playerBind.save.GetInstanceId(), playerBind.save.GetDifficultyID(), - playerBind.save.GetPlayerCount(), playerBind.save.GetGroupCount(), - playerBind.save.CanReset(), GetMapName(), mapSave.GetMapId(), mapSave.GetInstanceId(), - mapSave.GetDifficultyID(), mapSave.GetPlayerCount(), mapSave.GetGroupCount(), - mapSave.CanReset()); - return false; - } - } - else - { - if (group) - { - // solo saves should have been reset when the map was loaded - InstanceBind groupBind = group.GetBoundInstance(this); - if (playerBind != null && playerBind.save != mapSave) + InstanceLock playerLock = Global.InstanceLockMgr.FindActiveInstanceLock(player.GetGUID(), entries); + if (playerLock == null || (playerLock.IsExpired() && playerLock.IsExtended()) || + playerLock.GetData().CompletedEncountersMask != i_instanceLock.GetData().CompletedEncountersMask) { - Log.outError(LogFilter.Maps, - "InstanceMapAdd: player {0}({1}) is being put into instance {2} {3}, {4}, {5}, {6}, {7}, {8} but he is in group {9} and is bound to instance {10}, {11}, {12}, {13}, {14}, {15}!", - player.GetName(), player.GetGUID().ToString(), GetMapName(), mapSave.GetMapId(), mapSave.GetInstanceId(), - mapSave.GetDifficultyID(), mapSave.GetPlayerCount(), mapSave.GetGroupCount(), - mapSave.CanReset(), group.GetLeaderGUID().ToString(), - playerBind.save.GetMapId(), playerBind.save.GetInstanceId(), - playerBind.save.GetDifficultyID(), playerBind.save.GetPlayerCount(), - playerBind.save.GetGroupCount(), playerBind.save.CanReset()); - if (groupBind != null) - Log.outError(LogFilter.Maps, - "InstanceMap.Add: the group is bound to the instance {0} {1}, {2}, {3}, {4}, {5}, {6}", - GetMapName(), groupBind.save.GetMapId(), groupBind.save.GetInstanceId(), - groupBind.save.GetDifficultyID(), groupBind.save.GetPlayerCount(), - groupBind.save.GetGroupCount(), groupBind.save.CanReset()); - Cypher.Assert(false); - return false; + PendingRaidLock pendingRaidLock = new(); + pendingRaidLock.TimeUntilLock = 60000; + pendingRaidLock.CompletedMask = i_instanceLock.GetData().CompletedEncountersMask; + pendingRaidLock.Extending = playerLock != null && playerLock.IsExtended(); + pendingRaidLock.WarningOnly = entries.Map.IsFlexLocking(); // events it triggers: 1 : INSTANCE_LOCK_WARNING 0 : INSTANCE_LOCK_STOP / INSTANCE_LOCK_START + player.GetSession().SendPacket(pendingRaidLock); + if (!entries.Map.IsFlexLocking()) + player.SetPendingBind(GetInstanceId(), 60000); } - // bind to the group or keep using the group save - if (groupBind == null) - group.BindToInstance(mapSave, false); - else - { - // cannot jump to a different instance without resetting it - if (groupBind.save != mapSave) - { - Log.outError(LogFilter.Maps, - "InstanceMap.Add: player {0}({1}) is being put into instance {2}, {3}, {4} but he is in group {5} which is bound to instance {6}, {7}, {8}!", - player.GetName(), player.GetGUID().ToString(), mapSave.GetMapId(), mapSave.GetInstanceId(), - mapSave.GetDifficultyID(), group.GetLeaderGUID().ToString(), - groupBind.save.GetMapId(), groupBind.save.GetInstanceId(), - groupBind.save.GetDifficultyID()); - Log.outError(LogFilter.Maps, "MapSave players: {0}, group count: {1}", - mapSave.GetPlayerCount(), mapSave.GetGroupCount()); - if (groupBind.save != null) - Log.outError(LogFilter.Maps, "GroupBind save players: {0}, group count: {1}", - groupBind.save.GetPlayerCount(), groupBind.save.GetGroupCount()); - else - Log.outError(LogFilter.Maps, "GroupBind save NULL"); - return false; - } - // if the group/leader is permanently bound to the instance - // players also become permanently bound when they enter - if (groupBind.perm) - { - PendingRaidLock pendingRaidLock = new(); - pendingRaidLock.TimeUntilLock = 60000; - pendingRaidLock.CompletedMask = i_data != null ? i_data.GetCompletedEncounterMask() : 0; - pendingRaidLock.Extending = false; - pendingRaidLock.WarningOnly = false; // events it throws: 1 : INSTANCE_LOCK_WARNING 0 : INSTANCE_LOCK_STOP / INSTANCE_LOCK_START - player.SendPacket(pendingRaidLock); - player.SetPendingBind(mapSave.GetInstanceId(), 60000); - } - } - } - else - { - // set up a solo bind or continue using it - if (playerBind == null) - player.BindToInstance(mapSave, false); - else - // cannot jump to a different instance without resetting it - Cypher.Assert(playerBind.save == mapSave); } } @@ -4980,7 +4896,7 @@ namespace Game.Maps Global.InstanceSaveMgr.UnloadInstanceSave(GetInstanceId()); } - public void CreateInstanceData(bool load) + public void CreateInstanceData() { if (i_data != null) return; @@ -4995,24 +4911,19 @@ namespace Game.Maps if (i_data == null) return; - if (load) + if (i_instanceLock != null) { - // @todo make a global storage for this - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_INSTANCE); - stmt.AddValue(0, GetId()); - stmt.AddValue(1, i_InstanceId); - SQLResult result = DB.Characters.Query(stmt); - - if (!result.IsEmpty()) + MapDb2Entries entries = new(GetEntry(), GetMapDifficulty()); + if (entries.IsInstanceIdBound() || IsRaid() || entries.MapDifficulty.IsRestoringDungeonState() || !i_owningGroupRef.isValid()) { - var data = result.Read(0); - i_data.SetCompletedEncountersMask(result.Read(1)); - i_data.SetEntranceLocation(result.Read(2)); - if (data != "") + InstanceLockData lockData = i_instanceLock.GetInstanceInitializationData(); + i_data.SetCompletedEncountersMask(lockData.CompletedEncountersMask); + i_data.SetEntranceLocation(lockData.EntranceWorldSafeLocId); + if (!lockData.Data.IsEmpty()) { Log.outDebug(LogFilter.Maps, "Loading instance data for `{0}` with id {1}", Global.ObjectMgr.GetScriptName(i_script_id), i_InstanceId); - i_data.Load(data); + i_data.Load(lockData.Data); } } } @@ -5076,54 +4987,70 @@ namespace Game.Maps return Global.ObjectMgr.GetScriptName(i_script_id); } - public void PermBindAllPlayers() + public void UpdateInstanceLock(DungeonEncounterRecord dungeonEncounter, UpdateSaveDataEvent updateSaveDataEvent) { - if (!IsDungeon()) - return; - - InstanceSave save = Global.InstanceSaveMgr.GetInstanceSave(GetInstanceId()); - if (save == null) + if (i_instanceLock != null) { - Log.outError(LogFilter.Maps, "Cannot bind players to instance map (Name: {0}, Entry: {1}, Difficulty: {2}, ID: {3}) because no instance save is available!", GetMapName(), GetId(), GetDifficultyID(), GetInstanceId()); - return; + uint instanceCompletedEncounters = i_instanceLock.GetData().CompletedEncountersMask; + if (dungeonEncounter != null) + instanceCompletedEncounters |= 1u << dungeonEncounter.Bit; + + MapDb2Entries entries = new(GetEntry(), GetMapDifficulty()); + + SQLTransaction trans = new(); + + if (entries.IsInstanceIdBound()) + Global.InstanceLockMgr.UpdateSharedInstanceLock(trans, new InstanceLockUpdateEvent(GetInstanceId(), i_data.GetSaveData(), instanceCompletedEncounters, dungeonEncounter)); + + foreach (var player in GetPlayers()) + { + // never instance bind GMs with GM mode enabled + if (player.IsGameMaster()) + continue; + + InstanceLock playerLock = Global.InstanceLockMgr.FindActiveInstanceLock(player.GetGUID(), entries); + string oldData = ""; + if (playerLock != null) + oldData = playerLock.GetData().Data; + + bool isNewLock = playerLock == null || playerLock.GetData().CompletedEncountersMask == 0 || playerLock.IsExpired(); + + InstanceLock newLock = Global.InstanceLockMgr.UpdateInstanceLockForPlayer(trans, player.GetGUID(), entries, new InstanceLockUpdateEvent(GetInstanceId(), i_data.UpdateSaveData(oldData, updateSaveDataEvent), instanceCompletedEncounters, dungeonEncounter)); + + if (isNewLock) + { + InstanceSaveCreated data = new(); + data.Gm = player.IsGameMaster(); + player.SendPacket(data); + + player.GetSession().SendCalendarRaidLockoutAdded(newLock); + } + } + + DB.Characters.CommitTransaction(trans); } + } - // perm bind all players that are currently inside the instance - foreach (Player player in GetPlayers()) + public void CreateInstanceLockForPlayer(Player player) + { + MapDb2Entries entries = new(GetEntry(), GetMapDifficulty()); + InstanceLock playerLock = Global.InstanceLockMgr.FindActiveInstanceLock(player.GetGUID(), entries); + + bool isNewLock = playerLock == null || playerLock.GetData().CompletedEncountersMask == 0 || playerLock.IsExpired(); + + SQLTransaction trans = new(); + + InstanceLock newLock = Global.InstanceLockMgr.UpdateInstanceLockForPlayer(trans, player.GetGUID(), entries, new InstanceLockUpdateEvent(GetInstanceId(), i_data.GetSaveData(), i_instanceLock.GetData().CompletedEncountersMask, null)); + + DB.Characters.CommitTransaction(trans); + + if (isNewLock) { - // never instance bind GMs with GM mode enabled - if (player.IsGameMaster()) - continue; + InstanceSaveCreated data = new(); + data.Gm = player.IsGameMaster(); + player.SendPacket(data); - InstanceBind bind = player.GetBoundInstance(save.GetMapId(), save.GetDifficultyID()); - if (bind != null && bind.perm) - { - if (bind.save != null && bind.save.GetInstanceId() != save.GetInstanceId()) - { - Log.outError(LogFilter.Maps, "Player (GUID: {0}, Name: {1}) is in instance map (Name: {2}, Entry: {3}, Difficulty: {4}, ID: {5}) that is being bound, but already has a save for the map on ID {6}!", - player.GetGUID().GetCounter(), player.GetName(), GetMapName(), save.GetMapId(), save.GetDifficultyID(), save.GetInstanceId(), bind.save.GetInstanceId()); - } - else if (bind.save == null) - { - Log.outError(LogFilter.Maps, "Player (GUID: {0}, Name: {1}) is in instance map (Name: {2}, Entry: {3}, Difficulty: {4}, ID: {5}) that is being bound, but already has a bind (without associated save) for the map!", - player.GetGUID().GetCounter(), player.GetName(), GetMapName(), save.GetMapId(), save.GetDifficultyID(), save.GetInstanceId()); - } - } - else - { - player.BindToInstance(save, true); - InstanceSaveCreated data = new(); - data.Gm = player.IsGameMaster(); - player.SendPacket(data); - - player.GetSession().SendCalendarRaidLockout(save, true); - - // if group leader is in instance, group also gets bound - Group group = player.GetGroup(); - if (group) - if (group.GetLeaderGUID() == player.GetGUID()) - group.BindToInstance(save, true); - } + player.GetSession().SendCalendarRaidLockoutAdded(newLock); } } @@ -5194,7 +5121,7 @@ namespace Game.Maps return TeamId.Horde; return TeamId.Neutral; } - + public Team GetTeamInInstance() { return GetTeamIdInInstance() == TeamId.Alliance ? Team.Alliance : Team.Horde; } public uint GetScriptId() @@ -5213,11 +5140,15 @@ namespace Game.Maps } public InstanceScenario GetInstanceScenario() { return i_scenario; } + public void SetInstanceScenario(InstanceScenario scenario) { i_scenario = scenario; } + public InstanceLock GetInstanceLock() { return i_instanceLock; } + InstanceScript i_data; uint i_script_id; InstanceScenario i_scenario; + InstanceLock i_instanceLock; bool m_resetAfterUnload; bool m_unloadWhenEmpty; } diff --git a/Source/Game/Maps/MapManager.cs b/Source/Game/Maps/MapManager.cs index d47680e96..525d68cb3 100644 --- a/Source/Game/Maps/MapManager.cs +++ b/Source/Game/Maps/MapManager.cs @@ -70,7 +70,7 @@ namespace Game.Entities return map; } - InstanceMap CreateInstance(uint mapId, uint instanceId, InstanceSave save, Difficulty difficulty, int team) + InstanceMap CreateInstance(uint mapId, uint instanceId, InstanceLock instanceLock, Difficulty difficulty, int team, Group group) { // make sure we have a valid map id var entry = CliDB.MapStorage.LookupByKey(mapId); @@ -84,19 +84,16 @@ namespace Game.Entities // some instances only have one difficulty Global.DB2Mgr.GetDownscaledMapDifficultyData(mapId, ref difficulty); - Log.outDebug(LogFilter.Maps, $"MapInstanced::CreateInstance: {(save != null ? "" : "new ")} map instance {instanceId} for {mapId} created with difficulty {difficulty}"); + Log.outDebug(LogFilter.Maps, $"MapInstanced::CreateInstance: {(instanceLock?.GetInstanceId() != 0 ? "" : "new ")}map instance {instanceId} for {mapId} created with difficulty {difficulty}"); - InstanceMap map = new InstanceMap(mapId, i_gridCleanUpDelay, instanceId, difficulty, team); + InstanceMap map = new InstanceMap(mapId, i_gridCleanUpDelay, instanceId, difficulty, team, instanceLock); Cypher.Assert(map.IsDungeon()); map.LoadRespawnTimes(); map.LoadCorpseData(); - bool load_data = save != null; - map.CreateInstanceData(load_data); - var instanceScenario = Global.ScenarioMgr.CreateInstanceScenario(map, team); - if (instanceScenario != null) - map.SetInstanceScenario(instanceScenario); + map.CreateInstanceData(); + map.SetInstanceScenario(Global.ScenarioMgr.CreateInstanceScenario(map, team)); if (WorldConfig.GetBoolValue(WorldCfg.InstancemapLoadGrids)) map.LoadAllCells(); @@ -130,7 +127,7 @@ namespace Game.Entities /// /// /// the right instance for the object, based on its InstanceId - public Map CreateMap(uint mapId, Player player, uint loginInstanceId = 0) + public Map CreateMap(uint mapId, Player player) { if (!player) return null; @@ -167,63 +164,49 @@ namespace Game.Entities } else if (entry.IsDungeon()) { - InstanceBind pBind = player.GetBoundInstance(mapId, player.GetDifficultyID(entry)); - InstanceSave pSave = pBind != null ? pBind.save : null; - - // priority: - // 1. player's permanent bind - // 2. player's current instance id if this is at login - // 3. group's current bind - // 4. player's current bind - if (pBind == null || !pBind.perm) + Group group = player.GetGroup(); + Difficulty difficulty = group != null ? group.GetDifficultyID(entry) : player.GetDifficultyID(entry); + MapDb2Entries entries = new(entry, Global.DB2Mgr.GetDownscaledMapDifficultyData(mapId, ref difficulty)); + ObjectGuid instanceOwnerGuid = group != null ? group.GetRecentInstanceOwner(mapId) : player.GetGUID(); + InstanceLock instanceLock = Global.InstanceLockMgr.FindActiveInstanceLock(instanceOwnerGuid, entries); + if (instanceLock != null) { - if (loginInstanceId != 0) // if the player has a saved instance id on login, we either use this instance or relocate him out (return null) - { - map = FindMap_i(mapId, loginInstanceId); - if (map == null && pSave != null && pSave.GetInstanceId() == loginInstanceId) - { - map = CreateInstance(mapId, loginInstanceId, pSave, pSave.GetDifficultyID(), player.GetTeamId()); - i_maps[(map.GetId(), map.GetInstanceId())] = map; - } - return map; - } + newInstanceId = instanceLock.GetInstanceId(); - InstanceBind groupBind = null; - Group group = player.GetGroup(); - // use the player's difficulty setting (it may not be the same as the group's) - if (group != null) - { - groupBind = group.GetBoundInstance(entry); - if (groupBind != null) - { - // solo saves should be reset when entering a group's instance - player.UnbindInstance(mapId, player.GetDifficultyID(entry)); - pSave = groupBind.save; - } - } - } - if (pSave != null) - { - // solo/perm/group - newInstanceId = pSave.GetInstanceId(); - map = FindMap_i(mapId, newInstanceId); - // it is possible that the save exists but the map doesn't - if (!map) - map = CreateInstance(mapId, newInstanceId, pSave, pSave.GetDifficultyID(), player.GetTeamId()); + // Reset difficulty to the one used in instance lock + if (!entries.Map.IsFlexLocking()) + difficulty = instanceLock.GetDifficultyId(); } else { - Difficulty diff = player.GetGroup() ? player.GetGroup().GetDifficultyID(entry) : player.GetDifficultyID(entry); + // Try finding instance id for normal dungeon + if (!entries.MapDifficulty.HasResetSchedule()) + newInstanceId = group ? group.GetRecentInstanceId(mapId) : player.GetRecentInstanceId(mapId); - // if no instanceId via group members or instance saves is found - // the instance will be created for the first time + // If not found or instance is not a normal dungeon, generate new one + if (newInstanceId == 0) + newInstanceId = GenerateInstanceId(); + instanceLock = Global.InstanceLockMgr.CreateInstanceLockForNewInstance(instanceOwnerGuid, entries, newInstanceId); + } + + // it is possible that the save exists but the map doesn't + map = FindMap_i(mapId, newInstanceId); + + // is is also possible that instance id is already in use by another group for boss-based locks + if (!entries.IsInstanceIdBound() && instanceLock != null && map != null && map.ToInstanceMap().GetInstanceLock() != instanceLock) + { newInstanceId = GenerateInstanceId(); + instanceLock.SetInstanceId(newInstanceId); + map = null; + } - //Seems it is now possible, but I do not know if it should be allowed - //ASSERT(!FindInstanceMap(NewInstanceId)); - map = FindMap_i(mapId, newInstanceId); - if (!map) - map = CreateInstance(mapId, newInstanceId, null, diff, player.GetTeamId()); + if (!map) + { + map = CreateInstance(mapId, newInstanceId, instanceLock, difficulty, player.GetTeamId(), group); + if (group) + group.SetRecentInstance(mapId, instanceOwnerGuid, newInstanceId); + else + player.SetRecentInstance(mapId, newInstanceId); } } else if (entry.IsGarrison()) @@ -342,11 +325,16 @@ namespace Game.Entities { _nextInstanceId = 1; - SQLResult result = DB.Characters.Query("SELECT IFNULL(MAX(id), 0) FROM instance"); + ulong maxExistingInstanceId = 0; + SQLResult result = DB.Characters.Query("SELECT IFNULL(MAX(instanceId), 0) FROM instance"); if (!result.IsEmpty()) - _freeInstanceIds = new BitSet(result.Read(0) + 2, true); // make space for one extra to be able to access [_nextInstanceId] index in case all slots are taken - else - _freeInstanceIds = new BitSet((int)_nextInstanceId + 1, true); + maxExistingInstanceId = Math.Max(maxExistingInstanceId, result.Read(0)); + + result = DB.Characters.Query("SELECT IFNULL(MAX(instanceId), 0) FROM character_instance_lock"); + if (!result.IsEmpty()) + maxExistingInstanceId = Math.Max(maxExistingInstanceId, result.Read(0)); + + _freeInstanceIds.Length = (int)(maxExistingInstanceId + 2); // make space for one extra to be able to access [_nextInstanceId] index in case all slots are taken // never allow 0 id _freeInstanceIds[0] = false; diff --git a/Source/Game/Networking/Packets/CalendarPackets.cs b/Source/Game/Networking/Packets/CalendarPackets.cs index 0c9ce1d28..96297e7ca 100644 --- a/Source/Game/Networking/Packets/CalendarPackets.cs +++ b/Source/Game/Networking/Packets/CalendarPackets.cs @@ -852,13 +852,13 @@ namespace Game.Networking.Packets data.WriteUInt64(InstanceID); data.WriteInt32(MapID); data.WriteUInt32(DifficultyID); - data.WriteUInt32((uint)ExpireTime); + data.WriteInt32(ExpireTime); } public ulong InstanceID; public int MapID; public uint DifficultyID; - public long ExpireTime; + public int ExpireTime; } struct CalendarSendCalendarEventInfo diff --git a/Source/Game/Networking/Packets/InstancePackets.cs b/Source/Game/Networking/Packets/InstancePackets.cs index 2379d3727..689a8be81 100644 --- a/Source/Game/Networking/Packets/InstancePackets.cs +++ b/Source/Game/Networking/Packets/InstancePackets.cs @@ -41,11 +41,11 @@ namespace Game.Networking.Packets { _worldPacket.WriteInt32(LockList.Count); - foreach (InstanceLock lockInfos in LockList) + foreach (InstanceLockPkt lockInfos in LockList) lockInfos.Write(_worldPacket); } - public List LockList = new(); + public List LockList = new(); } class ResetInstances : ClientPacket @@ -270,7 +270,7 @@ namespace Game.Networking.Packets } //Structs - public struct InstanceLock + public struct InstanceLockPkt { public void Write(WorldPacket data) { diff --git a/Source/Game/World/WorldManager.cs b/Source/Game/World/WorldManager.cs index 1e4b877ef..e406d3217 100644 --- a/Source/Game/World/WorldManager.cs +++ b/Source/Game/World/WorldManager.cs @@ -516,6 +516,8 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loading instances..."); Global.InstanceSaveMgr.LoadInstances(); + Global.InstanceLockMgr.Load(); + Log.outInfo(LogFilter.ServerLoading, "Loading Localization strings..."); uint oldMSTime = Time.GetMSTime(); Global.ObjectMgr.LoadCreatureLocales(); diff --git a/Source/WorldServer/Server.cs b/Source/WorldServer/Server.cs index df37c569c..85ddfa9f6 100644 --- a/Source/WorldServer/Server.cs +++ b/Source/WorldServer/Server.cs @@ -122,6 +122,7 @@ namespace WorldServer Global.MapMgr.UnloadAll(); // unload all grids (including locked in memory) Global.TerrainMgr.UnloadAll(); + Global.InstanceLockMgr.Unload(); Global.ScriptMgr.Unload(); // set server offline diff --git a/Source/WorldServer/WorldServer.conf.dist b/Source/WorldServer/WorldServer.conf.dist index 6d9c54fa4..e5d2fee10 100644 --- a/Source/WorldServer/WorldServer.conf.dist +++ b/Source/WorldServer/WorldServer.conf.dist @@ -209,6 +209,7 @@ Logger.Network = 3,Console Server #Logger.Condition=3,Console Server #Logger.Gameevent=3,Console Server #Logger.Guild=3,Console Server +#Logger.Instance=3,Console Server #Logger.Lfg=3,Console Server #Logger.Loot=3,Console Server #Logger.MapsScript=3,Console Server