559 lines
22 KiB
C#
559 lines
22 KiB
C#
// <copyright file="GameContext.cs" company="MUnique">
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
// </copyright>
|
|
|
|
namespace MUnique.OpenMU.GameLogic;
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.Metrics;
|
|
using System.Globalization;
|
|
using System.Threading;
|
|
using MUnique.OpenMU.GameLogic.MiniGames;
|
|
using MUnique.OpenMU.GameLogic.PlugIns;
|
|
using MUnique.OpenMU.GameLogic.Views;
|
|
using MUnique.OpenMU.Interfaces;
|
|
using MUnique.OpenMU.Pathfinding;
|
|
using MUnique.OpenMU.Persistence;
|
|
using MUnique.OpenMU.PlugIns;
|
|
using Nito.AsyncEx;
|
|
using org.mariuszgromada.math.mxparser;
|
|
|
|
/// <summary>
|
|
/// The game context which holds all data of the game together.
|
|
/// </summary>
|
|
public class GameContext : AsyncDisposable, IGameContext
|
|
{
|
|
private const string DefaultExperienceFormula = "if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))";
|
|
private const string DefaultMasterExperienceFormula = "(505 * level * level * level) + (35278500 * level) + (228045 * level * level)";
|
|
|
|
private static readonly Meter Meter = new(MeterName);
|
|
|
|
private static readonly Counter<int> PlayerCounter = Meter.CreateCounter<int>("PlayerCount");
|
|
|
|
private static readonly Counter<int> MapCounter = Meter.CreateCounter<int>("MapCount");
|
|
|
|
private static readonly Counter<int> MiniGameCounter = Meter.CreateCounter<int>("MiniGameCount");
|
|
|
|
private static readonly IObjectPool<PathFinder> PathFinderPoolInstance = new LimitedObjectPool<PathFinder>(new PathFinderPoolingPolicy());
|
|
|
|
private readonly Dictionary<ushort, GameMap> _mapList = new();
|
|
|
|
private readonly Dictionary<MiniGameMapKey, MiniGameContext> _miniGames = new();
|
|
|
|
private readonly Timer _recoverTimer;
|
|
|
|
private readonly IMapInitializer _mapInitializer;
|
|
|
|
private readonly AsyncLock _mapInitializerLock = new();
|
|
|
|
private readonly Timer _tasksTimer;
|
|
|
|
private readonly AsyncReaderWriterLock _playerListLock = new();
|
|
|
|
/// <summary>
|
|
/// Keeps the list of all players.
|
|
/// </summary>
|
|
private readonly List<Player> _playerList = new();
|
|
|
|
private readonly IDisposable _configChangeHandlerRegistration;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GameContext" /> class.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="persistenceContextProvider">The persistence context provider.</param>
|
|
/// <param name="mapInitializer">The map initializer.</param>
|
|
/// <param name="loggerFactory">The logger factory.</param>
|
|
/// <param name="plugInManager">The plug in manager.</param>
|
|
/// <param name="dropGenerator">The drop generator.</param>
|
|
/// <param name="changeMediator">The cange mediator.</param>
|
|
public GameContext(GameConfiguration configuration, IPersistenceContextProvider persistenceContextProvider, IMapInitializer mapInitializer, ILoggerFactory loggerFactory, PlugInManager plugInManager, IDropGenerator dropGenerator, IConfigurationChangeMediator changeMediator)
|
|
{
|
|
try
|
|
{
|
|
this.Configuration = configuration;
|
|
this.PersistenceContextProvider = persistenceContextProvider;
|
|
this.PlugInManager = plugInManager;
|
|
this._mapInitializer = mapInitializer;
|
|
this.LoggerFactory = loggerFactory;
|
|
this.DropGenerator = dropGenerator;
|
|
this.ConfigurationChangeMediator = changeMediator;
|
|
this.ItemPowerUpFactory = new ItemPowerUpFactory(loggerFactory.CreateLogger<ItemPowerUpFactory>());
|
|
this.PartyManager = new PartyManager(configuration.MaximumPartySize, loggerFactory.CreateLogger<Party>());
|
|
this._recoverTimer = new Timer(this.RecoverTimerElapsed, null, this.Configuration.RecoveryInterval, this.Configuration.RecoveryInterval);
|
|
this._tasksTimer = new Timer(this.ExecutePeriodicTasks, null, 1000, 1000);
|
|
this.FeaturePlugIns = new FeaturePlugInContainer(this.PlugInManager);
|
|
this._configChangeHandlerRegistration = this.ConfigurationChangeMediator.RegisterObject(this.Configuration, this, this.OnGameConfigurationChangeAsync);
|
|
this.DuelRoomManager = new DuelRoomManager(this.Configuration.DuelConfiguration!);
|
|
this.ExperienceTable = CreateExpTable(this.Configuration.ExperienceFormula ?? DefaultExperienceFormula, this.Configuration.MaximumLevel);
|
|
this.MasterExperienceTable = CreateExpTable(this.Configuration.MasterExperienceFormula ?? DefaultMasterExperienceFormula, this.Configuration.MaximumMasterLevel);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
loggerFactory.CreateLogger<GameContext>().LogError(ex, "Unexpected error in constructor of GameContext.");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when a game map got created.
|
|
/// </summary>
|
|
public event EventHandler<GameMap>? GameMapCreated;
|
|
|
|
/// <summary>
|
|
/// Occurs when a game map got removed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Currently, maps are never removed.
|
|
/// It may make sense to remove unused maps after a certain period.
|
|
/// </remarks>
|
|
public event EventHandler<GameMap>? GameMapRemoved;
|
|
|
|
/// <inheritdoc />
|
|
public virtual float ExperienceRate => this.Configuration.ExperienceRate;
|
|
|
|
/// <inheritdoc />
|
|
public virtual float MasterExperienceRate => this.Configuration.MasterExperienceRate;
|
|
|
|
// ADAMU-CUSTOM: expose the map initializer so the Castle Siege can spawn guardian statues (Destructibles) at runtime.
|
|
/// <summary>Gets the map initializer (used to spawn NPCs/destructibles at runtime, e.g. Castle Siege statues).</summary>
|
|
public IMapInitializer MapInitializer => this._mapInitializer;
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool PvpEnabled { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public GameConfiguration Configuration { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public long[] ExperienceTable { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public long[] MasterExperienceTable { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public IConfigurationChangeMediator ConfigurationChangeMediator { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public PlugInManager PlugInManager { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public IDropGenerator DropGenerator { get; }
|
|
|
|
/// <inheritdoc />
|
|
public FeaturePlugInContainer FeaturePlugIns { get; }
|
|
|
|
/// <inheritdoc />
|
|
public Offline.OfflinePlayerManager OfflinePlayerManager { get; } = new();
|
|
|
|
/// <inheritdoc/>
|
|
public IItemPowerUpFactory ItemPowerUpFactory { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public IPersistenceContextProvider PersistenceContextProvider { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the players by character name dictionary.
|
|
/// </summary>
|
|
public ConcurrentDictionary<string, Player> PlayersByCharacterName { get; } = new ConcurrentDictionary<string, Player>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <inheritdoc />
|
|
public DuelRoomManager DuelRoomManager { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public IPartyManager PartyManager { get; }
|
|
|
|
/// <inheritdoc />
|
|
public ILoggerFactory LoggerFactory { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the path finder pool.
|
|
/// </summary>
|
|
public IObjectPool<PathFinder> PathFinderPool => PathFinderPoolInstance;
|
|
|
|
/// <inheritdoc />
|
|
public int PlayerCount => this._playerList.Count;
|
|
|
|
/// <summary>
|
|
/// Gets the name of the meter of this class.
|
|
/// </summary>
|
|
internal static string MeterName => typeof(GameContext).FullName ?? nameof(GameContext);
|
|
|
|
/// <summary>
|
|
/// Gets the initialized maps which are hosted on this context.
|
|
/// </summary>
|
|
public async ValueTask<IEnumerable<GameMap>> GetMapsAsync()
|
|
{
|
|
using var l = await this._mapInitializerLock.LockAsync();
|
|
return this._mapList.Values.Concat(this._miniGames.Values.Select(g => g.Map)).ToList();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<GameMap?> GetMapAsync(ushort mapId, bool createIfNotExists = true)
|
|
{
|
|
if (this._mapList.TryGetValue(mapId, out var map))
|
|
{
|
|
return map;
|
|
}
|
|
|
|
if (!createIfNotExists)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
GameMap? createdMap;
|
|
using (await this._mapInitializerLock.LockAsync())
|
|
{
|
|
if (this._mapList.TryGetValue(mapId, out map))
|
|
{
|
|
return map;
|
|
}
|
|
|
|
createdMap = this._mapInitializer.CreateGameMap(mapId);
|
|
if (createdMap is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
this._mapList.Add(mapId, createdMap);
|
|
createdMap.ObjectAdded += async args =>
|
|
{
|
|
if (this.PlugInManager.GetPlugInPoint<IObjectAddedToMapPlugIn>() is { } plugInPoint)
|
|
{
|
|
await plugInPoint.ObjectAddedToMapAsync(args.Map, args.Object).ConfigureAwait(false);
|
|
}
|
|
};
|
|
createdMap.ObjectRemoved += async args =>
|
|
{
|
|
if (this.PlugInManager.GetPlugInPoint<IObjectRemovedFromMapPlugIn>() is { } plugInPoint)
|
|
{
|
|
await plugInPoint.ObjectRemovedFromMapAsync(args.Map, args.Object).ConfigureAwait(false);
|
|
}
|
|
};
|
|
}
|
|
|
|
// ReSharper disable once InconsistentlySynchronizedField it's desired behavior to initialize the map outside the lock to keep locked timespan short.
|
|
await this._mapInitializer.InitializeStateAsync(createdMap).ConfigureAwait(false);
|
|
this.GameMapCreated?.Invoke(this, createdMap);
|
|
MapCounter.Add(1);
|
|
|
|
return createdMap;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the mini game map which is meant to be hosted by the game.
|
|
/// </summary>
|
|
/// <param name="miniGameDefinition">The mini game definition.</param>
|
|
/// <param name="requester">The requesting player.</param>
|
|
/// <returns>The hosted mini game instance.</returns>
|
|
public async ValueTask<MiniGameContext> GetMiniGameAsync(MiniGameDefinition miniGameDefinition, Player requester)
|
|
{
|
|
var miniGameKey = MiniGameMapKey.Create(miniGameDefinition, requester);
|
|
|
|
if (this._miniGames.TryGetValue(miniGameKey, out var miniGameContext) && miniGameContext is { IsDisposed: false, IsDisposing: false })
|
|
{
|
|
return miniGameContext;
|
|
}
|
|
|
|
using (await this._mapInitializerLock.LockAsync().ConfigureAwait(false))
|
|
{
|
|
if (this._miniGames.TryGetValue(miniGameKey, out miniGameContext))
|
|
{
|
|
if (miniGameContext.IsDisposed)
|
|
{
|
|
this._miniGames.Remove(miniGameKey);
|
|
}
|
|
else
|
|
{
|
|
return miniGameContext;
|
|
}
|
|
}
|
|
|
|
switch (miniGameDefinition.Type)
|
|
{
|
|
case MiniGameType.ChaosCastle:
|
|
miniGameContext = new ChaosCastleContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
|
break;
|
|
case MiniGameType.DevilSquare:
|
|
miniGameContext = new DevilSquareContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
|
break;
|
|
case MiniGameType.BloodCastle:
|
|
miniGameContext = new BloodCastleContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
|
break;
|
|
case MiniGameType.HeykelSavasi:
|
|
miniGameContext = new HeykelSavasiContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
|
break;
|
|
default:
|
|
miniGameContext = new MiniGameContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
|
break;
|
|
}
|
|
|
|
this._miniGames.Add(miniGameKey, miniGameContext);
|
|
}
|
|
|
|
var createdMap = miniGameContext.Map;
|
|
|
|
// ReSharper disable once InconsistentlySynchronizedField it's desired behavior to initialize the map outside the lock to keep locked timespan short.
|
|
await this._mapInitializer.InitializeStateAsync(createdMap).ConfigureAwait(false);
|
|
this.GameMapCreated?.Invoke(this, createdMap);
|
|
MiniGameCounter.Add(1);
|
|
return miniGameContext;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the currently open <see cref="MiniGames.HeykelSavasiContext"/> instance, if any.
|
|
/// </summary>
|
|
/// <returns>The open Heykel Savasi context, or <see langword="null" /> if none is currently open.</returns>
|
|
/// <remarks>
|
|
/// ADAMU-CUSTOM: used by <see cref="PlugIns.HeykelSavasiNpcPlugin"/> to look up the event context
|
|
/// when a player talks to NPC 560. Mutations of <see cref="_miniGames"/> (<see cref="GetMiniGameAsync"/>,
|
|
/// <see cref="RemoveMiniGameAsync"/>) are synchronized via <see cref="_mapInitializerLock"/>, so this
|
|
/// read takes the same lock and snapshots the values before searching.
|
|
/// </remarks>
|
|
public async ValueTask<MiniGames.HeykelSavasiContext?> GetOpenHeykelSavasiAsync()
|
|
{
|
|
using var l = await this._mapInitializerLock.LockAsync().ConfigureAwait(false);
|
|
return this._miniGames.Values.OfType<MiniGames.HeykelSavasiContext>()
|
|
.FirstOrDefault(g => g.State == MiniGameState.Open);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask RemoveMiniGameAsync(MiniGameContext miniGameContext)
|
|
{
|
|
using var l = await this._mapInitializerLock.LockAsync().ConfigureAwait(false);
|
|
MiniGameCounter.Add(-1);
|
|
miniGameContext.Dispose();
|
|
this._miniGames.Remove(miniGameContext.Key);
|
|
this.GameMapRemoved?.Invoke(this, miniGameContext.Map);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the player to the game.
|
|
/// </summary>
|
|
/// <param name="player">The player.</param>
|
|
public virtual async ValueTask AddPlayerAsync(Player player)
|
|
{
|
|
player.PlayerLeftWorld += this.PlayerLeftWorldAsync;
|
|
player.PlayerEnteredWorld += this.PlayerEnteredWorldAsync;
|
|
player.PlayerDisconnected += this.RemovePlayerAsync;
|
|
|
|
using (await this._playerListLock.WriterLockAsync())
|
|
{
|
|
this._playerList.Add(player);
|
|
}
|
|
|
|
PlayerCounter.Add(1);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<IList<Player>> GetPlayersAsync()
|
|
{
|
|
using var l = await this._playerListLock.ReaderLockAsync();
|
|
if (this._playerList.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return this._playerList.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the player from the game.
|
|
/// </summary>
|
|
/// <param name="player">The player.</param>
|
|
public virtual async ValueTask RemovePlayerAsync(Player player)
|
|
{
|
|
bool removed;
|
|
using (await this._playerListLock.WriterLockAsync())
|
|
{
|
|
removed = this._playerList.Remove(player);
|
|
}
|
|
|
|
if (!removed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PlayerCounter.Add(-1);
|
|
if (player.SelectedCharacter != null)
|
|
{
|
|
this.PlayersByCharacterName.TryRemove(player.SelectedCharacter.Name, out _);
|
|
}
|
|
|
|
player.CurrentMap?.RemoveAsync(player);
|
|
|
|
player.PlayerDisconnected -= this.RemovePlayerAsync;
|
|
player.PlayerEnteredWorld -= this.PlayerEnteredWorldAsync;
|
|
player.PlayerLeftWorld -= this.PlayerLeftWorldAsync;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the player by the character name.
|
|
/// </summary>
|
|
/// <param name="name">The character name.</param>
|
|
/// <returns>The player by character name.</returns>
|
|
public Player? GetPlayerByCharacterName(string name)
|
|
{
|
|
this.PlayersByCharacterName.TryGetValue(name, out var player);
|
|
return player;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask ForEachPlayerAsync(Func<Player, Task> action)
|
|
{
|
|
if (this._playerList.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var playerList = await this.GetPlayersAsync().ConfigureAwait(false);
|
|
await playerList.Select(action).WhenAll().ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes the specified action for each player, grouped by their culture.
|
|
/// </summary>
|
|
/// <param name="stateFactory">The state factory which creates a state for each culture group.</param>
|
|
/// <param name="action">The action to execute for each player and culture state.</param>
|
|
/// <typeparam name="TCultureState">The type of the culture state.</typeparam>
|
|
public async ValueTask ForEachPlayerGroupedByCultureAsync<TCultureState>(Func<CultureInfo, TCultureState> stateFactory, Func<Player, TCultureState, Task> action)
|
|
{
|
|
if (this._playerList.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var playerList = await this.GetPlayersAsync().ConfigureAwait(false);
|
|
await playerList
|
|
.GroupBy(p => p.Culture)
|
|
.SelectMany(g =>
|
|
{
|
|
var state = stateFactory(g.Key);
|
|
return g.Select(player => action(player, state));
|
|
})
|
|
.WhenAll().ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask ShowGlobalLocalizedMessageAsync(MessageType messageType, string messageKey, params object?[] formatArguments)
|
|
{
|
|
await this.ForEachPlayerGroupedByCultureAsync<string>(
|
|
cultureInfo =>
|
|
{
|
|
if (formatArguments.Length > 0)
|
|
{
|
|
return string.Format(PlayerMessage.ResourceManager.GetString(messageKey, cultureInfo) ?? string.Empty, formatArguments);
|
|
}
|
|
|
|
return PlayerMessage.ResourceManager.GetString(messageKey, cultureInfo) ?? string.Empty;
|
|
},
|
|
(player, message) => player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, messageType)).AsTask())
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask SendGlobalMessageAsync(string message, MessageType messageType)
|
|
{
|
|
await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, messageType)).AsTask()).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask SendGlobalChatMessageAsync(string sender, string message, ChatMessageType messageType)
|
|
{
|
|
await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync<IChatViewPlugIn>(p => p.ChatMessageAsync(message, sender, messageType)).AsTask()).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask SendGlobalNotificationAsync(string message)
|
|
{
|
|
var sendingMessage = message.TrimStart('!');
|
|
await this.SendGlobalMessageAsync(sendingMessage, MessageType.GoldenCenter).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override async ValueTask DisposeAsyncCore()
|
|
{
|
|
this._configChangeHandlerRegistration.Dispose();
|
|
await this._recoverTimer.DisposeAsync().ConfigureAwait(false);
|
|
await this._tasksTimer.DisposeAsync().ConfigureAwait(false);
|
|
await base.DisposeAsyncCore().ConfigureAwait(false);
|
|
}
|
|
|
|
private static long[] CreateExpTable(string experienceFormula, short maximumLevel)
|
|
{
|
|
var argument = new Argument("level");
|
|
var expression = new Expression(experienceFormula);
|
|
expression.addArguments(argument);
|
|
|
|
return Enumerable.Range(0, maximumLevel + 2)
|
|
.Select(level =>
|
|
{
|
|
argument.setArgumentValue(level);
|
|
return (long)expression.calculate();
|
|
})
|
|
.ToArray();
|
|
}
|
|
|
|
#pragma warning disable CS1998
|
|
private async ValueTask OnGameConfigurationChangeAsync(Action unregisterAction, GameConfiguration gameConfiguration, GameContext context)
|
|
#pragma warning restore CS1998
|
|
{
|
|
this._recoverTimer.Change(gameConfiguration.RecoveryInterval, gameConfiguration.RecoveryInterval);
|
|
this.ExperienceTable = CreateExpTable(gameConfiguration.ExperienceFormula ?? DefaultExperienceFormula, gameConfiguration.MaximumLevel);
|
|
this.MasterExperienceTable = CreateExpTable(gameConfiguration.MasterExperienceFormula ?? DefaultMasterExperienceFormula, gameConfiguration.MaximumMasterLevel);
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
|
private async void ExecutePeriodicTasks(object? state)
|
|
{
|
|
try
|
|
{
|
|
if (this.PlugInManager.GetPlugInPoint<IPeriodicTaskPlugIn>() is { } plugInPoint)
|
|
{
|
|
await plugInPoint.ExecuteTaskAsync(this).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.Fail(ex.Message, ex.StackTrace);
|
|
}
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
|
private async void RecoverTimerElapsed(object? state)
|
|
{
|
|
try
|
|
{
|
|
await this.ForEachPlayerAsync(player =>
|
|
{
|
|
if (player.SelectedCharacter != null && !player.PlayerState.CurrentState.IsDisconnectedOrFinished())
|
|
{
|
|
return player.RegenerateAsync();
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// This should never happen as we already handle Exceptions in player.RegenerateAsync.
|
|
// However, if the player disconnects in the meantime, it could happen :-).
|
|
}
|
|
}
|
|
|
|
private ValueTask PlayerEnteredWorldAsync(Player player)
|
|
{
|
|
this.PlayersByCharacterName.TryAdd(player.SelectedCharacter!.Name, player);
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
private ValueTask PlayerLeftWorldAsync(Player player)
|
|
{
|
|
this.PlayersByCharacterName.TryRemove(player.SelectedCharacter!.Name, out _);
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
} |