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