using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; using PhoenixLib.Events; using PhoenixLib.Logging; using Plugin.Act4.Event; using Plugin.Act4.Extension; using WingsAPI.Game.Extensions.PacketGeneration; using WingsAPI.Scripting.Attribute; using WingsAPI.Scripting.Converter; using WingsAPI.Scripting.Enum; using WingsAPI.Scripting.Event; using WingsAPI.Scripting.Event.Common; using WingsAPI.Scripting.Object.Common; using WingsAPI.Scripting.Object.Common.Map; using WingsAPI.Scripting.Object.Dungeon; using WingsAPI.Scripting.Object.Raid; using WingsAPI.Scripting.ScriptManager; using WingsEmu.DTOs.Maps; using WingsEmu.Game; using WingsEmu.Game.Act4; using WingsEmu.Game.Act4.Entities; using WingsEmu.Game.Act4.Event; using WingsEmu.Game.Entities; using WingsEmu.Game.Entities.Event; using WingsEmu.Game.Helpers.Damages; using WingsEmu.Game.Maps; using WingsEmu.Game.Portals; using WingsEmu.Game.Raids; using WingsEmu.Packets.Enums; namespace Plugin.Act4; public class DungeonFactory : IDungeonFactory { private static readonly IEnumerable Converters; private readonly IAsyncEventPipeline _eventPipeline; private readonly HatusDragonHead _hatusHead = new() { BluePositionX = 23, RedPositionX = 36, GreenPositionX = 49 }; private readonly IMapManager _mapManager; private readonly IMonsterEntityFactory _monsterEntityFactory; private readonly IPortalFactory _portalFactory; private readonly IDungeonScriptManager _raidScriptManager; static DungeonFactory() { Converters = typeof(IScriptedEventConverter).Assembly.GetTypes() .Concat(typeof(DungeonFactory).Assembly.GetTypes()) .Where(x => typeof(IScriptedEventConverter).IsAssignableFrom(x)) .Where(x => !x.IsAbstract && !x.IsInterface); } public DungeonFactory(IDungeonScriptManager raidScriptManager, IMapManager mapManager, IAsyncEventPipeline eventPipeline, IMonsterEntityFactory monsterEntityFactory, IPortalFactory portalFactory) { _raidScriptManager = raidScriptManager; _mapManager = mapManager; _eventPipeline = eventPipeline; _monsterEntityFactory = monsterEntityFactory; _portalFactory = portalFactory; } public DungeonInstance CreateDungeon(long familyId, DungeonType dungeonType) { SDungeon scriptedDungeon = _raidScriptManager.GetScriptedDungeon(dungeonType.ToSDungeonType()); if (scriptedDungeon == null) { Log.Error($"Unable to create dungeon of DungeonType: '{dungeonType.ToString()}', the script couldn't be found inside DungeonScriptManager", new Act4DungeonSystemException($"Unable to create act4 dungeon - DungeonType: {dungeonType.ToString()}, FamilyId: {familyId}")); return default; } var raidBoxRarities = new List(); foreach (SRaidBoxRarity boxRarity in scriptedDungeon.Reward.RaidBox.RaidBoxRarity) { var newRarity = new RaidBoxRarity(boxRarity.Rarity, boxRarity.Chance); raidBoxRarities.Add(newRarity); } var raidBox = new RaidBox(scriptedDungeon.Reward.RaidBox.RewardBox, raidBoxRarities); var raidReward = new RaidReward(raidBox, scriptedDungeon.Reward.DefaultReputation, scriptedDungeon.Reward.FixedReputation); //Preparing game object locators for the conversion of ScriptEvents to GameEvents Dictionary dungeonSubInstancesByScriptId = new(); Dictionary portalsByScriptId = new(); Dictionary monstersByScriptId = new(); Dictionary buttonsByScriptId = new(); Dictionary> events)>> eventsToConvertAndAdd = new(); HatusHeads hatusHeads = dungeonType == DungeonType.Hatus ? new HatusHeads(_hatusHead) : null; //We prepare first portals to allow the creation of portals //(portals connect two maps and pre-categorizing the maps by their ScriptIds lets us obtain their GameIds, allowing us to generate gamePortals as we require of GameIds to do so) foreach (SMap scriptedMap in scriptedDungeon.Maps) { IMapInstance map = scriptedMap.MapType == SMapType.MapId ? _mapManager.GenerateMapInstanceByMapId(scriptedMap.MapIdVnum, MapInstanceType.Act4Dungeon) : _mapManager.GenerateMapInstanceByMapVNum(new ServerMapDto { Flags = scriptedMap.Flags.Select(mapFlag => (MapFlags)(int)mapFlag).ToList(), Id = scriptedMap.MapIdVnum, MapVnum = scriptedMap.MapIdVnum, NameId = scriptedMap.NameId, MusicId = scriptedMap.MusicId }, MapInstanceType.Act4Dungeon); dungeonSubInstancesByScriptId[scriptedMap.Id] = new DungeonSubInstance(map, _eventPipeline, hatusHeads); eventsToConvertAndAdd[scriptedMap.Id] = new List<(IEventTriggerContainer container, IDictionary> events)> { (dungeonSubInstancesByScriptId[scriptedMap.Id], scriptedMap.Events) }; } foreach (SMap scriptedMap in scriptedDungeon.Maps) { DungeonSubInstance dungeonSubInstance = dungeonSubInstancesByScriptId[scriptedMap.Id]; var dungeonWaves = new List(); var dungeonLoopWaves = new List(); // Add Monster Waves foreach (SMonsterWave monsterWave in scriptedMap.MonsterWaves) { var summons = new List(); foreach (SMonster mobWave in monsterWave.Monsters) { summons.Add(new ToSummon { VNum = mobWave.Vnum, IsHostile = true, IsMoving = true, SpawnCell = mobWave.IsRandomPosition ? null : new Position(mobWave.Position.X, mobWave.Position.Y), SummonType = SummonType.MONSTER_WAVE }); } if (monsterWave.Loop && monsterWave.LoopTick.HasValue) { dungeonLoopWaves.Add(new DungeonLoopWave { Delay = TimeSpan.FromSeconds(monsterWave.TimeInSeconds), Monsters = summons, TickDelay = TimeSpan.FromSeconds(monsterWave.LoopTick.Value), IsScaledWithPlayerAmount = monsterWave.IsScaledWithPlayerAmount }); } else { dungeonWaves.Add(new DungeonLinearWave { Monsters = summons, Delay = TimeSpan.FromSeconds(monsterWave.TimeInSeconds) }); } } dungeonSubInstance.SetLinearWaves(dungeonWaves); dungeonSubInstance.SetLoopWaves(dungeonLoopWaves); var portalGenerators = new List(); foreach (SPortal scriptedPortal in scriptedMap.Portals) { bool isReturnPortal = scriptedPortal.IsReturn; var sourcePos = new Position(scriptedPortal.SourcePosition.X, scriptedPortal.SourcePosition.Y); IMapInstance sourceMap = dungeonSubInstancesByScriptId[scriptedMap.Id].MapInstance; var destPos = new Position(scriptedPortal.DestinationPosition.X, scriptedPortal.DestinationPosition.Y); IMapInstance destinationMap = isReturnPortal ? null : dungeonSubInstancesByScriptId[scriptedPortal.DestinationId].MapInstance; IPortalEntity portal = _portalFactory.CreatePortal((PortalType)scriptedPortal.Type, sourceMap, sourcePos, destinationMap, destPos); portalsByScriptId[scriptedPortal.Id] = portal; if (scriptedPortal.CreationDelay.HasValue) { portalGenerators.Add(new PortalGenerator { Portal = portal, Delay = TimeSpan.FromSeconds(scriptedPortal.CreationDelay.Value) }); continue; } dungeonSubInstance.MapInstance.AddPortalToMap(portal); } dungeonSubInstance.SetPortalGenerators(portalGenerators); // Add monsters foreach (SMonster scriptedMonster in scriptedMap.Monsters) { IMonsterEntity monster = _monsterEntityFactory.CreateMonster(scriptedMonster.Vnum, dungeonSubInstance.MapInstance, new MonsterEntityBuilder { IsRespawningOnDeath = false, IsBoss = scriptedMonster.IsBoss, IsTarget = scriptedMonster.IsTarget, IsWalkingAround = scriptedMonster.CanMove, IsHostile = true }); monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, scriptedMonster.Position.X, scriptedMonster.Position.Y)); monstersByScriptId[scriptedMonster.Id] = monster; eventsToConvertAndAdd[scriptedMap.Id].Add((monster, scriptedMonster.Events)); //Adding this to obtain the subInstance via the monsters' Script Id dungeonSubInstancesByScriptId[scriptedMonster.Id] = dungeonSubInstance; dungeonSubInstance.AddDungeonMonster(monster); } // Add map objects foreach (SMapObject scriptedObject in scriptedMap.Objects) { if (scriptedObject is not SButton scriptedButton) { continue; } var button = new ButtonMapItem(scriptedButton.Position.X, scriptedButton.Position.Y, scriptedButton.DeactivatedVnum, scriptedButton.ActivatedVnum, false, dungeonSubInstance.MapInstance, _eventPipeline, scriptedButton.IsObjective); eventsToConvertAndAdd[scriptedMap.Id].Add((button, scriptedButton.Events)); dungeonSubInstance.AddDungeonButton(button); } } //Yeah, we need an ugly wrapper because the instance is not constructed until the end, and our events require that instance for the future. DungeonInstanceWrapper dungeonInstanceWrapper = new(); IServiceCollection serviceLocator = new ServiceCollection(); serviceLocator.AddSingleton(dungeonSubInstancesByScriptId); serviceLocator.AddSingleton(portalsByScriptId); serviceLocator.AddSingleton(monstersByScriptId); serviceLocator.AddSingleton(buttonsByScriptId); serviceLocator.AddSingleton(dungeonInstanceWrapper); foreach (Type converter in Converters) { serviceLocator.AddTransient(typeof(IScriptedEventConverter), converter); } foreach ((Guid scriptedMapId, List<(IEventTriggerContainer container, IDictionary> events)> eventsForContainers) in eventsToConvertAndAdd) { serviceLocator.AddSingleton(dungeonSubInstancesByScriptId[scriptedMapId]); var converters = serviceLocator.BuildServiceProvider() .GetServices() .ToDictionary(x => x.EventType, x => x); foreach ((IEventTriggerContainer container, IDictionary> containerEvents) in eventsForContainers) { foreach ((string key, IEnumerable events) in containerEvents) { foreach (SEvent scriptedEvent in events) { IAsyncEvent e = converters.GetValueOrDefault(scriptedEvent.GetType())?.Convert(scriptedEvent); if (e == null) { throw new InvalidOperationException($"Failed to convert {scriptedEvent.GetType().Name} to async event"); } ScriptEventAttribute attribute = scriptedEvent.GetType().GetCustomAttribute(); if (attribute == null) { throw new InvalidOperationException($"Failed to find attribute for: {scriptedEvent.GetType().Name}"); } //quick win if (attribute.Name == SRemovePortalEvent.Name) { container.AddEvent(key, new Act4DungeonBroadcastBossClosedEvent { DungeonInstanceWrapper = dungeonInstanceWrapper }, attribute.IsRemovedOnTrigger); } container.AddEvent(key, e, attribute.IsRemovedOnTrigger); } } } } return dungeonInstanceWrapper.DungeonInstance = new DungeonInstance(familyId, dungeonType, dungeonSubInstancesByScriptId.Values, dungeonSubInstancesByScriptId[scriptedDungeon.Spawn.MapId], new Position(scriptedDungeon.Spawn.Position.X, scriptedDungeon.Spawn.Position.Y), raidReward); } }