// WingsEmu // // Developed by NosWings Team using System; using System.Collections.Generic; using System.Linq; using PhoenixLib.Events; using PhoenixLib.Logging; using Plugin.CoreImpl.Maps.Systems; using Plugin.CoreImpl.Pathfinding; using WingsEmu.DTOs.Maps; using WingsEmu.DTOs.ServerDatas; using WingsEmu.Game; using WingsEmu.Game._ECS; using WingsEmu.Game._enum; using WingsEmu.Game._i18n; using WingsEmu.Game._ItemUsage.Configuration; using WingsEmu.Game.Battle; using WingsEmu.Game.Buffs; using WingsEmu.Game.Characters; using WingsEmu.Game.Configurations; using WingsEmu.Game.Entities; using WingsEmu.Game.Entities.Extensions; using WingsEmu.Game.Extensions; using WingsEmu.Game.Helpers.Damages; using WingsEmu.Game.Items; using WingsEmu.Game.Managers.StaticData; using WingsEmu.Game.Maps; using WingsEmu.Game.Mates; using WingsEmu.Game.Miniland; using WingsEmu.Game.Networking; using WingsEmu.Game.Portals; using WingsEmu.Game.Raids; using WingsEmu.Game.RainbowBattle; using WingsEmu.Game.Skills; using WingsEmu.Packets.Enums; namespace Plugin.CoreImpl.Maps { public class MapInstance : SessionsContainer, IMapInstance { private static readonly TimeSpan _timeToIdleState = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("WINGSEMU_TICK_IDLE_DELAY") ?? "10")); private readonly BattleSystem _battleSystem; private readonly CharacterSystem _characterSystem; private readonly DropSystem _dropSystem; private readonly IEntityIdManager _entityIdManager; private readonly HashSet _flags = new(); private readonly IGameItemInstanceFactory _gameItemInstanceFactory; private readonly MateSystem _mateSystem; private readonly MonsterSystem _monsterSystem; private readonly NpcSystem _npcSystem; private readonly IPortalFactory _portalFactory; private readonly IRandomGenerator _randomGenerator; private readonly IMapSystem[] _systems; private readonly ITickManager _tickManager; private List _cells; private DateTime? _idleRequest; private bool _isTickRefreshRequested; public MapInstance(Map map, MapInstanceType type, ITickManager tickManager, GameMinMaxConfiguration minMaxConfiguration, ISpyOutManager spyOutManager, ISkillsManager skillsManager, IGameLanguageService languageService, IMeditationManager meditationManager, IAsyncEventPipeline asyncEventPipeline, IRandomGenerator randomGenerator, IBCardEffectHandlerContainer bCardEffectHandlerContainer, IBuffFactory buffFactory, IPortalFactory portalFactory, IGameItemInstanceFactory gameItemInstanceFactory, ISpPartnerConfiguration spPartnerConfiguration, IMonsterTalkingConfig monsterTalkingConfig) { Id = Guid.NewGuid(); Grid = map.Grid; Width = map.Width; Height = map.Height; MapId = map.MapId; MapVnum = map.MapVnum; MapNameId = map.MapNameId; Music = map.Music; foreach (MapFlags flag in map.Flags) { _flags.Add(flag); } IsPvp = HasMapFlag(MapFlags.HAS_PVP_ENABLED) || HasMapFlag(MapFlags.HAS_PVP_FACTION_ENABLED) || HasMapFlag(MapFlags.HAS_PVP_FAMILY_ENABLED); ShopAllowed = !HasMapFlag(MapFlags.HAS_USER_SHOPS_DISABLED) && !HasMapFlag(MapFlags.ACT_4) && !IsPvp && type == MapInstanceType.BaseMapInstance; MapInstanceType = type; IPathFinder pathfinder = new PathFinder(Grid, Width, Height); _tickManager = tickManager; _randomGenerator = randomGenerator; _portalFactory = portalFactory; _gameItemInstanceFactory = gameItemInstanceFactory; _npcSystem = new NpcSystem(bCardEffectHandlerContainer, this, asyncEventPipeline, randomGenerator, buffFactory, languageService, pathfinder, monsterTalkingConfig); _monsterSystem = new MonsterSystem(randomGenerator, bCardEffectHandlerContainer, asyncEventPipeline, this, buffFactory, languageService, pathfinder, monsterTalkingConfig); _characterSystem = new CharacterSystem(bCardEffectHandlerContainer, buffFactory, meditationManager, asyncEventPipeline, this, spyOutManager, randomGenerator, skillsManager, minMaxConfiguration, languageService); _mateSystem = new MateSystem(bCardEffectHandlerContainer, languageService, minMaxConfiguration, this, randomGenerator, buffFactory, languageService, spPartnerConfiguration); _battleSystem = new BattleSystem(asyncEventPipeline, this, bCardEffectHandlerContainer, languageService, buffFactory, randomGenerator); _dropSystem = new DropSystem(this, randomGenerator); _entityIdManager = new GenericEntityIdManager(); _systems = new IMapSystem[] { _monsterSystem, _characterSystem, _npcSystem, _mateSystem, _dropSystem, _battleSystem }; SetIdle(); } public MapInstanceState State { get; private set; } public bool IsDance { get; set; } public short? MapMusic { get; set; } public bool IsPvp { get; set; } public byte MapIndexX { get; set; } public byte MapIndexY { get; set; } public Guid Id { get; } public MapInstanceType MapInstanceType { get; } public IReadOnlyList Grid { get; } public int Width { get; } public int Height { get; } public int MapId { get; } public int Music { get; } public int MapVnum { get; } public int MapNameId { get; } public bool ShopAllowed { get; } public bool AIDisabled { get; set; } public List TimeSpacePortals { get; } = new(); public List MapDesignObjects { get; set; } = new(); public List Portals { get; } = new(); public override void RegisterSession(IClientSession session) { base.RegisterSession(session); AddCharacter(session.PlayerEntity); foreach (IMateEntity activeMate in session.PlayerEntity.MateComponent.TeamMembers()) { AddMate(activeMate); } ActivateMap(); } public override void UnregisterSession(IClientSession session) { base.UnregisterSession(session); RemoveCharacter(session.PlayerEntity); foreach (IMateEntity activeMate in session.PlayerEntity.MateComponent.TeamMembers()) { RemoveMate(activeMate); } if (State == MapInstanceState.Running && Sessions.Count == 0) { RequestIdleState(); } } public IReadOnlyList GetEntitiesOnMapPackets(bool onlyItemsAndPortals = false) { var packetCache = new List(); foreach (IPortalEntity portal in Portals) { packetCache.Add(portal.GenerateGp()); } if (!onlyItemsAndPortals) { bool firstBossSent = false; foreach (IMonsterEntity monster in GetAliveMonsters()) { if (!monster.IsStillAlive) { continue; } packetCache.Add(monster.GenerateIn()); if (monster.IsBoss && monster.IsTarget) { packetCache.Add(monster.GenerateRaidBossPacket(firstBossSent)); firstBossSent = true; } TryAddScal(packetCache, monster); packetCache.AddRange(monster.GenerateConstBuffEffects()); } } foreach (MapItem mapItem in Drops) { packetCache.Add(mapItem.GenerateIn()); } if (!onlyItemsAndPortals) { foreach (INpcEntity npc in GetAliveNpcs()) { if (!npc.IsStillAlive) { continue; } packetCache.Add(npc.GenerateIn()); if (npc.ShopNpc != null) { packetCache.Add($"shop 2 {npc.Id} 1 {(byte)npc.ShopNpc.MenuType} {npc.ShopNpc.ShopType} {npc.ShopNpc.Name}"); } TryAddScal(packetCache, npc); packetCache.AddRange(npc.GenerateConstBuffEffects()); } foreach (INpcEntity npc in GetPassiveNpcs()) { if (!npc.IsStillAlive) { continue; } packetCache.Add(npc.GenerateIn()); if (npc.ShopNpc != null) { packetCache.Add($"shop 2 {npc.Id} 1 {(byte)npc.ShopNpc.MenuType} {npc.ShopNpc.ShopType} {npc.ShopNpc.Name}"); } if (npc.TimeSpaceInfo != null) { packetCache.Add(npc.GenerateEffectGround(EffectType.BlueTimeSpace, npc.PositionX, npc.PositionY, false)); } if (npc.RainbowFlag != null) { packetCache.Add(npc.GenerateFlagPacket()); } TryAddScal(packetCache, npc); packetCache.AddRange(npc.GenerateConstBuffEffects()); } } return packetCache; } public IBattleEntity GetBattleEntity(VisualType type, long id) { switch (type) { case VisualType.Monster: return GetMonsterById(id); case VisualType.Player: return GetCharacterById(id); case VisualType.Npc: if (id >= 1_000_000) { return GetMateById(id); } return GetNpcById(id); default: return null; } } public bool HasMapFlag(MapFlags flags) => _flags.Contains(flags); public void LoadPortals(IEnumerable tmp = null) { if (tmp == null) { return; } foreach (PortalDTO portalInfo in tmp) { var sourcePos = new Position(portalInfo.SourceX, portalInfo.SourceY); var destPos = new Position(portalInfo.DestinationX, portalInfo.DestinationY); IPortalEntity portal = _portalFactory.CreatePortal((PortalType)portalInfo.Type, this, sourcePos, portalInfo.DestinationMapId, destPos, portalInfo.RaidType, portalInfo.MapNameId); Portals.Add(portal); } } public void Initialize(DateTime date) { State = MapInstanceState.Running; ProcessTick(date); State = MapInstanceState.Idle; } public void Destroy() { SetIdle(); foreach (IClientSession session in Sessions.ToList()) { session.ChangeToLastBaseMap(); } foreach (IMapSystem t in _systems) { t.Clear(); } } public Position GetRandomPosition() { if (_cells != null) { return _cells[_randomGenerator.RandomNumber(_cells.Count - 1)]; } _cells = new List(); for (short y = 0; y < Height; y++) { for (short x = 0; x < Width; x++) { if (this.CanWalkAround(x, y)) { _cells.Add(new Position(x, y)); } } } return _cells[_randomGenerator.RandomNumber(_cells.Count - 1)]; } public MapItem PutItem(ushort amount, ref GameItemInstance inv, IClientSession session) { var possibilities = new List(); for (short x = -1; x < 2; x++) { for (short y = -1; y < 2; y++) { possibilities.Add(new Position(x, y)); } } short mapX = 0; short mapY = 0; bool niceSpot = false; foreach (Position possibility in possibilities.OrderBy(s => _randomGenerator.RandomNumber())) { mapX = (short)(session.PlayerEntity.PositionX + possibility.X); mapY = (short)(session.PlayerEntity.PositionY + possibility.Y); if (this.IsBlockedZone(mapX, mapY)) { continue; } niceSpot = true; break; } if (!niceSpot) { return null; } if (amount <= 0 || amount > inv.Amount) { return null; } GameItemInstance newItemInstance = _gameItemInstanceFactory.DuplicateItem(inv); newItemInstance.Amount = amount; MapItem droppedItem = new CharacterMapItem(mapX, mapY, newItemInstance, this); AddDrop(droppedItem); return droppedItem; } public void DespawnMonster(IMonsterEntity monsterEntity) { monsterEntity.GenerateDeath(); Broadcast(monsterEntity.GenerateOut()); } public IReadOnlyList GetCharactersInRange(Position pos, short distance, Func predicate) => _characterSystem.GetCharactersInRange(pos, distance, predicate); public IReadOnlyList GetCharactersInRange(Position position, short distance) => GetCharactersInRange(position, distance, null); public IReadOnlyList GetClosestCharactersInRange(Position pos, short distance) => _characterSystem.GetClosestCharactersInRange(pos, distance); public IReadOnlyList GetAliveCharactersInRange(Position pos, short distance, Func predicate) => _characterSystem.GetAliveCharactersInRange(pos, distance, predicate); public IReadOnlyList GetAliveCharactersInRange(Position position, short distance) => GetAliveCharactersInRange(position, distance, null); public IReadOnlyList GetNonMonsterBattleEntities(Func predicate) { var battleEntitiesInRange = new List(); battleEntitiesInRange.AddRange(GetAliveCharacters(predicate)); battleEntitiesInRange.AddRange(GetAliveMates(predicate)); battleEntitiesInRange.AddRange(GetAliveNpcs(predicate)); return battleEntitiesInRange; } public IReadOnlyList GetBattleEntities(Func predicate) { var battleEntitiesInRange = new List(); battleEntitiesInRange.AddRange(GetAliveCharacters(predicate)); battleEntitiesInRange.AddRange(GetAliveMates(predicate)); battleEntitiesInRange.AddRange(GetAliveNpcs(predicate)); battleEntitiesInRange.AddRange(GetAliveMonsters(predicate)); return battleEntitiesInRange; } public IReadOnlyList GetBattleEntitiesInRange(Position pos, short distance) { var battleEntitiesInRange = new List(); battleEntitiesInRange.AddRange(GetAliveCharactersInRange(pos, distance)); battleEntitiesInRange.AddRange(GetAliveMatesInRange(pos, distance)); battleEntitiesInRange.AddRange(GetAliveNpcsInRange(pos, distance)); battleEntitiesInRange.AddRange(GetAliveMonstersInRange(pos, distance)); return battleEntitiesInRange; } public IReadOnlyList GetNonMonsterBattleEntitiesInRange(Position pos, short distance) => GetNonMonsterBattleEntitiesInRange(pos, distance, null); public IReadOnlyList GetNonMonsterBattleEntitiesInRange(Position pos, short distance, Func predicate) { var battleEntitiesInRange = new List(); battleEntitiesInRange.AddRange(GetAliveCharactersInRange(pos, distance, predicate)); battleEntitiesInRange.AddRange(GetAliveMatesInRange(pos, distance, predicate)); battleEntitiesInRange.AddRange(GetAliveNpcsInRange(pos, distance, predicate)); return battleEntitiesInRange; } public IReadOnlyList GetNonMonsterBattleEntities() { var battleEntitiesInRange = new List(); battleEntitiesInRange.AddRange(GetAliveCharacters()); battleEntitiesInRange.AddRange(GetAliveMates()); battleEntitiesInRange.AddRange(GetAliveNpcs()); return battleEntitiesInRange; } public IReadOnlyList GetDeadMonsters() => _monsterSystem.GetDeadMonsters(); public IReadOnlyList GetAliveMonsters(Func predicate) => _monsterSystem.GetAliveMonsters(predicate); public IReadOnlyList GetAliveMonstersInRange(Position pos, short distance) => _monsterSystem.GetAliveMonstersInRange(pos, distance); public IReadOnlyList GetClosestMonstersInRange(Position pos, short distance) => _monsterSystem.GetClosestMonstersInRange(pos, distance); public IReadOnlyList GetClosestBattleEntitiesInRange(Position pos, short distance) { var battleEntitiesInRange = new List(); battleEntitiesInRange.AddRange(GetClosestCharactersInRange(pos, distance)); battleEntitiesInRange.AddRange(GetClosestMatesInRange(pos, distance)); battleEntitiesInRange.AddRange(GetClosestNpcsInRange(pos, distance)); battleEntitiesInRange.AddRange(GetClosestMonstersInRange(pos, distance)); return battleEntitiesInRange; } public string Name => $"{nameof(MapInstance)}: {MapInstanceType.ToString()} - {MapId.ToString()}"; public void ProcessTick(DateTime date) { try { if (State == MapInstanceState.Idle) { return; } foreach (IMapSystem t in _systems) { t.ProcessTick(date, _isTickRefreshRequested); //_systemTicks.WithLabels(t.Name, Name).Observe(processingTime.ElapsedMilliseconds); } FlushPackets(); if (_isTickRefreshRequested) { _isTickRefreshRequested = false; } // idle state if (Sessions.Count > 0) { //Log.Warn($"[TICK_MAP_INSTANCE] {Map.MapId} SessionCount > 0"); return; } _idleRequest ??= date; if (_idleRequest.HasValue && _idleRequest.Value + _timeToIdleState < date) { SetIdle(); } } catch (Exception e) { Log.Error($"TICK - MapId: {MapId.ToString()}", e); } } public IMonsterEntity GetMonsterById(long id) => _monsterSystem.GetMonsterById(id); public IMonsterEntity GetMonsterByUniqueId(Guid id) => _monsterSystem.GetMonsterByUniqueId(id); public IReadOnlyList GetAliveMonsters() => _monsterSystem.GetAliveMonsters(); public void AddMonster(IMonsterEntity entity) { _monsterSystem.AddMonster(entity); } public void RemoveMonster(IMonsterEntity entity) { _monsterSystem.RemoveMonster(entity); } public void AddEntityToTargets(IMonsterEntity monsterEntity, IBattleEntity target) { _monsterSystem.AddEntityToTargets(monsterEntity, target); } public void MonsterRefreshTarget(IMonsterEntity target, IBattleEntity caster, in DateTime time, bool isByAttacking = false) { _monsterSystem.MonsterRefreshTarget(target, caster, time, isByAttacking); } public void ForgetAll(IMonsterEntity monsterEntity, in DateTime time, bool clearDamagers = true) { _monsterSystem.ForgetAll(monsterEntity, time, clearDamagers); } public void RemoveTarget(IMonsterEntity monsterEntity, IBattleEntity entity, bool checkIfPlayer = false) { _monsterSystem.RemoveTarget(monsterEntity, entity, checkIfPlayer); } public void ActivateMode(IMonsterEntity monsterEntity) { _monsterSystem.ActivateMode(monsterEntity); } public void DeactivateMode(IMonsterEntity monsterEntity) { _monsterSystem.DeactivateMode(monsterEntity); } public void IncreaseMonsterDeathsOnMap() { _monsterSystem.IncreaseMonsterDeathsOnMap(); } public long MonsterDeathsOnMap() => _monsterSystem.MonsterDeathsOnMap(); public byte CurrentVessels() => _monsterSystem.CurrentVessels(); public bool IsSummonLimitReached(int? summonerId, SummonType? summonSummonType) => _monsterSystem.IsSummonLimitReached(summonerId, summonSummonType); public IReadOnlyList GetAliveNpcs() => _npcSystem.GetAliveNpcs(); public IReadOnlyList GetAliveNpcs(Func predicate) => _npcSystem.GetAliveNpcs(predicate); public IReadOnlyList GetPassiveNpcs() => _npcSystem.GetPassiveNpcs(); public IReadOnlyList GetAliveNpcsInRange(Position pos, short distance) => _npcSystem.GetAliveNpcsInRange(pos, distance); public IReadOnlyList GetClosestNpcsInRange(Position pos, short distance) => _npcSystem.GetClosestNpcsInRange(pos, distance); public void NpcRefreshTarget(INpcEntity npcEntity, IBattleEntity target) { _npcSystem.NpcRefreshTarget(npcEntity, target); } public IReadOnlyList GetAliveNpcsInRange(Position pos, short distance, Func predicate) => _npcSystem.GetAliveNpcsInRange(pos, distance, predicate); public INpcEntity GetNpcById(long id) => _npcSystem.GetNpcById(id); public void AddNpc(INpcEntity entity) { _npcSystem.AddNpc(entity); } public void RemoveNpc(INpcEntity entity) { _npcSystem.RemoveNpc(entity); } public IPlayerEntity GetCharacterById(long id) => _characterSystem.GetCharacterById(id); public IReadOnlyList GetCharacters() => _characterSystem.GetCharacters(); public IReadOnlyList GetCharacters(Func predicate) => _characterSystem.GetCharacters(predicate); public IReadOnlyList GetAliveCharacters() => _characterSystem.GetAliveCharacters(); public IReadOnlyList GetAliveCharacters(Func predicate) => _characterSystem.GetAliveCharacters(predicate); public void AddCharacter(IPlayerEntity character) { _characterSystem.AddCharacter(character); } public void RemoveCharacter(IPlayerEntity entity) { _characterSystem.RemoveCharacter(entity); } public IReadOnlyList GetAliveMates() => _mateSystem.GetAliveMates(); public IReadOnlyList GetAliveMates(Func predicate) => _mateSystem.GetAliveMates(predicate); public IReadOnlyList GetAliveMatesInRange(Position position, short range) => _mateSystem.GetAliveMatesInRange(position, range); public IReadOnlyList GetClosestMatesInRange(Position position, short range) => _mateSystem.GetClosestMatesInRange(position, range); public IReadOnlyList GetAliveMatesInRange(Position position, short range, Func predicate) => _mateSystem.GetAliveMatesInRange(position, range, predicate); public IMateEntity GetMateById(long mateId) => _mateSystem.GetMateById(mateId); public void AddMate(IMateEntity entity) { _mateSystem.AddMate(entity); } public void RemoveMate(IMateEntity entity) { _mateSystem.RemoveMate(entity); } public IReadOnlyList Drops => _dropSystem.Drops; public void AddDrop(MapItem item) { _dropSystem.AddDrop(item); } public bool RemoveDrop(long dropId) => _dropSystem.RemoveDrop(dropId); public bool HasDrop(long dropId) => _dropSystem.HasDrop(dropId); public MapItem GetDrop(long dropId) => _dropSystem.GetDrop(dropId); public void AddCastHitRequest(HitProcessable hitProcessable) { _battleSystem.AddCastHitRequest(hitProcessable); } public void AddCastBuffRequest(BuffProcessable buffProcessable) { _battleSystem.AddCastBuffRequest(buffProcessable); } public void AddHitRequest(HitRequest hitRequest) { _battleSystem.AddHitRequest(hitRequest); } public void AddBuffRequest(BuffRequest buffRequest) { _battleSystem.AddBuffRequest(buffRequest); } public int GenerateEntityId() => _entityIdManager.GenerateEntityId(); private static void TryAddScal(ICollection list, IBattleEntity battleEntity) { if (!battleEntity.ShouldSendScal()) { return; } list.Add(battleEntity.GenerateScal()); } public void SetInactive() { SetIdle(); } private void RequestIdleState() { _idleRequest = DateTime.UtcNow; // Log.Warn($"[TICK_IDLE] Requested by: {Map.MapId.ToString()}"); } private void ActivateMap() { if (State == MapInstanceState.Running) { _idleRequest = null; return; } // Log.Warn($"[TICK_ACTIVATE] Activation of map: {Map.MapId.ToString()}"); State = MapInstanceState.Running; _isTickRefreshRequested = true; _tickManager.AddProcessable(this); } private void SetIdle() { if (State == MapInstanceState.Idle) { return; } // Log.Warn($"[TICK_IDLE] {Map.MapId.ToString()} is now in idle mode"); State = MapInstanceState.Idle; _idleRequest = null; _tickManager.RemoveProcessable(this); foreach (IMapSystem t in _systems) { t.PutIdleState(); } } } }