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,45 @@
// <copyright file="MerchantSpawnState.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.WanderingMerchants;
using MUnique.OpenMU.GameLogic.NPC;
/// <summary>
/// Spawn state of a wandering merchant.
/// </summary>
public class MerchantSpawnState
{
/// <summary>
/// Initializes a new instance of the <see cref="MerchantSpawnState"/> class.
/// </summary>
/// <param name="merchantDefinition">The merchant definition.</param>
/// <param name="possibleSpawns">The possible spawns.</param>
public MerchantSpawnState(MonsterDefinition merchantDefinition, List<MonsterSpawnArea> possibleSpawns)
{
this.MerchantDefinition = merchantDefinition;
this.PossibleSpawns = possibleSpawns;
this.NextWanderingAt = DateTime.UtcNow.AddSeconds(10);
}
/// <summary>
/// Gets or sets the merchant definition.
/// </summary>
public MonsterDefinition MerchantDefinition { get; set; }
/// <summary>
/// Gets the possible spawn points of the merchant.
/// </summary>
public List<MonsterSpawnArea> PossibleSpawns { get; }
/// <summary>
/// Gets or sets the spawned merchant.
/// </summary>
public NonPlayerCharacter? Merchant { get; set; }
/// <summary>
/// Gets or sets time when the next wandering should occur.
/// </summary>
public DateTime NextWanderingAt { get; set; }
}

View File

@@ -0,0 +1,25 @@
// <copyright file="WanderingMerchantsConfiguration.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.WanderingMerchants;
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
/// <summary>
/// Configuration of wandering merchants.
/// </summary>
public class WanderingMerchantsConfiguration : PeriodicTaskConfiguration
{
/// <summary>
/// Gets or sets the minimum duration of the spawn.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.WanderingMerchantsConfiguration_MinimumSpawnDuration_Name))]
public TimeSpan MinimumSpawnDuration { get; set; } = TimeSpan.FromMinutes(60);
/// <summary>
/// Gets or sets the maximum duration of the spawn.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.WanderingMerchantsConfiguration_MaximumSpawnDuration_Name))]
public TimeSpan MaximumSpawnDuration { get; set; } = TimeSpan.FromMinutes(180);
}

View File

@@ -0,0 +1,137 @@
// <copyright file="WanderingMerchantsPlugIn.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.WanderingMerchants;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This plugins spawns and moves the wandering merchants.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.WanderingMerchantsPlugIn_Name), Description = nameof(PlugInResources.WanderingMerchantsPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("8B2CD316-C4B0-452F-8C7D-CE696356D437")]
public class WanderingMerchantsPlugIn : PeriodicTaskBasePlugIn<WanderingMerchantsConfiguration, WanderingMerchantsState>, ISupportDefaultCustomConfiguration
{
/// <inheritdoc />
public object CreateDefaultConfig()
{
return new WanderingMerchantsConfiguration
{
PreStartMessageDelay = TimeSpan.Zero,
// we check every minute if we have to move a merchant.
TaskDuration = TimeSpan.FromMinutes(1),
MinimumSpawnDuration = TimeSpan.FromMinutes(60),
MaximumSpawnDuration = TimeSpan.FromMinutes(180),
};
}
/// <inheritdoc />
protected override bool IsItTimeToStart(IGameContext gameContext)
{
var state = this.GetStateByGameContext(gameContext);
if (!state.Merchants.Any())
{
return false;
}
var minNext = state.Merchants.Min(m => m.NextWanderingAt);
return DateTime.UtcNow >= minNext;
}
/// <inheritdoc />
protected override async ValueTask OnStartedAsync(WanderingMerchantsState state)
{
var wanderingNow = state.Merchants.Where(m => m.NextWanderingAt <= DateTime.UtcNow).ToList();
foreach (var merchantState in wanderingNow)
{
await this.HandleMerchantAsync(state, merchantState).ConfigureAwait(false);
}
}
/// <inheritdoc />
protected override WanderingMerchantsState CreateState(IGameContext gameContext)
{
return new WanderingMerchantsState(gameContext);
}
/// <inheritdoc />
protected override ValueTask OnFinishedAsync(WanderingMerchantsState state)
{
return default;
}
/// <inheritdoc />
protected override ValueTask OnPrepareEventAsync(WanderingMerchantsState state)
{
return default;
}
/// <inheritdoc />
protected override ValueTask OnPreparedAsync(WanderingMerchantsState state)
{
return default;
}
private async Task HandleMerchantAsync(WanderingMerchantsState state, MerchantSpawnState merchantState)
{
var logger = state.Context.LoggerFactory.CreateLogger(this.GetType().Name);
using var scope = logger.BeginScope(state.Context);
var oldMerchant = merchantState.Merchant;
var nextSpawn = merchantState.PossibleSpawns.Count == 1
? merchantState.PossibleSpawns.First()
: merchantState.PossibleSpawns.Where(s => s != merchantState.Merchant?.SpawnArea).SelectRandom()!;
if (oldMerchant?.SpawnArea == nextSpawn)
{
logger.LogDebug("Same spawn area for merchant {merchant}.", oldMerchant);
return;
}
if (nextSpawn.GameMap is not { } nextMapDefinition)
{
logger.LogWarning("Spawn area {spawnArea} has no map defined.", nextSpawn);
return;
}
var nextMap = await state.Context.GetMapAsync((ushort)nextMapDefinition.Number).ConfigureAwait(false);
if (nextMap is null)
{
logger.LogWarning("Could not create map {map} for next wandering.", nextMapDefinition);
return;
}
await this.RemoveOldMerchantIfNeededAsync(oldMerchant, logger, merchantState).ConfigureAwait(false);
await this.SpawnMerchantAsync(nextSpawn, merchantState, nextMap, logger).ConfigureAwait(false);
}
private async Task RemoveOldMerchantIfNeededAsync(NonPlayerCharacter? merchant, ILogger logger, MerchantSpawnState merchantState)
{
if (merchant is null)
{
return;
}
var oldMap = merchant.CurrentMap;
await oldMap.RemoveAsync(merchant).ConfigureAwait(false);
await merchant.DisposeAsync().ConfigureAwait(false);
logger.LogDebug("Old merchant {merchant} has been removed from {map}.", merchant, oldMap.Definition);
merchantState.Merchant = null;
}
private async Task SpawnMerchantAsync(MonsterSpawnArea nextSpawn, MerchantSpawnState merchantState, GameMap nextMap, ILogger logger)
{
var newMerchant = new NonPlayerCharacter(nextSpawn, merchantState.MerchantDefinition, nextMap);
merchantState.Merchant = newMerchant;
newMerchant.Initialize();
await nextMap.AddAsync(newMerchant).ConfigureAwait(false);
merchantState.NextWanderingAt = DateTime.UtcNow.AddMinutes(Rand.NextInt((int)this.Configuration!.MinimumSpawnDuration.TotalMinutes, (int)this.Configuration!.MaximumSpawnDuration.TotalMinutes));
logger.LogDebug("New merchant {merchant} has been created on {map}. Next wandering at {next}", newMerchant, nextMap.Definition, merchantState.NextWanderingAt);
}
}

View File

@@ -0,0 +1,38 @@
// <copyright file="WanderingMerchantsState.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.WanderingMerchants;
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
/// <summary>
/// Keeps track of the state of the wandering merchants of a game server.
/// </summary>
public class WanderingMerchantsState : PeriodicTaskGameServerState
{
/// <summary>
/// Initializes a new instance of the <see cref="WanderingMerchantsState"/> class.
/// </summary>
/// <param name="context">The context.</param>
public WanderingMerchantsState(IGameContext context)
: base(context)
{
var groupedMerchants = context.Configuration.Maps
.SelectMany(m => m.MonsterSpawns.Where(s => s.SpawnTrigger == SpawnTrigger.Wandering))
.GroupBy(s => s.MonsterDefinition)
.Where(s => s.Key is not null)
.Select(g => (g.Key, g.ToList()))
.ToList();
foreach (var (merchant, spawns) in groupedMerchants)
{
var merchantState = new MerchantSpawnState(merchant!, spawns);
this.Merchants.Add(merchantState);
}
}
/// <summary>
/// Gets the states of the merchants.
/// </summary>
public ICollection<MerchantSpawnState> Merchants { get; } = new List<MerchantSpawnState>();
}