/* * Copyright (C) 2012-2020 CypherCore * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using Framework.Constants; using Framework.Database; using Game.BattleGrounds; using Game.DataStorage; using Game.Garrisons; using Game.Groups; using Game.Maps; using System; using System.Collections; using System.Collections.Generic; using System.Linq; namespace Game.Entities { public class MapManager : Singleton { MapManager() { i_gridCleanUpDelay = WorldConfig.GetUIntValue(WorldCfg.IntervalGridclean); i_timer.SetInterval(WorldConfig.GetIntValue(WorldCfg.IntervalMapupdate)); } public void Initialize() { //todo needs alot of support for threadsafe. int num_threads = WorldConfig.GetIntValue(WorldCfg.Numthreads); // Start mtmaps if needed. if (num_threads > 0) m_updater = new MapUpdater(WorldConfig.GetIntValue(WorldCfg.Numthreads)); } public void InitializeVisibilityDistanceInfo() { foreach (var pair in i_maps) pair.Value.InitVisibilityDistance(); } Map FindMap_i(uint mapId, uint instanceId) { return i_maps.LookupByKey((mapId, instanceId)); } Map CreateWorldMap(uint mapId, uint instanceId) { Map map = new Map(mapId, i_gridCleanUpDelay, instanceId, Difficulty.None); map.LoadRespawnTimes(); map.LoadCorpseData(); if (WorldConfig.GetBoolValue(WorldCfg.BasemapLoadGrids)) map.LoadAllCells(); return map; } InstanceMap CreateInstance(uint mapId, uint instanceId, InstanceSave save, Difficulty difficulty, int team) { // make sure we have a valid map id var entry = CliDB.MapStorage.LookupByKey(mapId); if (entry == null) { Log.outError(LogFilter.Maps, $"CreateInstance: no entry for map {mapId}"); //ABORT(); return null; } // 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}"); InstanceMap map = new InstanceMap(mapId, i_gridCleanUpDelay, instanceId, difficulty, team); 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); if (WorldConfig.GetBoolValue(WorldCfg.InstancemapLoadGrids)) map.LoadAllCells(); return map; } BattlegroundMap CreateBattleground(uint mapId, uint instanceId, Battleground bg) { Log.outDebug(LogFilter.Maps, $"MapInstanced::CreateBattleground: map bg {instanceId} for {mapId} created."); BattlegroundMap map = new BattlegroundMap(mapId, i_gridCleanUpDelay, instanceId, Difficulty.None); Cypher.Assert(map.IsBattlegroundOrArena()); map.SetBG(bg); bg.SetBgMap(map); return map; } GarrisonMap CreateGarrison(uint mapId, uint instanceId, Player owner) { GarrisonMap map = new GarrisonMap(mapId, i_gridCleanUpDelay, instanceId, owner.GetGUID()); Cypher.Assert(map.IsGarrison()); return map; } /// /// create the instance if it's not created already /// the player is not actually added to the instance(only in InstanceMap::Add) /// /// /// /// /// the right instance for the object, based on its InstanceId public Map CreateMap(uint mapId, Player player, uint loginInstanceId = 0) { if (!player) return null; var entry = CliDB.MapStorage.LookupByKey(mapId); if (entry == null) return null; lock (_mapsLock) { Map map = null; uint newInstanceId = 0; // instanceId of the resulting map if (entry.IsBattlegroundOrArena()) { // instantiate or find existing bg map for player // the instance id is set in battlegroundid newInstanceId = player.GetBattlegroundId(); if (newInstanceId == 0) return null; map = FindMap(mapId, newInstanceId); if (!map) { Battleground bg = player.GetBattleground(); if (bg != null) map = CreateBattleground(mapId, newInstanceId, bg); else { player.TeleportToBGEntryPoint(); return null; } } } 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) { 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()); return map; } 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()); } else { Difficulty diff = player.GetGroup() ? player.GetGroup().GetDifficultyID(entry) : player.GetDifficultyID(entry); // if no instanceId via group members or instance saves is found // the instance will be created for the first time newInstanceId = GenerateInstanceId(); //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()); } } else if (entry.IsGarrison()) { newInstanceId = (uint)player.GetGUID().GetCounter(); map = FindMap_i(mapId, newInstanceId); if (!map) map = CreateGarrison(mapId, newInstanceId, player); } else { newInstanceId = 0; map = FindMap_i(mapId, newInstanceId); if (!map) map = CreateWorldMap(mapId, newInstanceId); } if (map) i_maps[(map.GetId(), map.GetInstanceId())] = map; return map; } } public Map FindMap(uint mapId, uint instanceId) { lock (_mapsLock) return FindMap_i(mapId, instanceId); } public void Update(uint diff) { i_timer.Update(diff); if (!i_timer.Passed()) return; var time = (uint)i_timer.GetCurrent(); foreach (var (key, map) in i_maps) { if (map.CanUnload(diff)) { if (DestroyMap(map)) i_maps.Remove(key); continue; } if (m_updater != null) m_updater.ScheduleUpdate(map, (uint)i_timer.GetCurrent()); else map.Update(time); } if (m_updater != null) m_updater.Wait(); foreach (var map in i_maps) map.Value.DelayedUpdate(time); i_timer.SetCurrent(0); } bool DestroyMap(Map map) { map.RemoveAllPlayers(); if (map.HavePlayers()) return false; 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()) FreeInstanceId(map.GetInstanceId()); // erase map map.Dispose(); return true; } public bool IsValidMAP(uint mapId) { return CliDB.MapStorage.ContainsKey(mapId); } public void UnloadAll() { // first unload maps foreach (var pair in i_maps) pair.Value.UnloadAll(); foreach (var pair in i_maps) pair.Value.Dispose(); i_maps.Clear(); if (m_updater != null) m_updater.Deactivate(); } public uint GetNumInstances() { lock (_mapsLock) return (uint)i_maps.Count(pair => pair.Value.IsDungeon()); } public uint GetNumPlayersInInstances() { lock (_mapsLock) return (uint)i_maps.Sum(pair => pair.Value.IsDungeon() ? pair.Value.GetPlayers().Count : 0); } public void InitInstanceIds() { _nextInstanceId = 1; SQLResult result = DB.Characters.Query("SELECT IFNULL(MAX(id), 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); // never allow 0 id _freeInstanceIds[0] = false; } public void RegisterInstanceId(uint instanceId) { _freeInstanceIds[(int)instanceId] = false; // Instances are pulled in ascending order from db and nextInstanceId is initialized with 1, // so if the instance id is used, increment until we find the first unused one for a potential new instance if (_nextInstanceId == instanceId) ++_nextInstanceId; } public uint GenerateInstanceId() { if (_nextInstanceId == 0xFFFFFFFF) { Log.outError(LogFilter.Maps, "Instance ID overflow!! Can't continue, shutting down server. "); Global.WorldMgr.StopNow(); return _nextInstanceId; } uint newInstanceId = _nextInstanceId; Cypher.Assert(newInstanceId < _freeInstanceIds.Length); _freeInstanceIds[(int)newInstanceId] = false; // Find the lowest available id starting from the current NextInstanceId (which should be the lowest according to the logic in FreeInstanceId() int nextFreeId = -1; for (var i = (int)_nextInstanceId++; i < _freeInstanceIds.Length; i++) { if (_freeInstanceIds[i]) { nextFreeId = i; break; } } if (nextFreeId == -1) { _nextInstanceId = (uint)_freeInstanceIds.Length; _freeInstanceIds.Length += 1; _freeInstanceIds[(int)_nextInstanceId] = true; } else _nextInstanceId = (uint)nextFreeId; return newInstanceId; } public void FreeInstanceId(uint instanceId) { // If freed instance id is lower than the next id available for new instances, use the freed one instead _nextInstanceId = Math.Min(instanceId, _nextInstanceId); _freeInstanceIds[(int)instanceId] = true; } public void SetGridCleanUpDelay(uint t) { if (t < MapConst.MinGridDelay) i_gridCleanUpDelay = MapConst.MinGridDelay; else i_gridCleanUpDelay = t; } public void SetMapUpdateInterval(int t) { if (t < MapConst.MinMapUpdateDelay) t = MapConst.MinMapUpdateDelay; i_timer.SetInterval(t); i_timer.Reset(); } public uint GetNextInstanceId() { return _nextInstanceId; } public void SetNextInstanceId(uint nextInstanceId) { _nextInstanceId = nextInstanceId; } public void DoForAllMaps(Action worker) { lock (_mapsLock) { foreach (var (_, map) in i_maps) worker(map); } } public void DoForAllMapsWithMapId(uint mapId, Action worker) { lock (_mapsLock) { var list = i_maps.Where(pair => pair.Key.mapId == mapId && pair.Key.instanceId >= 0); foreach (var (_, map) in list) worker(map); } } public void IncreaseScheduledScriptsCount() { ++_scheduledScripts; } public void DecreaseScheduledScriptCount() { --_scheduledScripts; } public void DecreaseScheduledScriptCount(uint count) { _scheduledScripts -= count; } public bool IsScriptScheduled() { return _scheduledScripts > 0; } Dictionary<(uint mapId, uint instanceId), Map> i_maps = new(); IntervalTimer i_timer = new(); object _mapsLock= new(); uint i_gridCleanUpDelay; BitSet _freeInstanceIds; uint _nextInstanceId; MapUpdater m_updater; uint _scheduledScripts; } }