//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
using System.Collections.Concurrent;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.PlugIns;
///
/// Base class for periodic task plugins.
///
/// Configuration type.
/// State type.
public abstract class PeriodicTaskBasePlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration
where TConfiguration : PeriodicTaskConfiguration
where TState : PeriodicTaskGameServerState
{
private static readonly ConcurrentDictionary> States = new();
private bool _isStartForced = false;
///
/// Gets or sets configuration for periodic invasion.
///
public TConfiguration? Configuration { get; set; }
///
/// Forces to start the task on the next start check.
///
public void ForceStart()
{
this._isStartForced = true;
}
///
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
{
var logger = gameContext.LoggerFactory.CreateLogger(this.GetType().Name);
using var scope = logger.BeginScope(gameContext);
var state = this.GetStateByGameContext(gameContext);
if (state.NextRunUtc > DateTime.UtcNow)
{
return;
}
var configuration = this.Configuration;
if (configuration is null && this is ISupportDefaultCustomConfiguration defaultConfigSupporter)
{
logger.LogWarning("{description} ({gameContext}):configuration is not set. Using default configuration.", state.Description, gameContext);
this.Configuration = configuration = defaultConfigSupporter.CreateDefaultConfig() as TConfiguration;
}
if (configuration is null)
{
logger.LogError("{description} ({gameContext}):no configuration available; can't execute task plugin.", state.Description, gameContext);
return;
}
switch (state.State)
{
case PeriodicTaskState.NotStarted:
{
if (!this.IsItTimeToStart(gameContext))
{
return;
}
if (this.IsPreviousEventStillRunning(state))
{
this._isStartForced = false;
return;
}
this._isStartForced = false;
state.NextRunUtc = DateTime.UtcNow.Add(configuration.PreStartMessageDelay);
await this.OnPrepareEventAsync(state).ConfigureAwait(false);
state.State = PeriodicTaskState.Prepared;
await this.OnPreparedAsync(state).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(state.Description))
{
logger.LogDebug("{description} ({gameContext}): event prepared", state.Description, gameContext);
}
break;
}
case PeriodicTaskState.Prepared:
{
state.NextRunUtc = DateTime.UtcNow.Add(configuration.TaskDuration);
state.State = PeriodicTaskState.Started;
state.LastRunUtc = DateTime.UtcNow;
await this.OnStartedAsync(state).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(state.Description))
{
logger.LogDebug("{description} ({gameContext}): event started", state.Description, gameContext);
}
break;
}
case PeriodicTaskState.Started:
{
state.State = PeriodicTaskState.NotStarted;
await this.OnFinishedAsync(state).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(state.Description))
{
logger.LogDebug("{description} ({gameContext}): event finished", state.Description, gameContext);
}
break;
}
default:
throw new NotImplementedException("Unknown state.");
}
}
///
/// Gets a value indicating whether if it's the right time to start the task.
///
/// The game context.
///
/// true if it's the right time to start the task; otherwise, false.
///
protected virtual bool IsItTimeToStart(IGameContext gameContext)
{
return this._isStartForced || (this.Configuration?.IsItTimeToStart() ?? false);
}
///
/// Determines whether the previous event run is still within its configured task duration.
/// Prevents a new event from starting before the previous one has fully elapsed.
///
/// The current task state.
/// true if the previous event duration has not elapsed yet; otherwise false.
protected virtual bool IsPreviousEventStillRunning(TState state)
=> state.LastRunUtc != DateTime.MinValue && state.LastRunUtc.Add(this.Configuration?.TaskDuration ?? TimeSpan.Zero) > DateTime.UtcNow;
///
/// Called when the task should be prepared before starting it.
///
/// The state.
protected abstract ValueTask OnPrepareEventAsync(TState state);
///
/// Creates the state for the given context.
///
/// The game context.
/// The created state object.
protected abstract TState CreateState(IGameContext gameContext);
///
/// Get a unique state per GameContext.
///
/// GameContext.
protected TState GetStateByGameContext(IGameContext gameContext)
{
var type = this.GetType();
var statesPerType = States.GetOrAdd(type, newType => new());
return statesPerType.GetOrAdd(gameContext, _ => this.CreateState(gameContext));
}
///
/// Calls after the state changed to Prepared.
///
/// The state.
protected abstract ValueTask OnPreparedAsync(TState state);
///
/// Calls after the state changed to Started.
///
/// State.
protected abstract ValueTask OnStartedAsync(TState state);
///
/// Calls after the state changed to Finished.
///
/// State.
protected abstract ValueTask OnFinishedAsync(TState state);
}