//
// 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;
}
}
}