Files
AdamuSw/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs
2026-07-14 19:00:35 +03:00

142 lines
4.9 KiB
C#

// <copyright file="InvasionGameServerState.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
using System.Collections.Concurrent;
using System.Linq;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
/// <summary>
/// Invasion state that is created for every periodic invasion run.
/// </summary>
public class InvasionGameServerState : PeriodicTaskGameServerState
{
private readonly HashSet<ushort> _mapIds = [];
private readonly Dictionary<ushort, ushort> _selectedMaps = [];
private readonly List<ushort> _announcedMapIds = [];
private readonly ConcurrentDictionary<Monster, byte> _monsters = new();
/// <summary>
/// Initializes a new instance of the <see cref="InvasionGameServerState"/> class.
/// </summary>
/// <param name="context">The game context.</param>
public InvasionGameServerState(IGameContext context)
: base(context)
{
}
/// <summary>
/// Gets or sets the map identifier used for UI display / map-event state broadcasts.
/// <c>null</c> means no event is active or the display map has not been selected yet.
/// </summary>
public ushort? MapId { get; set; }
/// <summary>
/// Gets the set of map identifiers on which monsters will spawn this run.
/// </summary>
public IReadOnlySet<ushort> MapIds => this._mapIds;
/// <summary>
/// Gets the read-only mapping of monster ID to selected map identifier.
/// Populated for spawns whose <see cref="SpawnMapStrategy"/> is <see cref="SpawnMapStrategy.RandomMap"/>.
/// </summary>
public IReadOnlyDictionary<ushort, ushort> SelectedMaps => this._selectedMaps;
/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<ushort> AnnouncedMapIds => this._announcedMapIds;
/// <summary>
/// Sets the maps named in the broadcast message and the single <see cref="MapId"/> used
/// for the map-event UI state.
/// </summary>
/// <param name="mapIds">The maps on which the announced monster spawns this run.</param>
internal void SetAnnouncedMaps(IReadOnlyCollection<ushort> mapIds)
{
this._announcedMapIds.Clear();
this._announcedMapIds.AddRange(mapIds);
this.MapId = this._announcedMapIds.Count > 0 ? this._announcedMapIds[0] : null;
}
/// <summary>
/// Registers a map as active for this run and optionally records which map was
/// randomly chosen for a particular monster type.
/// </summary>
/// <param name="mapId">The map identifier to register.</param>
/// <param name="monsterId">
/// When provided, records the <paramref name="mapId"/> as the chosen map for this monster.
/// Pass <c>null</c> for "spawn-on-all-maps" entries.
/// </param>
internal void RegisterMap(ushort mapId, ushort? monsterId = null)
{
this._mapIds.Add(mapId);
if (monsterId.HasValue)
{
this._selectedMaps[monsterId.Value] = mapId;
}
}
/// <summary>
/// Clears all state accumulated from a previous run so the object can be reused.
/// </summary>
internal void Reset()
{
this.MapId = null;
this._mapIds.Clear();
this._selectedMaps.Clear();
this._announcedMapIds.Clear();
}
/// <summary>
/// Tracks a monster spawned by this invasion and handles its cleanup on death.
/// </summary>
/// <param name="monster">The monster to track.</param>
internal void AddMonster(Monster monster)
{
if (this._monsters.TryAdd(monster, 0))
{
monster.Died += this.OnMonsterDied;
}
}
/// <summary>
/// Despawns and disposes all active monsters tracked by this invasion state.
/// </summary>
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;
}
}
}