Files
AdamuSw/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs
2026-07-14 19:00:35 +03:00

303 lines
14 KiB
C#

// <copyright file="ExperienceRateSplitTest.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests for experience rate splitting between normal and master experience.
/// </summary>
[TestFixture]
public class ExperienceRateSplitTest
{
/// <summary>
/// Verifies that master classes receive master experience at the global master rate,
/// while non-master classes receive normal experience.
/// </summary>
[Test]
public async ValueTask SoloKillUsesMasterExperienceRateForMasterClassesAsync()
{
var masterContext = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 5.0f,
maximumLevel: 10,
maximumMasterLevel: 200);
var normalContext = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 5.0f,
maximumLevel: 11,
maximumMasterLevel: 200);
var masterPlayer = await this.CreatePlayerAsync(masterContext, level: 10, totalLevel: 10, isMasterClass: true).ConfigureAwait(false);
var normalPlayer = await this.CreatePlayerAsync(normalContext, level: 10, totalLevel: 10, isMasterClass: false).ConfigureAwait(false);
var killedObject = CreateKilledObject(level: 100);
var masterGained = await masterPlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
var normalGained = await normalPlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
Assert.That(masterGained, Is.GreaterThan(0));
Assert.That(normalGained, Is.GreaterThan(0));
Assert.That(masterGained, Is.GreaterThan(normalGained * 3));
Assert.That(masterPlayer.SelectedCharacter!.MasterExperience, Is.EqualTo(masterGained));
Assert.That(normalPlayer.SelectedCharacter!.Experience, Is.EqualTo(normalGained));
}
/// <summary>
/// Verifies that the server experience rate is correctly applied to master experience gains.
/// </summary>
[Test]
public async ValueTask SoloKillAppliesServerExperienceRateToMasterExperienceAsync()
{
var highRateContext = this.CreateGameServerContext(
normalExperienceRate: 3.0f,
globalMasterExperienceRate: 2.0f,
maximumLevel: 10,
maximumMasterLevel: 200);
var baseRateContext = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 2.0f,
maximumLevel: 10,
maximumMasterLevel: 200);
var highRatePlayer = await this.CreatePlayerAsync(highRateContext, level: 10, totalLevel: 10, isMasterClass: true).ConfigureAwait(false);
var baseRatePlayer = await this.CreatePlayerAsync(baseRateContext, level: 10, totalLevel: 10, isMasterClass: true).ConfigureAwait(false);
var killedObject = CreateKilledObject(level: 100);
var highRateGain = await highRatePlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
var baseRateGain = await baseRatePlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
Assert.That(highRateGain, Is.GreaterThan(0));
Assert.That(baseRateGain, Is.GreaterThan(0));
Assert.That(highRateGain, Is.GreaterThan(baseRateGain * 2));
}
/// <summary>
/// Verifies that party experience distribution applies master experience rates for master class members.
/// </summary>
[Test]
public async ValueTask PartyDistributionUsesMasterExperienceRateForMasterMembersAsync()
{
var context = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 4.0f,
maximumLevel: 3,
maximumMasterLevel: 200);
var masterPlayer = await this.CreatePlayerAsync(context, level: 3, totalLevel: 2, isMasterClass: true).ConfigureAwait(false);
var normalPlayer = await this.CreatePlayerAsync(context, level: 2, totalLevel: 2, isMasterClass: false).ConfigureAwait(false);
var party = new Party(new PartyManager(5, new NullLogger<Party>()), 5, new NullLogger<Party>());
await party.AddAsync(masterPlayer).ConfigureAwait(false);
await party.AddAsync(normalPlayer).ConfigureAwait(false);
await masterPlayer.AddObserverAsync(normalPlayer).ConfigureAwait(false);
var killedObject = CreateKilledObject(level: 5);
_ = await party.DistributeExperienceAfterKillAsync(killedObject.Object, masterPlayer).ConfigureAwait(false);
var masterGained = masterPlayer.SelectedCharacter!.MasterExperience;
var normalGained = normalPlayer.SelectedCharacter!.Experience;
Assert.That(masterGained, Is.GreaterThan(0));
Assert.That(normalGained, Is.GreaterThan(0));
Assert.That(masterGained, Is.GreaterThan(normalGained * 3));
}
/// <summary>
/// Verifies that concurrent normal experience gains cannot exceed the maximum level.
/// </summary>
[Test]
public async ValueTask ConcurrentNormalExperienceCantExceedMaximumLevelAsync()
{
var context = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 1.0f,
maximumLevel: 2,
maximumMasterLevel: 200);
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
player.SelectedCharacter!.Experience = context.ExperienceTable[2] - 1;
var initialLevelUpPoints = player.SelectedCharacter.LevelUpPoints;
var pointsPerLevelUp = (int)player.Attributes![Stats.PointsPerLevelUp];
await Task.WhenAll(
player.AddExperienceAsync(10, null).AsTask(),
player.AddExperienceAsync(10, null).AsTask()).ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Level], Is.EqualTo(2));
Assert.That(player.SelectedCharacter.LevelUpPoints, Is.EqualTo(initialLevelUpPoints + pointsPerLevelUp));
}
/// <summary>
/// Verifies that concurrent master experience stays within configured maximum bounds.
/// </summary>
[Test]
public async ValueTask ConcurrentMasterExperienceStaysWithinConfiguredMaximumBoundsAsync()
{
var context = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 1.0f,
maximumLevel: 400,
maximumMasterLevel: 1);
var player = await this.CreatePlayerAsync(context, level: 400, totalLevel: 400, isMasterClass: true).ConfigureAwait(false);
player.Attributes![Stats.MasterLevel] = 0;
player.SelectedCharacter!.MasterExperience = context.MasterExperienceTable[1] - 1;
var maxMasterExperience = context.MasterExperienceTable[context.Configuration.MaximumMasterLevel];
await Task.WhenAll(
player.AddMasterExperienceAsync(10, null).AsTask(),
player.AddMasterExperienceAsync(10, null).AsTask()).ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.MasterLevel], Is.LessThanOrEqualTo(context.Configuration.MaximumMasterLevel));
Assert.That(player.SelectedCharacter.MasterExperience, Is.LessThanOrEqualTo(maxMasterExperience));
}
/// <summary>
/// Verifies that experience overflow is applied below max when not prevented.
/// </summary>
[Test]
public async ValueTask OverflowIsAppliedBelowMaxWhenNotPreventedAsync()
{
var context = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 1.0f,
maximumLevel: 10,
maximumMasterLevel: 200);
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
var requiredForLevel2 = context.ExperienceTable[2] - player.SelectedCharacter!.Experience;
await player.AddExperienceAsync((int)requiredForLevel2 + 10, null).ConfigureAwait(false);
Assert.That((int)player.Attributes![Stats.Level], Is.EqualTo(2));
Assert.That(player.SelectedCharacter.Experience, Is.EqualTo(context.ExperienceTable[2] + 10));
}
/// <summary>
/// Verifies that experience overflow is discarded below max when prevented.
/// </summary>
[Test]
public async ValueTask OverflowIsDiscardedBelowMaxWhenPreventedAsync()
{
var context = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 1.0f,
maximumLevel: 10,
maximumMasterLevel: 200,
preventExperienceOverflow: true);
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
var requiredForLevel2 = context.ExperienceTable[2] - player.SelectedCharacter!.Experience;
await player.AddExperienceAsync((int)requiredForLevel2 + 10, null).ConfigureAwait(false);
Assert.That((int)player.Attributes![Stats.Level], Is.EqualTo(2));
Assert.That(player.SelectedCharacter.Experience, Is.EqualTo(context.ExperienceTable[2]));
}
/// <summary>
/// Verifies that experience always stops at the maximum level regardless of the overflow setting.
/// </summary>
/// <param name="preventExperienceOverflow">Whether to prevent experience overflow.</param>
[TestCase(false)]
[TestCase(true)]
public async ValueTask ExperienceAlwaysStopsAtMaximumLevelRegardlessOfOverflowSettingAsync(bool preventExperienceOverflow)
{
var context = this.CreateGameServerContext(
normalExperienceRate: 1.0f,
globalMasterExperienceRate: 1.0f,
maximumLevel: 2,
maximumMasterLevel: 200,
preventExperienceOverflow);
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
await player.AddExperienceAsync(int.MaxValue, null).ConfigureAwait(false);
await player.AddExperienceAsync(int.MaxValue, null).ConfigureAwait(false);
Assert.That((int)player.Attributes![Stats.Level], Is.EqualTo(2));
}
private static Mock<IAttackable> CreateKilledObject(float level)
{
var attributes = new Mock<IAttributeSystem>();
attributes.Setup(a => a[Stats.Level]).Returns(level);
var result = new Mock<IAttackable>();
result.SetupGet(a => a.Attributes).Returns(attributes.Object);
result.SetupGet(a => a.CurrentMap).Returns((GameMap?)null);
return result;
}
private async ValueTask<Player> CreatePlayerAsync(IGameContext context, short level, float totalLevel, bool isMasterClass)
{
var player = await PlayerTestHelper.CreatePlayerAsync(context).ConfigureAwait(false);
player.SelectedCharacter!.CharacterClass!.IsMasterClass = isMasterClass;
player.Attributes![Stats.Level] = level;
player.Attributes[Stats.MasterLevel] = 0;
player.Attributes[Stats.PointsPerLevelUp] = 1;
player.Attributes[Stats.MasterPointsPerLevelUp] = 1;
player.Attributes.AddElement(new SimpleElement(1.0f, AggregateType.AddRaw), Stats.ExperienceRate);
player.Attributes.AddElement(new SimpleElement(1.0f, AggregateType.AddRaw), Stats.MasterExperienceRate);
player.Attributes.AddElement(new SimpleElement(totalLevel, AggregateType.AddRaw), Stats.TotalLevel);
player.SelectedCharacter.Experience = 0;
player.SelectedCharacter.MasterExperience = 0;
return player;
}
private IGameServerContext CreateGameServerContext(float normalExperienceRate, float globalMasterExperienceRate, short maximumLevel, short maximumMasterLevel, bool preventExperienceOverflow = false)
{
var contextProvider = new InMemoryPersistenceContextProvider();
var gameConfiguration = contextProvider.CreateNewContext().CreateNew<GameConfiguration>();
if (gameConfiguration.CharacterClasses is null)
{
typeof(GameConfiguration).GetProperty(nameof(GameConfiguration.CharacterClasses))?.SetValue(gameConfiguration, new List<CharacterClass>());
}
gameConfiguration.RecoveryInterval = int.MaxValue;
gameConfiguration.MaximumLevel = maximumLevel;
gameConfiguration.MaximumMasterLevel = maximumMasterLevel;
gameConfiguration.PreventExperienceOverflow = preventExperienceOverflow;
gameConfiguration.MinimumMonsterLevelForMasterExperience = 0;
gameConfiguration.ExperienceRate = 1.0f;
gameConfiguration.MasterExperienceRate = globalMasterExperienceRate;
var map = contextProvider.CreateNewContext().CreateNew<GameMapDefinition>();
map.ExpMultiplier = 1.0f;
gameConfiguration.Maps.Add(map);
var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var gameServerContext = new GameServerContext(
new GameServerDefinition
{
GameConfiguration = gameConfiguration,
ServerConfiguration = new GameServerConfiguration(),
ExperienceRate = normalExperienceRate,
},
new Mock<IGuildServer>().Object,
new Mock<IEventPublisher>().Object,
new Mock<ILoginServer>().Object,
new Mock<IFriendServer>().Object,
contextProvider,
mapInitializer,
new NullLoggerFactory(),
new PlugInManager(new List<PlugInConfiguration>(), new NullLoggerFactory(), null, null),
NullDropGenerator.Instance,
new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameServerContext.PlugInManager;
mapInitializer.PathFinderPool = gameServerContext.PathFinderPool;
return gameServerContext;
}
}