// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents; using System.Collections.Concurrent; using System.Linq; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; /// /// Invasion state that is created for every periodic invasion run. /// public class InvasionGameServerState : PeriodicTaskGameServerState { private readonly HashSet _mapIds = []; private readonly Dictionary _selectedMaps = []; private readonly List _announcedMapIds = []; private readonly ConcurrentDictionary _monsters = new(); /// /// Initializes a new instance of the class. /// /// The game context. public InvasionGameServerState(IGameContext context) : base(context) { } /// /// Gets or sets the map identifier used for UI display / map-event state broadcasts. /// null means no event is active or the display map has not been selected yet. /// public ushort? MapId { get; set; } /// /// Gets the set of map identifiers on which monsters will spawn this run. /// public IReadOnlySet MapIds => this._mapIds; /// /// Gets the read-only mapping of monster ID to selected map identifier. /// Populated for spawns whose is . /// public IReadOnlyDictionary SelectedMaps => this._selectedMaps; /// /// Gets the list of map identifiers that are named in the invasion start/end broadcast. /// These are the maps on which the announced (featured) monster actually spawns this run, /// so the message always points players at a map that really contains it. /// public IReadOnlyList AnnouncedMapIds => this._announcedMapIds; /// /// Sets the maps named in the broadcast message and the single used /// for the map-event UI state. /// /// The maps on which the announced monster spawns this run. internal void SetAnnouncedMaps(IReadOnlyCollection mapIds) { this._announcedMapIds.Clear(); this._announcedMapIds.AddRange(mapIds); this.MapId = this._announcedMapIds.Count > 0 ? this._announcedMapIds[0] : null; } /// /// Registers a map as active for this run and optionally records which map was /// randomly chosen for a particular monster type. /// /// The map identifier to register. /// /// When provided, records the as the chosen map for this monster. /// Pass null for "spawn-on-all-maps" entries. /// internal void RegisterMap(ushort mapId, ushort? monsterId = null) { this._mapIds.Add(mapId); if (monsterId.HasValue) { this._selectedMaps[monsterId.Value] = mapId; } } /// /// Clears all state accumulated from a previous run so the object can be reused. /// internal void Reset() { this.MapId = null; this._mapIds.Clear(); this._selectedMaps.Clear(); this._announcedMapIds.Clear(); } /// /// Tracks a monster spawned by this invasion and handles its cleanup on death. /// /// The monster to track. internal void AddMonster(Monster monster) { if (this._monsters.TryAdd(monster, 0)) { monster.Died += this.OnMonsterDied; } } /// /// Despawns and disposes all active monsters tracked by this invasion state. /// internal async ValueTask CleanUpMonstersAsync() { if (this._monsters.Count == 0) { return; } var monsters = this._monsters.Keys.ToArray(); var tasks = monsters.Select(async monster => { this._monsters.TryRemove(monster, out _); monster.Died -= this.OnMonsterDied; if (!monster.IsDisposed) { await monster.CurrentMap.RemoveAsync(monster).ConfigureAwait(false); monster.Dispose(); } }); await Task.WhenAll(tasks).ConfigureAwait(false); } private void OnMonsterDied(object? sender, DeathInformation e) { if (sender is Monster monster) { this._monsters.TryRemove(monster, out _); monster.Died -= this.OnMonsterDied; } } }