baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
// <copyright file="BloodCastleGameServerState.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.PeriodicTasks;
/// <summary>
/// The state of a game server state for a blood castle event.
/// </summary>
public class BloodCastleGameServerState : PeriodicTaskGameServerState
{
/// <summary>
/// Initializes a new instance of the <see cref="BloodCastleGameServerState"/> class.
/// </summary>
/// <param name="context">The context.</param>
public BloodCastleGameServerState(IGameContext context)
: base(context)
{
}
/// <inheritdoc />
public override string Description => "Blood Castle";
}

View File

@@ -0,0 +1,24 @@
// <copyright file="BloodCastleStartConfiguration.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.PeriodicTasks;
/// <summary>
/// The blood castle start configuration.
/// </summary>
public class BloodCastleStartConfiguration : MiniGameStartConfiguration
{
/// <summary>
/// Gets the default configuration for blood castle.
/// </summary>
public static BloodCastleStartConfiguration Default =>
new()
{
PreStartMessageDelay = TimeSpan.Zero,
EntranceOpenedMessage = "Blood Castle entrance is open and closes in {0} minute(s).",
EntranceClosedMessage = "Blood Castle entrance closed.",
TaskDuration = TimeSpan.FromMinutes(20),
Timetable = GenerateTimeSequence(TimeSpan.FromMinutes(120)).ToList(),
};
}

View File

@@ -0,0 +1,33 @@
// <copyright file="BloodCastleStartPlugIn.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.PeriodicTasks;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.MiniGames;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This plugin enables the start of the blood castle.
/// </summary>
[PlugIn]
[Display(Name = nameof(BloodCastleStartPlugIn), Description = "Blood Castle event")]
[Guid("95E68C14-AD87-4B3C-AF46-45B8F1C3BC2A")]
public sealed class BloodCastleStartPlugIn : MiniGameStartBasePlugIn<BloodCastleStartConfiguration, BloodCastleGameServerState>
{
/// <inheritdoc />
public override MiniGameType Key => MiniGameType.BloodCastle;
/// <inheritdoc />
public override object CreateDefaultConfig()
{
return BloodCastleStartConfiguration.Default;
}
/// <inheritdoc />
protected override BloodCastleGameServerState CreateState(IGameContext gameContext)
{
return new BloodCastleGameServerState(gameContext);
}
}

View File

@@ -0,0 +1,23 @@
// <copyright file="ChaosCastleGameServerState.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.PeriodicTasks;
/// <summary>
/// The state of a game server state for a chaos castle event.
/// </summary>
public class ChaosCastleGameServerState : PeriodicTaskGameServerState
{
/// <summary>
/// Initializes a new instance of the <see cref="ChaosCastleGameServerState"/> class.
/// </summary>
/// <param name="context">The context.</param>
public ChaosCastleGameServerState(IGameContext context)
: base(context)
{
}
/// <inheritdoc />
public override string Description => "Chaos Castle";
}

View File

@@ -0,0 +1,24 @@
// <copyright file="ChaosCastleStartConfiguration.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.PeriodicTasks;
/// <summary>
/// The chaos castle start configuration.
/// </summary>
public class ChaosCastleStartConfiguration : MiniGameStartConfiguration
{
/// <summary>
/// Gets the default configuration for chaos castle.
/// </summary>
public static ChaosCastleStartConfiguration Default =>
new()
{
PreStartMessageDelay = TimeSpan.Zero,
EntranceOpenedMessage = "Chaos Castle entrance is open and closes in {0} minute(s).",
EntranceClosedMessage = "Chaos Castle entrance closed.",
TaskDuration = TimeSpan.FromMinutes(15),
Timetable = PeriodicTaskConfiguration.GenerateTimeSequence(TimeSpan.FromMinutes(60)).ToList(),
};
}

View File

@@ -0,0 +1,33 @@
// <copyright file="ChaosCastleStartPlugIn.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.PeriodicTasks;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.MiniGames;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This plugin enables the start of the chaos castle.
/// </summary>
[PlugIn]
[Display(Name = nameof(ChaosCastleStartPlugIn), Description = "Chaos Castle event")]
[Guid("3AD96A70-ED24-4979-80B8-169E461E548F")]
public sealed class ChaosCastleStartPlugIn : MiniGameStartBasePlugIn<ChaosCastleStartConfiguration, ChaosCastleGameServerState>
{
/// <inheritdoc />
public override MiniGameType Key => MiniGameType.ChaosCastle;
/// <inheritdoc />
public override object CreateDefaultConfig()
{
return ChaosCastleStartConfiguration.Default;
}
/// <inheritdoc />
protected override ChaosCastleGameServerState CreateState(IGameContext gameContext)
{
return new ChaosCastleGameServerState(gameContext);
}
}

View File

@@ -0,0 +1,23 @@
// <copyright file="DevilSquareGameServerState.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.PeriodicTasks;
/// <summary>
/// The state of a game server state for a devil square event.
/// </summary>
public class DevilSquareGameServerState : PeriodicTaskGameServerState
{
/// <summary>
/// Initializes a new instance of the <see cref="DevilSquareGameServerState"/> class.
/// </summary>
/// <param name="context">The context.</param>
public DevilSquareGameServerState(IGameContext context)
: base(context)
{
}
/// <inheritdoc />
public override string Description => "Devil Square";
}

View File

@@ -0,0 +1,24 @@
// <copyright file="DevilSquareStartConfiguration.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.PeriodicTasks;
/// <summary>
/// The devil square start configuration.
/// </summary>
public class DevilSquareStartConfiguration : MiniGameStartConfiguration
{
/// <summary>
/// Gets the default configuration for devil square.
/// </summary>
public static DevilSquareStartConfiguration Default =>
new()
{
PreStartMessageDelay = TimeSpan.Zero,
EntranceOpenedMessage = "Devil Square entrance is open and closes in {0} minute(s).",
EntranceClosedMessage = "Devil Square entrance closed.",
TaskDuration = TimeSpan.FromMinutes(25),
Timetable = PeriodicTaskConfiguration.GenerateTimeSequence(TimeSpan.FromMinutes(240)).ToList(),
};
}

View File

@@ -0,0 +1,35 @@
// <copyright file="DevilSquareStartPlugIn.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.PeriodicTasks;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.MiniGames;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This plugin enables the start of the devil square.
/// </summary>
[PlugIn]
[Display(Name = nameof(DevilSquareStartPlugIn), Description = "Devil Square event")]
[Guid("61C61A58-211E-4D6A-9EA1-D25E0C4A47C5")]
public sealed class DevilSquareStartPlugIn : MiniGameStartBasePlugIn<DevilSquareStartConfiguration, DevilSquareGameServerState>
{
/// <summary>
/// Gets the key under which the strategy is getting registered.
/// </summary>
public override MiniGameType Key => MiniGameType.DevilSquare;
/// <inheritdoc />
public override object CreateDefaultConfig()
{
return DevilSquareStartConfiguration.Default;
}
/// <inheritdoc />
protected override DevilSquareGameServerState CreateState(IGameContext gameContext)
{
return new DevilSquareGameServerState(gameContext);
}
}

View File

@@ -0,0 +1,32 @@
// <copyright file="HappyHourConfiguration.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.PeriodicTasks;
using System;
/// <summary>
/// The Happy Hour event configuration.
/// </summary>
public class HappyHourConfiguration : PeriodicTaskConfiguration
{
/// <summary>
/// Gets the default configuration.
/// </summary>
public static HappyHourConfiguration Default => new()
{
TaskDuration = TimeSpan.FromHours(1),
PreStartMessageDelay = TimeSpan.FromSeconds(0),
StartMessage = "Happy Hour event has been started!",
EndMessage = "Happy Hour event has ended!",
Timetable = GenerateTimeSequence(TimeSpan.FromHours(6), new TimeOnly(0, 5)).ToList(), // Every 6 hours,
ExperienceMultiplier = 1.5f,
};
/// <summary>
/// Gets or sets the experience multiplier.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.HappyHourConfiguration_ExperienceMultiplier_Name))]
public float ExperienceMultiplier { get; set; } = 1.5f;
}

View File

@@ -0,0 +1,135 @@
// <copyright file="HappyHourPlugIn.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.PeriodicTasks;
using System.Runtime.InteropServices;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This plugin enables Happy Hour feature.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.HappyHourPlugIn_Name), Description = nameof(PlugInResources.HappyHourPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("6542E452-9780-45B8-85AE-4036422E9A6E")]
public class HappyHourPlugIn : PeriodicTaskBasePlugIn<HappyHourConfiguration, PeriodicTaskGameServerState>, ISupportDefaultCustomConfiguration, IPlayerStateChangedPlugIn
{
/// <summary>
/// The happy hour extra multiplier which gets added to the players <see cref="Stats.ExperienceRate"/>
/// and <see cref="Stats.MasterExperienceRate"/>.
/// </summary>
private readonly SimpleElement _happyHourExtraMultiplier = new(1.0f, AggregateType.Multiplicate);
/// <inheritdoc />
public object CreateDefaultConfig() => HappyHourConfiguration.Default;
/// <inheritdoc />
public async ValueTask PlayerStateChangedAsync(Player player, State previousState, State currentState)
{
try
{
if (currentState.IsDisconnectedOrFinished())
{
player.Attributes?.RemoveElement(this._happyHourExtraMultiplier, Stats.ExperienceRate);
player.Attributes?.RemoveElement(this._happyHourExtraMultiplier, Stats.MasterExperienceRate);
}
if (previousState != PlayerState.CharacterSelection || currentState != PlayerState.EnteredWorld)
{
return;
}
player.Attributes?.AddElement(this._happyHourExtraMultiplier, Stats.ExperienceRate);
player.Attributes?.AddElement(this._happyHourExtraMultiplier, Stats.MasterExperienceRate);
var state = this.GetStateByGameContext(player.GameContext);
var isEnabled = state.State != PeriodicTaskState.NotStarted;
if (!isEnabled)
{
return;
}
await this.TrySendStartMessageAsync(player).ConfigureAwait(false);
}
catch (Exception ex)
{
player.Logger.LogDebug(ex, "Unexpected error handling player state change.");
}
}
/// <inheritdoc/>
protected override PeriodicTaskGameServerState CreateState(IGameContext gameContext)
{
return new PeriodicTaskGameServerState(gameContext);
}
/// <inheritdoc/>
protected override ValueTask OnPrepareEventAsync(PeriodicTaskGameServerState state)
{
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
protected override async ValueTask OnPreparedAsync(PeriodicTaskGameServerState state)
{
await state.Context.ForEachPlayerAsync(this.TrySendStartMessageAsync).ConfigureAwait(false);
}
/// <inheritdoc/>
protected override ValueTask OnStartedAsync(PeriodicTaskGameServerState state)
{
this._happyHourExtraMultiplier.Value = this.Configuration?.ExperienceMultiplier ?? 1;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
protected override ValueTask OnFinishedAsync(PeriodicTaskGameServerState state)
{
this._happyHourExtraMultiplier.Value = 1;
return ValueTask.CompletedTask;
}
/// <summary>
/// Returns true if the player stays on the map.
/// </summary>
/// <param name="player">The player.</param>
protected bool IsPlayerOnMap(Player player)
{
return player.CurrentMap is not null
&& !player.PlayerState.CurrentState.IsDisconnectedOrFinished();
}
/// <summary>
/// Send a golden message to player's client.
/// </summary>
/// <param name="player">The player.</param>
protected async Task TrySendStartMessageAsync(Player player)
{
var configuration = this.Configuration;
if (configuration is null)
{
return;
}
if (!this.IsPlayerOnMap(player))
{
return;
}
try
{
var message = configuration.StartMessage.GetTranslation(player.Culture) is { Length: > 0 } translation ? translation : player.GetLocalizedMessage(nameof(PlayerMessage.HappyHourEventHasBeenStarted));
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, Interfaces.MessageType.GoldenCenter)).ConfigureAwait(false);
}
catch (Exception ex)
{
player.Logger.LogDebug(ex, "Unexpected error sending start message.");
}
}
}

View File

@@ -0,0 +1,35 @@
// <copyright file="IPeriodicMiniGameStartPlugIn.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.PeriodicTasks;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.MiniGames;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Interface for a mini game start plugin.
/// </summary>
[PlugInPoint("Periodic Mini Game Start Plugins", "Plugin Container for mini game start plugins, e.g. for chaos castle.")]
[Guid("D88FFCCF-5C57-49E0-8E05-1C75A95A9AF2")]
public interface IPeriodicMiniGameStartPlugIn : IStrategyPlugIn<MiniGameType>, IPeriodicTaskPlugIn
{
/// <summary>
/// Gets the duration until next start.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <param name="miniGameDefinition">The mini game definition.</param>
/// <returns>
/// The duration until next start.
/// </returns>
ValueTask<TimeSpan?> GetDurationUntilNextStartAsync(IGameContext gameContext, MiniGameDefinition miniGameDefinition);
/// <summary>
/// Gets the mini game context, if available.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <param name="miniGameDefinition">The mini game definition.</param>
/// <returns>The mini game context, if available.</returns>
ValueTask<MiniGameContext?> GetMiniGameContextAsync(IGameContext gameContext, MiniGameDefinition miniGameDefinition);
}

View File

@@ -0,0 +1,131 @@
// <copyright file="MiniGameStartBasePlugIn.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.PeriodicTasks;
using MUnique.OpenMU.GameLogic.MiniGames;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This base plugin allows you to implement mini game starts.
/// </summary>
/// <typeparam name="TConfiguration">Implementation of the mini game configuration class.</typeparam>
/// <typeparam name="TGameState">Implementation of the mini game state class.</typeparam>
public abstract class MiniGameStartBasePlugIn<TConfiguration, TGameState> : PeriodicTaskBasePlugIn<TConfiguration, TGameState>, ISupportDefaultCustomConfiguration, IPeriodicMiniGameStartPlugIn
where TConfiguration : MiniGameStartConfiguration
where TGameState : PeriodicTaskGameServerState
{
/// <inheritdoc />
public abstract MiniGameType Key { get; }
/// <inheritdoc />
public abstract object CreateDefaultConfig();
/// <inheritdoc />
public async ValueTask<TimeSpan?> GetDurationUntilNextStartAsync(IGameContext gameContext, MiniGameDefinition miniGameDefinition)
{
var state = this.GetStateByGameContext(gameContext);
if (state.State == PeriodicTaskState.Prepared)
{
// That's not totally correct, but should be sufficient.
return this.Configuration?.PreStartMessageDelay;
}
if (state.State == PeriodicTaskState.Started
&& await state.Context.GetMiniGameAsync(miniGameDefinition, null!).ConfigureAwait(false) is { State: MiniGameState.Open })
{
return TimeSpan.Zero;
}
var timeNow = new TimeOnly(DateTime.UtcNow.TimeOfDay.Ticks);
var nextRun = this.Configuration?.Timetable.Where(time => time > timeNow).Order().FirstOrDefault();
return nextRun - timeNow;
}
/// <inheritdoc />
public async ValueTask<MiniGameContext?> GetMiniGameContextAsync(IGameContext gameContext, MiniGameDefinition miniGameDefinition)
{
var state = this.GetStateByGameContext(gameContext);
if (state.State == PeriodicTaskState.Started)
{
return await state.Context.GetMiniGameAsync(miniGameDefinition, null!).ConfigureAwait(false);
}
return null;
}
/// <inheritdoc />
protected override ValueTask OnPrepareEventAsync(TGameState state)
{
// nothing to do
return ValueTask.CompletedTask;
}
/// <inheritdoc />
protected override ValueTask OnPreparedAsync(TGameState state)
{
// We keep it simple and don't send a message here.
return ValueTask.CompletedTask;
}
/// <inheritdoc />
protected override async ValueTask OnStartedAsync(TGameState state)
{
var miniGameDefinitions = state.Context.Configuration.MiniGameDefinitions
.Where(d => d.Type == this.Key && d.MapCreationPolicy == MiniGameMapCreationPolicy.Shared);
var enterDuration = TimeSpan.Zero;
foreach (var miniGameDefinition in miniGameDefinitions)
{
// we're causing that the event context gets created.
_ = await state.Context.GetMiniGameAsync(miniGameDefinition, null!).ConfigureAwait(false);
enterDuration = miniGameDefinition.EnterDuration;
}
_ = this.SendOpenedNotificationsAsync(state, enterDuration);
}
/// <inheritdoc />
protected override ValueTask OnFinishedAsync(TGameState state)
{
// nothing to do; The mini game will clean up itself.
return ValueTask.CompletedTask;
}
private async Task SendOpenedNotificationsAsync(TGameState state, TimeSpan enterDuration)
{
try
{
if (enterDuration <= TimeSpan.Zero)
{
return;
}
for (var remainingMinutes = (int)enterDuration.TotalMinutes; remainingMinutes > 0; remainingMinutes--)
{
if (this.Configuration?.EntranceOpenedMessage is { } openMessage)
{
await state.Context.SendGlobalNotificationAsync(string.Format(openMessage, remainingMinutes)).ConfigureAwait(false);
}
await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
}
var remainingSeconds = TimeSpan.FromSeconds(enterDuration.Seconds);
if (remainingSeconds > TimeSpan.Zero)
{
await Task.Delay(remainingSeconds).ConfigureAwait(false);
}
if (this.Configuration?.EntranceClosedMessage is { } closedMessage)
{
await state.Context.SendGlobalNotificationAsync(closedMessage).ConfigureAwait(false);
}
}
catch (Exception ex)
{
state.Context.LoggerFactory.CreateLogger<MiniGameStartBasePlugIn<TConfiguration, TGameState>>().LogError(ex, "Error when sending the notifications");
}
}
}

View File

@@ -0,0 +1,25 @@
// <copyright file="MiniGameStartConfiguration.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.PeriodicTasks;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// The minimal minigame start configuration.
/// </summary>
public abstract class MiniGameStartConfiguration : PeriodicTaskConfiguration
{
/// <summary>
/// Gets or sets the entrance opened message.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.MiniGameStartConfiguration_EntranceOpenedMessage_Name))]
public LocalizedString EntranceOpenedMessage { get; set; }
/// <summary>
/// Gets or sets the entrance closed message.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.MiniGameStartConfiguration_EntranceClosedMessage_Name))]
public LocalizedString EntranceClosedMessage { get; set; }
}

View File

@@ -0,0 +1,193 @@
// <copyright file="PeriodicTaskBasePlugIn.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.PeriodicTasks;
using System.Collections.Concurrent;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Base class for periodic task plugins.
/// </summary>
/// <typeparam name="TConfiguration">Configuration type.</typeparam>
/// <typeparam name="TState">State type.</typeparam>
public abstract class PeriodicTaskBasePlugIn<TConfiguration, TState> : IPeriodicTaskPlugIn, ISupportCustomConfiguration<TConfiguration>
where TConfiguration : PeriodicTaskConfiguration
where TState : PeriodicTaskGameServerState
{
private static readonly ConcurrentDictionary<Type, ConcurrentDictionary<IGameContext, TState>> States = new();
private bool _isStartForced = false;
/// <summary>
/// Gets or sets configuration for periodic invasion.
/// </summary>
public TConfiguration? Configuration { get; set; }
/// <summary>
/// Forces to start the task on the next start check.
/// </summary>
public void ForceStart()
{
this._isStartForced = true;
}
/// <inheritdoc />
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.");
}
}
/// <summary>
/// Gets a value indicating whether if it's the right time to start the task.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <returns>
/// <c>true</c> if it's the right time to start the task; otherwise, <c>false</c>.
/// </returns>
protected virtual bool IsItTimeToStart(IGameContext gameContext)
{
return this._isStartForced || (this.Configuration?.IsItTimeToStart() ?? false);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="state">The current task state.</param>
/// <returns><c>true</c> if the previous event duration has not elapsed yet; otherwise <c>false</c>.</returns>
protected virtual bool IsPreviousEventStillRunning(TState state)
=> state.LastRunUtc != DateTime.MinValue && state.LastRunUtc.Add(this.Configuration?.TaskDuration ?? TimeSpan.Zero) > DateTime.UtcNow;
/// <summary>
/// Called when the task should be prepared before starting it.
/// </summary>
/// <param name="state">The state.</param>
protected abstract ValueTask OnPrepareEventAsync(TState state);
/// <summary>
/// Creates the state for the given context.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <returns>The created state object.</returns>
protected abstract TState CreateState(IGameContext gameContext);
/// <summary>
/// Get a unique state per GameContext.
/// </summary>
/// <param name="gameContext">GameContext.</param>
protected TState GetStateByGameContext(IGameContext gameContext)
{
var type = this.GetType();
var statesPerType = States.GetOrAdd(type, newType => new());
return statesPerType.GetOrAdd(gameContext, _ => this.CreateState(gameContext));
}
/// <summary>
/// Calls after the state changed to Prepared.
/// </summary>
/// <param name="state">The state.</param>
protected abstract ValueTask OnPreparedAsync(TState state);
/// <summary>
/// Calls after the state changed to Started.
/// </summary>
/// <param name="state">State.</param>
protected abstract ValueTask OnStartedAsync(TState state);
/// <summary>
/// Calls after the state changed to Finished.
/// </summary>
/// <param name="state">State.</param>
protected abstract ValueTask OnFinishedAsync(TState state);
}

View File

@@ -0,0 +1,79 @@
// <copyright file="PeriodicTaskConfiguration.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.PeriodicTasks;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Configuration for the <see cref="PeriodicTaskBasePlugIn{TConfiguration,TState}"/>.
/// </summary>
public class PeriodicTaskConfiguration
{
/// <summary>
/// Gets or sets a time delay.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.PeriodicTaskConfiguration_PreStartMessageDelay_Name), Order = 1)]
public TimeSpan PreStartMessageDelay { get; set; } = TimeSpan.FromSeconds(3);
/// <summary>
/// Gets or sets the minimum time of the task duration.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.PeriodicTaskConfiguration_TaskDuration_Name), Order = 2)]
public TimeSpan TaskDuration { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets or sets the text which prints as a golden message in the game when task starts.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.PeriodicTaskConfiguration_StartMessage_Name), Order = 3)]
public LocalizedString StartMessage { get; set; }
/// <summary>
/// Gets or sets the text which prints as a golden message in the game when task ends.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.PeriodicTaskConfiguration_EndMessage_Name), Order = 4)]
public LocalizedString EndMessage { get; set; }
/// <summary>
/// Gets or sets a timetable for the event.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.PeriodicTaskConfiguration_Timetable_Name), Order = 5)]
public IList<TimeOnly> Timetable { get; set; } = new List<TimeOnly>();
/// <summary>
/// Generate a sequence of time points like [00:00, 00:01, ...].
/// </summary>
/// <param name="duration">The duration.</param>
/// <param name="startLimit">The start limit, inclusive.</param>
/// <param name="endLimit">The end limit, exclusive.</param>
public static IEnumerable<TimeOnly> GenerateTimeSequence(TimeSpan duration, TimeOnly? startLimit = null, TimeOnly? endLimit = null)
{
var limit = endLimit?.ToTimeSpan() ?? TimeSpan.FromDays(1);
var current = startLimit?.ToTimeSpan() ?? TimeSpan.Zero;
while (current < limit)
{
yield return TimeOnly.FromTimeSpan(current);
current = current.Add(duration);
}
}
/// <summary>
/// Check if current time is OK for starting an invasion.
/// </summary>
/// <returns>Returns true if the invasion can be started.</returns>
public virtual bool IsItTimeToStart()
{
if (this.Timetable.Count == 0)
{
return false;
}
var nowTime = TimeOnly.FromDateTime(DateTime.UtcNow);
var earlier = nowTime.Add(TimeSpan.FromSeconds(-5));
// For example, p = 00:00. Check that time between 00:00:00 and 00:00:05
return this.Timetable.Any(p => p.IsBetween(earlier, nowTime));
}
}

View File

@@ -0,0 +1,46 @@
// <copyright file="PeriodicTaskGameServerState.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.PeriodicTasks;
/// <summary>
/// Game server state per event.
/// </summary>
public class PeriodicTaskGameServerState
{
/// <summary>
/// Initializes a new instance of the <see cref="PeriodicTaskGameServerState"/> class.
/// </summary>
/// <param name="context">The context.</param>
public PeriodicTaskGameServerState(IGameContext context)
{
this.Context = context;
}
/// <summary>
/// Gets the context to which this state belongs.
/// </summary>
public IGameContext Context { get; }
/// <summary>
/// Gets or sets the next run in UTC.
/// </summary>
public DateTime NextRunUtc { get; set; } = DateTime.UtcNow;
/// <summary>
/// Gets or sets the UTC timestamp of when the last event run started.
/// Used to prevent a new event from starting before the previous task duration has elapsed.
/// </summary>
public DateTime LastRunUtc { get; set; } = DateTime.MinValue;
/// <summary>
/// Gets or sets the state.
/// </summary>
public PeriodicTaskState State { get; set; } = PeriodicTaskState.NotStarted;
/// <summary>
/// Gets the description about the state.
/// </summary>
public virtual string Description => string.Empty;
}

View File

@@ -0,0 +1,26 @@
// <copyright file="PeriodicTaskState.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.PeriodicTasks;
/// <summary>
/// Describe the state of invasion.
/// </summary>
public enum PeriodicTaskState : byte
{
/// <summary>
/// Task not started.
/// </summary>
NotStarted,
/// <summary>
/// Task prepared.
/// </summary>
Prepared,
/// <summary>
/// Task started.
/// </summary>
Started,
}