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,39 @@
// <copyright file="AppearanceSerializerTest.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 Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameServer.RemoteView;
/// <summary>
/// Tests the <see cref="AppearanceSerializer"/>.
/// </summary>
[TestFixture]
public class AppearanceSerializerTest
{
/// <summary>
/// Tests if a new (naked) dark knight with small axe would be serialized correctly.
/// </summary>
[Test]
public void NewDarkKnightWithSmallAxe()
{
var serializer = new AppearanceSerializer();
var appearanceData = new Mock<IAppearanceData>();
appearanceData.Setup(a => a.CharacterClass).Returns(new CharacterClass { Number = 0x20 >> 3 }); // Dark Knight;
appearanceData.Setup(a => a.EquippedItems).Returns(this.GetSmallAxeEquipped());
var data = new byte[serializer.NeededSpace];
serializer.WriteAppearanceData(data, appearanceData.Object, false);
var expected = new byte[] { 0x20, 0x00, 0xFF, 0xFF, 0xFF, 0xF3, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x20, 0xFF, 0xFF, 0xFF, 0x00, 0x00 };
Assert.That(data, Is.EquivalentTo(expected));
}
private IEnumerable<ItemAppearance> GetSmallAxeEquipped()
{
yield return new ItemAppearance { Definition = new ItemDefinition { Group = 1 } };
}
}

View File

@@ -0,0 +1,72 @@
// <copyright file="CharacterMoveTest.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 MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.GameServer.MessageHandler;
using MUnique.OpenMU.Pathfinding;
/// <summary>
/// Tests for the <see cref="CharacterWalkHandlerPlugIn"/>.
/// </summary>
[TestFixture]
public class CharacterMoveTest
{
private static readonly Point StartPoint = new(147, 120);
private static readonly Point EndPoint = new(151, 122);
/// <summary>
/// Tests if handling a walk packet results in the correct target coordinates.
/// </summary>
[Test]
public async ValueTask TestWalkTargetIsCorrectAsync()
{
var player = await this.DoTheWalkAsync().ConfigureAwait(false);
Assert.That(player.WalkTarget, Is.EqualTo(EndPoint));
}
/// <summary>
/// Tests if handling a walk packet results in the correct walk directions.
/// </summary>
[Test]
public async ValueTask TestWalkStepsAreCorrectAsync()
{
var player = await this.DoTheWalkAsync().ConfigureAwait(false);
// the next check is questionable - there is a timer which is removing a direction every 500ms. If the test runs "too slow", the count is 3 ;-)
Memory<WalkingStep> steps = new WalkingStep[16];
var count = await player.GetStepsAsync(steps).ConfigureAwait(false);
Assert.That(count, Is.EqualTo(4));
steps = steps.Slice(0, count);
steps.Span.Reverse();
Assert.That(steps.Span[0].From, Is.EqualTo(StartPoint));
Assert.That(steps.Span[steps.Length - 1].To, Is.EqualTo(EndPoint));
for (var index = 0; index < steps.Span.Length; index++)
{
var direction = steps.Span[index];
Assert.That(direction.From, Is.Not.EqualTo(direction.To));
}
}
/// <summary>
/// Creates the player and performs the example walk.
/// By example: walking from 147, 120 to 151, 122: C1 08 D4 93 78 44 33 44
/// The packet contains the starting coordinates and the target is determined by the given path.
/// </summary>
/// <returns>The player which walked.</returns>
private async ValueTask<Player> DoTheWalkAsync()
{
var packet = new byte[] { 0xC1, 0x08, (byte)PacketType.Walk, 0x93, 0x78, 0x44, 0x33, 0x44 };
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.PositionX = StartPoint.X;
player.SelectedCharacter.PositionY = StartPoint.Y;
var moveHandler = new CharacterWalkHandlerPlugIn();
await moveHandler.HandlePacketAsync(player, packet).ConfigureAwait(false);
return player;
}
}

View File

@@ -0,0 +1,147 @@
// <copyright file="ClientAttributeTest.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 MUnique.OpenMU.GameServer;
using MUnique.OpenMU.Network.PlugIns;
/// <summary>
/// Tests for <see cref="MinimumClientAttribute"/>.
/// </summary>
[TestFixture]
public class ClientAttributeTest
{
private static readonly MinimumClientAttribute Season6E3English = new(6, 3, ClientLanguage.English);
private static readonly MinimumClientAttribute Season6E3Japanese = new(6, 3, ClientLanguage.Japanese);
private static readonly MinimumClientAttribute Season9E2English = new(9, 2, ClientLanguage.English);
private static readonly MinimumClientAttribute Season9E2EnglishOtherInstance = new(9, 2, ClientLanguage.English);
/// <summary>
/// Tests less than using <see cref="IComparable"/>.
/// </summary>
[Test]
public void LessThan()
{
Assert.That(Season6E3English, Is.LessThan(Season9E2English));
}
/// <summary>
/// Tests greater than using <see cref="IComparable"/>.
/// </summary>
[Test]
public void GreaterThan()
{
Assert.That(Season9E2English, Is.GreaterThan(Season6E3English));
}
/// <summary>
/// Tests equality.
/// </summary>
[Test]
public void Equal()
{
Assert.That(Season9E2English, Is.EqualTo(Season9E2English));
}
/// <summary>
/// Tests non equality when version differs.
/// </summary>
[Test]
public void NotEqualWhenVersionDiffers()
{
Assert.That(Season6E3English, Is.Not.EqualTo(Season9E2English));
}
/// <summary>
/// Tests non equality when language differs.
/// </summary>
[Test]
public void NotEqualWhenLanguageDiffers()
{
Assert.That(Season6E3English, Is.Not.EqualTo(Season6E3Japanese));
}
/// <summary>
/// Tests less than using the overloaded operator.
/// </summary>
[Test]
public void OperatorLessThan()
{
Assert.That(Season6E3English < Season9E2English, Is.True);
}
/// <summary>
/// Tests greater than using the overloaded operator.
/// </summary>
[Test]
public void OperatorGreaterThan()
{
Assert.That(Season9E2English > Season6E3English, Is.True);
}
/// <summary>
/// Tests less than using the overloaded operator.
/// </summary>
[Test]
public void OperatorLessOrEqualThan()
{
Assert.That(Season6E3English <= Season9E2English, Is.True);
}
/// <summary>
/// Tests greater than using the overloaded operator.
/// </summary>
[Test]
public void OperatorGreaterOrEqualThan()
{
Assert.That(Season9E2English >= Season6E3English, Is.True);
}
/// <summary>
/// Tests less than using the overloaded operator.
/// </summary>
[Test]
public void OperatorLessOrEqualThanWhenEqual()
{
Assert.That(Season9E2EnglishOtherInstance <= Season9E2English, Is.True);
}
/// <summary>
/// Tests greater than using the overloaded operator.
/// </summary>
[Test]
public void OperatorGreaterOrEqualThanWhenEqual()
{
Assert.That(Season9E2English >= Season9E2EnglishOtherInstance, Is.True);
}
/// <summary>
/// Tests equality using the overloaded operator.
/// </summary>
[Test]
public void OperatorEqual()
{
Assert.That(Season9E2English == Season9E2EnglishOtherInstance, Is.True);
}
/// <summary>
/// Tests non-equality using the overloaded operator when version differs.
/// </summary>
[Test]
public void OperatorNotEqualWhenVersionDiffers()
{
Assert.That(Season6E3English != Season9E2English, Is.True);
}
/// <summary>
/// Tests non-equality using the overloaded operator when language differs.
/// </summary>
[Test]
public void OperatorNotEqualWhenLanguageDiffers()
{
Assert.That(Season6E3English != Season6E3Japanese, Is.True);
}
}

View File

@@ -0,0 +1,161 @@
// <copyright file="DropGeneratorTest.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 Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// Tests the drop generator.
/// </summary>
[TestFixture]
public class DropGeneratorTest
{
/// <summary>
/// Tests if the drop fails because the randomizer returns a number which causes a fail.
/// </summary>
[Test]
public async ValueTask TestDropFailAsync()
{
var config = this.GetGameConfig();
var generator = new DefaultDropGenerator(config, this.GetRandomizer(9999));
var (items, _) = await generator.GenerateItemDropsAsync(this.GetMonster(1, 0), 0, await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false));
var item = items.FirstOrDefault();
Assert.That(item, Is.Null);
}
/// <summary>
/// Tests the drops defined by a monster are getting considered.
/// </summary>
[Test]
public async ValueTask TestItemDropItemByMonsterAsync()
{
var config = this.GetGameConfig();
var monster = this.GetMonster(1, 0);
monster.DropItemGroups.AddBasicDropItemGroups();
monster.DropItemGroups.Add(3000, SpecialItemType.RandomItem, true);
var generator = new DefaultDropGenerator(config, this.GetRandomizer2(0, 0.5));
var (items, _) = await generator.GenerateItemDropsAsync(monster, 1, await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false));
var item = items.FirstOrDefault();
Assert.That(item, Is.Not.Null);
// ReSharper disable once PossibleNullReferenceException
Assert.That(item!.Definition, Is.EqualTo(monster.DropItemGroups.Last().PossibleItems.First()));
}
/// <summary>
/// Tests that items with a maximum drop level are filtered from generic monster drops.
/// </summary>
[Test]
public async ValueTask TestMaximumDropLevelAsync()
{
var config = this.GetGameConfig();
var cappedItem = this.CreateItemDefinition(12, 15, 12, 66);
var uncappedItem = this.CreateItemDefinition(14, 13, 25);
var dropGroup = new Mock<DropItemGroup>();
dropGroup.SetupAllProperties();
dropGroup.Object.Chance = 1.0;
dropGroup.Object.ItemType = SpecialItemType.Jewel;
dropGroup.Setup(g => g.PossibleItems).Returns(new List<ItemDefinition> { cappedItem, uncappedItem });
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.CurrentMap!.Definition.DropItemGroups.Add(dropGroup.Object);
var generator = new DefaultDropGenerator(config, this.GetRandomizer(0));
var (items, _) = await generator.GenerateItemDropsAsync(this.GetMonster(1, 67), 1, player).ConfigureAwait(false);
var item = items.FirstOrDefault();
Assert.That(item, Is.Not.Null);
Assert.That(item!.Definition, Is.EqualTo(uncappedItem));
}
/// <summary>
/// Tests the drops defined by a player are getting considered.
/// </summary>
public void TestItemDropItemByPlayer()
{
// to be implemented
}
/// <summary>
/// Tests the drops defined by a map are getting considered.
/// </summary>
public void TestItemDropItemByMap()
{
// to be implemented
}
/// <summary>
/// Tests that ExcellentItemDropLevelDelta property exists and has correct default.
/// </summary>
[Test]
public void TestExcellentItemDropLevelDelta_PropertyExists()
{
var config = this.GetGameConfig();
// The initializer sets default to 25 for backward compatibility
config.ExcellentItemDropLevelDelta = 25;
Assert.That(config.ExcellentItemDropLevelDelta, Is.EqualTo(25));
config.ExcellentItemDropLevelDelta = 0;
Assert.That(config.ExcellentItemDropLevelDelta, Is.EqualTo(0));
config.ExcellentItemDropLevelDelta = 50;
Assert.That(config.ExcellentItemDropLevelDelta, Is.EqualTo(50));
}
private MonsterDefinition GetMonster(int numberOfDrops, byte level)
{
var monster = new Mock<MonsterDefinition>();
monster.SetupAllProperties();
monster.Setup(m => m.DropItemGroups).Returns(new List<DropItemGroup>());
monster.Setup(m => m.Attributes).Returns(new List<MonsterAttribute>());
monster.Object.NumberOfMaximumItemDrops = numberOfDrops;
monster.Object.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.Level, Value = level });
return monster.Object;
}
private IRandomizer GetRandomizer(int randomValue)
{
var randomizer = new Mock<IRandomizer>();
randomizer.Setup(r => r.NextInt(It.IsAny<int>(), It.IsAny<int>())).Returns(randomValue);
randomizer.Setup(r => r.NextDouble()).Returns(randomValue / 10000.0);
return randomizer.Object;
}
private IRandomizer GetRandomizer2(int integerValue, double doubleValue)
{
var randomizer = new Mock<IRandomizer>();
randomizer.Setup(r => r.NextInt(It.IsAny<int>(), It.IsAny<int>())).Returns(integerValue);
randomizer.Setup(r => r.NextDouble()).Returns(doubleValue);
return randomizer.Object;
}
private GameConfiguration GetGameConfig()
{
var gameConfiguration = new Mock<GameConfiguration>();
gameConfiguration.Setup(c => c.Items).Returns(new List<ItemDefinition>());
return gameConfiguration.Object;
}
private ItemDefinition CreateItemDefinition(byte group, short number, byte dropLevel, byte? maximumDropLevel = null)
{
var itemDefinition = new Mock<ItemDefinition>();
itemDefinition.SetupAllProperties();
itemDefinition.Object.Group = group;
itemDefinition.Object.Number = number;
itemDefinition.Object.DropLevel = dropLevel;
itemDefinition.Object.MaximumDropLevel = maximumDropLevel;
itemDefinition.Object.Durability = 1;
itemDefinition.Setup(d => d.PossibleItemOptions).Returns(new List<ItemOptionDefinition>());
return itemDefinition.Object;
}
}

View File

@@ -0,0 +1,69 @@
// <copyright file="DropItemGroupExtensions.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 Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic;
/// <summary>
/// Some extensions methods for convenience at testing item drops.
/// </summary>
internal static class DropItemGroupExtensions
{
/// <summary>
/// Adds the basic drop item groups to the player.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>The same player.</returns>
public static Player WithBasicDropItemGroups(this Player player)
{
player.CurrentMap!.Definition.DropItemGroups.AddBasicDropItemGroups();
return player;
}
/// <summary>
/// Adds the basic drop item groups.
/// </summary>
/// <param name="itemGroups">The item groups.</param>
public static void AddBasicDropItemGroups(this ICollection<DropItemGroup> itemGroups)
{
itemGroups.Add(1, SpecialItemType.RandomItem, true);
itemGroups.Add(1000, SpecialItemType.Excellent, true);
itemGroups.Add(3000, SpecialItemType.Money, true);
}
/// <summary>
/// Adds a new drop item group with the specified data.
/// </summary>
/// <param name="list">The list.</param>
/// <param name="chance">The chance.</param>
/// <param name="itemType">Type of the item.</param>
/// <param name="addItem">if set to <c>true</c>, it adds a test item to the possible item list.</param>
/// <returns>The drop item group which has been added to the list.</returns>
public static DropItemGroup Add(this ICollection<DropItemGroup> list, int chance, SpecialItemType itemType, bool addItem)
{
var dropItemGroup = new Mock<DropItemGroup>();
dropItemGroup.SetupAllProperties();
dropItemGroup.Object.Chance = chance / 10000.0;
dropItemGroup.Object.ItemType = itemType;
var itemList = new List<ItemDefinition>();
dropItemGroup.Setup(g => g.PossibleItems).Returns(itemList);
if (addItem)
{
var itemDefinition = new Mock<ItemDefinition>();
itemDefinition.SetupAllProperties();
itemDefinition.Object.DropsFromMonsters = true;
itemDefinition.Setup(d => d.PossibleItemSetGroups).Returns(new List<ItemSetGroup>());
itemDefinition.Setup(d => d.PossibleItemOptions).Returns(new List<ItemOptionDefinition>());
itemList.Add(itemDefinition.Object);
}
list.Add(dropItemGroup.Object);
return dropItemGroup.Object;
}
}

View File

@@ -0,0 +1,302 @@
// <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;
}
}

View File

@@ -0,0 +1,229 @@
// <copyright file="FriendServerTest.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using Microsoft.Extensions.Logging.Abstractions;
using MUnique.OpenMU.FriendServer;
namespace MUnique.OpenMU.Tests;
using Moq;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence.InMemory;
/// <summary>
/// Tests for the friend server.
/// </summary>
[TestFixture]
public sealed class FriendServerTest
{
private Character _player1 = null!;
private Character _player2 = null!;
private Mock<IGameServer> _gameServer1 = null!;
private Mock<IGameServer> _gameServer2 = null!;
private IFriendServer _friendServer = null!;
private InMemoryPersistenceContextProvider _persistenceContextProvider = null!;
/// <summary>
/// Sets up the environment with 2 game servers.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameServer1 = new Mock<IGameServer>();
this._gameServer1.Setup(gs => gs.Id).Returns(1);
this._gameServer2 = new Mock<IGameServer>();
this._gameServer2.Setup(gs => gs.Id).Returns(2);
var gameServers = new Dictionary<int, IGameServer>
{
{ this._gameServer1.Object.Id, this._gameServer1.Object },
{ this._gameServer2.Object.Id, this._gameServer2.Object },
};
this._persistenceContextProvider = new InMemoryPersistenceContextProvider();
var notifier = new FriendNotifierToGameServer(gameServers); // todo: mock this
this._friendServer = new FriendServer.FriendServer(notifier, new Mock<IChatServer>().Object, this._persistenceContextProvider, NullLogger<FriendServer.FriendServer>.Instance);
var context = this._persistenceContextProvider.CreateNewContext();
this._player1 = context.CreateNew<Character>();
this._player1.Name = "player1";
this._player2 = context.CreateNew<Character>();
this._player2.Name = "player2";
}
/// <summary>
/// Tests what happens when a player adds a friend, while the friend is offline.
/// The player should have a friend list entry.
/// </summary>
[Test]
public async ValueTask FriendAddRequestOfflineAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
var added = await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
Assert.That(added, Is.True);
this._gameServer2.Verify(g => g.FriendRequestAsync(this._player1.Name, this._player2.Name), Times.Never);
await this.CheckFriendItemsAfterRequestAsync().ConfigureAwait(false);
}
/// <summary>
/// Tests what happens when a player adds a friend, while the friend is online.
/// The online friend should have got a friend request, the player should have a friend list entry.
/// </summary>
[Test]
public async ValueTask FriendAddRequestOnlineAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
var added = await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
Assert.That(added, Is.True);
this._gameServer2.Verify(g => g.FriendRequestAsync(this._player1.Name, this._player2.Name), Times.Once);
await this.CheckFriendItemsAfterRequestAsync().ConfigureAwait(false);
}
/// <summary>
/// Tests if a friend is not added twice when the player sends two friend requests for the same friend.
/// </summary>
[Test]
public async ValueTask FriendAddRequestRepeatedAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
var added = await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
Assert.That(added, Is.True);
var notAdded = await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
Assert.That(notAdded, Is.False);
this._gameServer2.Verify(g => g.FriendRequestAsync(this._player1.Name, this._player2.Name), Times.Exactly(2));
}
/// <summary>
/// Tests if both friends have each other in the friend list with visible server number, after the friend accepted friendship.
/// </summary>
[Test]
public async ValueTask FriendAddRequestAcceptAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
await this._friendServer.FriendResponseAsync(this._player2.Name, this._player1.Name, true).ConfigureAwait(false);
this._gameServer1.Verify(g => g.FriendOnlineStateChangedAsync(this._player1.Name, this._player2.Name, this._gameServer2.Object.Id), Times.AtLeastOnce);
this._gameServer2.Verify(g => g.FriendOnlineStateChangedAsync(this._player2.Name, this._player1.Name, this._gameServer1.Object.Id), Times.AtLeastOnce);
var context = this._persistenceContextProvider.CreateNewFriendServerContext();
var friendItem1 = (await context.GetFriendsAsync(this._player1.Id).ConfigureAwait(false)).FirstOrDefault();
Assert.That(friendItem1, Is.Not.Null);
Assert.That(friendItem1!.CharacterName, Is.EqualTo(this._player1.Name));
Assert.That(friendItem1.FriendName, Is.EqualTo(this._player2.Name));
Assert.That(friendItem1.RequestOpen, Is.False);
Assert.That(friendItem1.Accepted, Is.True);
var friendItem2 = (await context.GetFriendsAsync(this._player2.Id).ConfigureAwait(false)).FirstOrDefault();
Assert.That(friendItem2, Is.Not.Null);
Assert.That(friendItem2!.CharacterName, Is.EqualTo(this._player2.Name));
Assert.That(friendItem2.FriendName, Is.EqualTo(this._player1.Name));
Assert.That(friendItem2.RequestOpen, Is.False);
Assert.That(friendItem2.Accepted, Is.True);
}
/// <summary>
/// Tests if the player has the friend in his friendlist, but unaccepted.
/// Also checks if the friend which declined, does not get the friend list entry.
/// </summary>
[Test]
public async ValueTask FriendAddRequestDeclineAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
await this._friendServer.FriendResponseAsync(this._player2.Name, this._player1.Name, false).ConfigureAwait(false);
this._gameServer1.Verify(g => g.FriendOnlineStateChangedAsync(this._player1.Name, this._player2.Name, this._gameServer2.Object.Id), Times.Never);
this._gameServer2.Verify(g => g.FriendOnlineStateChangedAsync(this._player2.Name, this._player1.Name, this._gameServer1.Object.Id), Times.Never);
var context = this._persistenceContextProvider.CreateNewFriendServerContext();
var friendItem = (await context.GetFriendsAsync(this._player1.Id).ConfigureAwait(false)).FirstOrDefault();
Assert.That(friendItem, Is.Not.Null);
Assert.That(friendItem!.CharacterName, Is.EqualTo(this._player1.Name));
Assert.That(friendItem.FriendName, Is.EqualTo(this._player2.Name));
Assert.That(friendItem.RequestOpen, Is.False);
Assert.That(friendItem.Accepted, Is.False);
}
/// <summary>
/// Tests that a friend response without a corresponding request does not create friend list entries.
/// </summary>
[Test]
public async ValueTask FriendResponseWithoutRequestAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
await this._friendServer.FriendResponseAsync(this._player2.Name, this._player1.Name, true).ConfigureAwait(false);
this._gameServer1.Verify(g => g.FriendOnlineStateChangedAsync(this._player1.Name, this._player2.Name, this._gameServer2.Object.Id), Times.Never);
this._gameServer2.Verify(g => g.FriendOnlineStateChangedAsync(this._player2.Name, this._player1.Name, this._gameServer1.Object.Id), Times.Never);
}
/// <summary>
/// Tests if a friend can get deleted from the friend list, but the friend still has the player.
/// </summary>
[Test]
public async ValueTask FriendDeleteAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
await this._friendServer.FriendResponseAsync(this._player2.Name, this._player1.Name, true).ConfigureAwait(false);
await this._friendServer.DeleteFriendAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
this._gameServer2.Verify(g => g.FriendOnlineStateChangedAsync(this._player2.Name, this._player1.Name, FriendServer.FriendServer.OfflineServerId), Times.Once);
}
/// <summary>
/// Here is tested if the notifications between players in the friend list are working properly.
/// Is is tested with 2 players in 2 different gameservers.
/// player1 on gameServer1
/// player2 on gameServer2.
/// </summary>
[Test]
public async ValueTask TestOnlineListAsync()
{
await this.PlayerEnteredGameAsync(this._player1.Id, this._player1.Name, this._gameServer1.Object.Id).ConfigureAwait(false);
await this.PlayerEnteredGameAsync(this._player2.Id, this._player2.Name, this._gameServer2.Object.Id).ConfigureAwait(false);
await this._friendServer.FriendRequestAsync(this._player1.Name, this._player2.Name).ConfigureAwait(false);
await this._friendServer.FriendResponseAsync(this._player2.Name, this._player1.Name, true).ConfigureAwait(false);
await this._friendServer.PlayerLeftGameAsync(this._player1.Id, this._player1.Name).ConfigureAwait(false);
await this._friendServer.PlayerLeftGameAsync(this._player2.Id, this._player2.Name).ConfigureAwait(false);
this._gameServer1.Invocations.Clear();
this._gameServer2.Invocations.Clear();
await this._friendServer.PlayerEnteredGameAsync((byte)this._gameServer1.Object.Id, this._player1.Id, this._player1.Name).ConfigureAwait(false);
await this._friendServer.PlayerEnteredGameAsync((byte)this._gameServer2.Object.Id, this._player2.Id, this._player2.Name).ConfigureAwait(false);
this._gameServer1.Verify(gs => gs.FriendOnlineStateChangedAsync(this._player1.Name, this._player2.Name, this._gameServer2.Object.Id), Times.AtLeastOnce);
await this._friendServer.PlayerLeftGameAsync(this._player1.Id, this._player1.Name).ConfigureAwait(false);
this._gameServer2.Verify(gs => gs.FriendOnlineStateChangedAsync(this._player2.Name, this._player1.Name, FriendServer.FriendServer.OfflineServerId), Times.AtLeastOnce);
}
private async ValueTask PlayerEnteredGameAsync(Guid playerId, string playerName, int serverId)
{
await this._friendServer.PlayerEnteredGameAsync((byte)serverId, playerId, playerName).ConfigureAwait(false);
}
private async ValueTask CheckFriendItemsAfterRequestAsync()
{
var context = this._persistenceContextProvider.CreateNewFriendServerContext();
var friendItem = (await context.GetFriendsAsync(this._player1.Id).ConfigureAwait(false)).FirstOrDefault();
Assert.That(friendItem, Is.Not.Null);
Assert.That(friendItem!.CharacterName, Is.EqualTo(this._player1.Name));
Assert.That(friendItem.FriendName, Is.EqualTo(this._player2.Name));
Assert.That(friendItem.RequestOpen, Is.True);
Assert.That(friendItem.Accepted, Is.False);
}
}

View File

@@ -0,0 +1,165 @@
// <copyright file="FrustumBasedTargetFilterTest.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 Moq;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using MUnique.OpenMU.Pathfinding;
/// <summary>
/// Tests for the <see cref="FrustumBasedTargetFilter"/>.
/// </summary>
[TestFixture]
internal class FrustumBasedTargetFilterTest
{
/// <summary>
/// Tests that a single projectile can hit a target in the center of the frustum.
/// </summary>
[Test]
public void SingleProjectile_TargetInCenter_CanHit()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 1);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(100, 105); // Directly in front (positive Y)
// Rotation 128 points in +Y direction (180 degrees in 0-255 system)
var result = filter.IsTargetWithinBounds(attacker, target, 128, 0);
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that with triple shot, a target directly in front can be hit by the all projectiles.
/// </summary>
[Test]
public void TripleShot_TargetNear_CanBeHitByAllProjectiles()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(100, 101);
// Rotation 128 points in +Y direction
// Check if all projectiles can hit
Assert.That(filter.IsTargetWithinBounds(attacker, target, 128, 0), Is.True);
Assert.That(filter.IsTargetWithinBounds(attacker, target, 128, 1), Is.True);
Assert.That(filter.IsTargetWithinBounds(attacker, target, 128, 2), Is.True);
}
/// <summary>
/// Tests that with triple shot, a target in the center can be hit by the center projectile.
/// </summary>
[Test]
public void TripleShot_TargetInCenter_CanBeHitByCenterProjectile()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(100, 105); // Directly in front (positive Y)
// Rotation 128 points in +Y direction
// Check if the center projectile (index 1) can hit
var result = filter.IsTargetWithinBounds(attacker, target, 128, 1);
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that with triple shot, a target on the left side can be hit by the left projectile.
/// </summary>
[Test]
public void TripleShot_TargetOnLeft_CanBeHitByLeftProjectile()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(98, 105); // To the left and in front (2 units left, within frustum)
// Rotation 128 points in +Y direction
// Left projectile should be able to hit (index 0)
var leftResult = filter.IsTargetWithinBounds(attacker, target, 128, 0);
Assert.That(leftResult, Is.True);
// Right projectile should NOT be able to hit (index 2)
var rightResult = filter.IsTargetWithinBounds(attacker, target, 128, 2);
Assert.That(rightResult, Is.False);
}
/// <summary>
/// Tests that with triple shot, a target on the right side can be hit by the right projectile.
/// </summary>
[Test]
public void TripleShot_TargetOnRight_CanBeHitByRightProjectile()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(102, 105); // To the right and in front (2 units right, within frustum)
// Rotation 128 points in +Y direction
// Right projectile should be able to hit (index 2)
var rightResult = filter.IsTargetWithinBounds(attacker, target, 128, 2);
Assert.That(rightResult, Is.True);
// Left projectile should NOT be able to hit (index 0)
var leftResult = filter.IsTargetWithinBounds(attacker, target, 128, 0);
Assert.That(leftResult, Is.False);
}
/// <summary>
/// Tests that a target outside the frustum cannot be hit by any projectile.
/// </summary>
[Test]
public void TripleShot_TargetOutsideFrustum_CannotBeHit()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(110, 105); // Far to the right, outside frustum
// Rotation 128 points in +Y direction
// No projectile should be able to hit
for (int i = 0; i < 3; i++)
{
var result = filter.IsTargetWithinBounds(attacker, target, 128, i);
Assert.That(result, Is.False, $"Projectile {i} should not hit target outside frustum");
}
}
/// <summary>
/// Tests that the old IsTargetWithinBounds method still works for backward compatibility.
/// </summary>
[Test]
public void IsTargetWithinBounds_TargetInFrustum_ReturnsTrue()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(100, 105); // Directly in front
// Rotation 128 points in +Y direction
var result = filter.IsTargetWithinBounds(attacker, target, 128);
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that the old IsTargetWithinBounds method returns false for targets outside the frustum.
/// </summary>
[Test]
public void IsTargetWithinBounds_TargetOutsideFrustum_ReturnsFalse()
{
var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3);
var attacker = CreateLocateable(100, 100);
var target = CreateLocateable(110, 105); // Far to the right, outside frustum
// Rotation 128 points in +Y direction
var result = filter.IsTargetWithinBounds(attacker, target, 128);
Assert.That(result, Is.False);
}
private static ILocateable CreateLocateable(byte x, byte y)
{
var mock = new Mock<ILocateable>();
mock.Setup(l => l.Position).Returns(new Point(x, y));
return mock.Object;
}
}

View File

@@ -0,0 +1,51 @@
// <copyright file="GameContextTestHelper.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 MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Helper functions to create test game contexts.
/// </summary>
public static class GameContextTestHelper
{
/// <summary>
/// Creates a game context.
/// </summary>
/// <returns>The game context with MuHelperFeaturePlugIn configured.</returns>
public static IGameContext CreateGameContext()
{
var contextProvider = new InMemoryPersistenceContextProvider();
var context = contextProvider.CreateNewContext();
var gameConfig = context.CreateNew<MUnique.OpenMU.Persistence.BasicModel.GameConfiguration>();
var mapDef = context.CreateNew<MUnique.OpenMU.Persistence.BasicModel.GameMapDefinition>();
mapDef.Number = 0;
mapDef.TerrainData = new byte[ushort.MaxValue + 3];
gameConfig.Maps.Add(mapDef);
gameConfig.MaximumPartySize = 5;
gameConfig.RecoveryInterval = int.MaxValue;
gameConfig.MaximumInventoryMoney = int.MaxValue;
gameConfig.ItemDropDuration = TimeSpan.FromMinutes(1);
var mapInitializer = new MapInitializer(gameConfig, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var plugInConfigurations = new List<PlugInConfiguration>
{
new ()
{
TypeId = new Guid("E90A72C3-0459-4323-B6D3-171F88D35542"), // MuHelperFeaturePlugIn
IsActive = true,
},
};
var plugInManager = new PlugInManager(plugInConfigurations, new NullLoggerFactory(), null, null);
var gameContext = new GameContext(gameConfig, contextProvider, mapInitializer, new NullLoggerFactory(), plugInManager, NullDropGenerator.Instance, new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameContext.PlugInManager;
mapInitializer.PathFinderPool = gameContext.PathFinderPool;
return gameContext;
}
}

View File

@@ -0,0 +1,182 @@
// <copyright file="GameMapTest.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using Nito.AsyncEx;
namespace MUnique.OpenMU.Tests;
using Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Pathfinding;
/// <summary>
/// Tests for the game map.
/// </summary>
[TestFixture]
public class GameMapTest
{
private const byte ChunkSize = 8;
/// <summary>
/// An interface which combines several other interfaces which are needed in combination for this test.
/// </summary>
public interface ITestPlayer : ILocateable, IBucketMapObserver, IObservable, ISupportIdUpdate
{
}
/// <summary>
/// Tests if the discovery of players works when a new player is entering the map in the view range.
/// </summary>
[Test]
public async ValueTask TestPlayerEntersMapAsync()
{
var map = new GameMap(new GameMapDefinition(), TimeSpan.FromSeconds(60), ChunkSize);
var player1 = this.GetPlayer();
player1.Object.Position = new Point(100, 100);
await map.AddAsync(player1.Object).ConfigureAwait(false);
var player2 = this.GetPlayer();
player2.Object.Position = new Point(101, 100);
await map.AddAsync(player2.Object).ConfigureAwait(false);
player1.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player2.Object))), Times.Once);
player2.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player1.Object))), Times.Once);
player1.Verify(p => p.LocateableAddedAsync(It.IsAny<ILocateable>()), Times.Once);
}
/// <summary>
/// Tests if movements of a player into the view range of another player causes
/// that the players get notified about each other as soon as they are in view range.
/// </summary>
[Test]
public async ValueTask TestPlayerMovesInMapAsync()
{
var map = new GameMap(new GameMapDefinition(), TimeSpan.FromSeconds(60), ChunkSize);
var player1 = this.GetPlayer();
await map.AddAsync(player1.Object).ConfigureAwait(false);
var player2 = this.GetPlayer();
player2.Object.Position = new Point(101, 100);
await map.AddAsync(player2.Object).ConfigureAwait(false);
await map.MoveAsync(player1.Object, new Point(100, 100), new AsyncLock(), 0).ConfigureAwait(false);
player1.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player2.Object))), Times.Once);
player1.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player1.Object))), Times.Once);
player2.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player1.Object))), Times.Once);
player2.Verify(p => p.LocateableAddedAsync(It.IsAny<ILocateable>()), Times.Once);
}
/// <summary>
/// Tests if movements of a player out of the view range of another player causes
/// that the players get notified about it.
/// </summary>
[Test]
public async ValueTask PlayerMovesOutOfRangeAsync()
{
var map = new GameMap(new GameMapDefinition(), TimeSpan.FromSeconds(60), ChunkSize);
var player1 = this.GetPlayer();
player1.Object.Position = new Point(101, 100);
await map.AddAsync(player1.Object).ConfigureAwait(false);
var player2 = this.GetPlayer();
player2.Object.Position = new Point(101, 100);
await map.AddAsync(player2.Object).ConfigureAwait(false);
await map.MoveAsync(player1.Object, new Point(100, 130), new AsyncLock(), 0).ConfigureAwait(false);
player1.Verify(p => p.LocateablesOutOfScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player2.Object))), Times.Once);
player2.Verify(p => p.LocateableRemovedAsync(It.IsAny<ILocateable>()), Times.Once);
}
/// <summary>
/// Tests if movements of a player into and out of the view range of another player causes
/// that the players get notified about it.
/// </summary>
[Test]
public async ValueTask PlayerMovesOutAndIntoTheRangeAsync()
{
var map = new GameMap(new GameMapDefinition(), TimeSpan.FromSeconds(60), ChunkSize);
var player1 = this.GetPlayer();
player1.Object.Position = new Point(101, 100);
await map.AddAsync(player1.Object).ConfigureAwait(false);
var player2 = this.GetPlayer();
player2.Object.Position = new Point(101, 100);
await map.AddAsync(player2.Object).ConfigureAwait(false);
player1.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player2.Object))), Times.Once);
player2.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player1.Object))), Times.Once);
player1.Verify(p => p.LocateableAddedAsync(It.IsAny<ILocateable>()), Times.Once);
player1.Invocations.Clear();
player2.Invocations.Clear();
await map.MoveAsync(player1.Object, new Point(100, 130), new AsyncLock(), 0).ConfigureAwait(false);
player1.Verify(p => p.LocateablesOutOfScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player2.Object))), Times.Once);
player2.Verify(p => p.LocateableRemovedAsync(It.IsAny<ILocateable>()), Times.Once);
player1.Invocations.Clear();
player2.Invocations.Clear();
await map.MoveAsync(player2.Object, new Point(101, 130), new AsyncLock(), 0).ConfigureAwait(false);
player2.Verify(p => p.NewLocateablesInScopeAsync(It.Is<IEnumerable<ILocateable>>(n => n.Contains(player1.Object))), Times.Once);
player1.Verify(p => p.LocateableAddedAsync(It.IsAny<ILocateable>()), Times.Once);
}
/// <summary>
/// Tests the performance of the movements. Not a standard test.
/// </summary>
/// [Test]
public async ValueTask TestPerformanceMoveAsync()
{
var map = new GameMap(new GameMapDefinition(), TimeSpan.FromSeconds(60), ChunkSize);
var player1 = this.GetPlayer();
await map.AddAsync(player1.Object).ConfigureAwait(false);
var player2 = this.GetPlayer();
player2.Object.Position = new Point(101, 100);
await map.AddAsync(player2.Object).ConfigureAwait(false);
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
var moveLock = new AsyncLock();
for (int i = 0; i < 1000; i++)
{
await map.MoveAsync(player1.Object, new Point((byte)(100 + (i % 30)), (byte)(100 + (i % 30))), moveLock, 0).ConfigureAwait(false);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
/// <summary>
/// Tests if the players in view range get notified when another player leaves the map.
/// </summary>
[Test]
public async ValueTask TestPlayerLeavesMapAsync()
{
var map = new GameMap(new GameMapDefinition(), TimeSpan.FromSeconds(60), ChunkSize);
var player1 = this.GetPlayer();
player1.Object.Position = new Point(100, 100);
await map.AddAsync(player1.Object).ConfigureAwait(false);
var player2 = this.GetPlayer();
player2.Object.Position = new Point(101, 100);
await map.AddAsync(player2.Object).ConfigureAwait(false);
await map.RemoveAsync(player2.Object).ConfigureAwait(false);
Assert.AreEqual(player2.Object.ObservingBuckets.Count, 0);
player1.Verify(p => p.LocateableRemovedAsync(It.IsAny<ILocateable>()), Times.Once);
player2.Verify(p => p.LocateableRemovedAsync(It.IsAny<ILocateable>()), Times.Once);
Assert.That(player1.Object.Observers.Count, Is.EqualTo(0));
Assert.That(player2.Object.Observers.Count, Is.EqualTo(0));
}
private Mock<ITestPlayer> GetPlayer()
{
var player = new Mock<ITestPlayer>();
player.SetupAllProperties();
player.As<ILocateable>().SetupGet(p => p.Id).Returns(() => (player.Object as ISupportIdUpdate).Id);
player.Setup(p => p.ObservingBuckets).Returns(new List<Bucket<ILocateable>>());
player.Setup(p => p.Observers).Returns(new HashSet<IWorldObserver>());
player.Setup(p => p.ObserverLock).Returns(new AsyncReaderWriterLock());
player.Setup(p => p.InfoRange).Returns(20);
player.Setup(p => p.AddObserverAsync(It.IsAny<IWorldObserver>())).Callback<IWorldObserver>(o => player.Object.Observers.Add(o));
player.Setup(p => p.RemoveObserverAsync(It.IsAny<IWorldObserver>())).Callback<IWorldObserver>(o => player.Object.Observers.Remove(o));
return player;
}
}

View File

@@ -0,0 +1,177 @@
// <copyright file="GuildActionTest.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.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence.BasicModel;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests the guild player actions.
/// </summary>
[TestFixture]
public class GuildActionTest : GuildTestBase
{
private Player _guildMasterPlayer = null!;
private Player _player = null!;
/// <inheritdoc/>
[SetUp]
public override async ValueTask SetupAsync()
{
await base.SetupAsync().ConfigureAwait(false);
var gameServerContext = this.CreateGameServer();
this._guildMasterPlayer = await PlayerTestHelper.CreatePlayerAsync(gameServerContext).ConfigureAwait(false);
this._guildMasterPlayer.SelectedCharacter!.Id = this.GuildMaster.Id;
this._guildMasterPlayer.SelectedCharacter.Name = this.GuildMaster.Name;
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, 0).ConfigureAwait(false);
this._guildMasterPlayer.Attributes![Stats.Level] = 100;
this._player = await PlayerTestHelper.CreatePlayerAsync(gameServerContext).ConfigureAwait(false);
await this._player.CurrentMap!.AddAsync(this._guildMasterPlayer).ConfigureAwait(false);
this._player.SelectedCharacter!.Name = "Player";
this._player.SelectedCharacter.Id = Guid.NewGuid();
this._player.Attributes![Stats.Level] = 20;
}
/// <inheritdoc />
protected override void SetupGameServer(Mock<IGameServer> gameServer)
{
base.SetupGameServer(gameServer);
gameServer.Setup(gs => gs.AssignGuildToPlayerAsync(It.IsAny<string>(), It.IsAny<GuildMemberStatus>()))
.Callback((string name, GuildMemberStatus status) =>
{
if (this._player?.Name == name)
{
this._player.GuildStatus = status;
}
if (this._guildMasterPlayer?.Name == name)
{
this._guildMasterPlayer.GuildStatus = status;
}
});
}
/// <summary>
/// Tests if a guild request from a player to a guild master gets forwarded to the guild masters view.
/// </summary>
[Test]
public async ValueTask GuildRequestAsync()
{
var guildRequestAction = new GuildRequestAction();
await guildRequestAction.RequestGuildAsync(this._player, this._guildMasterPlayer.Id).ConfigureAwait(false);
Assert.That(this._guildMasterPlayer.LastGuildRequester, Is.SameAs(this._player));
Mock.Get(this._guildMasterPlayer.ViewPlugIns.GetPlugIn<IShowGuildJoinRequestPlugIn>()!).Verify(g => g!.ShowGuildJoinRequestAsync(this._player), Times.Once);
}
/// <summary>
/// Tests if the guild member object gets created when the guild master accepts the request.
/// </summary>
[Test]
public async ValueTask GuildRequestAcceptAsync()
{
await this.RequestGuildAndRespondAsync(true).ConfigureAwait(false);
Assert.That(this._player.GuildStatus, Is.Not.Null);
Assert.That(this._player.GuildStatus!.GuildId, Is.Not.EqualTo(0));
Mock.Get(this._player.ViewPlugIns.GetPlugIn<IGuildJoinResponsePlugIn>()!).Verify(g => g!.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.Accepted), Times.Once);
}
/// <summary>
/// Tests if the guild member objects does not get created when the guild master refuses the request.
/// </summary>
[Test]
public async ValueTask GuildRequestRefuseAsync()
{
await this.RequestGuildAndRespondAsync(false).ConfigureAwait(false);
Assert.That(this._player.GuildStatus, Is.Null);
Mock.Get(this._player.ViewPlugIns.GetPlugIn<IGuildJoinResponsePlugIn>()!).Verify(g => g!.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.Refused), Times.Once);
}
/// <summary>
/// Tests if the guild creation dialog gets displayed when a player requests it.
/// </summary>
[Test]
public async ValueTask GuildCreationDialogAsync()
{
var action = new GuildMasterAnswerAction();
this._player.OpenedNpc = new NonPlayerCharacter(null!, null!, null!);
await action.ProcessAnswerAsync(this._player, GuildMasterAnswerAction.Answer.ShowDialog).ConfigureAwait(false);
Mock.Get(this._player.ViewPlugIns.GetPlugIn<IShowGuildCreationDialogPlugIn>()!).Verify(g => g!.ShowGuildCreationDialogAsync(), Times.Once());
}
/// <summary>
/// Tests if a guild does get created correctly, when a player executes the creation action.
/// </summary>
[Test]
public async ValueTask GuildCreateAsync()
{
var action = new GuildCreateAction();
await action.CreateGuildAsync(this._player, "Foobar2", []).ConfigureAwait(false);
Assert.That(this._player.GuildStatus, Is.Not.Null);
Assert.That(this._player.GuildStatus!.Position, Is.EqualTo(GuildPosition.GuildMaster));
var context = this.PersistenceContextProvider.CreateNewGuildContext();
var newGuild = (await context.GetAsync<DataModel.Entities.Guild>().ConfigureAwait(false)).First(g => g.Name == "Foobar2");
Assert.That(newGuild.Members.Any(m => m.Id == this._player.SelectedCharacter!.Id), Is.True);
}
/// <summary>
/// Tests if the guild list request gets answered correctly.
/// </summary>
[Test]
public async ValueTask GetGuildListAsync()
{
await this.RequestGuildAndRespondAsync(true).ConfigureAwait(false);
var action = new GuildListRequestAction();
await action.RequestGuildListAsync(this._player).ConfigureAwait(false);
var guildList = await this.GuildServer.GetGuildListAsync(this._player.GuildStatus!.GuildId).ConfigureAwait(false);
Mock.Get(this._player.ViewPlugIns.GetPlugIn<IShowGuildListPlugIn>()!)
.Verify(v => v!.ShowGuildListAsync(
It.Is<IReadOnlyCollection<GuildListEntry>>(list => list.Any(entry => entry.PlayerName == this._player.SelectedCharacter!.Name)),
It.Is<Interfaces.Guild>(g => g.Name == GuildName)), Times.Once());
Assert.That(guildList.Any(entry => entry.PlayerName == this._player.SelectedCharacter!.Name), Is.True);
}
private async ValueTask RequestGuildAndRespondAsync(bool acceptRequest)
{
var guildRequestAction = new GuildRequestAction();
await guildRequestAction.RequestGuildAsync(this._player, this._guildMasterPlayer.Id).ConfigureAwait(false);
var guildResponseAction = new GuildRequestAnswerAction();
await guildResponseAction.AnswerRequestAsync(this._guildMasterPlayer, acceptRequest).ConfigureAwait(false);
}
private IGameServerContext CreateGameServer()
{
var gameConfiguration = new GameConfiguration();
gameConfiguration.Maps.Add(new GameMapDefinition());
var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var gameServer = new GameServerContext(
new GameServerDefinition { GameConfiguration = gameConfiguration, ServerConfiguration = new DataModel.Configuration.GameServerConfiguration() },
this.GuildServer,
new Mock<IEventPublisher>().Object,
new Mock<ILoginServer>().Object,
new Mock<IFriendServer>().Object,
new InMemoryPersistenceContextProvider(),
mapInitializer,
new NullLoggerFactory(),
new PlugInManager(new List<PlugIns.PlugInConfiguration>(), new NullLoggerFactory(), null, null),
NullDropGenerator.Instance,
new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameServer.PlugInManager;
mapInitializer.PathFinderPool = gameServer.PathFinderPool;
return gameServer;
}
}

View File

@@ -0,0 +1,601 @@
// <copyright file="GuildAllianceTest.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.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
using BasicModel = MUnique.OpenMU.Persistence.BasicModel;
/// <summary>
/// Unit tests for guild alliance and hostility logic in <see cref="MUnique.OpenMU.GuildServer.GuildServer"/>.
/// </summary>
[TestFixture]
public class GuildAllianceTest : GuildTestBase
{
private const string SecondGuildName = "SecondGuild";
private const string ThirdGuildName = "ThirdGuild";
private Character _secondGuildMaster = null!;
private Character _thirdGuildMaster = null!;
private uint _firstGuildId;
private uint _secondGuildId;
private uint _thirdGuildId;
/// <inheritdoc />
[SetUp]
public override async ValueTask SetupAsync()
{
await base.SetupAsync().ConfigureAwait(false);
var context = this.PersistenceContextProvider.CreateNewContext();
this._secondGuildMaster = context.CreateNew<Character>();
this._secondGuildMaster.Name = "SecondMaster";
this._thirdGuildMaster = context.CreateNew<Character>();
this._thirdGuildMaster.Name = "ThirdMaster";
await this.GuildServer.CreateGuildAsync(SecondGuildName, this._secondGuildMaster.Name, this._secondGuildMaster.Id, new byte[16], 0).ConfigureAwait(false);
await this.GuildServer.CreateGuildAsync(ThirdGuildName, this._thirdGuildMaster.Name, this._thirdGuildMaster.Id, new byte[16], 0).ConfigureAwait(false);
// Bring the first guild master back online (base.SetupAsync takes them offline)
// so that the first guild is in the in-memory dictionary and GetGuildIdByNameAsync can find it.
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, 0).ConfigureAwait(false);
this._firstGuildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
this._secondGuildId = await this.GuildServer.GetGuildIdByNameAsync(SecondGuildName).ConfigureAwait(false);
this._thirdGuildId = await this.GuildServer.GetGuildIdByNameAsync(ThirdGuildName).ConfigureAwait(false);
}
// -------------------------------------------------------------------------
// CreateAllianceAsync
// -------------------------------------------------------------------------
/// <summary>
/// Two online guilds can successfully form an alliance.
/// </summary>
[Test]
public async ValueTask CreateAlliance_Success()
{
var result = await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(AllianceCreationResult.Success));
}
/// <summary>
/// A guild that is already in an alliance cannot join another one.
/// </summary>
[Test]
public async ValueTask CreateAlliance_TargetAlreadyInAlliance_Fails()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
// Try to add the second guild (already in an alliance) to the third guild
var result = await this.GuildServer.CreateAllianceAsync(this._thirdGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(AllianceCreationResult.TargetGuildAlreadyInAlliance));
}
/// <summary>
/// The guild server does not enforce a maximum alliance size — that limit
/// is now applied at the action layer (<c>GuildRelationshipChangeAction</c>)
/// using the <c>Stats.MaximumAllianceSize</c> player attribute.
/// This test verifies that the server allows building an alliance larger than 5
/// (the old hard-coded constant).
/// </summary>
[Test]
public async ValueTask CreateAlliance_ServerDoesNotEnforceMaxSize()
{
// Add 6 guilds beyond the old hard-coded limit of 5 to confirm no server limit
const int beyondOldLimit = 6;
for (var i = 0; i < beyondOldLimit - 1; i++)
{
var memberName = $"FillGuildMaster{i}";
var fillContext = this.PersistenceContextProvider.CreateNewContext();
var master = fillContext.CreateNew<Character>();
master.Name = memberName;
var guildName = $"FillGuild{i}";
await this.GuildServer.CreateGuildAsync(guildName, memberName, master.Id, new byte[16], 0).ConfigureAwait(false);
await this.GuildServer.PlayerEnteredGameAsync(master.Id, memberName, 0).ConfigureAwait(false);
var fillGuildId = await this.GuildServer.GetGuildIdByNameAsync(guildName).ConfigureAwait(false);
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, fillGuildId).ConfigureAwait(false);
}
// Adding the sixth guild should still succeed — no server-side hard limit
var result = await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(AllianceCreationResult.Success));
}
// -------------------------------------------------------------------------
// IsAllianceMasterAsync
// -------------------------------------------------------------------------
/// <summary>
/// The guild that initiated the alliance is identified as the alliance master.
/// </summary>
[Test]
public async ValueTask IsAllianceMaster_MasterGuild_ReturnsTrue()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
var isMaster = await this.GuildServer.IsAllianceMasterAsync(this._firstGuildId).ConfigureAwait(false);
Assert.That(isMaster, Is.True);
}
/// <summary>
/// A member guild is not identified as the alliance master.
/// </summary>
[Test]
public async ValueTask IsAllianceMaster_MemberGuild_ReturnsFalse()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
var isMaster = await this.GuildServer.IsAllianceMasterAsync(this._secondGuildId).ConfigureAwait(false);
Assert.That(isMaster, Is.False);
}
// -------------------------------------------------------------------------
// GetAllianceGuildsAsync
// -------------------------------------------------------------------------
/// <summary>
/// GetAllianceGuildsAsync returns all guilds that are in the alliance.
/// </summary>
[Test]
public async ValueTask GetAllianceGuilds_ReturnsAllMembers()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._thirdGuildId).ConfigureAwait(false);
var guilds = await this.GuildServer.GetAllianceGuildsAsync(this._firstGuildId).ConfigureAwait(false);
Assert.That(guilds.Count, Is.EqualTo(3));
Assert.That(guilds.Select(g => g.Id), Is.EquivalentTo(new[] { this._firstGuildId, this._secondGuildId, this._thirdGuildId }));
}
/// <summary>
/// GetAllianceGuildsAsync returns an empty list when the guild has no alliance.
/// </summary>
[Test]
public async ValueTask GetAllianceGuilds_NoAlliance_ReturnsEmpty()
{
var guilds = await this.GuildServer.GetAllianceGuildsAsync(this._firstGuildId).ConfigureAwait(false);
Assert.That(guilds, Is.Empty);
}
// -------------------------------------------------------------------------
// RemoveAllianceGuildAsync
// -------------------------------------------------------------------------
/// <summary>
/// The alliance master can successfully remove a member guild.
/// </summary>
[Test]
public async ValueTask RemoveAllianceGuild_Member_Success()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._thirdGuildId).ConfigureAwait(false);
var removed = await this.GuildServer.RemoveAllianceAsync(this._secondGuildId).ConfigureAwait(false);
var guilds = await this.GuildServer.GetAllianceGuildsAsync(this._firstGuildId).ConfigureAwait(false);
Assert.That(removed, Is.True);
Assert.That(guilds.Select(g => g.Id), Does.Not.Contain(this._secondGuildId));
}
/// <summary>
/// The alliance master can successfully remove a member guild.
/// </summary>
[Test]
public async ValueTask RemoveAllianceGuild_Master_Disbands_Success()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._thirdGuildId).ConfigureAwait(false);
var removed = await this.GuildServer.RemoveAllianceAsync(this._firstGuildId).ConfigureAwait(false);
var guilds = await this.GuildServer.GetAllianceGuildsAsync(this._firstGuildId).ConfigureAwait(false);
Assert.That(removed, Is.True);
Assert.That(guilds, Is.Empty);
}
// -------------------------------------------------------------------------
// SetHostilityAsync / GetGuildRelationshipAsync
// -------------------------------------------------------------------------
/// <summary>
/// Creating hostility between two guilds yields a Rival relationship.
/// </summary>
[Test]
public async ValueTask SetHostility_Create_ReturnsRival()
{
var success = await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._secondGuildId, true).ConfigureAwait(false);
var relationship = await this.GuildServer.GetGuildRelationshipAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(relationship, Is.EqualTo(GuildRelationship.Rival));
}
/// <summary>
/// Cancelling a hostility between two solo guilds (no alliances) yields no relationship.
/// </summary>
[Test]
public async ValueTask SetHostility_Cancel_ReturnsNone()
{
await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._secondGuildId, true).ConfigureAwait(false);
await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._secondGuildId, false).ConfigureAwait(false);
var relationship = await this.GuildServer.GetGuildRelationshipAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(relationship, Is.EqualTo(GuildRelationship.None));
}
/// <summary>
/// Two guilds in the same alliance have a Union relationship.
/// </summary>
[Test]
public async ValueTask GetGuildRelationship_SameAlliance_ReturnsUnion()
{
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
var relationship = await this.GuildServer.GetGuildRelationshipAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(relationship, Is.EqualTo(GuildRelationship.Union));
}
/// <summary>
/// Two guilds with no shared alliance and no hostility have no relationship.
/// </summary>
[Test]
public async ValueTask GetGuildRelationship_NoRelation_ReturnsNone()
{
var relationship = await this.GuildServer.GetGuildRelationshipAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
Assert.That(relationship, Is.EqualTo(GuildRelationship.None));
}
// -------------------------------------------------------------------------
// SetHostilityAsync — AreAlliancesStillHostile guard (key bug-fix scenario)
// -------------------------------------------------------------------------
/// <summary>
/// When A↔X and B↔Y hostilities exist between two alliances, cancelling A↔X
/// must NOT notify game servers to remove all rival pairs, because B↔Y still
/// makes the alliances hostile.
///
/// Alliance A: guilds 1 and 2. Alliance X: guild 3.
/// After SetHostility(1, 3, false) the guild-2 ↔ guild-3 link still exists.
/// </summary>
[Test]
public async ValueTask SetHostility_Cancel_WithRemainingCrossAllianceHostility_DoesNotNotifyRemoval()
{
// Build Alliance A: guilds 1 and 2
await this.GuildServer.CreateAllianceAsync(this._firstGuildId, this._secondGuildId).ConfigureAwait(false);
// Hostility A↔X: guild 1 ↔ guild 3
await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._thirdGuildId, true).ConfigureAwait(false);
// Hostility B↔Y: guild 2 ↔ guild 3
await this.GuildServer.SetHostilityAsync(this._secondGuildId, this._thirdGuildId, true).ConfigureAwait(false);
// Reset the call counts recorded during the two SetHostility(create) calls
this.GameServer0.Invocations.Clear();
this.GameServer1.Invocations.Clear();
// Cancel only A↔X (guild 1 ↔ guild 3)
await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._thirdGuildId, false).ConfigureAwait(false);
// Game servers must NOT receive a removal notification because B↔Y (guild 2 ↔ guild 3)
// still makes all alliance members rivals.
this.GameServer0.Verify(
gs => gs.GuildHostilityChangedAsync(
It.IsAny<uint>(),
It.IsAny<IReadOnlyList<uint>>(),
It.IsAny<uint>(),
It.IsAny<IReadOnlyList<uint>>(),
false),
Times.Never);
}
/// <summary>
/// When the last remaining cross-alliance hostility is cancelled, game servers
/// ARE notified so they can remove the rival pairs from their caches.
/// </summary>
[Test]
public async ValueTask SetHostility_Cancel_LastHostility_NotifiesRemoval()
{
// Single hostility between two standalone guilds
await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._secondGuildId, true).ConfigureAwait(false);
this.GameServer0.Invocations.Clear();
this.GameServer1.Invocations.Clear();
await this.GuildServer.SetHostilityAsync(this._firstGuildId, this._secondGuildId, false).ConfigureAwait(false);
// Game servers must be notified that all hostility ended
this.GameServer0.Verify(
gs => gs.GuildHostilityChangedAsync(
It.IsAny<uint>(),
It.IsAny<IReadOnlyList<uint>>(),
It.IsAny<uint>(),
It.IsAny<IReadOnlyList<uint>>(),
false),
Times.Once);
}
/// <inheritdoc />
protected override void SetupGameServer(Mock<IGameServer> gameServer)
{
base.SetupGameServer(gameServer);
gameServer.Setup(gs => gs.GuildHostilityChangedAsync(
It.IsAny<uint>(),
It.IsAny<IReadOnlyList<uint>>(),
It.IsAny<uint>(),
It.IsAny<IReadOnlyList<uint>>(),
It.IsAny<bool>()))
.Returns(ValueTask.CompletedTask);
}
}
/// <summary>
/// Unit tests for the rival guild cache in <see cref="GameServerContext"/>.
/// </summary>
[TestFixture]
public class RivalGuildCacheTest
{
private GameServerContext _gameServerContext = null!;
/// <summary>
/// Sets up a minimal <see cref="GameServerContext"/> for each test.
/// </summary>
[SetUp]
public void Setup()
{
this._gameServerContext = CreateMinimalGameServerContext();
}
/// <summary>
/// After adding a hostility the two guilds are reported as rivals.
/// </summary>
[Test]
public void UpdateGuildHostility_Create_GuildsAreRivals()
{
const uint guildA = 1;
const uint guildB = 2;
this._gameServerContext.UpdateGuildHostility(guildA, [guildA], guildB, [guildB], true);
Assert.That(this._gameServerContext.AreGuildsRival(guildA, guildB), Is.True);
}
/// <summary>
/// After removing a hostility the two guilds are no longer rivals.
/// </summary>
[Test]
public void UpdateGuildHostility_Remove_GuildsAreNoLongerRivals()
{
const uint guildA = 1;
const uint guildB = 2;
this._gameServerContext.UpdateGuildHostility(guildA, [guildA], guildB, [guildB], true);
this._gameServerContext.UpdateGuildHostility(guildA, [guildA], guildB, [guildB], false);
Assert.That(this._gameServerContext.AreGuildsRival(guildA, guildB), Is.False);
}
/// <summary>
/// Guild ID order does not matter: (A, B) and (B, A) both return the same result.
/// </summary>
[Test]
public void AreGuildsRival_IdOrderIsNormalized()
{
const uint guildA = 5;
const uint guildB = 3; // B < A intentionally
this._gameServerContext.UpdateGuildHostility(guildA, [guildA], guildB, [guildB], true);
Assert.That(this._gameServerContext.AreGuildsRival(guildA, guildB), Is.True);
Assert.That(this._gameServerContext.AreGuildsRival(guildB, guildA), Is.True);
}
/// <summary>
/// When alliances are expanded, every cross-alliance pair is cached.
/// Alliance A = {1, 2}, Alliance X = {3, 4}.
/// All four cross-pairs (1↔3, 1↔4, 2↔3, 2↔4) should be rivals.
/// </summary>
[Test]
public void UpdateGuildHostility_AllianceExpansion_AllPairsAreCached()
{
uint[] allianceA = [1, 2];
uint[] allianceX = [3, 4];
this._gameServerContext.UpdateGuildHostility(1, allianceA, 3, allianceX, true);
foreach (var idA in allianceA)
{
foreach (var idX in allianceX)
{
Assert.That(this._gameServerContext.AreGuildsRival(idA, idX), Is.True,
$"Expected guilds {idA} and {idX} to be rivals.");
}
}
}
/// <summary>
/// Guilds that are not in the rival cache are not considered rivals.
/// </summary>
[Test]
public void AreGuildsRival_UnrelatedGuilds_ReturnsFalse()
{
Assert.That(this._gameServerContext.AreGuildsRival(100, 200), Is.False);
}
private static GameServerContext CreateMinimalGameServerContext()
{
var persistenceProvider = new InMemoryPersistenceContextProvider();
var gameConfiguration = new BasicModel.GameConfiguration();
gameConfiguration.Maps.Add(new BasicModel.GameMapDefinition());
var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var ctx = new GameServerContext(
new BasicModel.GameServerDefinition
{
GameConfiguration = gameConfiguration,
ServerConfiguration = new BasicModel.GameServerConfiguration(),
},
new Mock<IGuildServer>().Object,
new Mock<IEventPublisher>().Object,
new Mock<ILoginServer>().Object,
new Mock<IFriendServer>().Object,
persistenceProvider,
mapInitializer,
new NullLoggerFactory(),
new PlugInManager([], new NullLoggerFactory(), null, null),
NullDropGenerator.Instance,
new ConfigurationChangeMediator());
mapInitializer.PlugInManager = ctx.PlugInManager;
mapInitializer.PathFinderPool = ctx.PathFinderPool;
return ctx;
}
}
/// <summary>
/// Tests that rival guild members can fight each other without PK consequences and
/// without triggering self-defense.
/// </summary>
[TestFixture]
public class RivalGuildCombatTest
{
private GameServerContext _gameServerContext = null!;
private Player _killer = null!;
private Player _victim = null!;
private const uint KillerGuildId = 10;
private const uint VictimGuildId = 20;
/// <summary>
/// Creates two players in a game server context for each test.
/// </summary>
[SetUp]
public async ValueTask SetupAsync()
{
var persistenceProvider = new InMemoryPersistenceContextProvider();
var gameConfiguration = new BasicModel.GameConfiguration();
gameConfiguration.Maps.Add(new BasicModel.GameMapDefinition());
var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
this._gameServerContext = new GameServerContext(
new BasicModel.GameServerDefinition
{
GameConfiguration = gameConfiguration,
ServerConfiguration = new BasicModel.GameServerConfiguration(),
},
new Mock<IGuildServer>().Object,
new Mock<IEventPublisher>().Object,
new Mock<ILoginServer>().Object,
new Mock<IFriendServer>().Object,
persistenceProvider,
mapInitializer,
new NullLoggerFactory(),
new PlugInManager([], new NullLoggerFactory(), null, null),
NullDropGenerator.Instance,
new ConfigurationChangeMediator());
mapInitializer.PlugInManager = this._gameServerContext.PlugInManager;
mapInitializer.PathFinderPool = this._gameServerContext.PathFinderPool;
this._killer = await PlayerTestHelper.CreatePlayerAsync(this._gameServerContext).ConfigureAwait(false);
this._victim = await PlayerTestHelper.CreatePlayerAsync(this._gameServerContext).ConfigureAwait(false);
await this._killer.CurrentMap!.AddAsync(this._victim).ConfigureAwait(false);
this._killer.GuildStatus = new GuildMemberStatus(KillerGuildId, GuildPosition.GuildMaster);
this._victim.GuildStatus = new GuildMemberStatus(VictimGuildId, GuildPosition.GuildMaster);
}
// -------------------------------------------------------------------------
// PK state bypass
// -------------------------------------------------------------------------
/// <summary>
/// Killing a rival guild member does not change the killer's hero state or PK count.
/// </summary>
[Test]
public async ValueTask KillRivalGuildMember_DoesNotIncrementPkCount()
{
this._gameServerContext.UpdateGuildHostility(KillerGuildId, [KillerGuildId], VictimGuildId, [VictimGuildId], true);
var initialState = this._killer.SelectedCharacter!.State;
await InvokeAfterKilledPlayerAsync(this._killer, this._victim).ConfigureAwait(false);
Assert.That(this._killer.SelectedCharacter.State, Is.EqualTo(initialState),
"Hero state should not change when killing a rival guild member.");
Assert.That(this._killer.SelectedCharacter.PlayerKillCount, Is.Zero,
"PK count should not increase when killing a rival guild member.");
}
/// <summary>
/// Killing a non-rival guild member DOES increment the killer's hero state.
/// </summary>
[Test]
public async ValueTask KillNonRivalGuildMember_IncrementsHeroState()
{
// guilds are NOT rivals — no UpdateGuildHostility call
var initialState = this._killer.SelectedCharacter!.State;
await InvokeAfterKilledPlayerAsync(this._killer, this._victim).ConfigureAwait(false);
Assert.That(this._killer.SelectedCharacter.State, Is.GreaterThan(initialState),
"Hero state should increase when killing a non-rival player.");
}
// -------------------------------------------------------------------------
// Self-defense bypass
// -------------------------------------------------------------------------
/// <summary>
/// Hitting a rival guild member does not initiate a self-defense state on the victim.
/// </summary>
[Test]
public void HitRivalGuildMember_DoesNotInitiateSelfDefense()
{
this._gameServerContext.UpdateGuildHostility(KillerGuildId, [KillerGuildId], VictimGuildId, [VictimGuildId], true);
var plugIn = new SelfDefensePlugIn();
plugIn.AttackableGotHit(this._victim, this._killer, new HitInfo(100, 0, DamageAttributes.Undefined));
Assert.That(this._gameServerContext.SelfDefenseState, Is.Empty,
"No self-defense should be initiated between rival guild members.");
}
/// <summary>
/// Hitting a non-rival guild member DOES initiate a self-defense state on the victim.
/// </summary>
[Test]
public void HitNonRivalGuildMember_InitiatesSelfDefense()
{
// guilds are NOT rivals
var plugIn = new SelfDefensePlugIn();
plugIn.AttackableGotHit(this._victim, this._killer, new HitInfo(100, 0, DamageAttributes.Undefined));
Assert.That(this._gameServerContext.SelfDefenseState, Is.Not.Empty,
"Self-defense should be initiated when hit by a non-rival player.");
}
/// <summary>
/// Invokes <c>AfterKilledPlayerAsync</c> on <paramref name="killer"/>
/// passing <paramref name="killedPlayer"/> as the argument.
/// </summary>
private static async ValueTask InvokeAfterKilledPlayerAsync(Player killer, Player killedPlayer)
{
await killer.AfterKilledPlayerAsync(killedPlayer).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,87 @@
// <copyright file="GuildServerTest.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 Moq;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Tests for the guild server.
/// </summary>
public class GuildServerTest : GuildTestBase
{
/// <summary>
/// Tests if the entrance of guild members is registered correctly in the guild member list.
/// </summary>
[Test]
public async ValueTask GuildMemberEnterGameAsync()
{
const byte serverId = 1;
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, serverId).ConfigureAwait(false);
var guildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
var guildMaster = (await this.GuildServer.GetGuildListAsync(guildId).ConfigureAwait(false)).First();
Assert.That(guildMaster.ServerId, Is.EqualTo(serverId));
this.GameServer1.Verify(g => g.AssignGuildToPlayerAsync(this.GuildMaster.Name, It.Is<GuildMemberStatus>(s => s.GuildId == guildId && s.Position == GuildPosition.GuildMaster)));
}
/// <summary>
/// Tests if the exit of the last guild member removes (not deletes ;)) the guild from the guild server.
/// </summary>
[Test]
public async ValueTask LastGuildMemberLeaveGameAsync()
{
const byte serverId = 1;
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, serverId).ConfigureAwait(false);
var guildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
await this.GuildServer.GuildMemberLeftGameAsync(guildId, this.GuildMaster.Id, serverId).ConfigureAwait(false);
var guildList = await this.GuildServer.GetGuildListAsync(guildId).ConfigureAwait(false); // guild id is invalid now
Assert.That(guildList, Is.Empty);
}
/// <summary>
/// Tests if the exit of guild members is registered correctly in the guild member list.
/// </summary>
[Test]
public async ValueTask GuildMemberLeaveGameAsync()
{
const byte serverId = 1;
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, serverId).ConfigureAwait(false);
var guildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
await this.GuildServer.CreateGuildMemberAsync(guildId, Guid.Empty, "TestMember", GuildPosition.NormalMember, serverId).ConfigureAwait(false);
await this.GuildServer.GuildMemberLeftGameAsync(guildId, this.GuildMaster.Id, serverId).ConfigureAwait(false);
var guildMaster = (await this.GuildServer.GetGuildListAsync(guildId).ConfigureAwait(false)).First(m => m.PlayerPosition == GuildPosition.GuildMaster);
Assert.That(guildMaster.ServerId, Is.EqualTo(OpenMU.GuildServer.GuildServer.OfflineServerId));
}
/// <summary>
/// Tests if the removal of the whole guild is forwarded to all game servers when the guild master kicks himself.
/// </summary>
[Test]
public async ValueTask GuildPlayerKickDeletesGuildAsync()
{
const byte serverId = 1;
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, serverId).ConfigureAwait(false);
var guildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
await this.GuildServer.KickMemberAsync(guildId, this.GuildMaster.Name).ConfigureAwait(false);
this.GameServer1.Verify(g => g.GuildDeletedAsync(guildId), Times.Once);
}
/// <summary>
/// Tests if the removal of guild members is forwarded to all game servers.
/// </summary>
[Test]
public async ValueTask GuildPlayerKickRemovesPlayerFromGuildAsync()
{
const byte serverId = 1;
const string testMemberName = "TestMember";
await this.GuildServer.PlayerEnteredGameAsync(this.GuildMaster.Id, this.GuildMaster.Name, serverId).ConfigureAwait(false);
var guildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
await this.GuildServer.CreateGuildMemberAsync(guildId, Guid.Empty, testMemberName, GuildPosition.NormalMember, serverId).ConfigureAwait(false);
await this.GuildServer.KickMemberAsync(guildId, testMemberName).ConfigureAwait(false);
this.GameServer1.Verify(g => g.GuildPlayerKickedAsync(testMemberName), Times.Once);
}
}

View File

@@ -0,0 +1,93 @@
// <copyright file="GuildTestBase.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.DataModel.Entities;
using MUnique.OpenMU.GuildServer;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Persistence.InMemory;
/// <summary>
/// Base class for guild related tests.
/// </summary>
public class GuildTestBase
{
/// <summary>
/// The default guild name used in tests.
/// </summary>
protected const string GuildName = "Foobar";
/// <summary>
/// Gets or sets the first game server.
/// </summary>
protected Mock<IGameServer> GameServer0 { get; set; } = null!;
/// <summary>
/// Gets or sets the second game server.
/// </summary>
protected Mock<IGameServer> GameServer1 { get; set; } = null!;
/// <summary>
/// Gets or sets the repository provider.
/// </summary>
protected IPersistenceContextProvider PersistenceContextProvider { get; set; } = null!;
/// <summary>
/// Gets or sets the game servers.
/// </summary>
protected IDictionary<int, IGameServer> GameServers { get; set; } = null!;
/// <summary>
/// Gets or sets the guild server.
/// </summary>
protected IGuildServer GuildServer { get; set; } = null!;
/// <summary>
/// Gets or sets the guild master.
/// </summary>
protected Character GuildMaster { get; set; } = null!;
/// <summary>
/// Setups the test objects.
/// </summary>
[SetUp]
public virtual async ValueTask SetupAsync()
{
this.GameServer0 = new Mock<IGameServer>();
this.GameServer1 = new Mock<IGameServer>();
this.PersistenceContextProvider = new InMemoryPersistenceContextProvider();
this.GuildMaster = this.GetGuildMaster();
this.SetupGameServer(this.GameServer0);
this.SetupGameServer(this.GameServer1);
this.GameServers = new Dictionary<int, IGameServer> { { 0, this.GameServer0.Object }, { 1, this.GameServer1.Object } };
this.GuildServer = new OpenMU.GuildServer.GuildServer(new GuildChangeToGameServerPublisher(this.GameServers), this.PersistenceContextProvider, new NullLogger<GuildServer>());
await this.GuildServer.CreateGuildAsync(GuildName, this.GuildMaster.Name, this.GuildMaster.Id, new byte[16], 0).ConfigureAwait(false);
var guildId = await this.GuildServer.GetGuildIdByNameAsync(GuildName).ConfigureAwait(false);
await this.GuildServer.GuildMemberLeftGameAsync(guildId, this.GuildMaster.Id, 0).ConfigureAwait(false);
}
/// <summary>
/// Sets up the game server.
/// </summary>
/// <param name="gameServer">The game server.</param>
protected virtual void SetupGameServer(Mock<IGameServer> gameServer)
{
// can be overwritten.
}
private Character GetGuildMaster()
{
var context = this.PersistenceContextProvider.CreateNewContext();
var master = context.CreateNew<Character>();
master.Name = "GuildMaster";
return master;
}
}

View File

@@ -0,0 +1,418 @@
// <copyright file="ItemConsumptionTest.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 Moq;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.GameLogic.Views;
/// <summary>
/// Tests the item consumption action.
/// </summary>
[TestFixture]
public class ItemConsumptionTest
{
private const int ItemSlot = 12;
/// <summary>
/// Tests the jewel of bless consume.
/// </summary>
/// <param name="itemLevel">The item level.</param>
/// <param name="consumptionExpectation">if set to <c>true</c>, the item consumption is expected.</param>
[TestCase(0, true)]
[TestCase(1, true)]
[TestCase(2, true)]
[TestCase(3, true)]
[TestCase(4, true)]
[TestCase(5, true)]
[TestCase(6, false)]
[TestCase(7, false)]
public async ValueTask JewelOfBlessAsync(byte itemLevel, bool consumptionExpectation)
{
var consumeHandler = new BlessJewelConsumeHandlerPlugIn();
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var upgradeableItem = this.GetItemWithPossibleOption();
upgradeableItem.Level = itemLevel;
var upgradableItemSlot = (byte)(ItemSlot + 1);
await player.Inventory!.AddItemAsync(upgradableItemSlot, upgradeableItem).ConfigureAwait(false);
var bless = this.GetItem();
await player.Inventory.AddItemAsync(ItemSlot, bless).ConfigureAwait(false);
bless.Durability = 1;
var consumed = await consumeHandler.ConsumeItemAsync(player, bless, upgradeableItem, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(consumed, Is.EqualTo(consumptionExpectation));
Assert.That(upgradeableItem.Level, consumed ? Is.EqualTo(itemLevel + 1) : Is.EqualTo(itemLevel));
}
/// <summary>
/// Tests the jewel of soul consumption.
/// </summary>
/// <param name="itemLevel">The item level before consuming the jewel of soul.</param>
/// <param name="consumptionExpectation">If set to <c>true</c>, the consumption of the jewel of soul is expected.</param>
/// <param name="success">If set to <c>true</c>, the randomizer returns <c>true</c> when asked about wether the item level should be increased. However, it doesn't have any effect if the item is already level 9 or higher.</param>
/// <param name="expectedItemLevel">The expected item level after trying to consume the jewel of soul.</param>
[TestCase(0, true, true, 1)]
[TestCase(1, true, true, 2)]
[TestCase(2, true, true, 3)]
[TestCase(3, true, true, 4)]
[TestCase(4, true, true, 5)]
[TestCase(5, true, true, 6)]
[TestCase(6, true, true, 7)]
[TestCase(7, true, true, 8)]
[TestCase(8, true, true, 9)]
[TestCase(9, false, true, 9)]
[TestCase(10, false, true, 10)]
[TestCase(11, false, true, 11)]
[TestCase(12, false, true, 12)]
[TestCase(13, false, true, 13)]
[TestCase(14, false, true, 14)]
[TestCase(15, false, true, 15)]
[TestCase(0, true, false, 0)]
[TestCase(1, true, false, 0)]
[TestCase(2, true, false, 1)]
[TestCase(3, true, false, 2)]
[TestCase(4, true, false, 3)]
[TestCase(5, true, false, 4)]
[TestCase(6, true, false, 5)]
[TestCase(7, true, false, 0)]
[TestCase(8, true, false, 0)]
public async ValueTask JewelOfSoulAsync(byte itemLevel, bool consumptionExpectation, bool success, byte expectedItemLevel)
{
var randomizer = new Mock<IRandomizer>();
randomizer.Setup(r => r.NextRandomBool(50)).Returns(success);
var consumeHandler = new SoulJewelConsumeHandlerPlugIn(randomizer.Object);
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var upgradeableItem = this.GetItemWithPossibleOption();
upgradeableItem.Level = itemLevel;
var upgradableItemSlot = (byte)(ItemSlot + 1);
await player.Inventory!.AddItemAsync(upgradableItemSlot, upgradeableItem).ConfigureAwait(false);
var soul = this.GetItem();
await player.Inventory.AddItemAsync(ItemSlot, soul).ConfigureAwait(false);
soul.Durability = 1;
var consumed = await consumeHandler.ConsumeItemAsync(player, soul, upgradeableItem, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(consumed, Is.EqualTo(consumptionExpectation));
Assert.That(upgradeableItem.Level, Is.EqualTo(expectedItemLevel));
}
/// <summary>
/// Test if the jewel of life consumption increases the item option level by 1 until the maximum level is reached.
/// </summary>
/// <param name="numberOfOptions">The number of options.</param>
/// <param name="consumptionExpectation">If set to <c>true</c>, the item consumption is expected; Otherwise, not.</param>
[TestCase(1, true)]
[TestCase(2, true)]
[TestCase(3, true)]
[TestCase(4, true)]
[TestCase(5, false)]
public async ValueTask JewelOfLifeAsync(int numberOfOptions, bool consumptionExpectation)
{
var consumeHandler = new LifeJewelConsumeHandlerPlugIn();
consumeHandler.Configuration.SuccessChance = 1;
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var upgradeableItem = this.GetItemWithPossibleOption();
var upgradableItemSlot = (byte)(ItemSlot + 1);
await player.Inventory!.AddItemAsync(upgradableItemSlot, upgradeableItem).ConfigureAwait(false);
bool jolConsumed = false;
for (int i = 0; i < numberOfOptions; i++)
{
var item = this.GetItem();
await player.Inventory.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
item.Durability = 1;
jolConsumed = await consumeHandler.ConsumeItemAsync(player, item, upgradeableItem, FruitUsage.Undefined).ConfigureAwait(false);
}
Assert.That(jolConsumed, Is.EqualTo(consumptionExpectation));
if (jolConsumed)
{
Assert.That(upgradeableItem.ItemOptions.Count, Is.EqualTo(1));
Assert.That(upgradeableItem.ItemOptions.First().Level, Is.EqualTo(numberOfOptions));
}
}
/// <summary>
/// Tests if a failed Jewel of life removes the option at any level.
/// </summary>
/// <param name="numberOfOptions">The number of options.</param>
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public async ValueTask JewelOfLifeFailRemovesOptionAsync(int numberOfOptions)
{
var consumeHandler = new LifeJewelConsumeHandlerPlugIn();
consumeHandler.Configuration.SuccessChance = 1;
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var upgradeableItem = this.GetItemWithPossibleOption();
var upgradableItemSlot = (byte)(ItemSlot + 1);
await player.Inventory!.AddItemAsync(upgradableItemSlot, upgradeableItem).ConfigureAwait(false);
for (int i = 0; i < numberOfOptions; i++)
{
var jol1 = this.GetItem();
await player.Inventory.AddItemAsync(ItemSlot, jol1).ConfigureAwait(false);
jol1.Durability = 1;
await consumeHandler.ConsumeItemAsync(player, jol1, upgradeableItem, FruitUsage.Undefined).ConfigureAwait(false);
}
Assert.That(upgradeableItem.ItemOptions.Count, Is.EqualTo(1));
// then adding fails, so option needs to be removed
consumeHandler.Configuration.SuccessChance = 0;
var jol2 = this.GetItem();
await player.Inventory.AddItemAsync(ItemSlot, jol2).ConfigureAwait(false);
jol2.Durability = 1;
var jolConsumed = await consumeHandler.ConsumeItemAsync(player, jol2, upgradeableItem, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(jolConsumed, Is.True);
Assert.That(upgradeableItem.ItemOptions.Count, Is.EqualTo(0));
}
/// <summary>
/// Tests the jewel of harmony consume.
/// </summary>
public void JewelOfHarmony()
{
Assert.That(true, Is.False);
}
/// <summary>
/// Tests the refine stone consume.
/// </summary>
public void RefineStone()
{
// refine stone consume handler is not implemented yet
Assert.That(true, Is.False);
}
/// <summary>
/// Tests the complex potion consume.
/// </summary>
public void ComplexPotion()
{
// complex potion consume handler is not implemented yet
Assert.That(true, Is.False);
}
/// <summary>
/// Tests the shield potion consume.
/// </summary>
[Test]
public async ValueTask ShieldPotionAsync()
{
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
var consumeHandler = new LargeShieldPotionConsumeHandlerPlugIn();
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(player.Attributes!.GetValueOfAttribute(Stats.CurrentShield), Is.GreaterThan(0.0f));
}
/// <summary>
/// Tests if the consumption fails because of the player state.
/// </summary>
[Test]
public async ValueTask FailByWrongPlayerStateAsync()
{
var consumeHandler = new AlcoholConsumeHandlerPlugIn();
var player = await this.GetPlayerAsync().ConfigureAwait(false);
await player.PlayerState.TryAdvanceToAsync(PlayerState.TradeRequested).ConfigureAwait(false);
var item = this.GetItem();
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.False);
}
/// <summary>
/// Tests if the consumption of the item decreases its durability by one.
/// </summary>
[Test]
public async ValueTask ItemDurabilityDecreaseAsync()
{
var consumeHandler = new LargeShieldPotionConsumeHandlerPlugIn();
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
item.Durability = 3;
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(item.Durability, Is.EqualTo(2));
Assert.That(player.Inventory.Items.Any(), Is.True);
}
/// <summary>
/// Tests if the consumption of the item not causes the removal of the item, when the durability reaches 0.
/// The removal is handled in the <see cref="ItemConsumeAction"/>.
/// </summary>
[Test]
public async ValueTask ItemRemovalAsync()
{
var consumeHandler = new LargeShieldPotionConsumeHandlerPlugIn();
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(item.Durability, Is.EqualTo(0));
Assert.That(player.Inventory.Items.Any(), Is.True);
}
/// <summary>
/// Tests if the consumption of the alcohol fails when the item has no durability anymore.
/// </summary>
[Test]
public async ValueTask DrinkAlcoholFailAsync()
{
var consumeHandler = new AlcoholConsumeHandlerPlugIn();
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
item.Durability = 0;
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.False);
Mock.Get(player.ViewPlugIns.GetPlugIn<IConsumeSpecialItemPlugIn>()!).Verify(view => view!.ConsumeSpecialItemAsync(item, 80), Times.Never);
}
/// <summary>
/// Tests if the consumption of alcohol works and is forwarded to the player view.
/// </summary>
[Test]
public async ValueTask DrinkAlcoholSuccessAsync()
{
var consumeHandler = new AlcoholConsumeHandlerPlugIn();
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
item.Definition!.ConsumeEffect = new Persistence.BasicModel.MagicEffectDefinition
{
Duration = new Persistence.BasicModel.PowerUpDefinitionValue
{
ConstantValue = { Value = 80 }
},
PowerUpDefinitions =
{
new Persistence.BasicModel.PowerUpDefinition
{
TargetAttribute = Stats.AttackSpeedAny,
Boost = new Persistence.BasicModel.PowerUpDefinitionValue
{
ConstantValue = { Value = 20 }
}
}
}
};
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.True);
Mock.Get(player.ViewPlugIns.GetPlugIn<IConsumeSpecialItemPlugIn>()!).Verify(view => view!.ConsumeSpecialItemAsync(item, 80), Times.Once);
}
/// <summary>
/// Tests the health recover by drinking a health potion.
/// </summary>
[Test]
public async ValueTask HealthRecoverAsync()
{
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
var consumeHandler = new LargeHealthPotionConsumeHandlerPlugIn();
consumeHandler.Configuration = consumeHandler.CreateDefaultConfig() as RecoverConsumeHandlerConfiguration;
consumeHandler.Configuration!.RecoverSteps.Clear(); // When there are no steps, we recover all immediately.
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(player.Attributes!.GetValueOfAttribute(Stats.CurrentHealth), Is.GreaterThan(0.0f));
}
/// <summary>
/// Tests the mana recover by drinking a mana potion.
/// </summary>
[Test]
public async ValueTask ManaRecoverAsync()
{
var player = await this.GetPlayerAsync().ConfigureAwait(false);
var item = this.GetItem();
await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false);
var consumeHandler = new LargeManaPotionConsumeHandler();
var success = await consumeHandler.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(player.Attributes!.GetValueOfAttribute(Stats.CurrentMana), Is.GreaterThan(0.0f));
}
private Item GetItem()
{
return new()
{
Definition = new DataModel.Configuration.Items.ItemDefinition { Width = 1, Height = 1 },
Durability = 1,
};
}
private async ValueTask<Player> GetPlayerAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.Attributes.Add(new StatAttribute(Stats.Level, 100));
player.SelectedCharacter.Attributes.Add(new StatAttribute(Stats.CurrentHealth, 0));
player.SelectedCharacter.Attributes.Add(new StatAttribute(Stats.CurrentMana, 0));
player.SelectedCharacter.Attributes.Add(new StatAttribute(Stats.CurrentShield, 0));
return player;
}
private Item GetItemWithPossibleOption()
{
var item = new Mock<Item>();
item.SetupAllProperties();
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
var definition = new Mock<ItemDefinition>();
definition.SetupAllProperties();
definition.Setup(d => d.PossibleItemOptions).Returns(new List<ItemOptionDefinition>());
definition.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
definition.Object.MaximumItemLevel = 15;
var itemSlot = new Mock<ItemSlotType>();
itemSlot.Setup(s => s.ItemSlots).Returns(new List<int> { InventoryConstants.LeftHandSlot });
definition.Setup(d => d.ItemSlot).Returns(itemSlot.Object);
item.Object.Definition = definition.Object;
item.Object.Durability = 1;
item.Object.Definition.Width = 1;
item.Object.Definition.Height = 2;
var option = new Mock<ItemOptionDefinition>();
option.SetupAllProperties();
option.Setup(o => o.PossibleOptions).Returns(new List<IncreasableItemOption>());
option.Object.MaximumOptionsPerItem = 4;
option.Object.AddsRandomly = true;
option.Name = "Damage Option";
var possibleOption = new Mock<IncreasableItemOption>();
possibleOption.SetupAllProperties();
possibleOption.Setup(o => o.LevelDependentOptions).Returns(new List<ItemOptionOfLevel>());
possibleOption.Object.OptionType = ItemOptionTypes.Option;
option.Object.PossibleOptions.Add(possibleOption.Object);
for (int level = 1; level <= 4; level++)
{
var levelDependentOption = new ItemOptionOfLevel();
levelDependentOption.Level = level;
possibleOption.Object.LevelDependentOptions.Add(levelDependentOption);
}
item.Object.Definition.PossibleItemOptions.Add(option.Object);
return item.Object;
}
}

View File

@@ -0,0 +1,440 @@
// <copyright file="ItemPriceCalculatorTest.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 Moq;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// Tests the <see cref="ItemPriceCalculator"/> with some exemplary data.
/// </summary>
/// <remarks>
/// The most price values here are directly taken from stores on GMO.
/// However, I guess they are calculated and shown by the client, if you just show such an item in the merchant store.
/// </remarks>
[TestFixture]
public class ItemPriceCalculatorTest
{
/// <summary>
/// The calculator which is tested.
/// </summary>
private readonly ItemPriceCalculator _calculator = new();
/// <summary>
/// Tests if the apple price is calculated correctly.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 20)]
[TestCase(1, 40)]
public void Apple(byte level, int price)
{
this.CheckPrice(0, 1, 1, 1, 1, 14, 5, level, price);
}
/// <summary>
/// Tests if the small heal potion price is calculated correctly.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 80)]
[TestCase(1, 160)]
public void SmallHealPotion(byte level, int price)
{
this.CheckPrice(1, 40, 1, 1, 1, 14, 10, level, price);
}
/// <summary>
/// Tests if the heal potion price is calculated correctly.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 330)]
[TestCase(1, 660)]
public void HealPotion(byte level, int price)
{
this.CheckPrice(2, 40, 1, 1, 1, 14, 20, level, price);
}
/// <summary>
/// Tests if the large heal potion price is calculated correctly.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 1500)]
[TestCase(1, 3000)]
public void LargeHealPotion(byte level, int price)
{
this.CheckPrice(3, 40, 1, 1, 1, 14, 30, level, price);
}
/// <summary>
/// Tests if the small shield potion price is calculated correctly.
/// </summary>
[Test]
public void SmallShieldPotion()
{
this.CheckPrice(35, 40, 1, 1, 1, 14, 50, 0, 2000);
}
/// <summary>
/// Tests if the shield potion price is calculated correctly.
/// </summary>
[Test]
public void ShieldPotion()
{
this.CheckPrice(36, 40, 1, 1, 1, 14, 80, 0, 4000);
}
/// <summary>
/// Tests if the shield potion price is calculated correctly.
/// </summary>
[Test]
public void LargeShieldPotion()
{
this.CheckPrice(37, 40, 1, 1, 1, 14, 100, 0, 6000);
}
/// <summary>
/// Tests if the bolt price is calculated correctly.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 100)]
[TestCase(1, 1400)]
[TestCase(2, 2200)]
public void Bolts(byte level, int price)
{
this.CheckPrice(7, 0, 255, 1, 1, 4, 0, level, price);
}
/// <summary>
/// Tests if the arrow price is calculated correctly.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 70)]
[TestCase(1, 1200)]
[TestCase(2, 2000)]
public void Arrows(byte level, int price)
{
this.CheckPrice(15, 0, 255, 1, 1, 4, 0, level, price);
}
/// <summary>
/// Tests if the price of the fireball scroll is calculated as 300.
/// </summary>
[Test]
public void FireballScroll()
{
this.CheckPrice(3, 0, 1, 1, 1, 15, 300, 0, 300);
}
/// <summary>
/// Tests if the price of the powerwave scroll is calculated as 1100.
/// </summary>
[Test]
public void PowerwaveScroll()
{
this.CheckPrice(10, 0, 1, 1, 1, 15, 1100, 0, 1100);
}
/// <summary>
/// Tests if the price of the lightning scroll is calculated as 3000.
/// </summary>
[Test]
public void LightningScroll()
{
this.CheckPrice(2, 0, 1, 1, 1, 15, 3000, 0, 3000);
}
/// <summary>
/// Tests if the price of the meteorite scroll is calculated as 11000.
/// </summary>
[Test]
public void MeteoriteScroll()
{
this.CheckPrice(1, 0, 1, 1, 1, 15, 11000, 0, 11000);
}
/// <summary>
/// Tests if the price of the teleport scroll is calculated as 5000.
/// </summary>
[Test]
public void TeleportScroll()
{
this.CheckPrice(5, 0, 1, 1, 1, 15, 5000, 0, 5000);
}
/// <summary>
/// Tests if the price of the ice scroll is calculated as 14000.
/// </summary>
[Test]
public void IceScroll()
{
this.CheckPrice(6, 0, 1, 1, 1, 15, 14000, 0, 14000);
}
/// <summary>
/// Tests if the price of the poison scroll is calculated as 17000.
/// </summary>
[Test]
public void PoisonScroll()
{
this.CheckPrice(0, 0, 1, 1, 1, 15, 17000, 0, 17000);
}
/// <summary>
/// Tests if the items of a pad set +0+4+Luck is calculated correctly.
/// </summary>
/// <param name="group">The group.</param>
/// <param name="dropLevel">The drop level.</param>
/// <param name="maxDurability">The maximum durability.</param>
/// <param name="price">The price.</param>
/// <remarks>
/// pad helm+0+4+l 480
/// armor 1400
/// pants 960
/// gloves 290
/// boots 370.
/// </remarks>
[TestCase(7, 5, 28, 480, Description = "Pad Helm")]
[TestCase(8, 10, 28, 1400, Description = "Pad Armor")]
[TestCase(9, 8, 28, 960, Description = "Pad Pants")]
[TestCase(10, 3, 28, 290, Description = "Pad Gloves")]
[TestCase(11, 4, 28, 370, Description = "Pad Boots")]
public void PadSetItem_0_4_Luck(byte group, byte dropLevel, byte maxDurability, long price)
{
this.CheckPrice(2, dropLevel, maxDurability, 2, 2, group, 0, 0, price, true, true);
}
/// <summary>
/// Tests if the items of a bone set +2+4+Luck is calculated correctly.
/// </summary>
/// <param name="group">The group.</param>
/// <param name="dropLevel">The drop level.</param>
/// <param name="maxDurability">The maximum durability.</param>
/// <param name="price">The price.</param>
/// <remarks>
/// bone helm+2+4+l 9400
/// armor 13500
/// pants 11300
/// gloves 6200
/// boots 7700.
/// </remarks>
[TestCase(7, 18, 30, 9400, Description = "Bone Helm")]
[TestCase(8, 22, 30, 13500, Description = "Bone Armor")]
[TestCase(9, 20, 30, 11300, Description = "Bone Pants")]
[TestCase(10, 14, 30, 6200, Description = "Bone Gloves")]
[TestCase(11, 16, 30, 7700, Description = "Bone Boots")]
public void BoneSetItem_2_4_Luck(byte group, byte dropLevel, byte maxDurability, long price)
{
this.CheckPrice(4, dropLevel, maxDurability, 2, 2, group, 0, 2, price, true, true);
}
/// <summary>
/// Tests if the items of a sphinx set +3+4+Luck is calculated correctly.
/// </summary>
/// <param name="group">The group.</param>
/// <param name="dropLevel">The drop level.</param>
/// <param name="maxDurability">The maximum durability.</param>
/// <param name="price">The price.</param>
/// <remarks>
/// sphinx helm+3+4+l 34200
/// armor 48200
/// pants 38500
/// gloves 26500
/// boots 30200.
/// </remarks>
[TestCase(7, 32, 36, 34200, Description = "Sphinx Mask")]
[TestCase(8, 38, 36, 48200, Description = "Sphinx Armor")]
[TestCase(9, 34, 36, 38500, Description = "Sphinx Pants")]
[TestCase(10, 28, 36, 26500, Description = "Sphinx Gloves")]
[TestCase(11, 30, 36, 30200, Description = "Sphinx Boots")]
public void SphinxSetItem_3_4_Luck(byte group, byte dropLevel, byte maxDurability, long price)
{
this.CheckPrice(7, dropLevel, maxDurability, 2, 2, group, 0, 3, price, true, true);
}
/// <summary>
/// Tests the price calculations of some staffs.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="level">The level.</param>
/// <param name="dropLevel">The drop level.</param>
/// <param name="maxDurability">The maximum durability.</param>
/// <param name="width">The width.</param>
/// <param name="heigth">The heigth.</param>
/// <param name="price">The price.</param>
/// <remarks>
/// skull+0+4+l 480
/// angelic+2+4+l 9400
/// serpent+3+4+l 30200
/// thunder+3+4+l 59300.
/// </remarks>
[TestCase(0, 0, 6, 20, 1, 3, 480, Description = "skull+0+4+l")]
[TestCase(1, 2, 18, 38, 2, 3, 9400, Description = "angelic+2+4+l")]
[TestCase(2, 3, 30, 50, 2, 3, 30200, Description = "serpent+3+4+l")]
[TestCase(3, 3, 42, 60, 2, 4, 59300, Description = "thunder+3+4+l")]
public void Staffs(byte id, byte level, byte dropLevel, byte maxDurability, byte width, byte heigth, long price)
{
this.CheckPrice(id, dropLevel, maxDurability, heigth, width, 5, 0, level, price, true, true);
}
/// <summary>
/// Tests the price calculations of some shields.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="level">The level.</param>
/// <param name="dropLevel">The drop level.</param>
/// <param name="maxDurability">The maximum durability.</param>
/// <param name="width">The width.</param>
/// <param name="heigth">The heigth.</param>
/// <param name="skill">if set to <c>true</c> [skill].</param>
/// <param name="price">The price.</param>
/// <remarks>
/// small shield+0+5+l 230
/// buckler+1+5+s+l 2300
/// horn+2+5+l 2600
/// kite+3+5+l 5500
/// skull+3+5+s+l 18800.
/// </remarks>
[TestCase(0, 0, 3, 22, 2, 2, false, 230, Description = "small shield+0+5+l")]
[TestCase(4, 1, 6, 24, 2, 2, true, 2300, Description = "buckler+1+5+s+l")]
[TestCase(1, 2, 9, 28, 2, 2, false, 2600, Description = "horn+2+5+l")]
[TestCase(2, 3, 12, 32, 2, 2, false, 5500, Description = "kite+3+5+l")]
[TestCase(6, 3, 15, 34, 2, 2, true, 18800, Description = "skull+3+5+s+l")]
public void Shields(byte id, byte level, byte dropLevel, byte maxDurability, byte width, byte heigth, bool skill, long price)
{
this.CheckPrice(id, dropLevel, maxDurability, heigth, width, 6, 0, level, price, true, true, skill);
}
/// <summary>
/// Tests the price calculation of a small shield.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="price">The price.</param>
[TestCase(0, 110)]
[TestCase(1, 240)]
[TestCase(2, 470)]
[TestCase(3, 820)]
[TestCase(4, 1300)]
[TestCase(5, 3000)]
[TestCase(6, 6900)]
[TestCase(7, 21400)]
[TestCase(8, 58100)]
[TestCase(9, 121900)]
[TestCase(10, 275300)]
[TestCase(11, 617000)]
[TestCase(12, 1324700)]
[TestCase(13, 2693500)]
[TestCase(14, 4777500)]
[TestCase(15, 7726800)]
public void SmallShield(byte level, long price)
{
this.CheckPrice(0, 3, 22, 2, 2, 6, 0, level, price);
}
/// <summary>
/// Tests the price calculations of some swords.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="level">The level.</param>
/// <param name="dropLevel">The drop level.</param>
/// <param name="maxDurability">The maximum durability.</param>
/// <param name="width">The width.</param>
/// <param name="heigth">The heigth.</param>
/// <param name="skill">if set to <c>true</c> [skill].</param>
/// <param name="price">The price.</param>
/// <remarks>
/// short sword+0+4+l 230
/// hand axe+1+4+l 610
/// kris+2+4+l 1600
/// mace+2+4+l 1900
/// rapier+2+4+l 2600
/// double+2+4+s+l 12400
/// blade+3+4+s+l 86400.
/// </remarks>
[TestCase(1, 0, 3, 22, 1, 2, false, 230, Description = "short sword+0+4+l")]
[TestCase(0, 2, 6, 20, 1, 2, false, 1600, Description = "kris+2+4+l")]
[TestCase(2, 2, 9, 23, 1, 3, false, 2600, Description = "rapier+2+4+l")]
[TestCase(5, 3, 36, 39, 1, 3, true, 86400, Description = "blade+3+4+s+l")]
public void Swords(byte id, byte level, byte dropLevel, byte maxDurability, byte width, byte heigth, bool skill, long price)
{
this.CheckPrice(id, dropLevel, maxDurability, heigth, width, 0, 0, level, price, true, true, skill);
}
private void CheckPrice(byte id, byte dropLevel, byte maxDurability, byte height, byte width, byte group, int value, byte level, long price, bool luck = false, bool option = false, bool skill = false)
{
var itemDefinitionMock = new Mock<ItemDefinition>();
itemDefinitionMock.SetupAllProperties();
itemDefinitionMock.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
var itemDefinition = itemDefinitionMock.Object;
itemDefinition.DropLevel = dropLevel;
itemDefinition.Durability = maxDurability;
itemDefinition.Height = height;
itemDefinition.Width = width;
itemDefinition.Group = group;
itemDefinition.Value = value;
itemDefinition.Number = id;
if (group <= 11)
{
itemDefinition.ItemSlot = new ItemSlotType();
}
if (group < 6)
{
// weapons should have an attack speed attribute
itemDefinition.BasePowerUpAttributes.Add(new ItemBasePowerUpDefinition { TargetAttribute = Stats.AttackSpeedByWeapon });
}
var itemMock = new Mock<Item>();
itemMock.SetupAllProperties();
itemMock.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
itemMock.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
var item = itemMock.Object;
item.Definition = itemDefinition;
item.Level = level;
item.Durability = Math.Max(item.GetMaximumDurabilityOfOnePiece(), maxDurability);
if (luck)
{
var optionLink = new ItemOptionLink
{
ItemOption = new IncreasableItemOption
{
OptionType = ItemOptionTypes.Luck,
},
};
item.ItemOptions.Add(optionLink);
}
if (option)
{
var optionLink = new ItemOptionLink
{
ItemOption = new IncreasableItemOption
{
OptionType = ItemOptionTypes.Option,
},
Level = 1,
};
item.ItemOptions.Add(optionLink);
}
if (skill)
{
item.HasSkill = true;
}
var buyingPrice = this._calculator.CalculateFinalBuyingPrice(item);
Assert.That(buyingPrice, Is.EqualTo(price));
}
}

View File

@@ -0,0 +1,176 @@
// <copyright file="ItemRequirementCalculationTest.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 MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.Persistence.BasicModel;
using IncreasableItemOption = MUnique.OpenMU.Persistence.BasicModel.IncreasableItemOption;
using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition;
using ItemSlotType = MUnique.OpenMU.Persistence.BasicModel.ItemSlotType;
/// <summary>
/// Unit tests for <see cref="ItemExtensions.GetRequirement"/>.
/// </summary>
[TestFixture]
public class ItemRequirementCalculationTest
{
/// <summary>
/// Tests requirement calculation for the 'Vine Helm'.
/// </summary>
/// <param name="itemLevel">The item level.</param>
/// <param name="requiredStrength">The required strength.</param>
/// <param name="requiredAgility">The required agility.</param>
[TestCase(0, 25, 30)]
[TestCase(1, 28, 36)]
[TestCase(2, 30, 41)]
[TestCase(3, 33, 47)]
[TestCase(4, 36, 52)]
[TestCase(5, 38, 57)]
[TestCase(6, 41, 63)]
[TestCase(7, 44, 68)]
[TestCase(8, 47, 74)]
[TestCase(9, 49, 79)]
[TestCase(10, 52, 84)]
[TestCase(11, 55, 90)]
[TestCase(12, 57, 95)]
[TestCase(13, 60, 101)]
[TestCase(14, 63, 106)]
[TestCase(15, 65, 111)]
public void VineHelm(byte itemLevel, int requiredStrength, int requiredAgility)
{
var item = new Item();
item.Level = itemLevel;
item.Definition = new ItemDefinition();
item.Definition.DropLevel = 6;
item.Definition.Group = 7;
item.Definition.ItemSlot = new ItemSlotType();
var strengthRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalStrengthRequirementValue, MinimumValue = 30 };
var agilityRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalAgilityRequirementValue, MinimumValue = 60 };
var strengthValue = item.GetRequirement(strengthRequirement);
var agilityValue = item.GetRequirement(agilityRequirement);
Assert.That(strengthValue.Item1, Is.EqualTo(Stats.TotalStrength));
Assert.That(strengthValue.Item2, Is.EqualTo(requiredStrength));
Assert.That(agilityValue.Item1, Is.EqualTo(Stats.TotalAgility));
Assert.That(agilityValue.Item2, Is.EqualTo(requiredAgility));
}
/// <summary>
/// Tests if item options add 4 strength each.
/// </summary>
[Test]
public void OptionAdds4Strength()
{
var item = new Item();
item.Definition = new ItemDefinition();
item.Definition.DropLevel = 6;
item.Definition.Group = 7;
item.Definition.ItemSlot = new ItemSlotType();
item.ItemOptions.Add(new ItemOptionLink { Level = 2, ItemOption = new IncreasableItemOption { OptionType = ItemOptionTypes.Option } });
var strengthRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalStrengthRequirementValue, MinimumValue = 30 };
var agilityRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalAgilityRequirementValue, MinimumValue = 60 };
var strengthValue = item.GetRequirement(strengthRequirement);
var agilityValue = item.GetRequirement(agilityRequirement);
Assert.That(strengthValue.Item1, Is.EqualTo(Stats.TotalStrength));
Assert.That(strengthValue.Item2, Is.EqualTo(33));
Assert.That(agilityValue.Item1, Is.EqualTo(Stats.TotalAgility));
Assert.That(agilityValue.Item2, Is.EqualTo(30));
}
/// <summary>
/// Tests requirement calculation for the 'Sunlight Armor'.
/// </summary>
/// <param name="itemLevel">The item level.</param>
/// <param name="requiredStrength">The required strength.</param>
/// <param name="requiredAgility">The required agility.</param>
[TestCase(0, 293, 90)]
[TestCase(1, 299, 92)]
[TestCase(2, 304, 93)]
[TestCase(3, 310, 94)]
[TestCase(4, 315, 96)]
[TestCase(5, 321, 97)]
[TestCase(6, 326, 99)]
[TestCase(7, 332, 100)]
[TestCase(8, 338, 102)]
[TestCase(9, 343, 103)]
[TestCase(10, 349, 104)]
[TestCase(11, 354, 106)]
[TestCase(12, 360, 107)]
[TestCase(13, 365, 109)]
[TestCase(14, 371, 110)]
[TestCase(15, 377, 112)]
public void SunlightArmor(byte itemLevel, int requiredStrength, int requiredAgility)
{
var item = new Item();
item.Level = itemLevel;
item.Definition = new ItemDefinition();
item.Definition.DropLevel = 147;
item.Definition.Group = 8;
item.Definition.ItemSlot = new ItemSlotType();
var strengthRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalStrengthRequirementValue, MinimumValue = 62 };
var agilityRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalAgilityRequirementValue, MinimumValue = 16 };
var strengthValue = item.GetRequirement(strengthRequirement);
var agilityValue = item.GetRequirement(agilityRequirement);
Assert.That(strengthValue.Item1, Is.EqualTo(Stats.TotalStrength));
Assert.That(strengthValue.Item2, Is.EqualTo(requiredStrength));
Assert.That(agilityValue.Item1, Is.EqualTo(Stats.TotalAgility));
Assert.That(agilityValue.Item2, Is.EqualTo(requiredAgility));
}
/// <summary>
/// Tests the requirement calculation of the 'Book of Neil'.
/// Energy requirement calculation of summoner books are different from other items, so a unit test makes sense here.
/// </summary>
/// <param name="itemLevel">The item level.</param>
/// <param name="requiredEnergy">The required energy.</param>
/// <param name="requiredAgility">The required agility.</param>
[TestCase(0, 317, 64)]
[TestCase(1, 322, 66)]
[TestCase(2, 327, 68)]
[TestCase(3, 332, 71)]
[TestCase(4, 337, 73)]
[TestCase(5, 342, 75)]
[TestCase(6, 347, 77)]
[TestCase(7, 352, 80)]
[TestCase(8, 357, 82)]
[TestCase(9, 362, 84)]
[TestCase(10, 367, 86)]
[TestCase(11, 372, 89)]
[TestCase(12, 377, 91)]
[TestCase(13, 382, 93)]
[TestCase(14, 387, 95)]
[TestCase(15, 392, 98)]
public void BookOfNeil(byte itemLevel, int requiredEnergy, int requiredAgility)
{
var item = new Item();
item.Level = itemLevel;
item.Definition = new ItemDefinition();
item.Definition.Skill = new Skill();
item.Definition.DropLevel = 59;
item.Definition.Group = 5;
item.Definition.ItemSlot = new ItemSlotType();
var energyRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalEnergyRequirementValue, MinimumValue = 168 };
var agilityRequirement = new Persistence.BasicModel.AttributeRequirement { Attribute = Stats.TotalAgilityRequirementValue, MinimumValue = 25 };
var energyValue = item.GetRequirement(energyRequirement);
var agilityValue = item.GetRequirement(agilityRequirement);
Assert.That(energyValue.Item1, Is.EqualTo(Stats.TotalEnergy));
Assert.That(energyValue.Item2, Is.EqualTo(requiredEnergy));
Assert.That(agilityValue.Item1, Is.EqualTo(Stats.TotalAgility));
Assert.That(agilityValue.Item2, Is.EqualTo(requiredAgility));
}
}

View File

@@ -0,0 +1,315 @@
// <copyright file="ItemSerializerTests.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 MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameServer.RemoteView;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix;
using MUnique.OpenMU.Persistence.InMemory;
/// <summary>
/// Unit tests for the <see cref="ItemSerializer"/>.
/// </summary>
[TestFixture]
public class ItemSerializerTests : ItemSerializerTests<ItemSerializer>;
/// <summary>
/// Unit tests for the <see cref="ItemSerializerExtended"/>.
/// </summary>
[TestFixture]
public class ItemSerializerExtendedTests : ItemSerializerTests<ItemSerializerExtended>;
/// <summary>
/// Generic unit tests for the <see cref="IItemSerializer"/>s.
/// </summary>
[Ignore("Generic test")]
public class ItemSerializerTests<T>
where T : IItemSerializer, new()
{
private GameConfiguration _gameConfiguration = null!;
private IPersistenceContextProvider _contextProvider = null!;
private IItemSerializer _itemSerializer = null!;
/// <summary>
/// Sets up the test environment by initializing configuration data and a <see cref="IPersistenceContextProvider"/>.
/// </summary>
[OneTimeSetUp]
public async ValueTask SetupAsync()
{
this._contextProvider = new InMemoryPersistenceContextProvider();
await new DataInitialization(this._contextProvider, new NullLoggerFactory()).CreateInitialDataAsync(3, true).ConfigureAwait(false);
this._gameConfiguration = (await this._contextProvider.CreateNewConfigurationContext().GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
this._itemSerializer = new T();
}
/// <summary>
/// Tests if <see cref="Item.Definition"/> is correctly (de)serialized.
/// </summary>
[Test]
public void Definition()
{
var tuple = this.SerializeAndDeserializeBlade();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.Definition, Is.EqualTo(item.Definition));
}
/// <summary>
/// Tests if <see cref="Item.Level"/> is correctly (de)serialized.
/// </summary>
[Test]
public void Level()
{
var tuple = this.SerializeAndDeserializeBlade();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.Level, Is.EqualTo(item.Level));
}
/// <summary>
/// Tests if <see cref="Item.Durability"/> is correctly (de)serialized.
/// </summary>
[Test]
public void Durability()
{
var tuple = this.SerializeAndDeserializeBlade();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.Durability, Is.EqualTo(item.Durability));
}
/// <summary>
/// Tests if <see cref="Item.HasSkill" /> is correctly (de)serialized.
/// </summary>
/// <param name="hasSkill">If set to <c>true</c>, the tested item has skill.</param>
[TestCase(true)]
[TestCase(false)]
public void Skill(bool hasSkill)
{
var tuple = this.SerializeAndDeserializeBlade();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.HasSkill, Is.EqualTo(item.HasSkill));
}
/// <summary>
/// Tests if <see cref="Item.ItemOptions"/> are correctly (de)serialized.
/// </summary>
/// <remarks>
/// This test could be done in more detail, for each item option type.
/// </remarks>
[Test]
public void Options()
{
var tuple = this.SerializeAndDeserializeBlade();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.ItemOptions.Count, Is.EqualTo(item.ItemOptions.Count));
foreach (var optionLink in item.ItemOptions)
{
var deserializedOptionLink = deserializedItem.ItemOptions
.FirstOrDefault(link => link.Level == optionLink.Level
&& link.ItemOption!.OptionType == optionLink.ItemOption!.OptionType
&& link.ItemOption.Number == optionLink.ItemOption.Number);
Assert.That(deserializedOptionLink, Is.Not.Null, () => $"Option Link not found: {optionLink.ItemOption!.OptionType!.Name}, {optionLink.ItemOption.PowerUpDefinition}, Level: {optionLink.Level}");
}
}
/// <summary>
/// Tests if ancient items are correctly (de)serialized.
/// </summary>
[Test]
public void Ancient()
{
var tuple = this.SerializeAndDeserializeHyonLightingSword();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.ItemOptions.Count, Is.EqualTo(item.ItemOptions.Count));
foreach (var optionLink in item.ItemOptions)
{
var deserializedOptionLink = deserializedItem.ItemOptions
.FirstOrDefault(link => link.Level == optionLink.Level
&& link.ItemOption!.OptionType == optionLink.ItemOption!.OptionType
&& link.ItemOption.Number == optionLink.ItemOption.Number);
Assert.That(deserializedOptionLink, Is.Not.Null, () => $"Option Link not found: {optionLink.ItemOption!.OptionType!.Name}, {optionLink.ItemOption.PowerUpDefinition}, Level: {optionLink.Level}");
}
Assert.That(deserializedItem.ItemSetGroups.Count, Is.EqualTo(item.ItemSetGroups.Count));
foreach (var setGroup in item.ItemSetGroups)
{
Assert.That(deserializedItem.ItemSetGroups, Contains.Item(setGroup));
}
}
/// <summary>
/// Tests if ancient items without bonus option are correctly (de)serialized.
/// </summary>
[Test]
public void AncientWithoutBonus()
{
var tuple = this.SerializeAndDeserializeGywenPendant();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.ItemOptions.Count, Is.EqualTo(item.ItemOptions.Count));
foreach (var optionLink in item.ItemOptions)
{
var deserializedOptionLink = deserializedItem.ItemOptions
.FirstOrDefault(link => link.Level == optionLink.Level
&& link.ItemOption!.OptionType == optionLink.ItemOption!.OptionType
&& link.ItemOption.Number == optionLink.ItemOption.Number);
Assert.That(deserializedOptionLink, Is.Not.Null, () => $"Option Link not found: {optionLink.ItemOption!.OptionType!.Name}, {optionLink.ItemOption.PowerUpDefinition}, Level: {optionLink.Level}");
}
Assert.That(deserializedItem.ItemSetGroups.Count, Is.EqualTo(item.ItemSetGroups.Count));
foreach (var setGroup in item.ItemSetGroups)
{
Assert.That(deserializedItem.ItemSetGroups, Contains.Item(setGroup));
}
}
/// <summary>
/// Tests if socket items are correctly (de)serialized.
/// </summary>
[Test]
public void Sockets()
{
var tuple = this.SerializeAndDeserializeBraveHelm();
var item = tuple.Item1;
var deserializedItem = tuple.Item2;
Assert.That(deserializedItem.ItemOptions.Count, Is.EqualTo(item.ItemOptions.Count));
foreach (var optionLink in item.ItemOptions)
{
var deserializedOptionLink = deserializedItem.ItemOptions
.FirstOrDefault(link => link.Level == optionLink.Level
&& link.ItemOption!.OptionType == optionLink.ItemOption!.OptionType
&& link.ItemOption.Number == optionLink.ItemOption.Number);
Assert.That(deserializedOptionLink, Is.Not.Null, () => $"Option Link not found: {optionLink.ItemOption!.OptionType!.Name}, {optionLink.ItemOption.PowerUpDefinition}, Level: {optionLink.Level}");
}
Assert.That(deserializedItem.SocketCount, Is.EqualTo(item.SocketCount));
}
private Tuple<Item, Item> SerializeAndDeserializeBraveHelm()
{
using var context = this._contextProvider.CreateNewContext(this._gameConfiguration);
var item = context.CreateNew<Item>();
item.Definition = this._gameConfiguration.Items.First(i => i.Group == 7 && i.Number == 46);
item.Level = 3;
item.Durability = 100;
item.SocketCount = 2;
var option = context.CreateNew<ItemOptionLink>();
option.ItemOption = item.Definition.PossibleItemOptions.SelectMany(def =>
def.PossibleOptions.Where(p => p.OptionType == ItemOptionTypes.Option)).First();
option.Level = 4;
item.ItemOptions.Add(option);
for (var i = 0; i < item.SocketCount; i++)
{
var socketOption = context.CreateNew<ItemOptionLink>();
socketOption.ItemOption = item.Definition.PossibleItemOptions
.SelectMany(o => o.PossibleOptions)
.Where(o => o.OptionType == ItemOptionTypes.SocketOption)
.Skip(i)
.First();
socketOption.Index = i;
socketOption.Level = 1;
item.ItemOptions.Add(socketOption);
}
var bonusOption = context.CreateNew<ItemOptionLink>();
bonusOption.ItemOption = item.Definition.PossibleItemOptions.SelectMany(o => o.PossibleOptions).First(o => o.OptionType == ItemOptionTypes.SocketBonusOption);
item.ItemOptions.Add(bonusOption);
var array = new byte[this._itemSerializer.NeededSpace];
this._itemSerializer.SerializeItem(array, item);
var deserializedItem = this._itemSerializer.DeserializeItem(array, this._gameConfiguration, context);
return new Tuple<Item, Item>(item, deserializedItem);
}
private Tuple<Item, Item> SerializeAndDeserializeHyonLightingSword()
{
using var context = this._contextProvider.CreateNewContext(this._gameConfiguration);
var item = context.CreateNew<Item>();
item.Definition = this._gameConfiguration.Items.First(i => i.Name == "Lighting Sword");
item.Level = 15;
item.Durability = 100;
item.HasSkill = true;
var ancientSet = this._gameConfiguration.ItemSetGroups.First(i => i.Name == "Hyon");
var itemOfSet = ancientSet.Items.First(i => i.ItemDefinition == item.Definition);
var ancientBonus = context.CreateNew<ItemOptionLink>();
ancientBonus.ItemOption = itemOfSet.BonusOption;
ancientBonus.Level = 2; // 10 Str
item.ItemOptions.Add(ancientBonus);
item.ItemSetGroups.Add(itemOfSet);
var array = new byte[this._itemSerializer.NeededSpace];
this._itemSerializer.SerializeItem(array, item);
var deserializedItem = this._itemSerializer.DeserializeItem(array, this._gameConfiguration, context);
return new Tuple<Item, Item>(item, deserializedItem);
}
private Tuple<Item, Item> SerializeAndDeserializeGywenPendant()
{
using var context = this._contextProvider.CreateNewContext(this._gameConfiguration);
var item = context.CreateNew<Item>();
item.Definition = this._gameConfiguration.Items.First(i => i.Name == "Pendant of Ability");
item.Durability = 10;
var ancientSet = this._gameConfiguration.ItemSetGroups.First(i => i.Name == "Gywen");
var itemOfSet = ancientSet.Items.First(i => i.ItemDefinition == item.Definition);
item.ItemSetGroups.Add(itemOfSet);
var array = new byte[this._itemSerializer.NeededSpace];
this._itemSerializer.SerializeItem(array, item);
var deserializedItem = this._itemSerializer.DeserializeItem(array, this._gameConfiguration, context);
return new Tuple<Item, Item>(item, deserializedItem);
}
private Tuple<Item, Item> SerializeAndDeserializeBlade(bool hasSkill = true)
{
using var context = this._contextProvider.CreateNewContext(this._gameConfiguration);
var item = context.CreateNew<Item>();
item.Definition = this._gameConfiguration.Items.First(i => i.Name == "Blade");
item.Level = 15;
item.Durability = 23;
item.HasSkill = hasSkill;
var option = context.CreateNew<ItemOptionLink>();
option.ItemOption = item.Definition.PossibleItemOptions.SelectMany(def =>
def.PossibleOptions.Where(p => p.OptionType == ItemOptionTypes.Option)).First();
option.Level = 2;
item.ItemOptions.Add(option);
var luck = context.CreateNew<ItemOptionLink>();
luck.ItemOption = item.Definition.PossibleItemOptions.SelectMany(def =>
def.PossibleOptions.Where(p => p.OptionType == ItemOptionTypes.Luck)).First();
item.ItemOptions.Add(luck);
var excellent1 = context.CreateNew<ItemOptionLink>();
excellent1.ItemOption = item.Definition.PossibleItemOptions.SelectMany(def =>
def.PossibleOptions.Where(p => p.OptionType == ItemOptionTypes.Excellent && p.PowerUpDefinition!.TargetAttribute == Stats.ExcellentDamageChance)).First();
item.ItemOptions.Add(excellent1);
var excellent2 = context.CreateNew<ItemOptionLink>();
excellent2.ItemOption = item.Definition.PossibleItemOptions.SelectMany(def =>
def.PossibleOptions.Where(p => p.OptionType == ItemOptionTypes.Excellent && p.PowerUpDefinition!.TargetAttribute == Stats.AttackSpeedAny)).First();
item.ItemOptions.Add(excellent2);
var array = new byte[this._itemSerializer.NeededSpace];
this._itemSerializer.SerializeItem(array, item);
var deserializedItem = this._itemSerializer.DeserializeItem(array, this._gameConfiguration, context);
return new Tuple<Item, Item>(item, deserializedItem);
}
}

View File

@@ -0,0 +1,431 @@
// <copyright file="LocalizedStringTests.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 System.Globalization;
using MUnique.OpenMU.Interfaces;
using NUnit.Framework;
/// <summary>
/// Tests for the <see cref="LocalizedString"/> type and its localization behavior.
/// </summary>
public class LocalizedStringTests
{
/// <summary>
/// Tests that the implicit conversion from <see cref="string"/> to <see cref="LocalizedString"/> allows a <see langword="null"/> value.
/// </summary>
[Test]
public void ImplicitConversion_FromString_ToLocalizedString_AllowsNull()
{
string? source = null;
LocalizedString? result = source;
Assert.IsNull(result);
}
/// <summary>
/// Tests that the implicit conversion from <see cref="string"/> to <see cref="LocalizedString"/> correctly sets the <see cref="LocalizedString.Value"/>.
/// </summary>
[Test]
public void ImplicitConversion_FromString_ToLocalizedString_SetsValue()
{
const string text = "Some text";
LocalizedString result = text;
Assert.That(result.Value, Is.EqualTo(text));
}
/// <summary>
/// Tests that the implicit conversion from <see cref="LocalizedString"/> to nullable <see cref="string"/> allows a <see langword="null"/> source.
/// </summary>
[Test]
public void ImplicitConversion_FromLocalizedString_ToNullableString_AllowsNull()
{
LocalizedString? source = null;
string? result = source;
Assert.IsNull(result);
}
/// <summary>
/// Tests that the implicit conversion from <see cref="LocalizedString"/> to <see cref="string"/> returns an empty string when the underlying value is <see langword="null"/>.
/// </summary>
[Test]
public void ImplicitConversion_FromLocalizedString_ToString_ReturnsEmptyWhenNull()
{
var source = new LocalizedString(null!);
string result = source;
Assert.That(result, Is.EqualTo(string.Empty));
}
/// <summary>
/// Tests that <see cref="LocalizedString.ToString"/> uses the current culture and returns the neutral language text for a neutral culture.
/// </summary>
[Test]
public void ToString_UsesCurrentCulture_NeutralCulture()
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
var culture = new CultureInfo(LocalizedString.NeutralLanguageCode);
CultureInfo.CurrentCulture = culture;
var value = "Some text||de=Etwas Text";
var localized = new LocalizedString(value);
var result = localized.ToString();
Assert.That(result, Is.EqualTo("Some text"));
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> returns an empty span when the underlying value is <see langword="null"/>.
/// </summary>
[Test]
public void GetTranslation_ReturnsEmptySpan_WhenValueIsNull()
{
var localized = new LocalizedString(null!);
var result = localized.GetTranslation(new CultureInfo("en"));
Assert.IsNull(result);
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> returns the neutral translation when the requested culture is neutral.
/// </summary>
[Test]
public void GetTranslation_ReturnsNeutral_WhenCultureIsNeutral()
{
var localized = new LocalizedString("Some text||de=Etwas Text");
var result = localized.GetTranslation(new CultureInfo("en"));
Assert.That(result, Is.EqualTo("Some text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> returns a specific translation when it is available for the requested culture.
/// </summary>
[Test]
public void GetTranslation_ReturnsSpecificTranslation_WhenAvailable()
{
var localized = new LocalizedString("Some text||de=Etwas Text||fr=Un peu de texte");
var result = localized.GetTranslation(new CultureInfo("de"));
Assert.That(result, Is.EqualTo("Etwas Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> is tolerant to extra separators between translations.
/// </summary>
[Test]
public void GetTranslation_ReturnsSpecificTranslation_TolerantToExtraSeparator()
{
var localized = new LocalizedString("Some text|||de=Etwas Text|||fr=Un peu de texte");
var result = localized.GetTranslation(new CultureInfo("de"));
Assert.That(result, Is.EqualTo("Etwas Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> is not tolerant to additional separators inside the translation text itself.
/// </summary>
[Test]
public void GetTranslation_ReturnsSpecificTranslation_IntolerantToExtraSeparatorInText()
{
var localized = new LocalizedString("Some text|||de=Etwas ||Text|||fr=Un peu de texte");
var result = localized.GetTranslation(new CultureInfo("de"));
Assert.That(result, Is.EqualTo("Etwas "));
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> returns the last translation entry even when it does not end with a separator.
/// </summary>
[Test]
public void GetTranslation_ReturnsSpecificTranslation_LastEntryWithoutTrailingSeparator()
{
var localized = new LocalizedString("Some text||de=Etwas Text");
var result = localized.GetTranslation(new CultureInfo("de"));
Assert.That(result, Is.EqualTo("Etwas Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> falls back to the neutral text when the requested translation is missing and fallback is enabled.
/// </summary>
[Test]
public void GetTranslation_FallsBackToNeutral_WhenTranslationMissing_AndFallbackEnabled()
{
var localized = new LocalizedString("Some text||de=Etwas Text");
var result = localized.GetTranslation(new CultureInfo("fr"), true);
Assert.That(result, Is.EqualTo("Some text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> returns an empty span when the requested translation is missing and fallback is disabled.
/// </summary>
[Test]
public void GetTranslation_ReturnsEmpty_WhenTranslationMissing_AndFallbackDisabled()
{
var localized = new LocalizedString("Some text||de=Etwas Text");
var result = localized.GetTranslation(new CultureInfo("fr"), false);
Assert.IsNull(result);
}
/// <summary>
/// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> performs a case-insensitive lookup for language codes.
/// </summary>
[Test]
public void GetTranslation_UsesCaseInsensitiveLanguageLookup()
{
var localized = new LocalizedString("Some text||DE=Etwas Text");
var result = localized.GetTranslation(new CultureInfo("de"));
Assert.That(result, Is.EqualTo("Etwas Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> sets the neutral translation as base text when the current value is empty.
/// </summary>
[Test]
public void WithTranslation_SetNeutral_OnEmptyValue_SetsBaseText()
{
var localized = new LocalizedString(null!);
var result = localized.WithTranslation(new CultureInfo("en"), "Base");
Assert.That(result.Value, Is.EqualTo("Base"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> replaces the neutral translation when no other translations exist.
/// </summary>
[Test]
public void WithTranslation_ReplaceNeutral_WithoutOtherTranslations()
{
var localized = new LocalizedString("Base");
var result = localized.WithTranslation(new CultureInfo("en"), "NewBase");
Assert.That(result.Value, Is.EqualTo("NewBase"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> replaces the neutral translation and keeps the non-neutral suffix.
/// </summary>
[Test]
public void WithTranslation_ReplaceNeutral_WithOtherTranslations_KeepsSuffix()
{
var localized = new LocalizedString("Base||de=Etwas Text||fr=Un peu de texte");
var result = localized.WithTranslation(new CultureInfo("en"), "NewBase");
Assert.That(result.Value, Is.EqualTo("NewBase||de=Etwas Text||fr=Un peu de texte"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> clears the neutral translation and sets the value to empty when only the neutral entry exists.
/// </summary>
[Test]
public void WithTranslation_ClearNeutral_WhenOnlyNeutralExists_SetsEmpty()
{
var localized = new LocalizedString("Base");
var result = localized.WithTranslation(new CultureInfo("en"), null);
Assert.That(result.Value, Is.EqualTo(string.Empty));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> does not change the value when the neutral translation is already empty and set to <see langword="null"/>.
/// </summary>
[Test]
public void WithTranslation_ClearNeutral_WhenEmptyAndNull_NoChange()
{
var localized = new LocalizedString(string.Empty);
var result = localized.WithTranslation(new CultureInfo("en"), null);
Assert.That(result.Value, Is.EqualTo(localized.Value));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> adds a non-neutral translation when no value exists yet.
/// </summary>
[Test]
public void WithTranslation_AddNonNeutral_WhenNoValueYet()
{
var localized = new LocalizedString(null!);
var result = localized.WithTranslation(new CultureInfo("de"), "Etwas Text");
Assert.That(result.Value, Is.EqualTo("||de=Etwas Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> adds a non-neutral translation when a base text already exists.
/// </summary>
[Test]
public void WithTranslation_AddNonNeutral_WhenBaseExists()
{
var localized = new LocalizedString("Base");
var result = localized.WithTranslation(new CultureInfo("de"), "Etwas Text");
Assert.That(result.Value, Is.EqualTo("Base||de=Etwas Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> can add multiple non-neutral translations.
/// </summary>
[Test]
public void WithTranslation_AddMultipleNonNeutral()
{
var localized = new LocalizedString("Base");
var withDe = localized.WithTranslation(new CultureInfo("de"), "Etwas Text");
var withFr = withDe.WithTranslation(new CultureInfo("fr"), "Un peu de texte");
Assert.That(withFr.Value, Is.EqualTo("Base||de=Etwas Text||fr=Un peu de texte"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> replaces an existing non-neutral translation in the middle of the list.
/// </summary>
[Test]
public void WithTranslation_ReplaceExistingNonNeutral_InMiddle()
{
var localized = new LocalizedString("Base||de=Etwas Text||fr=Un peu de texte");
var result = localized.WithTranslation(new CultureInfo("de"), "Neuer Text");
Assert.That(result.Value, Is.EqualTo("Base||de=Neuer Text||fr=Un peu de texte"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> replaces an existing non-neutral translation at the end of the list.
/// </summary>
[Test]
public void WithTranslation_ReplaceExistingNonNeutral_AtEnd()
{
var localized = new LocalizedString("Base||de=Etwas Text");
var result = localized.WithTranslation(new CultureInfo("de"), "Neuer Text");
Assert.That(result.Value, Is.EqualTo("Base||de=Neuer Text"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> removes an existing non-neutral translation in the middle of the list.
/// </summary>
[Test]
public void WithTranslation_RemoveExistingNonNeutral_InMiddle()
{
var localized = new LocalizedString("Base||de=Etwas Text||fr=Un peu de texte");
var result = localized.WithTranslation(new CultureInfo("de"), null);
Assert.That(result.Value, Is.EqualTo("Base||fr=Un peu de texte"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> removes an existing non-neutral translation at the end of the list.
/// </summary>
[Test]
public void WithTranslation_RemoveExistingNonNeutral_AtEnd()
{
var localized = new LocalizedString("Base||de=Etwas Text");
var result = localized.WithTranslation(new CultureInfo("de"), string.Empty);
Assert.That(result.Value, Is.EqualTo("Base"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.WithTranslation(CultureInfo,string)"/> does not change the value when trying to remove a non-existing non-neutral translation.
/// </summary>
[Test]
public void WithTranslation_RemoveNonExistingNonNeutral_NoChange()
{
var localized = new LocalizedString("Base||de=Etwas Text");
var result = localized.WithTranslation(new CultureInfo("fr"), null);
Assert.That(result.Value, Is.EqualTo(localized.Value));
}
/// <summary>
/// Tests that <see cref="LocalizedString.ValueInNeutralLanguage"/> returns the whole string when no separator is present.
/// </summary>
[Test]
public void GetValueInNeutralLanguage_ReturnsWholeString_WhenNoSeparator()
{
var localized = new LocalizedString("Base");
var result = localized.ValueInNeutralLanguage;
Assert.That(result, Is.EqualTo("Base"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.ValueInNeutralLanguage"/> returns the text up to the first separator.
/// </summary>
[Test]
public void GetValueInNeutralLanguage_ReturnsUpToFirstSeparator()
{
var localized = new LocalizedString("Base||de=Etwas Text");
var result = localized.ValueInNeutralLanguage;
Assert.That(result, Is.EqualTo("Base"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.ValueInNeutralLanguage"/> is tolerant to triple separators and still returns the base text.
/// </summary>
[Test]
public void GetValueInNeutralLanguage_TolerantToTripleSeparator()
{
var localized = new LocalizedString("Base|||de=Etwas Text");
var result = localized.ValueInNeutralLanguage;
Assert.That(result, Is.EqualTo("Base"));
}
/// <summary>
/// Tests that <see cref="LocalizedString.ValueInNeutralLanguage"/> returns an empty span when the value is <see langword="null"/>.
/// </summary>
[Test]
public void GetValueInNeutralLanguage_ReturnsEmpty_WhenValueIsNull()
{
var localized = new LocalizedString(null!);
var result = localized.ValueInNeutralLanguage;
Assert.IsEmpty(result);
}
}

View File

@@ -0,0 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
<DocumentationFile>bin\Debug\MUnique.OpenMU.Tests.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>bin\Release\</OutputPath>
<DocumentationFile>bin\Release\MUnique.OpenMU.Tests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
<Compile Include="..\..\src\SharedGlobalUsings.cs" Link="SharedGlobalUsings.cs" />
<Compile Include="..\SharedTestUsings.cs" Link="SharedTestUsings.cs" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\src\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\src\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AttributeSystem\MUnique.OpenMU.AttributeSystem.csproj" />
<ProjectReference Include="..\..\src\DataModel\MUnique.OpenMU.DataModel.csproj" />
<ProjectReference Include="..\..\src\FriendServer\MUnique.OpenMU.FriendServer.csproj" />
<ProjectReference Include="..\..\src\GameLogic\MUnique.OpenMU.GameLogic.csproj" />
<ProjectReference Include="..\..\src\GameServer\MUnique.OpenMU.GameServer.csproj" />
<ProjectReference Include="..\..\src\GuildServer\MUnique.OpenMU.GuildServer.csproj" />
<ProjectReference Include="..\..\src\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
<ProjectReference Include="..\..\src\Pathfinding\MUnique.OpenMU.Pathfinding.csproj" />
<ProjectReference Include="..\..\src\Persistence\Initialization\MUnique.OpenMU.Persistence.Initialization.csproj" />
<ProjectReference Include="..\..\src\Persistence\InMemory\MUnique.OpenMU.Persistence.InMemory.csproj" />
<ProjectReference Include="..\..\src\Persistence\MUnique.OpenMU.Persistence.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,255 @@
// <copyright file="MasterSystemTest.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 Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlayerActions.Character;
/// <summary>
/// Tests the master level system.
/// </summary>
[TestFixture]
public class MasterSystemTest
{
private readonly int _skillIdRank1 = 1;
private readonly int _skillIdRank2 = 2;
private readonly int _skillIdRank3 = 3;
private Player _player = null!;
private Skill _skillRank1 = null!;
private Skill _skillRank2 = null!;
private Skill _skillRank3 = null!;
private AddMasterPointAction _addAction = null!;
/// <summary>
/// Setups the test data.
/// </summary>
[SetUp]
public async Task SetupAsync()
{
this._player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var context = this._player.GameContext;
this._skillRank1 = this.CreateSkill(1, 1, 1, null, this._player.SelectedCharacter!.CharacterClass!);
this._skillRank2 = this.CreateSkill(2, 2, 1, null, this._player.SelectedCharacter!.CharacterClass!);
this._skillRank3 = this.CreateSkill((short)this._skillIdRank3, 3, 1, null, this._player.SelectedCharacter!.CharacterClass!);
this._skillRank3.MasterDefinition!.MinimumLevel = 10;
context.Configuration.Skills.Add(this._skillRank1);
context.Configuration.Skills.Add(this._skillRank2);
context.Configuration.Skills.Add(this._skillRank3);
this._addAction = new AddMasterPointAction();
}
/// <summary>
/// Tests if the adding of master points failes because of insufficient level up points.
/// </summary>
[Test]
public async Task FailedInsufficientLevelUpPointsAsync()
{
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter!.LearnedSkills, Is.Empty);
}
/// <summary>
/// Tests if the adding of master points succeeds.
/// </summary>
[Test]
public async Task SucceededAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 1;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Is.Not.Empty);
}
/// <summary>
/// Tests if the adding of master points fails because of an insufficient reached skill rank.
/// </summary>
[Test]
public async Task RankNotSufficientAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 1;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Is.Empty);
}
/// <summary>
/// Tests if the adding of master points fails because the skill of the previous rank does not have the required level 10.
/// </summary>
[Test]
public async Task PreviousRankTooLowLevelAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 9;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Has.Count.EqualTo(1));
Assert.That(this._player.SelectedCharacter.LearnedSkills.First().Skill, Is.SameAs(this._skillRank1));
}
/// <summary>
/// Tests if the adding of master points succeeds because the skill of the previous rank has the required level 10.
/// </summary>
[Test]
public async Task PreviousRankEnoughLevelsAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 10;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Has.Count.EqualTo(2));
Assert.That(this._player.SelectedCharacter.LearnedSkills.Any(l => l.Skill == this._skillRank1), Is.True);
Assert.That(this._player.SelectedCharacter.LearnedSkills.Any(l => l.Skill == this._skillRank2), Is.True);
}
/// <summary>
/// Tests if the adding of master points succeeds when a skill has a minium level of 10 and the character has enough master points.
/// </summary>
[Test]
public async Task MinimumLevel10WithEnoughPointsAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 10;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.Last().Level = 10;
this._player.SelectedCharacter.MasterLevelUpPoints = 10;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank3).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Has.Count.EqualTo(3));
Assert.That(this._player.SelectedCharacter.LearnedSkills.Any(l => l.Skill == this._skillRank3), Is.True);
}
/// <summary>
/// Tests if the adding of master points succeeds when a skill has a minimum level of 10 and the character has not enough master points.
/// </summary>
[Test]
public async Task MinimumLevel10WithoutEnoughPointsAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 10;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.Last().Level = 10;
this._player.SelectedCharacter.MasterLevelUpPoints = 9;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank3).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Has.Count.EqualTo(2));
Assert.That(this._player.SelectedCharacter.LearnedSkills.Any(l => l.Skill == this._skillRank3), Is.False);
}
/// <summary>
/// Tests if adding a point to a new skill results in the skill having level 1.
/// </summary>
[Test]
public async Task AddedSkillGotLevelAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 1;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills.First().Level, Is.EqualTo(1));
}
/// <summary>
/// Tests if adding a point to a skill increases its level by one.
/// </summary>
[Test]
public async Task AddLevelToLearnedSkillAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills.First().Level, Is.EqualTo(2));
}
/// <summary>
/// Tests if adding a point to a new skill fails because the required skill is not learned yet.
/// </summary>
[Test]
public async Task RequiredSkillNotLearnedAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 10;
this._skillRank2.MasterDefinition!.RequiredMasterSkills.Add(new Skill());
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Has.Count.EqualTo(1));
Assert.That(this._player.SelectedCharacter.LearnedSkills.Any(l => l.Skill == this._skillRank2), Is.False);
}
/// <summary>
/// Tests if adding a point to a new skill not fails because the required skill has been learned.
/// </summary>
[Test]
public async Task RequiredSkillLearnedAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 2;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 10;
this._skillRank2.MasterDefinition!.RequiredMasterSkills.Add(this._skillRank1);
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills, Has.Count.EqualTo(2));
Assert.That(this._player.SelectedCharacter.LearnedSkills.Any(l => l.Skill == this._skillRank2), Is.True);
}
/// <summary>
/// Tests if adding master points decreases the available master points.
/// </summary>
[Test]
public async Task MasterLevelUpPointDecreasedWhenLearnedAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 1;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.MasterLevelUpPoints, Is.EqualTo(0));
}
/// <summary>
/// Tests if a failed adding of master points does not decrease the available master points.
/// </summary>
[Test]
public async Task MasterLevelUpPointNotDecreasedWhenNotLearnedAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 1;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank2).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.MasterLevelUpPoints, Is.EqualTo(1));
}
/// <summary>
/// Tests if the adding of master points fails when the maximum level (20) of a skill has been reached.
/// </summary>
[Test]
public async Task MasterLevelMaximumReachedAsync()
{
this._player.SelectedCharacter!.MasterLevelUpPoints = 3;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
this._player.SelectedCharacter.LearnedSkills.First().Level = 19;
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
await this._addAction.AddMasterPointAsync(this._player, (ushort)this._skillIdRank1).ConfigureAwait(false);
Assert.That(this._player.SelectedCharacter.LearnedSkills.First().Level, Is.EqualTo(20));
Assert.That(this._player.SelectedCharacter.MasterLevelUpPoints, Is.EqualTo(1));
}
private Skill CreateSkill(short id, byte rank, byte rootId, Skill? requiredSkill, CharacterClass charClass)
{
var masterDef = new Mock<MasterSkillDefinition>();
masterDef.SetupAllProperties();
masterDef.Object.Rank = rank;
masterDef.Object.MaximumLevel = 20;
masterDef.Object.MinimumLevel = 1;
masterDef.Object.Root = new MasterSkillRoot { Id = new Guid(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, rootId) };
masterDef.Setup(m => m.RequiredMasterSkills).Returns(new List<Skill>());
if (requiredSkill != null)
{
masterDef.Object.RequiredMasterSkills.Add(requiredSkill);
}
var skill = new Mock<Skill>();
skill.SetupAllProperties();
skill.Object.Number = id;
skill.Setup(s => s.QualifiedCharacters).Returns(new List<CharacterClass>());
skill.Object.QualifiedCharacters.Add(charClass);
skill.Object.MasterDefinition = masterDef.Object;
return skill.Object;
}
}

View File

@@ -0,0 +1,30 @@
// <copyright file="MockViewPlugInContainer.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 Moq;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// A view plugin container which automatically create mocks for requested view plugins.
/// </summary>
public class MockViewPlugInContainer : ICustomPlugInContainer<IViewPlugIn>
{
private readonly Dictionary<Type, IViewPlugIn> _mocks = new();
/// <inheritdoc />
public T GetPlugIn<T>()
where T : class, IViewPlugIn
{
if (!this._mocks.TryGetValue(typeof(T), out var mock))
{
mock = new Mock<T>().Object;
this._mocks.Add(typeof(T), mock);
}
return (T)mock;
}
}

View File

@@ -0,0 +1,137 @@
// // <copyright file="ModelResourcesTest.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 System;
using System.Globalization;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Entities;
/// <summary>
/// Unit tests for verifying that <see cref="ModelResourceProvider"/> returns expected captions and descriptions
/// for types, properties and enum values, including fallbacks for unknown languages and types.
/// </summary>
[TestFixture]
public class ModelResourcesTest
{
/// <summary>
/// Verifies that <see cref="ModelResourceProvider.GetTypeCaption{T}"/> returns the expected caption
/// for <see cref="AreaSkillSettings"/> in English culture.
/// </summary>
[Test]
public void TypeCaption()
{
var typeName = ModelResourceProvider.GetTypeCaption<AreaSkillSettings>(CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo("Area Skill Settings"));
}
/// <summary>
/// Verifies that <see cref="ModelResourceProvider.GetPluralizedTypeCaption{T}"/> returns the pluralized caption
/// for <see cref="Account"/> in English culture.
/// </summary>
[Test]
public void TypeCaptionPlural()
{
var typeName = ModelResourceProvider.GetPluralizedTypeCaption<Account>(CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo("Accounts"));
}
/// <summary>
/// Verifies that <see cref="ModelResourceProvider.GetTypeDescription{T}"/> returns the expected (empty) description
/// for <see cref="AreaSkillSettings"/> in English culture.
/// </summary>
[Test]
public void TypeDescription()
{
var typeName = ModelResourceProvider.GetTypeDescription<AreaSkillSettings>(CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo(""));
}
/// <summary>
/// Verifies that <see cref="ModelResourceProvider.GetPropertyCaption{T}"/> returns a humanized caption
/// for the property <see cref="AreaSkillSettings.DelayBetweenHits"/>.
/// </summary>
[Test]
public void PropertyCaption()
{
var typeName = ModelResourceProvider.GetPropertyCaption<AreaSkillSettings>(nameof(AreaSkillSettings.DelayBetweenHits), CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo("Delay Between Hits"));
}
/// <summary>
/// Verifies that a caption can be retrieved for a property inherited from a base type,
/// here <see cref="Gate.X1"/>.
/// </summary>
[Test]
public void InheritedPropertyCaption()
{
var typeName = ModelResourceProvider.GetPropertyCaption<ExitGate>(nameof(ExitGate.X1), CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo("X1"));
}
/// <summary>
/// Verifies that <see cref="ModelResourceProvider.GetPropertyDescription{T}"/> returns the expected (empty) description
/// for property <see cref="AreaSkillSettings.DelayBetweenHits"/>.
/// </summary>
[Test]
public void PropertyDescription()
{
var typeName = ModelResourceProvider.GetPropertyDescription<AreaSkillSettings>(nameof(AreaSkillSettings.DelayBetweenHits), CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo(""));
}
/// <summary>
/// Verifies that an unknown language (Swahili here) falls back to a default caption for a known type.
/// </summary>
[Test]
public void TypeCaptionUnknownLanguage()
{
var typeName = ModelResourceProvider.GetTypeCaption<AreaSkillSettings>(CultureInfo.GetCultureInfo("sw"));
Assert.That(typeName, Is.EqualTo("Area Skill Settings"));
}
/// <summary>
/// Verifies that unknown types get a humanized caption from their type name (splitting PascalCase).
/// </summary>
[Test]
public void TypeCaptionUnknownType()
{
var typeName = ModelResourceProvider.GetTypeCaption<ModelResourcesTest>(CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo("Model Resources Test"));
}
/// <summary>
/// Verifies that an unknown type and property name gets a humanized caption (PascalCase splitting).
/// </summary>
[Test]
public void PropertyCaptionUnknownTypeAndProperty()
{
var typeName = ModelResourceProvider.GetPropertyCaption<ModelResourcesTest>("FooBar", CultureInfo.GetCultureInfo("en"));
Assert.That(typeName, Is.EqualTo("Foo Bar"));
}
/// <summary>
/// Verifies that the generic overload of <see cref="ModelResourceProvider.GetEnumCaption{TEnum}"/>
/// returns the expected caption for an enum value.
/// </summary>
[Test]
public void EnumCaptionGeneric()
{
var caption = ModelResourceProvider.GetEnumCaption<AccountState>(AccountState.GameMaster, CultureInfo.GetCultureInfo("en"));
Assert.That(caption, Is.EqualTo("Game Master"));
}
/// <summary>
/// Verifies that the non-generic overload of <see cref="ModelResourceProvider.GetEnumCaption(Type, Enum, CultureInfo)"/>
/// returns the expected caption for an enum value.
/// </summary>
[Test]
public void EnumCaption()
{
var caption = ModelResourceProvider.GetEnumCaption(typeof(AccountState), AccountState.GameMaster, CultureInfo.GetCultureInfo("en"));
Assert.That(caption, Is.EqualTo("Game Master"));
}
}

View File

@@ -0,0 +1,132 @@
// <copyright file="MonsterAttributeReloadTests.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 Moq;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.Pathfinding;
using MonsterDefinition = MUnique.OpenMU.Persistence.BasicModel.MonsterDefinition;
using MonsterAttribute = MUnique.OpenMU.Persistence.BasicModel.MonsterAttribute;
/// <summary>
/// Tests for applying changes of a <see cref="MonsterDefinition"/> to an already spawned
/// <see cref="AttackableNpcBase"/> via <see cref="AttackableNpcBase.ReloadAttributes"/>.
/// </summary>
[TestFixture]
public class MonsterAttributeReloadTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that changing a value of a <see cref="MonsterAttribute"/> takes effect on an
/// already spawned monster after <see cref="AttackableNpcBase.ReloadAttributes"/> is called.
/// </summary>
[Test]
public async ValueTask ReloadAttributesAppliesChangedValueAsync()
{
var monster = await this.CreateMonsterAsync().ConfigureAwait(false);
var maximumHealthAttribute = monster.Definition.Attributes.First(a => a.AttributeDefinition == Stats.MaximumHealth);
Assert.That(monster.Attributes[Stats.MaximumHealth], Is.EqualTo(1000));
maximumHealthAttribute.Value = 2000;
monster.ReloadAttributes();
Assert.That(monster.Attributes[Stats.MaximumHealth], Is.EqualTo(2000));
}
/// <summary>
/// Tests that adding a new <see cref="MonsterAttribute"/> takes effect on an already spawned
/// monster after <see cref="AttackableNpcBase.ReloadAttributes"/> is called.
/// </summary>
[Test]
public async ValueTask ReloadAttributesAppliesAddedAttributeAsync()
{
var monster = await this.CreateMonsterAsync().ConfigureAwait(false);
Assert.That(monster.Attributes[Stats.AttackRatePvm], Is.EqualTo(0));
monster.Definition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.AttackRatePvm, Value = 50 });
monster.ReloadAttributes();
Assert.That(monster.Attributes[Stats.AttackRatePvm], Is.EqualTo(50));
}
/// <summary>
/// Tests that all spawned instances of the same monster definition pick up an attribute change.
/// </summary>
[Test]
public async ValueTask ReloadAttributesAppliesToAllInstancesOfDefinitionAsync()
{
var monsterDefinition = CreateMonsterDefinition();
var monster1 = await this.CreateMonsterAsync(monsterDefinition).ConfigureAwait(false);
var monster2 = await this.CreateMonsterAsync(monsterDefinition).ConfigureAwait(false);
var maximumHealthAttribute = monsterDefinition.Attributes.First(a => a.AttributeDefinition == Stats.MaximumHealth);
maximumHealthAttribute.Value = 2000;
monster1.ReloadAttributes();
monster2.ReloadAttributes();
Assert.That(monster1.Attributes[Stats.MaximumHealth], Is.EqualTo(2000));
Assert.That(monster2.Attributes[Stats.MaximumHealth], Is.EqualTo(2000));
}
private static MonsterDefinition CreateMonsterDefinition()
{
var monsterDefinition = new MonsterDefinition
{
ObjectKind = NpcObjectKind.Monster,
};
monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.MaximumHealth, Value = 1000 });
monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.DefenseBase, Value = 100 });
return monsterDefinition;
}
private ValueTask<Monster> CreateMonsterAsync()
{
return this.CreateMonsterAsync(CreateMonsterDefinition());
}
private async ValueTask<Monster> CreateMonsterAsync(MonsterDefinition monsterDefinition)
{
var map = await this._gameContext.GetMapAsync(0).ConfigureAwait(false);
var spawnArea = new MonsterSpawnArea
{
MonsterDefinition = monsterDefinition,
GameMap = map!.Definition,
X1 = 100,
Y1 = 100,
X2 = 100,
Y2 = 100,
Quantity = 1,
};
var monster = new Monster(
spawnArea,
monsterDefinition,
map,
NullDropGenerator.Instance,
new Mock<INpcIntelligence>().Object,
this._gameContext.PlugInManager,
this._gameContext.PathFinderPool);
monster.Initialize();
return monster;
}
}

View File

@@ -0,0 +1,307 @@
// <copyright file="MoveItemActionTests.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;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Trade;
using MUnique.OpenMU.GameLogic.Views.Trade;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests for <see cref="MoveItemAction"/>.
/// </summary>
[TestFixture]
public class MoveItemActionTests
{
/// <summary>
/// Verifies that a complete stack move consumes the source item.
/// </summary>
[Test]
public async ValueTask CompleteStackConsumesSourceItemAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var definition = CreateDefinition(1, 1, 10);
var source = CreateItem(definition, 3);
var target = CreateItem(definition, 2);
await player.Inventory!.AddItemAsync(20, source).ConfigureAwait(false);
await player.Inventory.AddItemAsync(21, target).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 20, Storages.Inventory, 21, Storages.Inventory).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(20), Is.Null);
Assert.That(player.Inventory.GetItem(21), Is.SameAs(target));
Assert.That(target.Durability, Is.EqualTo(5));
Assert.That(player.Inventory.ItemStorage.Items.Count(i => ReferenceEquals(i, source)), Is.EqualTo(0));
Assert.That(player.Inventory.ItemStorage.Items.Count(i => ReferenceEquals(i, target)), Is.EqualTo(1));
}
/// <summary>
/// Verifies that completing a stack and relogging keeps a single persisted item.
/// </summary>
[Test]
public async ValueTask CompleteStackAndRelogKeepsSinglePersistedItemAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var selectedCharacter = player.SelectedCharacter!;
var definition = CreateDefinition(1, 1, 10);
var source = CreateItem(definition, 3);
var target = CreateItem(definition, 2);
await player.Inventory!.AddItemAsync(20, source).ConfigureAwait(false);
await player.Inventory.AddItemAsync(21, target).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 20, Storages.Inventory, 21, Storages.Inventory).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(20), Is.Null);
Assert.That(player.Inventory.GetItem(21), Is.Not.Null);
Assert.That(player.Inventory.GetItem(21)!.Durability, Is.EqualTo(5));
await player.RemoveFromGameAsync().ConfigureAwait(false);
await player.SetSelectedCharacterAsync(selectedCharacter).ConfigureAwait(false);
var persistedTarget = player.Inventory!.GetItem(21);
Assert.That(persistedTarget, Is.Not.Null);
Assert.That(persistedTarget!.Durability, Is.EqualTo(5));
Assert.That(player.Inventory.GetItem(20), Is.Null);
Assert.That(player.Inventory.Items.Count(i => i.Definition == definition), Is.EqualTo(1));
}
/// <summary>
/// Verifies that a failed move to an occupied slot keeps the source at its original slot.
/// </summary>
[Test]
public async ValueTask FailedMoveToOccupiedSlotKeepsSourceAtOriginalSlotAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var source = CreateItem(CreateDefinition(), 1);
var blocker = CreateItem(CreateDefinition(2, 2), 1);
await player.Inventory!.AddItemAsync(30, source).ConfigureAwait(false);
await player.Inventory.AddItemAsync(20, blocker).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 30, Storages.Inventory, 21, Storages.Inventory).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(30), Is.SameAs(source));
Assert.That(player.Inventory.GetItem(20), Is.SameAs(blocker));
Assert.That(player.Inventory.GetItem(21), Is.Null);
}
/// <summary>
/// Verifies that a failed vault to inventory move keeps the item in the vault.
/// </summary>
[Test]
public async ValueTask FailedVaultToInventoryMoveKeepsItemInVaultAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var vaultStorage = CreateVaultStorage();
player.Vault = vaultStorage;
player.IsVaultLocked = true;
var source = CreateItem(CreateDefinition(), 1);
await vaultStorage.AddItemAsync(0, source).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 0, Storages.Vault, 20, Storages.Inventory).ConfigureAwait(false);
Assert.That(vaultStorage.GetItem(0), Is.SameAs(source));
Assert.That(player.Inventory!.GetItem(20), Is.Null);
}
/// <summary>
/// Verifies that a move to a slot outside grid bounds is rejected without mutation.
/// </summary>
[Test]
public async ValueTask MoveToSlotOutsideGridBoundsIsRejectedWithoutMutationAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var source = CreateItem(CreateDefinition(2, 1), 1);
await player.Inventory!.AddItemAsync(20, source).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 20, Storages.Inventory, 27, Storages.Inventory).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(20), Is.SameAs(source));
Assert.That(player.Inventory.GetItem(27), Is.Null);
Assert.That(source.ItemSlot, Is.EqualTo(20));
}
/// <summary>
/// Verifies that a move request in an invalid player state is rejected without mutation.
/// </summary>
[Test]
public async ValueTask MoveRequestInInvalidPlayerStateIsRejectedWithoutMutationAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var source = CreateItem(CreateDefinition(), 1);
await player.Inventory!.AddItemAsync(20, source).ConfigureAwait(false);
Assert.That(await player.PlayerState.TryAdvanceToAsync(PlayerState.CharacterSelection).ConfigureAwait(false), Is.True);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 20, Storages.Inventory, 22, Storages.Inventory).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(20), Is.SameAs(source));
Assert.That(player.Inventory.GetItem(22), Is.Null);
}
/// <summary>
/// Verifies that a move to trade storage outside of a trade is rejected without mutation.
/// </summary>
[Test]
public async ValueTask MoveToTradeStorageOutsideTradeIsRejectedWithoutMutationAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var source = CreateItem(CreateDefinition(), 1);
await player.Inventory!.AddItemAsync(20, source).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 20, Storages.Inventory, 0, Storages.Trade).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(20), Is.SameAs(source));
Assert.That(player.TemporaryStorage!.Items, Does.Not.Contain(source));
}
/// <summary>
/// Verifies that a move request when the trade button is pressed is rejected without mutation.
/// </summary>
[Test]
public async ValueTask MoveRequestInTradeButtonPressedStateIsRejectedWithoutMutationAsync()
{
var trader1 = await CreateTestPlayerAsync().ConfigureAwait(false);
var trader2 = await CreateTestPlayerAsync().ConfigureAwait(false);
var tradeRequestAction = new TradeRequestAction();
var tradeResponseAction = new TradeAcceptAction();
var tradeButtonAction = new TradeButtonAction();
var itemInTrade = CreateItem(CreateDefinition(), 1);
var blockedMoveItem = CreateItem(CreateDefinition(), 1);
await trader1.Inventory!.AddItemAsync(20, itemInTrade).ConfigureAwait(false);
await trader1.Inventory.AddItemAsync(21, blockedMoveItem).ConfigureAwait(false);
await tradeRequestAction.RequestTradeAsync(trader1, trader2).ConfigureAwait(false);
await tradeResponseAction.HandleTradeAcceptAsync(trader2, true).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(trader1, 20, Storages.Inventory, 0, Storages.Trade).ConfigureAwait(false);
await tradeButtonAction.TradeButtonChangedAsync(trader1, TradeButtonState.Checked).ConfigureAwait(false);
Assert.That(trader1.PlayerState.CurrentState, Is.EqualTo(PlayerState.TradeButtonPressed));
await action.MoveItemAsync(trader1, 21, Storages.Inventory, 1, Storages.Trade).ConfigureAwait(false);
Assert.That(trader1.Inventory.GetItem(21), Is.SameAs(blockedMoveItem));
Assert.That(trader1.TemporaryStorage!.GetItem(1), Is.Null);
}
/// <summary>
/// Verifies that logging out and back in after an inventory to vault move keeps a single persisted copy.
/// </summary>
[Test]
public async ValueTask LogoutAndRelogAfterInventoryToVaultMoveKeepsSinglePersistedCopyAsync()
{
var player = await CreateTestPlayerAsync().ConfigureAwait(false);
var selectedCharacter = player.SelectedCharacter!;
var vaultStorage = CreateVaultStorage();
player.Vault = vaultStorage;
player.OpenedNpc = new NonPlayerCharacter(null!, new MonsterDefinition { NpcWindow = NpcWindow.VaultStorage }, null!);
Assert.That(await player.PlayerState.TryAdvanceToAsync(PlayerState.NpcDialogOpened).ConfigureAwait(false), Is.True);
var movedItem = CreateItem(CreateDefinition(), 1);
await player.Inventory!.AddItemAsync(20, movedItem).ConfigureAwait(false);
var action = new MoveItemAction();
await action.MoveItemAsync(player, 20, Storages.Inventory, 0, Storages.Vault).ConfigureAwait(false);
Assert.That(player.Inventory.GetItem(20), Is.Null);
Assert.That(vaultStorage.GetItem(0), Is.SameAs(movedItem));
await player.RemoveFromGameAsync().ConfigureAwait(false);
await player.SetSelectedCharacterAsync(selectedCharacter).ConfigureAwait(false);
Assert.That(player.Inventory!.Items.Count(i => ReferenceEquals(i, movedItem)), Is.EqualTo(0));
Assert.That(vaultStorage.Items.Count(i => ReferenceEquals(i, movedItem)), Is.EqualTo(1));
}
private static Storage CreateVaultStorage()
{
var itemStorage = new Mock<ItemStorage>();
itemStorage.Setup(i => i.Items).Returns(new List<Item>());
return new Storage(InventoryConstants.WarehouseSize, itemStorage.Object);
}
private static async ValueTask<Player> CreateTestPlayerAsync()
{
var gameConfig = new Mock<GameConfiguration>();
gameConfig.SetupAllProperties();
gameConfig.Setup(c => c.Maps).Returns(new List<GameMapDefinition>());
gameConfig.Setup(c => c.Items).Returns(new List<ItemDefinition>());
gameConfig.Setup(c => c.Skills).Returns(new List<Skill>());
gameConfig.Setup(c => c.PlugInConfigurations).Returns(new List<PlugInConfiguration>());
gameConfig.Setup(c => c.CharacterClasses).Returns(new List<CharacterClass>());
gameConfig.Setup(c => c.Attributes).Returns(new List<AttributeDefinition>());
gameConfig.Setup(c => c.GlobalAttributeCombinations).Returns(new List<AttributeRelationship>());
gameConfig.Setup(c => c.GlobalBaseAttributeValues).Returns(new List<ConstValueAttribute>
{
new(1, Stats.MoneyAmountRate),
});
var map = new Mock<GameMapDefinition>();
map.SetupAllProperties();
map.Setup(m => m.DropItemGroups).Returns(new List<DropItemGroup>());
map.Setup(m => m.MonsterSpawns).Returns(new List<MonsterSpawnArea>());
map.Object.TerrainData = new byte[ushort.MaxValue + 3];
gameConfig.Object.RecoveryInterval = int.MaxValue;
gameConfig.Object.Maps.Add(map.Object);
var mapInitializer = new MapInitializer(gameConfig.Object, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var gameContext = new GameContext(
gameConfig.Object,
new InMemoryPersistenceContextProvider(),
mapInitializer,
new NullLoggerFactory(),
new PlugInManager(null, new NullLoggerFactory(), null, null),
NullDropGenerator.Instance,
new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameContext.PlugInManager;
mapInitializer.PathFinderPool = gameContext.PathFinderPool;
return await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
}
private static ItemDefinition CreateDefinition(byte width = 1, byte height = 1, byte durability = 1)
{
return new ItemDefinition
{
Width = width,
Height = height,
Durability = durability,
};
}
private static Item CreateItem(ItemDefinition definition, double durability, byte level = 0)
{
var item = new Mock<Item>();
item.SetupAllProperties();
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
item.Object.Definition = definition;
item.Object.Durability = durability;
item.Object.Level = level;
return item.Object;
}
}

View File

@@ -0,0 +1,89 @@
// <copyright file="ObserverToWorldAdapterTest.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using Nito.AsyncEx;
namespace MUnique.OpenMU.Tests;
using Moq;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests for the <see cref="ObserverToWorldViewAdapter"/>.
/// </summary>
[TestFixture]
public class ObserverToWorldAdapterTest
{
/// <summary>
/// Tests if a <see cref="ILocateable"/> is only reported once to the <see cref="INewNpcsInScopePlugIn"/> when it's already known to it.
/// </summary>
[Test]
public async ValueTask LocateableAddedAlreadyExistsAsync()
{
var worldObserver = new Mock<IWorldObserver>();
var view = new Mock<INewNpcsInScopePlugIn>();
var viewPlugIns = new Mock<ICustomPlugInContainer<IViewPlugIn>>();
viewPlugIns.Setup(v => v.GetPlugIn<INewNpcsInScopePlugIn>()).Returns(view.Object);
worldObserver.Setup(o => o.ViewPlugIns).Returns(viewPlugIns.Object);
var adapter = new ObserverToWorldViewAdapter(worldObserver.Object, 12);
var map = new GameMap(new DataModel.Configuration.GameMapDefinition(), TimeSpan.FromSeconds(10), 8);
var nonPlayer = new NonPlayerCharacter(new DataModel.Configuration.MonsterSpawnArea(), new DataModel.Configuration.MonsterDefinition(), map)
{
Position = new Point(128, 128),
};
await map.AddAsync(nonPlayer).ConfigureAwait(false);
await adapter.LocateableAddedAsync(nonPlayer).ConfigureAwait(false);
adapter.ObservingBuckets.Add(nonPlayer.NewBucket!);
nonPlayer.OldBucket = nonPlayer.NewBucket; // oldbucket would be set, if it got moved on the map
await adapter.LocateableAddedAsync(nonPlayer).ConfigureAwait(false);
view.Verify(v => v.NewNpcsInScopeAsync(It.Is<IEnumerable<NonPlayerCharacter>>(arg => arg.Contains(nonPlayer)), true), Times.Once);
}
/// <summary>
/// Tests if a <see cref="ILocateable"/> is not reported as out of scope to the view plugins when its new bucket is still observed.
/// </summary>
[Test]
public async ValueTask LocateableNotOutOfScopeWhenMovedToObservedBucketAsync()
{
var worldObserver = new Mock<IWorldObserver>();
var view1 = new Mock<INewNpcsInScopePlugIn>();
var view2 = new Mock<IObjectsOutOfScopePlugIn>();
var view3 = new Mock<IObjectMovedPlugIn>();
var viewPlugIns = new Mock<ICustomPlugInContainer<IViewPlugIn>>();
viewPlugIns.Setup(v => v.GetPlugIn<INewNpcsInScopePlugIn>()).Returns(view1.Object);
viewPlugIns.Setup(v => v.GetPlugIn<IObjectsOutOfScopePlugIn>()).Returns(view2.Object);
viewPlugIns.Setup(v => v.GetPlugIn<IObjectMovedPlugIn>()).Returns(view3.Object);
worldObserver.Setup(o => o.ViewPlugIns).Returns(viewPlugIns.Object);
var adapter = new ObserverToWorldViewAdapter(worldObserver.Object, 12);
var map = new GameMap(new DataModel.Configuration.GameMapDefinition(), TimeSpan.FromSeconds(10), 8);
var nonPlayer1 = new NonPlayerCharacter(new DataModel.Configuration.MonsterSpawnArea(), new DataModel.Configuration.MonsterDefinition(), map)
{
Position = new Point(128, 128),
};
await map.AddAsync(nonPlayer1).ConfigureAwait(false);
var nonPlayer2 = new NonPlayerCharacter(new DataModel.Configuration.MonsterSpawnArea(), new DataModel.Configuration.MonsterDefinition(), map)
{
Position = new Point(100, 128),
};
await map.AddAsync(nonPlayer2).ConfigureAwait(false);
adapter.ObservingBuckets.Add(nonPlayer1.NewBucket!);
adapter.ObservingBuckets.Add(nonPlayer2.NewBucket!);
await adapter.LocateableAddedAsync(nonPlayer1).ConfigureAwait(false);
await adapter.LocateableAddedAsync(nonPlayer2).ConfigureAwait(false);
await map.MoveAsync(nonPlayer1, nonPlayer2.Position, new AsyncLock(), MoveType.Instant).ConfigureAwait(false);
view1.Verify(v => v.NewNpcsInScopeAsync(It.Is<IEnumerable<NonPlayerCharacter>>(arg => arg.Contains(nonPlayer1)), true), Times.Once);
view1.Verify(v => v.NewNpcsInScopeAsync(It.Is<IEnumerable<NonPlayerCharacter>>(arg => arg.Contains(nonPlayer2)), true), Times.Once);
view2.Verify(v => v.ObjectsOutOfScopeAsync(It.IsAny<IEnumerable<IIdentifiable>>()), Times.Never);
view3.Verify(v => v.ObjectMovedAsync(It.Is<ILocateable>(arg => arg == nonPlayer1), MoveType.Instant), Times.Once);
}
}

View File

@@ -0,0 +1,77 @@
// <copyright file="BuffHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using NUnit.Framework;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
/// <summary>
/// Tests for <see cref="BuffHandler"/>.
/// </summary>
[TestFixture]
public class BuffHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="BuffHandler.PerformBuffsAsync"/> returns true immediately
/// when no buff skills are configured.
/// </summary>
[Test]
public async ValueTask ReturnsTrueWhenNoBuffsConfiguredAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var config = new MuHelperSettings
{
BuffSkill0Id = 0,
BuffSkill1Id = 0,
BuffSkill2Id = 0,
};
var handler = new BuffHandler(player, config);
// Act
var result = await handler.PerformBuffsAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that <see cref="BuffHandler.PerformBuffsAsync"/> returns true when config is null.
/// </summary>
[Test]
public async ValueTask ReturnsTrueWhenConfigNullAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var handler = new BuffHandler(player, null);
// Act
var result = await handler.PerformBuffsAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,160 @@
// <copyright file="CombatHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using MUnique.OpenMU.Pathfinding;
using MonsterDefinition = MUnique.OpenMU.Persistence.BasicModel.MonsterDefinition;
using MonsterAttribute = MUnique.OpenMU.Persistence.BasicModel.MonsterAttribute;
/// <summary>
/// Tests for <see cref="CombatHandler"/>.
/// </summary>
[TestFixture]
public class CombatHandlerTests
{
private IGameContext _gameContext = null!;
private readonly Point _origin = new(100, 100);
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="CombatHandler.PerformAttackAsync"/> moves closer to the target if out of range.
/// </summary>
[Test]
public async ValueTask PerformAttackAsync_MovesCloserToTargetAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.Position = this._origin;
var monster = await this.CreateMonsterAsync(new Point(105, 105)).ConfigureAwait(false);
await player.CurrentMap!.AddAsync(monster).ConfigureAwait(false);
var config = new MuHelperSettings { HuntingRange = 10 };
var movementHandler = new MovementHandler(player, config, this._origin);
var handler = new CombatHandler(player, config, movementHandler, this._origin);
// Act
await handler.PerformAttackAsync().ConfigureAwait(false);
// Assert
// Should have initiated a walk closer to the monster
Assert.That(player.IsWalking, Is.True);
}
/// <summary>
/// Tests that <see cref="CombatHandler.PerformDrainLifeRecoveryAsync"/> uses Drain Life when HP is low.
/// </summary>
[Test]
public async ValueTask PerformDrainLifeRecoveryAsync_UsesDrainLifeWhenLowHpAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.Position = this._origin;
player.Attributes![Stats.CurrentHealth] = 10; // Low HP
player.Attributes[Stats.MaximumHealth] = 100;
// Place monster slightly away from player so GetDirectionTo doesn't return Undefined
var monsterPosition = new Point((byte)(this._origin.X + 1), this._origin.Y);
var monster = await this.CreateMonsterAsync(monsterPosition).ConfigureAwait(false);
await player.CurrentMap!.AddAsync(monster).ConfigureAwait(false);
var config = new MuHelperSettings
{
UseDrainLife = true,
HealThresholdPercent = 50,
HuntingRange = 10
};
// Add Drain Life skill to player
var drainSkill = new TestSkill
{
Number = 214,
};
await player.SkillList!.AddLearnedSkillAsync(drainSkill).ConfigureAwait(false);
var movementHandler = new MovementHandler(player, config, this._origin);
var handler = new CombatHandler(player, config, movementHandler, this._origin);
// Act
await handler.PerformDrainLifeRecoveryAsync().ConfigureAwait(false);
// Assert
// We verify that rotation was updated, which happens during ExecuteAttackAsync
Assert.That(player.Rotation, Is.Not.EqualTo(default(Direction)));
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private async ValueTask<Monster> CreateMonsterAsync(Point position)
{
var monsterDefinition = new MonsterDefinition
{
ObjectKind = NpcObjectKind.Monster,
};
monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.MaximumHealth, Value = 1000 });
monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.DefenseBase, Value = 100 });
var map = await this._gameContext.GetMapAsync(0).ConfigureAwait(false)!;
var spawnArea = new MonsterSpawnArea
{
MonsterDefinition = monsterDefinition,
GameMap = map!.Definition,
X1 = position.X,
Y1 = position.Y,
X2 = position.X,
Y2 = position.Y,
Quantity = 1,
};
var monster = new Monster(
spawnArea,
monsterDefinition,
map,
NullDropGenerator.Instance,
new Mock<INpcIntelligence>().Object,
this._gameContext.PlugInManager,
this._gameContext.PathFinderPool);
monster.Initialize();
monster.Attributes[Stats.CurrentHealth] = 100;
return monster;
}
private class TestSkill : Skill
{
public TestSkill()
{
this.Requirements = new List<AttributeRequirement>();
this.ConsumeRequirements = new List<AttributeRequirement>();
this.Range = 1;
this.SkillType = SkillType.DirectHit;
this.DamageType = DamageType.Curse;
}
}
}

View File

@@ -0,0 +1,103 @@
// <copyright file="HealingHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
/// <summary>
/// Tests for <see cref="HealingHandler"/>.
/// </summary>
[TestFixture]
public class HealingHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="HealingHandler.PerformHealthRecoveryAsync"/> does nothing
/// when config is null.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenConfigNullAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var handler = new HealingHandler(player, null);
// Act
await handler.PerformHealthRecoveryAsync().ConfigureAwait(false);
}
/// <summary>
/// Tests that <see cref="HealingHandler.PerformHealthRecoveryAsync"/> does not consume
/// a potion when the player's HP is above the threshold.
/// </summary>
[Test]
public async ValueTask DoesNotUsePotionAboveThresholdAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var maxHp = player.Attributes![Stats.MaximumHealth];
player.Attributes[Stats.CurrentHealth] = maxHp * 0.9f;
var potion = this.CreateHealthPotion();
await player.Inventory!.AddItemAsync((byte)(InventoryConstants.FirstEquippableItemSlotIndex + 12), potion).ConfigureAwait(false);
var config = new MuHelperSettings
{
UseHealPotion = true,
PotionThresholdPercent = 50,
};
var handler = new HealingHandler(player, config);
// Act
await handler.PerformHealthRecoveryAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Inventory?.GetItem(potion.ItemSlot), Is.Not.Null);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private MUnique.OpenMU.DataModel.Entities.Item CreateHealthPotion()
{
var definition = new Mock<MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition>();
definition.SetupAllProperties();
definition.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
definition.Setup(d => d.PossibleItemOptions).Returns(new List<ItemOptionDefinition>());
definition.Object.Number = ItemConstants.SmallHealingPotion.Number!.Value;
definition.Object.Group = ItemConstants.SmallHealingPotion.Group;
var item = new Mock<MUnique.OpenMU.DataModel.Entities.Item>();
item.SetupAllProperties();
item.Setup(i => i.Definition).Returns(definition.Object);
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
item.Object.Durability = 1;
return item.Object;
}
}

View File

@@ -0,0 +1,134 @@
// <copyright file="ItemPickupHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
/// <summary>
/// Tests for <see cref="ItemPickupHandler"/>.
/// </summary>
[TestFixture]
public class ItemPickupHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that pickup does nothing when all pickup options are disabled.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenAllDisabledAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var config = new MuHelperSettings
{
PickAllItems = false,
PickJewel = false,
PickAncient = false,
PickZen = false,
PickExcellent = false,
};
var handler = new ItemPickupHandler(player, config);
// Act
await handler.PickupItemsAsync().ConfigureAwait(false);
}
/// <summary>
/// Tests that pickup does nothing when config is null.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenConfigNullAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var handler = new ItemPickupHandler(player, null);
// Act
await handler.PickupItemsAsync().ConfigureAwait(false);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private Item CreateItem(byte group, short number)
{
var definition = new Mock<ItemDefinition>();
definition.SetupAllProperties();
definition.Object.Group = group;
definition.Object.Number = number;
definition.Object.Width = 1;
definition.Object.Height = 1;
var item = new Mock<Item>();
item.SetupAllProperties();
item.Setup(i => i.Definition).Returns(definition.Object);
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
return item.Object;
}
/// <summary>
/// Tests that a jewel pickup option only picks up actual jewels.
/// </summary>
[Test]
public async ValueTask PickJewel_PicksUpJewelsOnlyAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
Assert.That(player.CurrentMap, Is.Not.Null);
var config = new MuHelperSettings
{
PickSelectItems = true,
PickJewel = true,
ObtainRange = 5,
};
var handler = new ItemPickupHandler(player, config);
var bless = this.CreateItem(14, 13);
var chaos = this.CreateItem(12, 15);
var potion = this.CreateItem(14, 0);
var eye = this.CreateItem(14, 17);
var blessDrop = new DroppedItem(bless, player.Position, player.CurrentMap!, player);
var chaosDrop = new DroppedItem(chaos, player.Position, player.CurrentMap!, player);
var potionDrop = new DroppedItem(potion, player.Position, player.CurrentMap!, player);
var eyeDrop = new DroppedItem(eye, player.Position, player.CurrentMap!, player);
await player.CurrentMap!.AddAsync(blessDrop).ConfigureAwait(false);
await player.CurrentMap.AddAsync(chaosDrop).ConfigureAwait(false);
await player.CurrentMap.AddAsync(potionDrop).ConfigureAwait(false);
await player.CurrentMap.AddAsync(eyeDrop).ConfigureAwait(false);
// Act
await handler.PickupItemsAsync().ConfigureAwait(false);
// Assert
Assert.That(player.CurrentMap.GetObject(blessDrop.Id), Is.Null, "Bless should be picked up");
Assert.That(player.CurrentMap.GetObject(chaosDrop.Id), Is.Null, "Chaos should be picked up");
Assert.That(player.CurrentMap.GetObject(potionDrop.Id), Is.Not.Null, "Potion should NOT be picked up");
Assert.That(player.CurrentMap.GetObject(eyeDrop.Id), Is.Not.Null, "Devil's Eye should NOT be picked up");
}
}

View File

@@ -0,0 +1,61 @@
// <copyright file="MovementHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using MUnique.OpenMU.Pathfinding;
/// <summary>
/// Tests for <see cref="MovementHandler"/>.
/// </summary>
[TestFixture]
public class MovementHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="MovementHandler.RegroupAsync"/> does nothing when within range.
/// </summary>
[Test]
public async ValueTask RegroupAsync_DoesNothingWhenWithinRangeAsync()
{
// Arrange
var origin = new Point(100, 100);
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.Position = new Point(100, 101); // Within RegroupDistanceThreshold (1)
var config = new MuHelperSettings
{
ReturnToOriginalPosition = true,
HuntingRange = 5,
};
var handler = new MovementHandler(player, config, origin);
// Act
var result = await handler.RegroupAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
Assert.That(player.IsWalking, Is.False);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,146 @@
// <copyright file="OfflinePlayerManagerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.Persistence;
/// <summary>
/// Tests for <see cref="OfflinePlayerManager"/>.
/// </summary>
[TestFixture]
public class OfflinePlayerManagerTests
{
private const string TestUserLoginName = "test";
private const string TestCharacterName = "testChar";
private IGameContext _gameContext = null!;
private IPersistenceContextProvider _contextProvider = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
this._contextProvider = this._gameContext.PersistenceContextProvider;
}
/// <summary>
/// Tests that <see cref="OfflinePlayerManager.StartAsync"/> returns true on success.
/// </summary>
[Test]
public async ValueTask StartAsync_WithValidPlayer_ReturnsTrueAsync()
{
// Arrange
var manager = new OfflinePlayerManager();
var realPlayer = await this.CreatePlayerWithPersistedAccountAsync().ConfigureAwait(false);
realPlayer.TryAddMoney(1_000_000);
realPlayer.Attributes![Stats.Level] = 100;
// Act
var result = await manager.StartAsync(realPlayer, TestUserLoginName).ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
Assert.That(manager.IsActive(TestUserLoginName), Is.True);
}
/// <summary>
/// Tests that <see cref="OfflinePlayerManager.StartAsync"/> fails if the player has insufficient Zen.
/// </summary>
[Test]
public async ValueTask StartAsync_WithInsufficientZen_ReturnsFalseAsync()
{
// Arrange
var manager = new OfflinePlayerManager();
var realPlayer = await this.CreatePlayerWithPersistedAccountAsync().ConfigureAwait(false);
realPlayer.Money = 0; // No money
realPlayer.Attributes![Stats.Level] = 100;
// Act
var result = await manager.StartAsync(realPlayer, TestUserLoginName).ConfigureAwait(false);
// Assert
Assert.That(result, Is.False);
Assert.That(manager.IsActive(TestUserLoginName), Is.False);
}
/// <summary>
/// Tests that <see cref="OfflinePlayerManager.StopAsync"/> successfully stops a session.
/// </summary>
[Test]
public async ValueTask StopAsync_WhenSessionActive_StopsSuccessfullyAsync()
{
// Arrange
var manager = new OfflinePlayerManager();
var realPlayer = await this.CreatePlayerWithPersistedAccountAsync().ConfigureAwait(false);
realPlayer.TryAddMoney(1_000_000);
realPlayer.Attributes![Stats.Level] = 100;
var started = await manager.StartAsync(realPlayer, TestUserLoginName).ConfigureAwait(false);
Assert.That(started, Is.True);
Assert.That(manager.IsActive(TestUserLoginName), Is.True);
// Act
await manager.StopAsync(TestUserLoginName).ConfigureAwait(false);
// Assert
Assert.That(manager.IsActive(TestUserLoginName), Is.False);
}
/// <summary>
/// Creates a player whose account is stored in the in-memory repository,
/// so that <see cref="OfflinePlayer.InitializeAsync"/> can find it.
/// </summary>
private async ValueTask<Player> CreatePlayerWithPersistedAccountAsync()
{
var config = this._gameContext.Configuration;
// Create persisted account + character in the in-memory repository.
using (var ctx = this._contextProvider.CreateNewPlayerContext(config))
{
var account = ctx.CreateNew<MUnique.OpenMU.DataModel.Entities.Account>();
account.LoginName = TestUserLoginName;
var character = ctx.CreateNew<MUnique.OpenMU.DataModel.Entities.Character>();
character.Name = TestCharacterName;
if (config.CharacterClasses.FirstOrDefault() is { } existingClass)
{
character.CharacterClass = existingClass;
}
else
{
// Build a minimal CharacterClass so OnPlayerEnteredWorldAsync does not throw.
var characterClass = ctx.CreateNew<MUnique.OpenMU.DataModel.Configuration.CharacterClass>();
characterClass.HomeMap = config.Maps.FirstOrDefault();
character.CharacterClass = characterClass;
}
account.Characters.Add(character);
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
var player = await PlayerTestHelper.CreatePlayerAsync(this._gameContext).ConfigureAwait(false);
player.Account!.LoginName = TestUserLoginName;
// Ensure the mock character name matches the persisted one.
var mockCharacter = player.SelectedCharacter!;
mockCharacter.Name = TestCharacterName;
mockCharacter.CharacterClass ??= player.Account.UnlockedCharacterClasses.FirstOrDefault();
if (mockCharacter.CharacterClass is null && config.CharacterClasses.FirstOrDefault() is { } cc)
{
mockCharacter.CharacterClass = cc;
}
return player;
}
}

View File

@@ -0,0 +1,64 @@
// <copyright file="OfflinePlayerMuHelperTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Offline;
/// <summary>
/// Tests for <see cref="OfflinePlayerMuHelper"/>.
/// </summary>
[TestFixture]
public class OfflinePlayerMuHelperTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that the intelligence can be created and started.
/// </summary>
[Test]
public async ValueTask StartsWithoutExceptionAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
// Act
var intelligence = new OfflinePlayerMuHelper(player);
intelligence.Start();
intelligence?.Dispose();
}
/// <summary>
/// Tests that disposing the intelligence twice does not throw.
/// </summary>
[Test]
public async ValueTask DisposeTwiceDoesNotThrowAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var intelligence = new OfflinePlayerMuHelper(player);
intelligence.Start();
// Act & Assert
intelligence.Dispose();
intelligence.Dispose();
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="OfflinePlayerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Offline;
/// <summary>
/// Tests for <see cref="OfflinePlayer"/>.
/// </summary>
[TestFixture]
public class OfflinePlayerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that the offline player is created and started successfully.
/// </summary>
[Test]
public async ValueTask InitializesSuccessfullyAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
// Assert
Assert.That(player.PlayerState.CurrentState, Is.EqualTo(PlayerState.EnteredWorld));
Assert.That(player.SelectedCharacter, Is.Not.Null);
}
/// <summary>
/// Tests that <see cref="OfflinePlayer.StopAsync"/> cleans up resources.
/// </summary>
[Test]
public async ValueTask StopAsync_CleansUpAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
// Act
await player.StopAsync().ConfigureAwait(false);
// Assert
Assert.That(player.PlayerState.CurrentState, Is.EqualTo(PlayerState.Finished));
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,117 @@
// <copyright file="PetHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using MUnique.OpenMU.GameLogic.Pet;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using Item = MUnique.OpenMU.Persistence.BasicModel.Item;
using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition;
using ItemSlotType = MUnique.OpenMU.Persistence.BasicModel.ItemSlotType;
/// <summary>
/// Tests for <see cref="PetHandler"/>.
/// </summary>
[TestFixture]
public class PetHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="PetHandler.InitializeAsync"/> sets the correct Dark Raven behavior.
/// </summary>
[Test]
[TestCase(1, PetBehaviour.AttackRandom)]
[TestCase(2, PetBehaviour.AttackWithOwner)]
public async ValueTask InitializeAsync_SetsDarkRavenBehaviorAsync(int mode, PetBehaviour expectedBehaviour)
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var petCommandManagerMock = new Mock<IPetCommandManager>();
var config = new MuHelperSettings
{
UseDarkRaven = true,
DarkRavenMode = (byte)mode,
};
var handler = new PetHandler(player, config, petCommandManagerMock.Object);
// Act
await handler.InitializeAsync().ConfigureAwait(false);
// Assert
petCommandManagerMock.Verify(m => m.SetBehaviourAsync(expectedBehaviour, null), Times.Once);
}
/// <summary>
/// Tests that <see cref="PetHandler.CheckPetDurabilityAsync"/> sets pet behavior to Idle when durability is 0.
/// </summary>
[Test]
public async ValueTask CheckPetDurabilityAsync_SetsIdleWhenDurabilityIsZeroAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var petCommandManagerMock = new Mock<IPetCommandManager>();
var petItem = new Item
{
Definition = new ItemDefinition
{
ItemSlot = new ItemSlotType(),
Width = 1,
Height = 1,
},
Durability = 0,
};
await player.Inventory!.AddItemAsync(InventoryConstants.PetSlot, petItem).ConfigureAwait(false);
var handler = new PetHandler(player, new MuHelperSettings(), petCommandManagerMock.Object);
// Act
await handler.CheckPetDurabilityAsync().ConfigureAwait(false);
// Assert
petCommandManagerMock.Verify(m => m.SetBehaviourAsync(PetBehaviour.Idle, null), Times.Once);
}
/// <summary>
/// Tests that <see cref="PetHandler.StopAsync"/> sets pet behavior to Idle.
/// </summary>
[Test]
public async ValueTask StopAsync_SetsIdleAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var petCommandManagerMock = new Mock<IPetCommandManager>();
var handler = new PetHandler(player, new MuHelperSettings(), petCommandManagerMock.Object);
// Act
await handler.StopAsync().ConfigureAwait(false);
// Assert
petCommandManagerMock.Verify(m => m.SetBehaviourAsync(PetBehaviour.Idle, null), Times.Once);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,221 @@
// <copyright file="RepairHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using Item = MUnique.OpenMU.Persistence.BasicModel.Item;
using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition;
using ItemSlotType = MUnique.OpenMU.Persistence.BasicModel.ItemSlotType;
/// <summary>
/// Tests for <see cref="RepairHandler"/>.
/// </summary>
[TestFixture]
public class RepairHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Items at or below the 50% durability threshold should be repaired when the player
/// has enough Zen. Uses 10% (well below threshold) to confirm the happy path.
/// </summary>
[Test]
public async ValueTask RepairsItemWhenSufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(100));
}
/// <summary>
/// Auto-repair does nothing when disabled in the configuration, regardless of durability.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenDisabledAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = false };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(10));
}
/// <summary>
/// Auto-repair does not repair when the player has insufficient Zen.
/// </summary>
[Test]
public async ValueTask DoesNotRepairWhenInsufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(10));
}
/// <summary>
/// Fully durable items are skipped and no Zen is spent.
/// </summary>
[Test]
public async ValueTask SkipsFullyDurableItemsAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 100);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var initialMoney = 1_000_000;
player.TryAddMoney(initialMoney);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.EqualTo(initialMoney));
}
/// <summary>
/// An item at exactly 50% durability (the threshold boundary) must be repaired.
/// Mirrors the client check: iHealth &lt;= DEFAULT_DURABILITY_THRESHOLD (50).
/// </summary>
[Test]
public async ValueTask RepairsItemAtExactlyFiftyPercentThresholdAsync()
{
// Arrange — 50 / 100 = 50% (ceiling-integer: (50*100 + 99) / 100 = 50)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 50);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(100));
}
/// <summary>
/// An item at 51% durability is above the threshold and must NOT be repaired,
/// so no Zen is spent. Mirrors the client check: iHealth &lt;= 50.
/// </summary>
[Test]
public async ValueTask SkipsItemAboveFiftyPercentThresholdAsync()
{
// Arrange — 51 / 100 = 51% (ceiling-integer: (51*100 + 99) / 100 = 51)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 51);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var initialMoney = 1_000_000;
player.TryAddMoney(initialMoney);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert — durability unchanged, no money spent
Assert.That(item.Durability, Is.EqualTo(51));
Assert.That(player.Money, Is.EqualTo(initialMoney));
}
/// <summary>
/// Verifies that the ceiling-integer formula handles non-round max-durability values
/// correctly: 13 / 25 = 52% (above threshold → skip), but 12 / 25 = 48% (≤ 50% → repair).
/// </summary>
[TestCase(13, false, TestName = "NonRoundMax_52pct_Skipped")]
[TestCase(12, true, TestName = "NonRoundMax_48pct_Repaired")]
public async ValueTask ThresholdWithNonRoundMaxDurabilityAsync(byte currentDurability, bool expectRepair)
{
// Arrange — max = 25; ceiling(d*100/25) = ceiling(d*4)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 25, currentDurability: currentDurability);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
if (expectRepair)
{
Assert.That(item.Durability, Is.EqualTo(25));
}
else
{
Assert.That(item.Durability, Is.EqualTo(currentDurability));
}
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private Item CreateDamagedItem(byte maxDurability, byte currentDurability)
{
return new Item
{
Definition = new ItemDefinition
{
Durability = maxDurability,
ItemSlot = new ItemSlotType(),
Width = 1,
Height = 1,
Value = 1000,
},
Durability = currentDurability,
};
}
}

View File

@@ -0,0 +1,97 @@
// <copyright file="ZenConsumptionHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
/// <summary>
/// Tests for <see cref="ZenConsumptionHandler"/>.
/// </summary>
[TestFixture]
public class ZenConsumptionHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that Zen is deducted when the pay interval has passed.
/// </summary>
[Test]
public async ValueTask DeductsZenWhenIntervalPassedAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.TryAddMoney(1_000_000);
player.Attributes![Stats.Level] = 100;
var handler = new ZenConsumptionHandler(player);
player.StartTimestamp = DateTime.UtcNow.AddMinutes(-2);
// Act
await handler.DeductZenAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.LessThan(1_000_000));
}
/// <summary>
/// Tests that Zen is not deducted when the pay interval has not passed.
/// </summary>
[Test]
public async ValueTask DoesNotDeductZenWhenIntervalNotPassedAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var handler = new ZenConsumptionHandler(player);
// Act
await handler.DeductZenAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.EqualTo(1_000_000));
}
/// <summary>
/// Tests that <see cref="ZenConsumptionHandler.DeductZenAsync"/> returns false
/// when the player has insufficient Zen.
/// </summary>
[Test]
public async ValueTask ReturnsFalseWhenInsufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.TryAddMoney(100);
player.Attributes![Stats.Level] = 100;
var handler = new ZenConsumptionHandler(player);
player.StartTimestamp = DateTime.UtcNow.AddMinutes(-2);
// Act
var result = await handler.DeductZenAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.False);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
var player = await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
player.Attributes![Stats.MasterLevel] = 0;
return player;
}
}

View File

@@ -0,0 +1,215 @@
// <copyright file="PKClearChatCommandPlugInTest.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 MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
/// <summary>
/// Tests for the <see cref="PkClearChatCommandPlugIn"/>.
/// </summary>
[TestFixture]
public class PKClearChatCommandPlugInTest
{
/// <summary>
/// Verifies that a regular player is not allowed to run the command if AllowRegularPlayers configuration is false.
/// </summary>
[Test]
public async ValueTask RegularPlayerNotAllowedAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
player.SelectedCharacter.PlayerKillCount = 3;
player.SelectedCharacter.State = HeroState.PlayerKiller2ndStage;
var plugin = new PkClearChatCommandPlugIn
{
Configuration = new PkClearChatCommandPlugIn.PKClearConfiguration
{
AllowRegularPlayers = false,
ZenCostPerKill = 10_000_000,
}
};
await plugin.HandleCommandAsync(player, "/pkclear").ConfigureAwait(false);
// Verify status not cleared
Assert.That(player.SelectedCharacter.PlayerKillCount, Is.EqualTo(3));
Assert.That(player.SelectedCharacter.State, Is.EqualTo(HeroState.PlayerKiller2ndStage));
}
/// <summary>
/// Verifies that a regular player successfully clears their PK status and gets charged Zen if AllowRegularPlayers is true.
/// </summary>
[Test]
public async ValueTask RegularPlayerSuccessAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
player.SelectedCharacter.PlayerKillCount = 3;
player.SelectedCharacter.State = HeroState.PlayerKiller2ndStage;
player.Money = 50_000_000;
var plugin = new PkClearChatCommandPlugIn
{
Configuration = new PkClearChatCommandPlugIn.PKClearConfiguration
{
AllowRegularPlayers = true,
ZenCostPerKill = 10_000_000,
}
};
await plugin.HandleCommandAsync(player, "/pkclear").ConfigureAwait(false);
// Verify status cleared and money deducted
Assert.That(player.SelectedCharacter.PlayerKillCount, Is.EqualTo(0));
Assert.That(player.SelectedCharacter.State, Is.EqualTo(HeroState.Normal));
Assert.That(player.Money, Is.EqualTo(20_000_000));
}
/// <summary>
/// Verifies that a regular player with insufficient Zen fails to clear their PK status.
/// </summary>
[Test]
public async ValueTask RegularPlayerNotEnoughMoneyAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
player.SelectedCharacter.PlayerKillCount = 3;
player.SelectedCharacter.State = HeroState.PlayerKiller2ndStage;
player.Money = 5_000_000;
var plugin = new PkClearChatCommandPlugIn
{
Configuration = new PkClearChatCommandPlugIn.PKClearConfiguration
{
AllowRegularPlayers = true,
ZenCostPerKill = 10_000_000,
}
};
await plugin.HandleCommandAsync(player, "/pkclear").ConfigureAwait(false);
// Verify status not cleared and money not deducted
Assert.That(player.SelectedCharacter.PlayerKillCount, Is.EqualTo(3));
Assert.That(player.SelectedCharacter.State, Is.EqualTo(HeroState.PlayerKiller2ndStage));
Assert.That(player.Money, Is.EqualTo(5_000_000));
}
/// <summary>
/// Verifies that a Game Master can clear a target player's PK status for free.
/// </summary>
[Test]
public async ValueTask GameMasterClearsTargetForFreeAsync()
{
// Set up the GM
var gm = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
gm.SelectedCharacter!.CharacterStatus = CharacterStatus.GameMaster;
gm.SelectedCharacter.Name = "GM_Character";
// Set up the target player
var targetPlayer = await PlayerTestHelper.CreatePlayerAsync(gm.GameContext).ConfigureAwait(false);
targetPlayer.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
targetPlayer.SelectedCharacter.Name = "Target_Character";
targetPlayer.SelectedCharacter.PlayerKillCount = 5;
targetPlayer.SelectedCharacter.State = HeroState.PlayerKiller2ndStage;
targetPlayer.Money = 0;
// Register target player in GameContext
var gameContext = gm.GameContext as GameContext;
Assert.That(gameContext, Is.Not.Null);
gameContext!.PlayersByCharacterName.TryAdd(targetPlayer.SelectedCharacter.Name, targetPlayer);
var plugin = new PkClearChatCommandPlugIn
{
Configuration = new PkClearChatCommandPlugIn.PKClearConfiguration
{
AllowRegularPlayers = true,
ZenCostPerKill = 10_000_000,
}
};
await plugin.HandleCommandAsync(gm, "/pkclear Target_Character").ConfigureAwait(false);
// Verify target status cleared and no money deducted
Assert.That(targetPlayer.SelectedCharacter.PlayerKillCount, Is.EqualTo(0));
Assert.That(targetPlayer.SelectedCharacter.State, Is.EqualTo(HeroState.Normal));
Assert.That(targetPlayer.Money, Is.EqualTo(0));
}
/// <summary>
/// Verifies that a regular player cannot specify another character to clear their status.
/// </summary>
[Test]
public async ValueTask RegularPlayerCannotClearTargetAsync()
{
// Set up the calling player
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
player.SelectedCharacter.Name = "Player_Character";
player.SelectedCharacter.PlayerKillCount = 0;
player.SelectedCharacter.State = HeroState.Normal;
player.Money = 50_000_000;
// Set up the target player
var targetPlayer = await PlayerTestHelper.CreatePlayerAsync(player.GameContext).ConfigureAwait(false);
targetPlayer.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
targetPlayer.SelectedCharacter.Name = "Target_Character";
targetPlayer.SelectedCharacter.PlayerKillCount = 3;
targetPlayer.SelectedCharacter.State = HeroState.PlayerKiller2ndStage;
// Register target player in GameContext
var gameContext = player.GameContext as GameContext;
Assert.That(gameContext, Is.Not.Null);
gameContext!.PlayersByCharacterName.TryAdd(targetPlayer.SelectedCharacter.Name, targetPlayer);
var plugin = new PkClearChatCommandPlugIn
{
Configuration = new PkClearChatCommandPlugIn.PKClearConfiguration
{
AllowRegularPlayers = true,
ZenCostPerKill = 10_000_000,
}
};
await plugin.HandleCommandAsync(player, "/pkclear Target_Character").ConfigureAwait(false);
// Target should NOT be cleared since player is regular and target is different character
Assert.That(targetPlayer.SelectedCharacter.PlayerKillCount, Is.EqualTo(3));
Assert.That(targetPlayer.SelectedCharacter.State, Is.EqualTo(HeroState.PlayerKiller2ndStage));
Assert.That(player.Money, Is.EqualTo(50_000_000));
}
/// <summary>
/// Verifies that integer overflow in cost calculation is prevented and capped at int.MaxValue.
/// </summary>
[Test]
public async ValueTask RegularPlayerZenOverflowPreventionAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.CharacterStatus = CharacterStatus.Normal;
player.SelectedCharacter.PlayerKillCount = 215;
player.SelectedCharacter.State = HeroState.PlayerKiller2ndStage;
player.Money = int.MaxValue - 1;
var plugin = new PkClearChatCommandPlugIn
{
Configuration = new PkClearChatCommandPlugIn.PKClearConfiguration
{
AllowRegularPlayers = true,
ZenCostPerKill = 10_000_000,
}
};
await plugin.HandleCommandAsync(player, "/pkclear").ConfigureAwait(false);
// Verify status is NOT cleared and money is NOT deducted.
Assert.That(player.SelectedCharacter.PlayerKillCount, Is.EqualTo(215));
Assert.That(player.SelectedCharacter.State, Is.EqualTo(HeroState.PlayerKiller2ndStage));
Assert.That(player.Money, Is.EqualTo(int.MaxValue - 1));
}
}

View File

@@ -0,0 +1,251 @@
// <copyright file="PacketHandlerPlugInContainerTest.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.GameLogic;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.GameServer.MessageHandler;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Unit tests for the <see cref="MainPacketHandlerPlugInContainer"/>.
/// </summary>
[TestFixture]
public class PacketHandlerPlugInContainerTest
{
private const byte HandlerKey = 0xF1;
private static readonly ClientVersion Season6E3English = new(6, 3, ClientLanguage.English);
private static readonly ClientVersion Season9E2English = new(9, 2, ClientLanguage.English);
/// <summary>
/// Tests if the the plug in of correct version is selected when the plugin for the exact version is available.
/// </summary>
[Test]
public void SelectPlugInOfCorrectVersionWhenExactVersionIsAvailable()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason1>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason9>();
var clientVersionProvider = new Mock<IClientVersionProvider>();
clientVersionProvider.Setup(p => p.ClientVersion).Returns(Season6E3English);
var containerForSeason6 = new MainPacketHandlerPlugInContainer(clientVersionProvider.Object, manager, new NullLoggerFactory());
containerForSeason6.Initialize();
var handler = containerForSeason6[HandlerKey];
Assert.That(handler!.GetType(), Is.EqualTo(typeof(PacketHandlerSeason6)));
}
/// <summary>
/// Tests if the the plug in of correct version is selected when only plugins for lower versions are available.
/// </summary>
[Test]
public void SelectPlugInOfCorrectVersionWhenLowerVersionsAreAvailable()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason1>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6>();
var clientVersionProvider = new Mock<IClientVersionProvider>();
clientVersionProvider.Setup(p => p.ClientVersion).Returns(Season9E2English);
var containerForSeason9 = new MainPacketHandlerPlugInContainer(clientVersionProvider.Object, manager, new NullLoggerFactory());
containerForSeason9.Initialize();
var handler = containerForSeason9[HandlerKey];
Assert.That(handler!.GetType(), Is.EqualTo(typeof(PacketHandlerSeason6)));
}
/// <summary>
/// Tests if plugins of the correct language are selected.
/// </summary>
[Test]
public void SelectPlugInOfCorrectLanguage()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6Chinese>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6>();
var clientVersionProvider = new Mock<IClientVersionProvider>();
clientVersionProvider.Setup(p => p.ClientVersion).Returns(Season6E3English);
var containerForSeason6 = new MainPacketHandlerPlugInContainer(clientVersionProvider.Object, manager, new NullLoggerFactory());
containerForSeason6.Initialize();
var handler = containerForSeason6[HandlerKey];
Assert.That(handler!.GetType(), Is.EqualTo(typeof(PacketHandlerSeason6)));
}
/// <summary>
/// Tests if plugins of invariant language and version are selected.
/// </summary>
[Test]
public void SelectInvariantPlugIn()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerInvariant>();
var clientVersionProvider = new Mock<IClientVersionProvider>();
clientVersionProvider.Setup(p => p.ClientVersion).Returns(Season6E3English);
var containerForSeason6 = new MainPacketHandlerPlugInContainer(clientVersionProvider.Object, manager, new NullLoggerFactory());
containerForSeason6.Initialize();
var handler = containerForSeason6[HandlerKey];
Assert.That(handler!.GetType(), Is.EqualTo(typeof(PacketHandlerInvariant)));
}
/// <summary>
/// Tests if another plugin is getting 'effective' when the currently effective plugin gets deactivated.
/// </summary>
[Test]
public void SelectPlugInAfterDeactivation()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason1>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6>();
var clientVersionProvider = new Mock<IClientVersionProvider>();
clientVersionProvider.Setup(p => p.ClientVersion).Returns(Season6E3English);
var containerForSeason6 = new MainPacketHandlerPlugInContainer(clientVersionProvider.Object, manager, new NullLoggerFactory());
containerForSeason6.Initialize();
manager.DeactivatePlugIn(typeof(PacketHandlerSeason6));
var handler = containerForSeason6[HandlerKey];
Assert.That(handler, Is.Not.Null);
Assert.That(handler!.GetType(), Is.EqualTo(typeof(PacketHandlerSeason1)));
}
/// <summary>
/// Tests if the language specific plugin has priority over the invariant one.
/// </summary>
[Test]
public void SelectLanguageSpecificOverInvariant()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6Chinese>();
manager.RegisterPlugIn<IPacketHandlerPlugIn, PacketHandlerSeason6English>();
var clientVersionProvider = new Mock<IClientVersionProvider>();
clientVersionProvider.Setup(p => p.ClientVersion).Returns(Season6E3English);
var containerForSeason6 = new MainPacketHandlerPlugInContainer(clientVersionProvider.Object, manager, new NullLoggerFactory());
containerForSeason6.Initialize();
var handler = containerForSeason6[HandlerKey];
Assert.That(handler!.GetType(), Is.EqualTo(typeof(PacketHandlerSeason6English)));
}
/// <summary>
/// Test packet handler implementation for season 1.
/// </summary>
[MinimumClient(1, 0, ClientLanguage.Invariant)]
public class PacketHandlerSeason1 : IPacketHandlerPlugIn
{
/// <inheritdoc />
public byte Key => HandlerKey;
/// <inheritdoc />
public bool IsEncryptionExpected => false;
/// <inheritdoc />
public ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
// does nothing here
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Test packet handler implementation for season 6.
/// </summary>
[MinimumClient(6, 3, ClientLanguage.Invariant)]
public class PacketHandlerSeason6 : IPacketHandlerPlugIn
{
/// <inheritdoc />
public byte Key => HandlerKey;
/// <inheritdoc />
public bool IsEncryptionExpected => false;
/// <inheritdoc />
public ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
// does nothing here
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Test packet handler implementation for season 6 english.
/// </summary>
[MinimumClient(6, 3, ClientLanguage.English)]
public class PacketHandlerSeason6English : IPacketHandlerPlugIn
{
/// <inheritdoc />
public byte Key => HandlerKey;
/// <inheritdoc />
public bool IsEncryptionExpected => false;
/// <inheritdoc />
public ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
// does nothing here
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Test packet handler implementation for season 6 chinese.
/// </summary>
[MinimumClient(6, 3, ClientLanguage.Chinese)]
public class PacketHandlerSeason6Chinese : IPacketHandlerPlugIn
{
/// <inheritdoc />
public byte Key => HandlerKey;
/// <inheritdoc />
public bool IsEncryptionExpected => false;
/// <inheritdoc />
public ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
// does nothing here
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Test packet handler implementation for season 9.
/// </summary>
[MinimumClient(9, 2, ClientLanguage.Invariant)]
public class PacketHandlerSeason9 : IPacketHandlerPlugIn
{
/// <inheritdoc />
public byte Key => HandlerKey;
/// <inheritdoc />
public bool IsEncryptionExpected => false;
/// <inheritdoc />
public ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
// does nothing here
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Invariant test packet handler implementation for all versions.
/// </summary>
public class PacketHandlerInvariant : IPacketHandlerPlugIn
{
/// <inheritdoc />
public byte Key => HandlerKey;
/// <inheritdoc />
public bool IsEncryptionExpected => false;
/// <inheritdoc />
public ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
// does nothing here
return ValueTask.CompletedTask;
}
}
}

View File

@@ -0,0 +1,205 @@
// <copyright file="PartyManagerTest.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 MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests for <see cref="PartyManager"/>.
/// </summary>
[TestFixture]
public class PartyManagerTest
{
private const byte MaxPartySize = 5;
private PartyManager _partyManager = null!;
/// <summary>
/// Sets up a fresh <see cref="PartyManager"/> before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._partyManager = new PartyManager(MaxPartySize, new NullLogger<Party>());
}
/// <summary>
/// Tests that <see cref="PartyManager.CreateParty"/> returns a party with the configured max size.
/// </summary>
[Test]
public void CreateParty_RespectsMaxPartySize()
{
var party = this._partyManager.CreateParty();
Assert.That(party.MaxPartySize, Is.EqualTo(MaxPartySize));
}
/// <summary>
/// Tests that a member added to the party is present in the party list.
/// </summary>
[Test]
public async ValueTask CreateParty_AddedMemberIsInPartyList()
{
var member = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var party = this._partyManager.CreateParty();
await party.AddAsync(member).ConfigureAwait(false);
Assert.That(party.PartyList, Contains.Item(member));
}
/// <summary>
/// Tests that <see cref="PartyManager.OnMemberReconnectedAsync"/> does nothing
/// when the member has no cached party.
/// </summary>
[Test]
public async ValueTask OnMemberReconnected_DoesNothingWhenNoCachedParty()
{
var member = await this.CreatePartyMemberAsync().ConfigureAwait(false);
await this._partyManager.OnMemberReconnectedAsync(member).ConfigureAwait(false);
Assert.That(member.Party, Is.Null);
}
/// <summary>
/// Tests that when a member is kicked the party list no longer contains them.
/// </summary>
[Test]
public async ValueTask KickedMember_IsRemovedFromPartyList()
{
var (party, _, member2) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
await party.KickPlayerAsync(GetPartyMemberIndex(party, member2)).ConfigureAwait(false);
Assert.That(party.PartyList, Is.Not.Contains(member2));
Assert.That(member2.Party, Is.Null);
}
/// <summary>
/// Tests that <see cref="Party.LeaveTemporarilyAsync"/> replaces a live member
/// with an <see cref="OfflinePartyMember"/> snapshot.
/// </summary>
[Test]
public async ValueTask LeaveTemporarily_ReplacesWithOfflineSnapshot()
{
var (party, member1, _) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
await party.LeaveTemporarilyAsync(member1).ConfigureAwait(false);
Assert.That(party.PartyList[0], Is.InstanceOf<OfflinePartyMember>());
Assert.That(party.PartyList[0].Name, Is.EqualTo(member1.Name));
}
/// <summary>
/// Tests that <see cref="PartyManager.OnMemberReconnectedAsync"/> restores the live member
/// from an <see cref="OfflinePartyMember"/> snapshot.
/// </summary>
[Test]
public async ValueTask OnMemberReconnected_RestoresLiveMember()
{
var (party, member1, _) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
var originalName = member1.Name;
// Member disconnects - replaced with offline snapshot
await party.LeaveTemporarilyAsync(member1).ConfigureAwait(false);
// Member reconnects - restored from snapshot
await this._partyManager.OnMemberReconnectedAsync(member1).ConfigureAwait(false);
Assert.That(party.PartyList[0], Is.SameAs(member1));
Assert.That(party.PartyList[0].Name, Is.EqualTo(originalName));
}
/// <summary>
/// Tests that after reconnect, the live member's Party reference is restored.
/// </summary>
[Test]
public async ValueTask OnMemberReconnected_RestoresPartyReference()
{
var (party, member1, _) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
await party.LeaveTemporarilyAsync(member1).ConfigureAwait(false);
await this._partyManager.OnMemberReconnectedAsync(member1).ConfigureAwait(false);
Assert.That(member1.Party, Is.SameAs(party));
}
/// <summary>
/// Tests that the offline snapshot preserves the disconnected member's name.
/// </summary>
[Test]
public async ValueTask LeaveTemporarily_SnapshotPreservesName()
{
var (party, member1, _) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
await party.LeaveTemporarilyAsync(member1).ConfigureAwait(false);
Assert.That(party.PartyList[0].Name, Is.EqualTo(member1.Name));
}
/// <summary>
/// Tests that <see cref="Party.ReplaceMemberAsync"/> preserves the party master status
/// when replacing the master.
/// </summary>
[Test]
public async ValueTask ReplaceMember_PreservesMasterStatus()
{
var (party, member1, _) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
// member1 is master (first member added)
var snapshot = new OfflinePartyMember(member1);
await party.ReplaceMemberAsync(member1, snapshot).ConfigureAwait(false);
Assert.That(party.PartyMaster, Is.SameAs(snapshot));
}
/// <summary>
/// Tests that <see cref="Party.ReplaceMemberAsync"/> clears the old member's party reference.
/// </summary>
[Test]
public async ValueTask ReplaceMember_ClearsOldMemberPartyReference()
{
var (party, member1, _) = await this.CreatePartyWithTwoMembersAsync().ConfigureAwait(false);
var snapshot = new OfflinePartyMember(member1);
await party.ReplaceMemberAsync(member1, snapshot).ConfigureAwait(false);
Assert.That(member1.Party, Is.Null);
}
private async ValueTask<(Party Party, Player Member1, Player Member2)> CreatePartyWithTwoMembersAsync()
{
var member1 = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var member2 = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var party = this._partyManager.CreateParty();
await party.AddAsync(member1).ConfigureAwait(false);
await party.AddAsync(member2).ConfigureAwait(false);
return (party, member1, member2);
}
private static byte GetPartyMemberIndex(Party party, IPartyMember member)
{
for (byte index = 0; index < party.PartyList.Count; index++)
{
if (party.PartyList[index] == member)
{
return index;
}
}
throw new ArgumentException("The member is not part of the party.", nameof(member));
}
private async ValueTask<Player> CreatePartyMemberAsync()
{
var result = await PlayerTestHelper.CreatePlayerAsync(GameContextTestHelper.CreateGameContext()).ConfigureAwait(false);
await result.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false);
return result;
}
}

View File

@@ -0,0 +1,408 @@
// <copyright file="PartyTest.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.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.PlayerActions.Party;
using MUnique.OpenMU.GameLogic.Views.Party;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests the party functions.
/// </summary>
[TestFixture]
public class PartyTest
{
private readonly PartyKickAction _kickAction = new();
/// <summary>
/// Tests if an added party member gets added to the party list.
/// </summary>
[Test]
public async ValueTask PartyMemberAddAsync()
{
var partyMember = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var party = new Party(new PartyManager(5, new NullLogger<Party>()), 5, new NullLogger<Party>());
await party.AddAsync(partyMember).ConfigureAwait(false);
Assert.That(party.PartyList, Contains.Item(partyMember));
}
/// <summary>
/// Tests a kick request by a non-party-master for another player, which should fail.
/// </summary>
[Test]
public async ValueTask PartyMemberKickFailByNonMasterAsync()
{
var party = await this.CreatePartyWithMembersAsync(3).ConfigureAwait(false);
var partyMember2 = (Player)party.PartyList[1];
var partyMember3 = party.PartyList[2];
await this._kickAction.KickPlayerAsync(partyMember2, GetPartyMemberIndex(party, partyMember3)).ConfigureAwait(false);
Assert.That(party.PartyList, Contains.Item(partyMember3));
}
/// <summary>
/// Tests if the player is kicking himself works, even if the player is no party master.
/// </summary>
[Test]
public async ValueTask PartyMemberKickHimselfAsync()
{
var party = await this.CreatePartyWithMembersAsync(3).ConfigureAwait(false);
var partyMember2 = party.PartyList[1];
await this._kickAction.KickPlayerAsync((Player)partyMember2, GetPartyMemberIndex(party, partyMember2)).ConfigureAwait(false);
Assert.That(party.PartyList, Is.Not.Contains(partyMember2));
Assert.That(party.PartyList, Has.Count.EqualTo(2));
}
/// <summary>
/// Tests if another player can be kicked by the party master.
/// </summary>
[Test]
public async ValueTask PartyMemberKickByMasterAsync()
{
var party = await this.CreatePartyWithMembersAsync(3).ConfigureAwait(false);
var partyMaster = (Player)party.PartyList[0];
var partyMember = (Player)party.PartyList[1];
await this._kickAction.KickPlayerAsync(partyMaster, GetPartyMemberIndex(party, partyMember)).ConfigureAwait(false);
Assert.That(party.PartyList, Is.Not.Contains(partyMember));
Assert.That(party.PartyList, Has.Count.EqualTo(2));
}
/// <summary>
/// Tests if the party disbands when the master kicks himself.
/// </summary>
[Test]
public async ValueTask PartyMasterKicksHimselfAsync()
{
var party = await this.CreatePartyWithMembersAsync(3).ConfigureAwait(false);
var partyMaster = party.PartyList[0];
var partyMember = party.PartyList[1];
await this._kickAction.KickPlayerAsync((Player)partyMaster, GetPartyMemberIndex(party, partyMaster)).ConfigureAwait(false);
// Master leaves the party; the remaining 2 members stay.
Assert.That(partyMaster.Party, Is.Null);
Assert.That(party.PartyList, Does.Not.Contain(partyMaster));
Assert.That(partyMember.Party, Is.SameAs(party));
Assert.That(party.PartyList, Has.Count.EqualTo(2));
}
/// <summary>
/// Tests if the party automatically closes when one player is left.
/// </summary>
[Test]
public async ValueTask PartyAutoCloseAsync()
{
var partyMember1 = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var party = new Party(new PartyManager(5, new NullLogger<Party>()), 5, new NullLogger<Party>());
await party.AddAsync(partyMember1).ConfigureAwait(false);
var partyMember1Index = (byte)(party.PartyList.Count - 1);
var partyMember2 = await this.CreatePartyMemberAsync().ConfigureAwait(false);
await party.AddAsync(partyMember2).ConfigureAwait(false);
var partyMember2Index = (byte)(party.PartyList.Count - 1);
await this._kickAction.KickPlayerAsync(partyMember1, partyMember2Index).ConfigureAwait(false);
Assert.That(partyMember1.Party, Is.Null);
Assert.That(partyMember2.Party, Is.Null);
Assert.That(party.PartyList, Is.Null.Or.Empty);
Mock.Get(partyMember1.ViewPlugIns.GetPlugIn<IPartyMemberRemovedPlugIn>()!).Verify(v => v!.PartyMemberRemovedAsync(partyMember1Index), Times.Once);
Mock.Get(partyMember2.ViewPlugIns.GetPlugIn<IPartyMemberRemovedPlugIn>()!).Verify(v => v!.PartyMemberRemovedAsync(partyMember2Index), Times.Once);
}
/// <summary>
/// Tests the adding of party members.
/// </summary>
[Test]
public async ValueTask PartyHandlerAddAsync()
{
var handler = new PartyRequestAction();
var player = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var toRequest = await this.CreatePartyMemberAsync().ConfigureAwait(false);
player.Observers.Add(toRequest);
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once);
Assert.That(toRequest.LastPartyRequester, Is.SameAs(player));
}
/// <summary>
/// Tests if the party gets created after the requested player responses with accepting the party.
/// </summary>
[Test]
public async ValueTask PartyResponseAcceptNewPartyAsync()
{
var handler = new PartyResponseAction();
var player = await this.CreatePartyMemberAsync().ConfigureAwait(false);
var requester = await this.CreatePartyMemberAsync().ConfigureAwait(false);
player.LastPartyRequester = requester;
await player.PlayerState.TryAdvanceToAsync(PlayerState.PartyRequest).ConfigureAwait(false);
await handler.HandleResponseAsync(player, true).ConfigureAwait(false);
Assert.That(player.Party, Is.Not.Null);
Assert.That(player.Party!.PartyMaster, Is.SameAs(requester));
Assert.That(player.LastPartyRequester, Is.Null);
Assert.That(player.Party.PartyList, Contains.Item(player));
Mock.Get(player.ViewPlugIns.GetPlugIn<IUpdatePartyListPlugIn>()!).Verify(v => v!.UpdatePartyListAsync(), Times.AtLeastOnce);
Mock.Get(requester.ViewPlugIns.GetPlugIn<IUpdatePartyListPlugIn>()!).Verify(v => v!.UpdatePartyListAsync(), Times.AtLeastOnce);
}
/// <summary>
/// Tests if a request to a player which is already in a party (however this happened...)
/// does not cause the player to be added to the other party.
/// </summary>
[Test]
public async ValueTask PartyResponseAcceptExistingPartyAsync()
{
var handler = new PartyResponseAction();
// first put the player in a party with another player
var player = await this.CreatePartyMemberAsync().ConfigureAwait(false);
player.LastPartyRequester = await this.CreatePartyMemberAsync().ConfigureAwait(false);
await player.PlayerState.TryAdvanceToAsync(PlayerState.PartyRequest).ConfigureAwait(false);
await handler.HandleResponseAsync(player, true).ConfigureAwait(false);
// now another player will try to request party from the player, which should fail
var requester = await this.CreatePartyMemberAsync().ConfigureAwait(false);
player.LastPartyRequester = requester;
await handler.HandleResponseAsync(player, true).ConfigureAwait(false);
Assert.That(player.Party!.PartyList, Is.Not.Contains(requester));
Assert.That(player.LastPartyRequester, Is.Null);
Assert.That(requester.Party, Is.Null);
Mock.Get(player.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(requester), Times.Never);
}
/// <summary>
/// Tests if a party request is auto-accepted when <see cref="IMuHelperSettings.AutoAcceptFriend"/> is true
/// and the requester is a friend.
/// </summary>
[Test]
public async ValueTask PartyRequestAutoAcceptByFriendAsync()
{
var friendServer = new Mock<IFriendServer>();
friendServer.Setup(f => f.IsFriendAsync(It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(true);
var gameContext = PartyTest.CreateGameServerContext(friendServer.Object);
var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
player.SelectedCharacter!.Name = "Requester";
toRequest.SelectedCharacter!.Name = "Receiver";
player.Observers.Add(toRequest);
var settingsMock = new Mock<IMuHelperSettings>();
settingsMock.Setup(s => s.AutoAcceptFriend).Returns(true);
toRequest.MuHelperSettings = settingsMock.Object;
var handler = new PartyRequestAction();
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Assert.That(player.Party, Is.Not.Null);
Assert.That(toRequest.Party, Is.SameAs(player.Party));
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Never);
}
/// <summary>
/// Tests if a party request still shows the dialog when <see cref="IMuHelperSettings.AutoAcceptFriend"/>
/// is true but the requester is not a friend.
/// </summary>
[Test]
public async ValueTask PartyRequestAutoAcceptByFriendNotFriendAsync()
{
var friendServer = new Mock<IFriendServer>();
friendServer.Setup(f => f.IsFriendAsync(It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(false);
var gameContext = PartyTest.CreateGameServerContext(friendServer.Object);
var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
player.SelectedCharacter!.Name = "Requester";
toRequest.SelectedCharacter!.Name = "Receiver";
player.Observers.Add(toRequest);
var settingsMock = new Mock<IMuHelperSettings>();
settingsMock.Setup(s => s.AutoAcceptFriend).Returns(true);
toRequest.MuHelperSettings = settingsMock.Object;
var handler = new PartyRequestAction();
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Assert.That(player.Party, Is.Null);
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once);
}
/// <summary>
/// Tests if a party request is auto-accepted when <see cref="IMuHelperSettings.AutoAcceptGuild"/> is true
/// and both players are members of the same guild.
/// </summary>
[Test]
public async ValueTask PartyRequestAutoAcceptByGuildAsync()
{
var gameContext = GameContextTestHelper.CreateGameContext();
var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
player.Observers.Add(toRequest);
var guildId = 1u;
player.GuildStatus = new GuildMemberStatus(guildId, GuildPosition.GuildMaster);
toRequest.GuildStatus = new GuildMemberStatus(guildId, GuildPosition.NormalMember);
var settingsMock = new Mock<IMuHelperSettings>();
settingsMock.Setup(s => s.AutoAcceptGuild).Returns(true);
toRequest.MuHelperSettings = settingsMock.Object;
var handler = new PartyRequestAction();
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Assert.That(player.Party, Is.Not.Null);
Assert.That(toRequest.Party, Is.SameAs(player.Party));
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Never);
}
/// <summary>
/// Tests if a party request still shows the dialog when <see cref="IMuHelperSettings.AutoAcceptGuild"/>
/// is true but the players are in different guilds.
/// </summary>
[Test]
public async ValueTask PartyRequestAutoAcceptByGuildDifferentGuildAsync()
{
var gameContext = GameContextTestHelper.CreateGameContext();
var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
player.Observers.Add(toRequest);
player.GuildStatus = new GuildMemberStatus(1u, GuildPosition.GuildMaster);
toRequest.GuildStatus = new GuildMemberStatus(2u, GuildPosition.NormalMember);
var settingsMock = new Mock<IMuHelperSettings>();
settingsMock.Setup(s => s.AutoAcceptGuild).Returns(true);
toRequest.MuHelperSettings = settingsMock.Object;
var handler = new PartyRequestAction();
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Assert.That(player.Party, Is.Null);
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once);
}
/// <summary>
/// Tests that a party request is not auto-accepted when no relevant flags are set.
/// </summary>
[Test]
public async ValueTask PartyRequestAutoAcceptNoFlagsAsync()
{
var friendServer = new Mock<IFriendServer>();
var gameContext = PartyTest.CreateGameServerContext(friendServer.Object);
var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
player.SelectedCharacter!.Name = "Requester";
toRequest.SelectedCharacter!.Name = "Receiver";
player.Observers.Add(toRequest);
var settingsMock = new Mock<IMuHelperSettings>();
toRequest.MuHelperSettings = settingsMock.Object;
var handler = new PartyRequestAction();
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Assert.That(player.Party, Is.Null);
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once);
}
/// <summary>
/// Tests that a party request is not auto-accepted when MuHelperSettings is null.
/// </summary>
[Test]
public async ValueTask PartyRequestAutoAcceptNoSettingsAsync()
{
var friendServer = new Mock<IFriendServer>();
var gameContext = PartyTest.CreateGameServerContext(friendServer.Object);
var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false);
player.Observers.Add(toRequest);
var handler = new PartyRequestAction();
await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false);
Assert.That(player.Party, Is.Null);
Mock.Get(toRequest.ViewPlugIns.GetPlugIn<IShowPartyRequestPlugIn>()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once);
}
private static IGameServerContext CreateGameServerContext(IFriendServer friendServer)
{
var contextProvider = new InMemoryPersistenceContextProvider();
var context = contextProvider.CreateNewContext();
var gameConfiguration = context.CreateNew<MUnique.OpenMU.Persistence.BasicModel.GameConfiguration>();
gameConfiguration.MaximumPartySize = 5;
gameConfiguration.RecoveryInterval = int.MaxValue;
gameConfiguration.MaximumInventoryMoney = int.MaxValue;
var mapDef = context.CreateNew<MUnique.OpenMU.Persistence.BasicModel.GameMapDefinition>();
mapDef.Number = 0;
mapDef.TerrainData = new byte[ushort.MaxValue + 3];
gameConfiguration.Maps.Add(mapDef);
var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var gameServer = new GameServerContext(
new GameServerDefinition
{
GameConfiguration = gameConfiguration,
ServerConfiguration = new GameServerConfiguration(),
},
new Mock<IGuildServer>().Object,
new Mock<IEventPublisher>().Object,
new Mock<ILoginServer>().Object,
friendServer,
new InMemoryPersistenceContextProvider(),
mapInitializer,
new NullLoggerFactory(),
new PlugInManager(new List<PlugInConfiguration>(), new NullLoggerFactory(), null, null),
NullDropGenerator.Instance,
new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameServer.PlugInManager;
mapInitializer.PathFinderPool = gameServer.PathFinderPool;
return gameServer;
}
private async ValueTask<Player> CreatePartyMemberAsync()
{
var result = await PlayerTestHelper.CreatePlayerAsync(GameContextTestHelper.CreateGameContext()).ConfigureAwait(false);
await result.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false);
return result;
}
private static byte GetPartyMemberIndex(Party party, IPartyMember member)
{
for (byte index = 0; index < party.PartyList.Count; index++)
{
if (party.PartyList[index] == member)
{
return index;
}
}
throw new ArgumentException("The member is not part of the party.", nameof(member));
}
private async ValueTask<Party> CreatePartyWithMembersAsync(int numberOfMembers)
{
var party = new Party(new PartyManager(5, new NullLogger<Party>()), 5, new NullLogger<Party>());
for (ushort i = 0; i < numberOfMembers; i++)
{
var partyMember = await this.CreatePartyMemberAsync().ConfigureAwait(false);
await party.AddAsync(partyMember).ConfigureAwait(false);
}
return party;
}
}

View File

@@ -0,0 +1,172 @@
// <copyright file="PlayerTestHelper.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.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Helper functions to create test players.
/// </summary>
public static class PlayerTestHelper
{
/// <summary>
/// Gets a test player with a new in-memory game context.
/// </summary>
/// <returns>The test player.</returns>
public static async ValueTask<Player> CreatePlayerAsync()
{
var gameConfig = new Mock<GameConfiguration>();
gameConfig.SetupAllProperties();
gameConfig.Setup(c => c.Maps).Returns(new List<GameMapDefinition>());
gameConfig.Setup(c => c.Items).Returns(new List<ItemDefinition>());
gameConfig.Setup(c => c.Skills).Returns(new List<Skill>());
gameConfig.Setup(c => c.PlugInConfigurations).Returns(new List<PlugInConfiguration>());
gameConfig.Setup(c => c.CharacterClasses).Returns(new List<CharacterClass>());
gameConfig.Setup(c => c.GlobalAttributeCombinations).Returns(new List<AttributeRelationship>());
gameConfig.Setup(c => c.GlobalBaseAttributeValues).Returns(new List<ConstValueAttribute>
{
new(1, Stats.MoneyAmountRate),
});
var map = new Mock<GameMapDefinition>();
map.SetupAllProperties();
map.Setup(m => m.DropItemGroups).Returns(new List<DropItemGroup>());
map.Setup(m => m.MonsterSpawns).Returns(new List<MonsterSpawnArea>());
map.Object.TerrainData = new byte[ushort.MaxValue + 3];
gameConfig.Object.RecoveryInterval = int.MaxValue;
gameConfig.Object.Maps.Add(map.Object);
var mapInitializer = new MapInitializer(gameConfig.Object, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var gameContext = new GameContext(gameConfig.Object, new InMemoryPersistenceContextProvider(), mapInitializer, new NullLoggerFactory(), new PlugInManager(null, new NullLoggerFactory(), null, null), NullDropGenerator.Instance, new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameContext.PlugInManager;
mapInitializer.PathFinderPool = gameContext.PathFinderPool;
return await CreatePlayerAsync(gameContext).ConfigureAwait(false);
}
/// <summary>
/// Gets a test player with the specified game context.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <returns>The test player.</returns>
public static async ValueTask<Player> CreatePlayerAsync(IGameContext gameContext)
{
var characterMock = new Mock<Character>();
characterMock.SetupAllProperties();
characterMock.Setup(c => c.LearnedSkills).Returns(new List<SkillEntry>());
characterMock.Setup(c => c.Attributes).Returns(new List<StatAttribute>());
characterMock.Setup(c => c.DropItemGroups).Returns(new List<DropItemGroup>());
var inventoryMock = new Mock<ItemStorage>();
inventoryMock.SetupAllProperties();
inventoryMock.Setup(i => i.Items).Returns(new List<Item>());
var character = characterMock.Object;
character.Inventory = inventoryMock.Object;
character.CurrentMap = gameContext.Configuration.Maps.FirstOrDefault(m => m.Number == 0);
var characterClassMock = new Mock<CharacterClass>();
characterClassMock.Setup(c => c.StatAttributes).Returns(
new List<StatAttributeDefinition>
{
new (Stats.Level, 0, false),
new (Stats.BaseStrength, 28, true),
new (Stats.BaseAgility, 20, true),
new (Stats.BaseVitality, 25, true),
new (Stats.BaseEnergy, 10, true),
new (Stats.CurrentHealth, 0, false),
new (Stats.CurrentMana, 0, false),
new (Stats.CurrentShield, 0, false),
new (Stats.Resets, 0, false),
new (Stats.PointsPerReset, 0, false),
});
characterClassMock.Setup(c => c.AttributeCombinations).Returns(new List<AttributeRelationship>
{
new (Stats.TotalStrength, 1, Stats.BaseStrength),
new (Stats.TotalAgility, 1, Stats.BaseAgility),
new (Stats.TotalVitality, 1, Stats.BaseVitality),
new (Stats.TotalEnergy, 1, Stats.BaseEnergy),
new (Stats.MaximumAbility, 1, Stats.TotalEnergy),
new (Stats.MaximumAbility, 0.3f, Stats.TotalVitality),
new (Stats.MaximumAbility, 0.2f, Stats.TotalAgility),
new (Stats.MaximumAbility, 0.15f, Stats.TotalStrength),
new (Stats.MaximumShield, 1.2f, Stats.TotalEnergy),
new (Stats.MaximumShield, 1.2f, Stats.TotalVitality),
new (Stats.MaximumShield, 1.2f, Stats.TotalAgility),
new (Stats.MaximumShield, 1.2f, Stats.TotalStrength),
new (Stats.MaximumShield, 0.5f, Stats.DefenseBase),
new (Stats.MaximumMana, 1, Stats.TotalEnergy),
new (Stats.MaximumMana, 0.5f, Stats.Level),
new (Stats.MaximumHealth, 2, Stats.Level),
new (Stats.MaximumHealth, 3, Stats.TotalVitality),
});
characterClassMock.Setup(c => c.BaseAttributeValues).Returns(new List<ConstValueAttribute>
{
new (10, Stats.MaximumMana),
new (35, Stats.MaximumHealth),
new (2, Stats.SkillMultiplier),
new (2, Stats.AbilityRecoveryMultiplier),
new (1, Stats.DamageReceiveDecrement),
new (1, Stats.AttackDamageIncrease),
});
character.CharacterClass = characterClassMock.Object;
foreach (var attributeDef in character.CharacterClass.StatAttributes)
{
character.Attributes.Add(new StatAttribute(attributeDef.Attribute!, attributeDef.BaseValue));
}
var accountMock = new Mock<Account>();
accountMock.Setup(mock => mock.Attributes).Returns(new List<StatAttribute>());
accountMock.Setup(mock => mock.UnlockedCharacterClasses).Returns(new List<CharacterClass>());
var player = new TestPlayer(gameContext) { Account = accountMock.Object };
await player.PlayerState.TryAdvanceToAsync(PlayerState.LoginScreen).ConfigureAwait(false);
await player.PlayerState.TryAdvanceToAsync(PlayerState.Authenticated).ConfigureAwait(false);
await player.PlayerState.TryAdvanceToAsync(PlayerState.CharacterSelection).ConfigureAwait(false);
await player.SetSelectedCharacterAsync(character).ConfigureAwait(false);
return player;
}
/// <summary>
/// Creates an offline player at <see cref="PlayerState.EnteredWorld"/>.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <returns>The offline player.</returns>
public static async ValueTask<OfflinePlayer> CreateOfflineLevelingPlayerAsync(IGameContext gameContext)
{
var regularPlayer = await CreatePlayerAsync(gameContext).ConfigureAwait(false);
var offlinePlayer = new OfflinePlayer(gameContext) { Account = regularPlayer.Account };
await offlinePlayer.PlayerState.TryAdvanceToAsync(PlayerState.LoginScreen).ConfigureAwait(false);
await offlinePlayer.PlayerState.TryAdvanceToAsync(PlayerState.Authenticated).ConfigureAwait(false);
await offlinePlayer.PlayerState.TryAdvanceToAsync(PlayerState.CharacterSelection).ConfigureAwait(false);
await offlinePlayer.SetSelectedCharacterAsync(regularPlayer.SelectedCharacter!).ConfigureAwait(false);
return offlinePlayer;
}
private class TestPlayer : Player
{
public TestPlayer(IGameContext gameContext)
: base(gameContext)
{
}
protected override ICustomPlugInContainer<IViewPlugIn> CreateViewPlugInContainer()
{
return new MockViewPlugInContainer();
}
}
}

View File

@@ -0,0 +1,67 @@
// <copyright file="CharacterMoveTest.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 MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Pathfinding;
/// <summary>
/// Tests for the <see cref="PointExtensions"/>.
/// </summary>
[TestFixture]
internal class PointExtensionsTest
{
/// <summary>
/// Gets the angle degree of 180 °.
/// </summary>
[Test]
public void GetAngleDegreeTo_180Degree()
{
var start = new Point(100, 100);
var end = new Point(100, 101);
var degree = start.GetAngleDegreeTo(end);
Assert.That(degree, Is.EqualTo(180.0));
}
/// <summary>
/// Gets the angle degree of 0 °.
/// </summary>
[Test]
public void GetAngleDegreeTo_0Degree()
{
var start = new Point(100, 100);
var end = new Point(100, 99);
var degree = start.GetAngleDegreeTo(end);
Assert.That(degree, Is.EqualTo(0.0));
}
/// <summary>
/// Gets the angle degree of 90 °.
/// </summary>
[Test]
public void GetAngleDegreeTo_90Degree()
{
var start = new Point(100, 100);
var end = new Point(101, 100);
var degree = start.GetAngleDegreeTo(end);
Assert.That(degree, Is.EqualTo(90.0));
}
/// <summary>
/// Gets the angle degree of 270 °.
/// </summary>
[Test]
public void GetAngleDegreeTo_270Degree()
{
var start = new Point(100, 100);
var end = new Point(99, 100);
var degree = start.GetAngleDegreeTo(end);
Assert.That(degree, Is.EqualTo(270.0));
}
}

View File

@@ -0,0 +1,383 @@
// <copyright file="PowerUpFactoryTest.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.Attributes;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// Tests the power up factory which is creating power ups based on the items a player has equipped.
/// </summary>
[TestFixture]
public class PowerUpFactoryTest
{
private const byte UnwearableSlot = 0xFF;
private const int PowerUpStrength = 16;
private readonly float[] _levelBonus = new float[] { 0, 1, 3, 7, 14 };
/// <summary>
/// Tests if the item option results in an corresponding power up.
/// </summary>
[Test]
public async ValueTask ItemOptionsAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var factory = this.GetPowerUpFactory();
var item = this.GetItemWithOption();
var result = factory.GetPowerUps(item, player.Attributes!);
Assert.That(result.Sum(p => p.Value), Is.EqualTo(PowerUpStrength));
}
/// <summary>
/// Tests if the item option of level 0 results in the corresponding power up.
/// </summary>
[Test]
public async ValueTask ItemBasePowerUpLevel0Async()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var factory = this.GetPowerUpFactory();
var item = this.GetItemWithBasePowerUp();
var result = factory.GetPowerUps(item, player.Attributes!);
Assert.That(result.Sum(p => p.Value), Is.EqualTo(PowerUpStrength));
}
/// <summary>
/// Tests if the item option of level 3 results in the corresponding power up.
/// </summary>
[Test]
public async ValueTask ItemBasePowerUpLevel3Async()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var factory = this.GetPowerUpFactory();
var item = this.GetItemWithBasePowerUp();
item.Level = 3;
var result = factory.GetPowerUps(item, player.Attributes!);
Assert.That(result.Sum(p => p.Value), Is.EqualTo(PowerUpStrength + this._levelBonus[3]));
}
/// <summary>
/// Tests if the power ups don't get created if the item has no more durability.
/// </summary>
[Test]
public async ValueTask NoPowerUpsWhenItemBrokenAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var factory = this.GetPowerUpFactory();
var item = this.GetItemWithBasePowerUp();
item.Durability = 0;
var result = factory.GetPowerUps(item, player.Attributes!);
Assert.That(result.Sum(p => p.Value), Is.EqualTo(0));
}
/// <summary>
/// Tests if the power ups don't get created if the item is not equipped at the player.
/// </summary>
[Test]
public async ValueTask NoPowerUpsWhenItemUnwearableAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var factory = this.GetPowerUpFactory();
var item = this.GetItemWithBasePowerUp();
item.ItemSlot = UnwearableSlot;
var result = factory.GetPowerUps(item, player.Attributes!);
Assert.That(result.Sum(p => p.Value), Is.EqualTo(0));
}
/// <summary>
/// Tests if the power ups don't get created when the item has no options.
/// </summary>
[Test]
public async ValueTask NoPowerUpsInItemAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var factory = this.GetPowerUpFactory();
var item = this.GetItem();
var result = factory.GetPowerUps(item, player.Attributes!);
Assert.That(result.Sum(p => p.Value), Is.EqualTo(0));
}
/// <summary>
/// Tests if a complete set of level 11, gives the power up defined for level 11.
/// </summary>
[Test]
public void SetCompleteGivesPowerUp()
{
var factory = this.GetPowerUpFactory();
var items = this.GetDefenseBonusSet(10, 11, 11, 11, 11);
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.Not.EqualTo(0));
}
/// <summary>
/// Tests if a complete set of level 11, gives the power up defined for level 11 even if one item has a higher level.
/// </summary>
[Test]
public void SetCompleteGivesPowerUpWhenItemIsHigherLevel()
{
var factory = this.GetPowerUpFactory();
var items = this.GetDefenseBonusSet(10, 11, 12, 11, 11);
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.Not.EqualTo(0));
}
/// <summary>
/// Tests if an incomplete set of level 11 gives no power up, because at least one item has not the required level.
/// </summary>
[Test]
public void SetIncompleteDueLowerLevelItemsGivesNoPowerUp()
{
var factory = this.GetPowerUpFactory();
var items = this.GetDefenseBonusSet(10, 11, 11, 15, 9);
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.EqualTo(0));
}
/// <summary>
/// Tests if no items give no power ups.
/// </summary>
[Test]
public void NoItemsGiveNoSetPowerUps()
{
var factory = this.GetPowerUpFactory();
var items = Enumerable.Empty<Item>();
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.EqualTo(0));
}
/// <summary>
/// Tests if an incomplete set (=not all required items equipped) gives also no power up.
/// </summary>
[Test]
public void SetIncompleteGivesNoPowerUp()
{
var factory = this.GetPowerUpFactory();
var items = this.GetDefenseBonusSet(30, 15, 15, 15);
var result = factory.GetSetPowerUps(items.Skip(1), this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.EqualTo(0));
}
/// <summary>
/// Tests if the correct value is set in the power up, as defined.
/// </summary>
[Test]
public void SetBonusDefSetValueCorrect()
{
var factory = this.GetPowerUpFactory();
var items = this.GetDefenseBonusSet(5, 10, 10, 15, 10);
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration()).ToList();
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First().Value, Is.EqualTo(5.0));
}
/// <summary>
/// Tests if no power up is given for a specific level, when all of the items are of a higher level.
/// This is the expected behavior, because otherwise, multiple level-dependent set bonuses would take effect.
/// </summary>
[Test]
public void SetBonusNotAppliedWhenFullSetOfHigherLevel()
{
var factory = this.GetPowerUpFactory();
var items = this.GetDefenseBonusSet(5, 10, 15, 15, 15);
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.EqualTo(0));
}
/// <summary>
/// Tests if one ancient item gives just the bonus option.
/// </summary>
[Test]
public void OneAncientItemGivesJustBonusOption()
{
var factory = this.GetPowerUpFactory();
var items = this.GetAncientSet(5, 4).ToList();
var bonusOptions = factory.GetPowerUps(items.First(), this.GetAttributeSystem());
Assert.That(bonusOptions.Count(), Is.EqualTo(1));
var result = factory.GetSetPowerUps(items.Take(1), this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result, Is.Empty);
}
/// <summary>
/// Tests if a partially complete ancient set gives just the bonus option plus the number of unlocked options (item count - 1).
/// </summary>
/// <param name="setSize">The item count.</param>
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
public void AncientSetPartiallyComplete(int setSize)
{
var factory = this.GetPowerUpFactory();
var setItems = this.GetAncientSet(5, setSize).ToList();
var items = setItems.SkipLast(1).ToList();
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.EqualTo(items.Count - 1));
var bonusOptions = items.SelectMany(item => factory.GetPowerUps(item, this.GetAttributeSystem()));
Assert.That(bonusOptions.Count(), Is.EqualTo(items.Count));
}
/// <summary>
/// Tests if a complete ancient set unlocks all options.
/// In case of 5 items in a set, there are 9 power ups (4 unlocked options + 5 bonus options).
/// </summary>
[Test]
public void AncientSetComplete()
{
var factory = this.GetPowerUpFactory();
var items = this.GetAncientSet(6, 5).ToList();
var result = factory.GetSetPowerUps(items, this.GetAttributeSystem(), new GameConfiguration());
Assert.That(result.Count(), Is.EqualTo(6));
var bonusOptions = items.SelectMany(item => factory.GetPowerUps(item, this.GetAttributeSystem()));
Assert.That(bonusOptions.Count(), Is.EqualTo(5));
}
private IItemPowerUpFactory GetPowerUpFactory()
{
return new ItemPowerUpFactory(new NullLogger<ItemPowerUpFactory>());
}
private ItemDefinition GetItemDefinition()
{
var itemDefinition = new Mock<ItemDefinition>();
itemDefinition.Object.Durability = 100;
itemDefinition.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
itemDefinition.Setup(d => d.PossibleItemSetGroups).Returns(new List<ItemSetGroup>());
return itemDefinition.Object;
}
private Item GetItem()
{
var item = new Mock<Item>();
item.SetupAllProperties();
item.Object.Definition = this.GetItemDefinition();
item.Object.ItemSlot = 0;
item.Object.Durability = item.Object.Definition.Durability;
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
return item.Object;
}
private Item GetItemWithOption()
{
var item = this.GetItem();
item.ItemOptions.Add(this.GetOption(Stats.MaximumPhysBaseDmg, PowerUpStrength));
return item;
}
private Item GetItemWithBasePowerUp()
{
var item = this.GetItem();
item.Definition!.BasePowerUpAttributes.Add(this.GetBasePowerUpDefinition());
return item;
}
private ItemBasePowerUpDefinition GetBasePowerUpDefinition()
{
var resultMock = new Mock<ItemBasePowerUpDefinition>();
resultMock.SetupAllProperties();
var bonusTableMock = new Mock<ItemLevelBonusTable>();
bonusTableMock.Setup(r => r.BonusPerLevel).Returns(new List<LevelBonus>());
var result = resultMock.Object;
result.BaseValue = PowerUpStrength;
result.TargetAttribute = Stats.MaximumPhysBaseDmg;
var bonusTable = bonusTableMock.Object;
result.BonusPerLevelTable = bonusTable;
bonusTable.BonusPerLevel.Add(new LevelBonus(1, this._levelBonus[1]));
bonusTable.BonusPerLevel.Add(new LevelBonus(2, this._levelBonus[2]));
bonusTable.BonusPerLevel.Add(new LevelBonus(3, this._levelBonus[3]));
bonusTable.BonusPerLevel.Add(new LevelBonus(4, this._levelBonus[4]));
return result;
}
private ItemOptionLink GetOption(AttributeDefinition targetAttribute, float value)
{
var option = new IncreasableItemOption
{
OptionType = ItemOptionTypes.Option,
PowerUpDefinition = new PowerUpDefinition
{
TargetAttribute = targetAttribute,
Boost = new TestPowerUpDefinitionValue(new SimpleElement { Value = value }),
},
};
return new ItemOptionLink { ItemOption = option, Level = 1 };
}
private IEnumerable<Item> GetDefenseBonusSet(float setBonusDefense, byte minimumLevel, params byte[] levels)
{
var itemOptionDef = new Mock<ItemOptionDefinition>();
itemOptionDef.Setup(a => a.PossibleOptions).Returns(new List<IncreasableItemOption>());
itemOptionDef.Object.PossibleOptions.Add(this.GetOption(Stats.DefenseBase, setBonusDefense).ItemOption!);
var armorSet = new Mock<ItemSetGroup>();
armorSet.Setup(a => a.Items).Returns(new List<ItemOfItemSet>());
armorSet.Setup(a => a.Options).Returns(itemOptionDef.Object);
armorSet.Object.MinimumItemCount = levels.Length;
armorSet.Object.SetLevel = minimumLevel;
foreach (var level in levels)
{
var item = this.GetItem();
var itemOfItemSet = new ItemOfItemSet { ItemDefinition = item.Definition, ItemSetGroup = armorSet.Object };
item.Definition!.PossibleItemSetGroups.Add(armorSet.Object);
item.ItemSetGroups.Add(itemOfItemSet);
armorSet.Object.Items.Add(itemOfItemSet);
item.Level = level;
yield return item;
}
}
private IEnumerable<Item> GetAncientSet(int ancientOptionCount, int itemCount)
{
var itemOptionDef = new Mock<ItemOptionDefinition>();
itemOptionDef.Setup(a => a.PossibleOptions).Returns(new List<IncreasableItemOption>());
var ancientSet = new Mock<ItemSetGroup>();
ancientSet.Setup(a => a.Items).Returns(new List<ItemOfItemSet>());
ancientSet.Setup(a => a.Options).Returns(itemOptionDef.Object);
ancientSet.Object.MinimumItemCount = 2;
for (int i = 0; i < ancientOptionCount; i++)
{
var setOption = this.GetOption(Stats.DefenseBase, i + 10).ItemOption;
setOption!.Number = i + 1;
itemOptionDef.Object.PossibleOptions.Add(setOption);
}
var bonusOption = this.GetOption(Stats.TotalStrength, 5).ItemOption;
for (int i = 0; i < itemCount; i++)
{
var item = this.GetItem();
var itemOfSet = new ItemOfItemSet { BonusOption = bonusOption, ItemDefinition = item.Definition, ItemSetGroup = ancientSet.Object };
item.Definition!.PossibleItemSetGroups.Add(ancientSet.Object);
item.ItemSetGroups.Add(itemOfSet);
item.ItemOptions.Add(new ItemOptionLink { ItemOption = bonusOption, Level = 1 });
ancientSet.Object.Items.Add(itemOfSet);
yield return item;
}
}
private AttributeSystem GetAttributeSystem() => new(Enumerable.Empty<IAttribute>(), Enumerable.Empty<IAttribute>(), Enumerable.Empty<AttributeRelationship>());
private class TestPowerUpDefinitionValue : PowerUpDefinitionValue
{
public TestPowerUpDefinitionValue(SimpleElement constantValue)
{
this.ConstantValue = constantValue;
}
}
}

View File

@@ -0,0 +1,10 @@
// <copyright file="AssemblyInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using System.Reflection;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MUnique.OpenMU.Tests")]

View File

@@ -0,0 +1,191 @@
// <copyright file="ResetCharacterActionTest.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 MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Resets;
/// <summary>
/// Tests for <see cref="ResetCharacterAction"/>.
/// </summary>
[TestFixture]
public class ResetCharacterActionTest
{
/// <summary>
/// Verifies that reset is rejected when required items are missing, without charging zen.
/// </summary>
[Test]
public async Task NotEnoughItemsRejectsResetAndKeepsZenAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 1).ConfigureAwait(false);
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(0));
Assert.That(player.Money, Is.EqualTo(1_000));
Assert.That(player.Inventory!.Items.Count(i => i.Definition == requiredItem), Is.EqualTo(1));
}
/// <summary>
/// Verifies that reset consumes configured zen and required items when all conditions are met.
/// </summary>
[Test]
public async Task EnoughItemsAndZenConsumesCostsOnceAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 2).ConfigureAwait(false);
Assert.That(player.Level, Is.EqualTo(400));
Assert.That(player.Money, Is.EqualTo(1_000));
Assert.That(
player.Inventory!.Items.Count(i => i.Definition is { } definition && definition.Group == requiredItem.Group && definition.Number == requiredItem.Number),
Is.EqualTo(2));
var progression = ResetProgressionCalculator.Calculate((int)player.Attributes[Stats.Resets], (int)player.Attributes[Stats.PointsPerReset], configuration);
Assert.That(progression.RequiredItemAmount, Is.EqualTo(2));
Assert.That(progression.RequiredZen, Is.EqualTo(500));
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.Money, Is.EqualTo(500));
Assert.That(player.Inventory!.Items.Count(i => i.Definition == requiredItem), Is.EqualTo(0));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(800));
Assert.That((int)player.Attributes[Stats.Level], Is.EqualTo(1));
}
/// <summary>
/// Verifies cumulative point replacement across multiple resets when point tiers are configured.
/// </summary>
[Test]
public async Task ReplacePointsUsesCumulativeTierTotalAcrossResetsAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 2_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 4).ConfigureAwait(false);
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(800));
Assert.That(player.Money, Is.EqualTo(1_500));
player.Attributes[Stats.Level] = 400;
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(2));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(1_600));
Assert.That(player.Money, Is.EqualTo(500));
}
/// <summary>
/// Verifies add mode behavior when replace mode is disabled.
/// </summary>
[Test]
public async Task TieredPointsAreAddedWhenReplaceIsDisabledAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
player.SelectedCharacter!.LevelUpPoints = 1_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
configuration.ReplacePointsPerReset = false;
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 2).ConfigureAwait(false);
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(1_800));
}
private static async ValueTask AddRequiredItemsAsync(Player player, ItemDefinition requiredItem, int count)
{
for (byte i = 0; i < count; i++)
{
var item = player.PersistenceContext.CreateNew<Item>();
item.Definition = requiredItem;
item.Durability = 1;
var added = await player.Inventory!.AddItemAsync(item).ConfigureAwait(false);
Assert.That(added, Is.True);
}
}
private static async ValueTask<NonPlayerCharacter> CreateResetNpcAsync(Player player)
{
var spawnArea = new MonsterSpawnArea
{
X1 = 125,
X2 = 125,
Y1 = 125,
Y2 = 125,
};
var definition = new MonsterDefinition
{
Number = ResetCharacterNpcPlugin.ResetNpcNumber,
ObjectKind = NpcObjectKind.PassiveNpc,
Designation = "Reset Helper",
};
var map = await player.GameContext.GetMapAsync(0).ConfigureAwait(false)
?? throw new InvalidOperationException("Could not resolve map 0 for NPC test setup.");
return new NonPlayerCharacter(spawnArea, definition, map);
}
private ResetConfiguration CreateConfiguration(ItemDefinition requiredItem)
{
return new ResetConfiguration
{
RequiredLevel = 400,
LevelAfterReset = 1,
RequiredMoney = 500,
MultiplyRequiredMoneyByResetCount = true,
RequiredResetItem = requiredItem,
ItemCostTiers =
[
new() { MinimumResetCount = 1, RequiredItemAmount = 2 },
],
PointsTiers =
[
new() { MinimumResetCount = 1, PointsGranted = 800 },
],
MoveHome = false,
LogOut = false,
ResetStats = false,
ReplacePointsPerReset = true,
};
}
}

View File

@@ -0,0 +1,142 @@
// <copyright file="ResetProgressionCalculatorTest.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 MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Resets;
/// <summary>
/// Tests for <see cref="ResetProgressionCalculator"/>.
/// </summary>
[TestFixture]
public class ResetProgressionCalculatorTest
{
/// <summary>
/// Verifies boundary behavior for point tiers.
/// </summary>
[TestCase(0, 800)]
[TestCase(9, 800)]
[TestCase(10, 600)]
[TestCase(19, 600)]
[TestCase(20, 400)]
[TestCase(29, 400)]
[TestCase(30, 200)]
public void PointsTierBoundaries(int currentResetCount, int expectedPoints)
{
var configuration = this.CreateTieredConfiguration();
var progression = ResetProgressionCalculator.Calculate(currentResetCount, 0, configuration);
Assert.That(progression.PointsForReset, Is.EqualTo(expectedPoints));
}
/// <summary>
/// Verifies cumulative totals for point tiers.
/// </summary>
[TestCase(0, 800)]
[TestCase(1, 1600)]
[TestCase(9, 8000)]
[TestCase(10, 8600)]
[TestCase(11, 9200)]
[TestCase(20, 14400)]
public void PointsTierTotals(int currentResetCount, int expectedTotalPoints)
{
var configuration = this.CreateTieredConfiguration();
var progression = ResetProgressionCalculator.Calculate(currentResetCount, 0, configuration);
Assert.That(progression.TotalPointsAfterReset, Is.EqualTo(expectedTotalPoints));
}
/// <summary>
/// Verifies boundary behavior for item cost tiers with capped final tier.
/// </summary>
[TestCase(0, 2)]
[TestCase(1, 4)]
[TestCase(2, 8)]
[TestCase(3, 16)]
[TestCase(4, 32)]
[TestCase(5, 64)]
[TestCase(6, 64)]
[TestCase(20, 64)]
public void ItemCostTierBoundaries(int currentResetCount, int expectedItemAmount)
{
var configuration = this.CreateTieredConfiguration();
var progression = ResetProgressionCalculator.Calculate(currentResetCount, 0, configuration);
Assert.That(progression.RequiredItemAmount, Is.EqualTo(expectedItemAmount));
}
/// <summary>
/// Verifies legacy fallback behavior when no tier collections are configured.
/// </summary>
[Test]
public void LegacyFallbackWhenTiersAreEmpty()
{
var configuration = new ResetConfiguration
{
RequiredMoney = 2,
MultiplyRequiredMoneyByResetCount = true,
PointsPerReset = 300,
MultiplyPointsByResetCount = true,
PointsTiers = [],
ItemCostTiers = [],
RequiredResetItem = new ItemDefinition { Name = "Jewel of Creation" },
};
var progression = ResetProgressionCalculator.Calculate(2, 0, configuration);
Assert.That(progression.NextResetCount, Is.EqualTo(3));
Assert.That(progression.RequiredZen, Is.EqualTo(6));
Assert.That(progression.PointsForReset, Is.EqualTo(900));
Assert.That(progression.TotalPointsAfterReset, Is.EqualTo(900));
Assert.That(progression.RequiredItemAmount, Is.EqualTo(0));
}
/// <summary>
/// Verifies that the per-character points override applies in legacy mode.
/// </summary>
[Test]
public void PlayerSpecificPointsOverrideIsAppliedInLegacyMode()
{
var configuration = new ResetConfiguration
{
PointsPerReset = 300,
MultiplyPointsByResetCount = true,
PointsTiers = [],
};
var progression = ResetProgressionCalculator.Calculate(4, 120, configuration);
Assert.That(progression.PointsForReset, Is.EqualTo(600));
Assert.That(progression.TotalPointsAfterReset, Is.EqualTo(600));
}
private ResetConfiguration CreateTieredConfiguration()
{
return new ResetConfiguration
{
RequiredResetItem = new ItemDefinition { Name = "Jewel of Creation" },
PointsTiers =
[
new() { MinimumResetCount = 1, PointsGranted = 800 },
new() { MinimumResetCount = 11, PointsGranted = 600 },
new() { MinimumResetCount = 21, PointsGranted = 400 },
new() { MinimumResetCount = 31, PointsGranted = 200 },
],
ItemCostTiers =
[
new() { MinimumResetCount = 1, RequiredItemAmount = 2 },
new() { MinimumResetCount = 2, RequiredItemAmount = 4 },
new() { MinimumResetCount = 3, RequiredItemAmount = 8 },
new() { MinimumResetCount = 4, RequiredItemAmount = 16 },
new() { MinimumResetCount = 5, RequiredItemAmount = 32 },
new() { MinimumResetCount = 6, RequiredItemAmount = 64 },
new() { MinimumResetCount = 7, RequiredItemAmount = 64 },
],
};
}
}

View File

@@ -0,0 +1,37 @@
// <copyright file="SelfDefensePlugInTest.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 Moq;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// Tests for <see cref="SelfDefensePlugIn"/>.
/// </summary>
[TestFixture]
public class SelfDefensePlugInTest
{
/// <summary>
/// Ensures that hitting an own summon doesn't start self-defense.
/// </summary>
[Test]
public async ValueTask OwnSummonHitDoesNotStartSelfDefenseAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var summonMock = new Mock<IAttackable>();
var summonable = summonMock.As<ISummonable>();
summonable.Setup(s => s.SummonedBy).Returns(player);
summonable.Setup(s => s.Definition).Returns(new MonsterDefinition());
var plugIn = new SelfDefensePlugIn();
plugIn.AttackableGotHit(summonMock.Object, player, new HitInfo(1, 0, DamageAttributes.Undefined));
Assert.That(player.GameContext.SelfDefenseState, Is.Empty);
}
}

View File

@@ -0,0 +1,107 @@
// <copyright file="SkillListTest.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 Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
/// <summary>
/// Tests the skill list.
/// </summary>
[TestFixture]
public class SkillListTest
{
private const ushort LearnedSkillId = 10;
private const ushort NonLearnedSkillId = 999;
private const ushort QualifiedItemSkillId = 1;
private const ushort NonQualifiedItemSkillId = 9;
/// <summary>
/// Tests if the created skill list contains a skill that was learned by the character before.
/// </summary>
[Test]
public async ValueTask LearnedSkillAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.SelectedCharacter!.LearnedSkills.Add(this.CreateSkillEntry(LearnedSkillId));
var skillList = new SkillList(player);
Assert.That(skillList.ContainsSkill(LearnedSkillId), Is.True);
}
/// <summary>
/// Tests if the skill of an item is or isn't getting added to the skill list, depending if it's suitable to the character's class.
/// </summary>
[Test]
public async ValueTask ItemSkillAddedAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var skillList = player.SkillList as SkillList;
await player.Inventory!.AddItemAsync(0, this.CreateItemWithSkill(QualifiedItemSkillId, player.SelectedCharacter!.CharacterClass)).ConfigureAwait(false);
await player.Inventory!.AddItemAsync(1, this.CreateItemWithSkill(NonQualifiedItemSkillId)).ConfigureAwait(false);
Assert.That(skillList!.ContainsSkill(QualifiedItemSkillId), Is.True);
Assert.That(skillList!.ContainsSkill(NonQualifiedItemSkillId), Is.False);
}
/// <summary>
/// Tests the removal of item skills.
/// </summary>
[Test]
public async ValueTask ItemSkillRemovedAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var item = this.CreateItemWithSkill(QualifiedItemSkillId, player.SelectedCharacter!.CharacterClass);
item.Durability = 1;
await player.Inventory!.AddItemAsync(0, item).ConfigureAwait(false);
var skillList = new SkillList(player);
Assert.That(await skillList.RemoveItemSkillAsync(item.Definition!.Skill!.Number.ToUnsigned()).ConfigureAwait(false), Is.True);
Assert.That(skillList.ContainsSkill(QualifiedItemSkillId), Is.False);
}
/// <summary>
/// Tests if the skill list does not contain non-learned skills.
/// </summary>
[Test]
public async ValueTask NonLearnedSkillAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
Assert.That(player.SkillList!.ContainsSkill(NonLearnedSkillId), Is.False);
}
private Item CreateItemWithSkill(ushort skillId, CharacterClass? qualifiedClass = null)
{
var itemDefinition = new Mock<ItemDefinition>();
itemDefinition.SetupAllProperties();
var skillDefinition = new Mock<Skill>();
skillDefinition.Object.Number = skillId.ToSigned();
skillDefinition.Setup(sd => sd.QualifiedCharacters).Returns(new List<CharacterClass>());
if (qualifiedClass is not null)
{
skillDefinition.Object.QualifiedCharacters.Add(qualifiedClass);
}
itemDefinition.Object.Skill = skillDefinition.Object;
itemDefinition.Object.Height = 1;
itemDefinition.Object.Width = 1;
itemDefinition.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
var item = new Item
{
HasSkill = true,
Definition = itemDefinition.Object,
};
return item;
}
private SkillEntry CreateSkillEntry(ushort skillId)
{
var skillEntry = new SkillEntry { Skill = new OpenMU.DataModel.Configuration.Skill { Number = skillId.ToSigned() } };
return skillEntry;
}
}

View File

@@ -0,0 +1,513 @@
// <copyright file="SpeedHackAntiCheatTests.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 System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
using MUnique.OpenMU.GameLogic.PlugIns;
using NUnit.Framework;
/// <summary>
/// Tests for the speed hack anti-cheat detection logic.
/// </summary>
[TestFixture]
public class SpeedHackAntiCheatTests
{
private static readonly Point StartPoint = new(100, 100);
/// <summary>
/// Tests that the speed check detects speed hacks on walk and bans the account after exceeding limits.
/// </summary>
[Test]
public async Task TestWalkSpeedHackDetectionBansAccountAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
// Perform rapid walks to exceed the 3-warnings limit.
// We need 12 iterations because speedhack detection uses a rolling history of recent walks
// which requires 3 walks to trigger the first warning. Since the history is cleared on violation,
// we need 3 walks * 4 warnings = 12 iterations to trigger account ban.
for (int i = 0; i < 12; i++)
{
var nextFrom = new Point((byte)(StartPoint.X + i * 2), StartPoint.Y);
var nextTo = new Point((byte)(StartPoint.X + i * 2 + 2), StartPoint.Y);
// Set player position to nextFrom to avoid startOffset validation check rejecting the walk.
player.Position = nextFrom;
// Reset the alert debounce to allow consecutive warnings.
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// The account should be banned because we sent multiple walk requests triggering 4 warnings
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Banned));
}
/// <summary>
/// Tests that a large walk start offset triggers rubberbanding and doesn't record a speed hack warning.
/// </summary>
[Test]
public async Task TestWalkStartOffsetRubberbandsPlayerAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
player.Position = StartPoint;
var startPointTooFar = new Point((byte)(StartPoint.X + 10), StartPoint.Y);
var targetPoint = new Point((byte)(StartPoint.X + 12), StartPoint.Y);
WalkingStep[] steps = [new() { From = startPointTooFar, To = targetPoint, Direction = Direction.East }];
await player.WalkToAsync(targetPoint, steps).ConfigureAwait(false);
// Verify that no violation was recorded
Assert.That(player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.GetWarningCount(player), Is.EqualTo(0));
// Verify that the view plugin container for IObjectMovedPlugIn was called to warp the player back
var objectMovedPlugIn = player.ViewPlugIns.GetPlugIn<IObjectMovedPlugIn>();
Assert.That(objectMovedPlugIn, Is.Not.Null);
var objectMovedMock = Mock.Get<IObjectMovedPlugIn>(objectMovedPlugIn!);
objectMovedMock.Verify(m => m.ObjectMovedAsync(player, MoveType.Instant), Times.Once);
}
/// <summary>
/// Tests that walking in a safe zone does not trigger speed hack warnings/violations.
/// </summary>
[Test]
public async Task TestWalkSpeedHackBypassedInSafezoneAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
// Mark current map terrain to have safe zone at the positions player walks through
var mapTerrain = player.CurrentMap!.Terrain;
for (int x = 0; x < mapTerrain.SafezoneMap.GetLength(0); x++)
{
for (int y = 0; y < mapTerrain.SafezoneMap.GetLength(1); y++)
{
mapTerrain.SafezoneMap[x, y] = true;
}
}
// Verify that IsAtSafezone returns true
Assert.That(player.IsAtSafezone(), Is.True);
// Perform rapid walks to show it is bypassed.
for (int i = 0; i < 12; i++)
{
var nextFrom = new Point((byte)(StartPoint.X + i * 2), StartPoint.Y);
var nextTo = new Point((byte)(StartPoint.X + i * 2 + 2), StartPoint.Y);
player.Position = nextFrom;
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// Account should remain normal
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
Assert.That(player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.GetWarningCount(player), Is.EqualTo(0));
}
/// <summary>
/// Tests that attacking too fast triggers attack speed hack detection.
/// </summary>
[Test]
public async Task TestAttackSpeedHackDetectionAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
player.Attributes![Stats.AttackSpeed] = 20;
var speedCheck = player.GameContext.PlugInManager.GetPlugInPoint<ISpeedHackCheatCheckPlugIn>();
Assert.That(speedCheck, Is.Not.Null);
// The first attack sets the timestamp tracker
var argsFirst = new SpeedHackCheckEventArgs();
await speedCheck!.AttackCheatCheckAsync(player, argsFirst).ConfigureAwait(false);
Assert.That(argsFirst.IsCheatDetected, Is.False);
// Perform rapid attacks in succession (0ms elapsed)
// With attack speed 20, min expected interval is 1000 - 60 = 940ms.
// Doing this multiple times should trigger the rolling average detection.
bool detectedHack = false;
for (int i = 0; i < 5; i++)
{
var args = new SpeedHackCheckEventArgs();
await speedCheck.AttackCheatCheckAsync(player, args).ConfigureAwait(false);
if (args.IsCheatDetected)
{
detectedHack = true;
break;
}
}
Assert.That(detectedHack, Is.True);
}
/// <summary>
/// Tests that teleporting/warping to a different coordinate clears recent walks
/// and doesn't trigger speed hack warnings on subsequent movements.
/// </summary>
[Test]
public async Task TestWarpClearsWalkHistoryAndPreventsFalsePositiveAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
player.Position = StartPoint;
// Perform a normal walk
WalkingStep[] steps1 = [new() { From = StartPoint, To = new Point((byte)(StartPoint.X + 1), StartPoint.Y), Direction = Direction.East }];
await player.WalkToAsync(new Point((byte)(StartPoint.X + 1), StartPoint.Y), steps1).ConfigureAwait(false);
// Simulate instant warp to different coordinates far away (e.g. via WarpToAsync/PlaceAtGate)
var gate = new ExitGate
{
Map = player.CurrentMap!.Definition,
X1 = 200,
X2 = 200,
Y1 = 200,
Y2 = 200,
Direction = Direction.West
};
await player.WarpToAsync(gate).ConfigureAwait(false);
// Perform a walk immediately at the new coordinates
var newPosition = player.Position;
WalkingStep[] steps2 = [new() { From = newPosition, To = new Point((byte)(newPosition.X + 1), newPosition.Y), Direction = Direction.East }];
await player.WalkToAsync(new Point((byte)(newPosition.X + 1), newPosition.Y), steps2).ConfigureAwait(false);
// Verify that no violation was recorded
Assert.That(player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.GetWarningCount(player), Is.EqualTo(0));
}
/// <summary>
/// Tests that a speedhacker using Cheat Engine (moving extremely fast without waiting for the server's Position to update)
/// is successfully detected and banned by the speed check.
/// </summary>
[Test]
public async Task TestWalkSpeedHackDetectionWithCheatEngineAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
player.Position = StartPoint;
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
var objectMovedPlugIn = player.ViewPlugIns.GetPlugIn<IObjectMovedPlugIn>();
var objectMovedMock = Mock.Get(objectMovedPlugIn!);
Point currentClientPos = StartPoint;
objectMovedMock.Setup(m => m.ObjectMovedAsync(player, MoveType.Instant))
.Callback(() => currentClientPos = player.Position);
// Perform rapid walks to exceed the warnings limit.
// We run 16 iterations because warnings occur every 3rd step, followed by an offset desync rubberband on the 4th,
// requiring 16 iterations to reach 4 warnings and trigger account ban.
for (int i = 0; i < 16; i++)
{
var nextFrom = currentClientPos;
var nextTo = new Point((byte)(nextFrom.X + 2), nextFrom.Y);
currentClientPos = nextTo;
// Reset the alert debounce to allow consecutive warnings.
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// The account should be banned because we sent rapid walks, even though the server position was out of sync.
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Banned));
}
/// <summary>
/// Tests that a player equipped with a mount walking with Cheat Engine (rapid walk requests)
/// is successfully detected and banned.
/// </summary>
[Test]
public async Task TestWalkSpeedHackWithMountDetectedAsync()
{
var mountItem = new Item
{
ItemSlot = MUnique.OpenMU.DataModel.InventoryConstants.PetSlot,
Definition = new ItemDefinition
{
Group = 13,
Number = 37, // Fenrir
}
};
var inventoryItems = new List<Item> { mountItem };
var player = await CreatePlayerWithSpeedAttributesAsync(inventoryItems).ConfigureAwait(false);
player.Attributes![Stats.MovementSpeed] = 17;
Assert.That(player.StepDelay.TotalMilliseconds, Is.EqualTo(TimeSpan.FromMilliseconds(4000.0 / 17.0).TotalMilliseconds).Within(1.0));
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
var objectMovedPlugIn = player.ViewPlugIns.GetPlugIn<IObjectMovedPlugIn>();
var objectMovedMock = Mock.Get(objectMovedPlugIn!);
Point currentClientPos = StartPoint;
objectMovedMock.Setup(m => m.ObjectMovedAsync(player, MoveType.Instant))
.Callback(() => currentClientPos = player.Position);
// Perform rapid walks to exceed the warnings limit.
for (int i = 0; i < 16; i++)
{
var nextFrom = currentClientPos;
var nextTo = new Point((byte)(nextFrom.X + 2), nextFrom.Y);
currentClientPos = nextTo;
// Reset the alert debounce to allow consecutive warnings.
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// The account should be banned because we sent rapid walks.
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Banned));
}
/// <summary>
/// Tests that a high movement speed character walking at their maximum speed (which resolves
/// to step delay below the minimum expected delay floor of 50ms) does not trigger false positive bans.
/// </summary>
[Test]
public async Task TestHighMovementSpeedDoesNotTriggerFalsePositivesAsync()
{
// Speed attribute is 100, which yields expected step delay: 4000.0 / 100.0 = 40ms.
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
player.Attributes![Stats.MovementSpeed] = 100;
player.Position = StartPoint;
// Perform rapid walks spaced according to the expected 40ms step delay.
// Even though 40ms is below the 50ms minimum limit floor, it should not trigger violations
// because of our dynamic flooring scaling fix.
for (int i = 0; i < 20; i++)
{
var nextFrom = new Point((byte)(StartPoint.X + i * 2), StartPoint.Y);
var nextTo = new Point((byte)(StartPoint.X + i * 2 + 2), StartPoint.Y);
player.Position = nextFrom;
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
// Simulate walk packet processing spaced at 80ms interval (40ms expected per tile * 2 tiles).
await Task.Delay(80).ConfigureAwait(false);
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// The account should remain normal.
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
Assert.That(player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.GetWarningCount(player), Is.EqualTo(0));
}
/// <summary>
/// Tests that the speed check is bypassed when the plugin is disabled.
/// </summary>
[Test]
public async Task TestBypassedWhenPluginDisabledAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
// Deactivate the plugin
player.GameContext.PlugInManager.DeactivatePlugIn<SpeedHackDetectPlugIn>();
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
// Rapid walks which would normally ban the player
for (int i = 0; i < 12; i++)
{
var nextFrom = new Point((byte)(StartPoint.X + i * 2), StartPoint.Y);
var nextTo = new Point((byte)(StartPoint.X + i * 2 + 2), StartPoint.Y);
player.Position = nextFrom;
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()?.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// The account should remain normal because plugin is disabled
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
Assert.That(player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()?.GetWarningCount(player) ?? 0, Is.EqualTo(0));
}
/// <summary>
/// Tests that customized warning thresholds and ban rules are respected.
/// </summary>
[Test]
public async Task TestCustomizedWarningThresholdAndNoBanAsync()
{
var player = await CreatePlayerWithSpeedAttributesAsync().ConfigureAwait(false);
var plugin = player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>();
Assert.That(plugin, Is.Not.Null);
// Customize configuration: 5 warnings limit, no autoban, no disconnect
plugin!.Configuration!.MaxWarnings = 5;
plugin.Configuration.AutoBan = false;
plugin.Configuration.DisconnectOnViolation = false;
// Perform rapid walks to exceed the original 3 warnings limit, and the new 5 warnings limit.
// Needs 3 walks per warning, so 3 * 6 warnings = 18 walks to trigger warnings beyond limit.
for (int i = 0; i < 18; i++)
{
var nextFrom = new Point((byte)(StartPoint.X + i * 2), StartPoint.Y);
var nextTo = new Point((byte)(StartPoint.X + i * 2 + 2), StartPoint.Y);
player.Position = nextFrom;
player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.SetLastAlertTime(player, DateTime.MinValue);
WalkingStep[] steps = [new() { From = nextFrom, To = nextTo, Direction = Direction.East }];
await player.WalkToAsync(nextTo, steps).ConfigureAwait(false);
}
// The account should still be normal (no autoban) and not disconnected
Assert.That(player.Account?.State, Is.EqualTo(AccountState.Normal));
Assert.That(player.GameContext.FeaturePlugIns.GetPlugIn<SpeedHackDetectPlugIn>()!.GetWarningCount(player), Is.GreaterThan(5));
}
private static async ValueTask<Player> CreatePlayerWithSpeedAttributesAsync(List<Item>? inventoryItems = null)
{
var gameConfig = new Mock<GameConfiguration>();
gameConfig.SetupAllProperties();
gameConfig.Setup(c => c.Maps).Returns(new List<GameMapDefinition>());
gameConfig.Setup(c => c.Items).Returns(new List<ItemDefinition>());
gameConfig.Setup(c => c.Skills).Returns(new List<Skill>());
gameConfig.Setup(c => c.PlugInConfigurations).Returns(new List<PlugInConfiguration>());
gameConfig.Setup(c => c.CharacterClasses).Returns(new List<CharacterClass>());
gameConfig.Setup(c => c.GlobalAttributeCombinations).Returns(new List<AttributeRelationship>());
gameConfig.Setup(c => c.GlobalBaseAttributeValues).Returns(new List<ConstValueAttribute>
{
new(1, Stats.MoneyAmountRate),
});
var mapDefinition = new Mock<GameMapDefinition>();
mapDefinition.SetupAllProperties();
mapDefinition.Setup(m => m.DropItemGroups).Returns(new List<DropItemGroup>());
mapDefinition.Setup(m => m.MonsterSpawns).Returns(new List<MonsterSpawnArea>());
mapDefinition.Object.TerrainData = new byte[ushort.MaxValue + 3];
gameConfig.Object.RecoveryInterval = int.MaxValue;
gameConfig.Object.Maps.Add(mapDefinition.Object);
var mapInitializer = new MapInitializer(gameConfig.Object, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var gameContext = new GameContext(gameConfig.Object, new InMemoryPersistenceContextProvider(), mapInitializer, new NullLoggerFactory(), new PlugInManager(null, new NullLoggerFactory(), null, null), NullDropGenerator.Instance, new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameContext.PlugInManager;
mapInitializer.PathFinderPool = gameContext.PathFinderPool;
var characterMock = new Mock<Character>();
characterMock.SetupAllProperties();
characterMock.Setup(c => c.LearnedSkills).Returns(new List<SkillEntry>());
characterMock.Setup(c => c.Attributes).Returns(new List<StatAttribute>());
characterMock.Setup(c => c.DropItemGroups).Returns(new List<DropItemGroup>());
var inventoryMock = new Mock<ItemStorage>();
inventoryMock.SetupAllProperties();
inventoryMock.Setup(i => i.Items).Returns(inventoryItems ?? new List<Item>());
var character = characterMock.Object;
character.Inventory = inventoryMock.Object;
character.CurrentMap = gameContext.Configuration.Maps.FirstOrDefault(m => m.Number == 0);
var characterClassMock = new Mock<CharacterClass>();
characterClassMock.Setup(c => c.StatAttributes).Returns(
new List<StatAttributeDefinition>
{
new(Stats.Level, 1, false),
new(Stats.BaseStrength, 28, true),
new(Stats.BaseAgility, 20, true),
new(Stats.BaseVitality, 25, true),
new(Stats.BaseEnergy, 10, true),
new(Stats.CurrentHealth, 100, false),
new(Stats.CurrentMana, 100, false),
new(Stats.CurrentShield, 100, false),
new(Stats.Resets, 0, false),
new(Stats.AttackSpeed, 20, false), // Added Stats.AttackSpeed to StatAttributes
new(Stats.MovementSpeed, 0, false),
new(Stats.MovementSpeedUnderwater, 0, false),
new(Stats.MovementSpeedFactor, 1f, false)
});
characterClassMock.Setup(c => c.AttributeCombinations).Returns(new List<AttributeRelationship>
{
new(Stats.TotalStrength, 1, Stats.BaseStrength),
new(Stats.TotalAgility, 1, Stats.BaseAgility),
new(Stats.TotalVitality, 1, Stats.BaseVitality),
new(Stats.TotalEnergy, 1, Stats.BaseEnergy),
new(Stats.MaximumMana, 1, Stats.BaseEnergy),
new(Stats.MaximumHealth, 2, Stats.BaseVitality),
});
characterClassMock.Setup(c => c.BaseAttributeValues).Returns(new List<ConstValueAttribute>
{
new(10, Stats.MaximumMana),
new(35, Stats.MaximumHealth),
});
character.CharacterClass = characterClassMock.Object;
foreach (var attributeDef in character.CharacterClass.StatAttributes)
{
character.Attributes.Add(new StatAttribute(attributeDef.Attribute!, attributeDef.BaseValue));
}
var account = new TestAccount { State = AccountState.Normal };
var player = new TestPlayer(gameContext) { Account = account };
var speedHackDetectPlugIn = new SpeedHackDetectPlugIn { Configuration = new SpeedHackDetectConfiguration() };
player.GameContext.PlugInManager.RegisterPlugInAtPlugInPoint<IFeaturePlugIn>(speedHackDetectPlugIn);
player.GameContext.PlugInManager.RegisterPlugInAtPlugInPoint<ISpeedHackCheatCheckPlugIn>(speedHackDetectPlugIn);
player.GameContext.FeaturePlugIns.AddPlugIn(speedHackDetectPlugIn, true);
await player.PlayerState.TryAdvanceToAsync(PlayerState.LoginScreen).ConfigureAwait(false);
await player.PlayerState.TryAdvanceToAsync(PlayerState.Authenticated).ConfigureAwait(false);
await player.PlayerState.TryAdvanceToAsync(PlayerState.CharacterSelection).ConfigureAwait(false);
await player.SetSelectedCharacterAsync(character).ConfigureAwait(false);
return player;
}
private class TestAccount : Account
{
public TestAccount()
{
this.Attributes = new List<StatAttribute>();
this.UnlockedCharacterClasses = new List<CharacterClass>();
this.Characters = new List<Character>();
}
}
private class TestPlayer : Player
{
public TestPlayer(IGameContext gameContext)
: base(gameContext)
{
}
protected override ICustomPlugInContainer<IViewPlugIn> CreateViewPlugInContainer()
{
return new MockViewPlugInContainer();
}
}
}

View File

@@ -0,0 +1,134 @@
// <copyright file="StateMachineTest.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 MUnique.OpenMU.GameLogic;
/// <summary>
/// Tests for the state machine.
/// </summary>
[TestFixture]
public class StateMachineTest
{
private StateMachine _stateMachine = null!;
private State _initialState = null!;
private State _isolatedState = null!;
private State _nextState = null!;
private State _finishedState = null!;
/// <summary>
/// Sets up the test data.
/// </summary>
[SetUp]
public void SetUp()
{
this._initialState = new State(new Guid("ADBEC1FA-7DB8-4A80-B054-2297B20AF32B"))
{
Name = "Initial State",
PossibleTransitions = new List<State>(),
};
this._nextState = new State(new Guid("9954D837-D5FC-4204-AD96-6BD9F19353EA"))
{
Name = "Next State",
PossibleTransitions = new List<State>(),
};
this._initialState.PossibleTransitions.Add(this._nextState);
this._nextState.PossibleTransitions.Add(this._initialState);
this._finishedState = new State(new Guid("F3658D9E-581B-451A-9C35-92A6B13B8C64"))
{
Name = "Finished",
};
this._nextState.PossibleTransitions.Add(this._finishedState);
this._isolatedState = new State(new Guid("4D45D4B0-1CA5-4222-91CC-B05DC5D87D56"))
{
Name = "Isolated State",
};
this._stateMachine = new StateMachine(this._initialState);
}
/// <summary>
/// Tests if the transition to the next allowed state is successful.
/// </summary>
[Test]
public async ValueTask TransitionToNextStateAsync()
{
var success = await this._stateMachine.TryAdvanceToAsync(this._nextState).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(this._stateMachine.CurrentState, Is.EqualTo(this._nextState));
Assert.That(this._stateMachine.Finished, Is.False);
}
/// <summary>
/// Tests if the transition to an isolated state fails.
/// </summary>
[Test]
public async ValueTask TransitionToIsolatedStateAsync()
{
var success = await this._stateMachine.TryAdvanceToAsync(this._isolatedState).ConfigureAwait(false);
Assert.That(success, Is.False);
Assert.That(this._stateMachine.CurrentState, Is.EqualTo(this._initialState));
}
/// <summary>
/// Tests if the transition to the finished state succeeds and if the state machine takes notice of it.
/// </summary>
[Test]
public async ValueTask TransitionToFinishedStateAsync()
{
await this._stateMachine.TryAdvanceToAsync(this._nextState).ConfigureAwait(false);
var success = await this._stateMachine.TryAdvanceToAsync(this._finishedState).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(this._stateMachine.CurrentState, Is.EqualTo(this._finishedState));
Assert.That(this._stateMachine.Finished, Is.True);
}
/// <summary>
/// Tests if the state change event does get raised with the next state in the event arguments.
/// </summary>
[Test]
public async ValueTask ChangesEventStateObjectAsync()
{
State? stateInEvent = null;
this._stateMachine.StateChanges += async args =>
{
stateInEvent = args.NextState;
};
await this._stateMachine.TryAdvanceToAsync(this._nextState).ConfigureAwait(false);
Assert.That(stateInEvent, Is.EqualTo(this._nextState));
}
/// <summary>
/// Tests the cancellation of state changes.
/// </summary>
[Test]
public async ValueTask ChangesEventCancelsAsync()
{
this._stateMachine.StateChanges += async args =>
{
args.Cancel = true;
};
var success = await this._stateMachine.TryAdvanceToAsync(this._nextState).ConfigureAwait(false);
Assert.That(success, Is.False);
Assert.That(this._stateMachine.CurrentState, Is.EqualTo(this._initialState));
}
/// <summary>
/// Tests if the state change event does get raised.
/// </summary>
[Test]
public async ValueTask ChangedEventAsync()
{
var stateChangeEventCalled = false;
this._stateMachine.StateChanged += async _ => stateChangeEventCalled = true;
await this._stateMachine.TryAdvanceToAsync(this._nextState).ConfigureAwait(false);
Assert.That(stateChangeEventCalled, Is.True);
}
}

View File

@@ -0,0 +1,139 @@
// <copyright file="StorageTest.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 Moq;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
/// <summary>
/// Tests the <see cref="Storage"/>.
/// </summary>
[TestFixture]
public class StorageTest
{
/// <summary>
/// Tests the adding of a small item to the upper left corner of the storage.
/// </summary>
[Test]
public async ValueTask AddItem1X1TopLeftAsync()
{
var itemStorage = this.CreateItemStorage();
var storage = new Storage(12 + 64, 12, 0, itemStorage) as IStorage;
var item = this.GetItem(1, 1);
var added = await storage.AddItemAsync(12, item).ConfigureAwait(false);
Assert.That(added, Is.True);
Assert.That(storage.Items.Contains(item), Is.True);
Assert.That(storage.FreeSlots.Contains((byte)12), Is.False);
}
/// <summary>
/// Tests adding a 2x2 item to the right bottom corner of the storage.
/// </summary>
[Test]
public async ValueTask AddItem2X2AtRightBottomAsync()
{
var itemStorage = this.CreateItemStorage();
var storage = new Storage(12 + 64, 12, 0, itemStorage) as IStorage;
var item = this.GetItem(2, 2);
byte slot = 12 + (6 * 8) + 6;
var added = await storage.AddItemAsync(slot, item).ConfigureAwait(false);
Assert.That(added, Is.True);
Assert.That(storage.Items.Contains(item), Is.True);
Assert.That(storage.FreeSlots.Contains(slot), Is.False);
}
/// <summary>
/// Tests if the adding of the item fails because the same slot is already in use by another item.
/// </summary>
[Test]
public async ValueTask AddItemFailSpaceInUseAsync()
{
var itemStorage = this.CreateItemStorage();
var storage = new Storage(12 + 64, 12, 0, itemStorage) as IStorage;
await storage.AddItemAsync(12, this.GetItem(1, 1)).ConfigureAwait(false);
var item = this.GetItem(1, 1);
var added = await storage.AddItemAsync(12, item).ConfigureAwait(false);
Assert.That(added, Is.False);
Assert.That(storage.Items.Contains(item), Is.False);
Assert.That(storage.FreeSlots.Contains((byte)12), Is.False);
}
/// <summary>
/// Tests if the adding of the item one line below another 2x2 item fails.
/// </summary>
[Test]
public async ValueTask AddItemFailSpaceInUseVerticalAsync()
{
var itemStorage = this.CreateItemStorage();
var storage = new Storage(12 + 64, 12, 0, itemStorage) as IStorage;
var addedItem = this.GetItem(2, 2);
await storage.AddItemAsync(12, addedItem).ConfigureAwait(false);
var added = await storage.AddItemAsync(13, this.GetItem(1, 1)).ConfigureAwait(false);
Assert.That(added, Is.False);
added = await storage.AddItemAsync(12 + 8, this.GetItem(1, 1)).ConfigureAwait(false);
Assert.That(added, Is.False);
Assert.That(storage.Items.Count(), Is.EqualTo(1));
Assert.That(storage.Items.Contains(addedItem), Is.True);
Assert.That(storage.FreeSlots.Contains((byte)12), Is.False);
Assert.That(storage.FreeSlots.Contains((byte)13), Is.False);
Assert.That(storage.FreeSlots.Contains((byte)(12 + 8)), Is.False);
Assert.That(storage.FreeSlots.Contains((byte)(13 + 8)), Is.False);
}
/// <summary>
/// Tests if adding an 2x1 item at the right border fails,
/// because the left part of the item would hang over.
/// </summary>
[Test]
public async ValueTask AddItem2X1FailRightBorderAsync()
{
var itemStorage = this.CreateItemStorage();
var storage = new Storage(12 + 64, 12, 0, itemStorage) as IStorage;
var added = await storage.AddItemAsync(12 + 7, this.GetItem(2, 1)).ConfigureAwait(false);
Assert.That(added, Is.False);
Assert.That(storage.FreeSlots.Contains((byte)(12 + 7)), Is.True);
}
/// <summary>
/// Tests if adding an 1x2 item at the bottom border fails,
/// because the bottom part of the item would hang over.
/// </summary>
[Test]
public async ValueTask AddItem1X2FailBottomAsync()
{
var itemStorage = this.CreateItemStorage();
var storage = new Storage(12 + 64, 12, 0, itemStorage) as IStorage;
var added = await storage.AddItemAsync(12 + (7 * 8), this.GetItem(1, 2)).ConfigureAwait(false);
Assert.That(added, Is.False);
Assert.That(storage.FreeSlots.Contains((byte)(12 + (7 * 8))), Is.True);
}
private ItemDefinition GetItemDefintion(byte width, byte heigth)
{
var itemDefinition = new ItemDefinition
{
Durability = 100,
Width = width,
Height = heigth,
};
return itemDefinition;
}
private Item GetItem(byte width, byte heigth)
{
var item = new Item { Definition = this.GetItemDefintion(width, heigth) };
item.Durability = item.Definition.Durability;
return item;
}
private ItemStorage CreateItemStorage()
{
var storage = new Mock<ItemStorage>();
storage.Setup(s => s.Items).Returns(new List<Item>());
return storage.Object;
}
}

View File

@@ -0,0 +1,254 @@
// <copyright file="TradeTest.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.DataModel;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Trade;
using MUnique.OpenMU.GameLogic.Views.Trade;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Tests for the trade actions.
/// </summary>
[TestFixture]
public class TradeTest
{
/// <summary>
/// Tests the trade request.
/// </summary>
[Test]
public async ValueTask TestTradeRequestAsync()
{
var player = this.CreateTrader(PlayerState.EnteredWorld); // The player which will send the trade request
var tradePartner = this.CreateTrader(PlayerState.EnteredWorld); // The player which will receive the trade request
var packetHandler = new TradeRequestAction();
var success = await packetHandler.RequestTradeAsync(player, tradePartner).ConfigureAwait(false);
Assert.AreEqual(true, success);
Assert.AreSame(tradePartner, player.TradingPartner);
Assert.AreSame(player, tradePartner.TradingPartner);
Assert.AreEqual(PlayerState.TradeRequested, player.PlayerState.CurrentState);
Assert.AreEqual(PlayerState.TradeRequested, tradePartner.PlayerState.CurrentState);
Mock.Get(tradePartner.ViewPlugIns.GetPlugIn<IShowTradeRequestPlugIn>()!).Verify(view => view!.ShowTradeRequestAsync(player), Times.Once);
}
/// <summary>
/// Tests the trade response.
/// </summary>
[Test]
public async ValueTask TestTradeResponseAsync()
{
var requester = this.CreateTrader(PlayerState.TradeRequested);
var responder = this.CreateTrader(PlayerState.TradeRequested);
requester.TradingPartner = responder;
responder.TradingPartner = requester;
var responseHandler = new TradeAcceptAction();
await responseHandler.HandleTradeAcceptAsync(responder, true).ConfigureAwait(false);
Assert.AreEqual(requester.PlayerState.CurrentState, PlayerState.TradeOpened);
Assert.AreEqual(responder.PlayerState.CurrentState, PlayerState.TradeOpened);
Mock.Get(requester.ViewPlugIns.GetPlugIn<IShowTradeRequestAnswerPlugIn>()!).Verify(view => view!.ShowTradeRequestAnswerAsync(true), Times.Once);
}
/// <summary>
/// Tests the cancellation of a trade.
/// </summary>
[Test]
public async ValueTask TradeCancelTestAsync()
{
var trader1 = this.CreateTrader(PlayerState.TradeOpened);
var trader2 = this.CreateTrader(PlayerState.TradeOpened);
trader1.TradingPartner = trader2;
trader2.TradingPartner = trader1;
var cancelTrader = new TradeCancelAction();
await cancelTrader.CancelTradeAsync(trader1).ConfigureAwait(false);
Assert.AreEqual(PlayerState.EnteredWorld, trader1.PlayerState.CurrentState);
Assert.AreEqual(PlayerState.EnteredWorld, trader2.PlayerState.CurrentState);
Mock.Get(trader1.ViewPlugIns.GetPlugIn<ITradeFinishedPlugIn>()!).Verify(view => view!.TradeFinishedAsync(TradeResult.Cancelled), Times.Once);
Mock.Get(trader2.ViewPlugIns.GetPlugIn<ITradeFinishedPlugIn>()!).Verify(view => view!.TradeFinishedAsync(TradeResult.Cancelled), Times.Once);
}
/// <summary>
/// Tests the finishing of a trade.
/// </summary>
[Test]
public async ValueTask TradeFinishTestAsync()
{
var trader1 = this.CreateTrader(PlayerState.TradeOpened);
var trader2 = this.CreateTrader(PlayerState.TradeOpened);
trader1.TradingPartner = trader2;
trader2.TradingPartner = trader1;
var gameContext = new Mock<IGameContext>();
gameContext.Setup(c => c.PlugInManager).Returns(new PlugInManager(null, new NullLoggerFactory(), null, null));
gameContext.Setup(c => c.PersistenceContextProvider).Returns(new InMemoryPersistenceContextProvider());
Mock.Get(trader1).Setup(m => m.GameContext).Returns(gameContext.Object);
Mock.Get(trader2).Setup(m => m.GameContext).Returns(gameContext.Object);
var tradeButtonHandler = new TradeButtonAction();
await tradeButtonHandler.TradeButtonChangedAsync(trader1, TradeButtonState.Unchecked).ConfigureAwait(false);
Assert.AreEqual(trader1.PlayerState.CurrentState, PlayerState.TradeOpened);
await tradeButtonHandler.TradeButtonChangedAsync(trader1, TradeButtonState.Checked).ConfigureAwait(false);
Assert.AreEqual(trader1.PlayerState.CurrentState, PlayerState.TradeButtonPressed);
Assert.AreEqual(trader2.PlayerState.CurrentState, PlayerState.TradeOpened);
await tradeButtonHandler.TradeButtonChangedAsync(trader2, TradeButtonState.Checked).ConfigureAwait(false);
Assert.AreEqual(trader1.PlayerState.CurrentState, PlayerState.EnteredWorld);
Assert.AreEqual(trader2.PlayerState.CurrentState, PlayerState.EnteredWorld);
Mock.Get(trader1.ViewPlugIns.GetPlugIn<ITradeFinishedPlugIn>()!).Verify(view => view!.TradeFinishedAsync(TradeResult.Success), Times.Once);
Mock.Get(trader2.ViewPlugIns.GetPlugIn<ITradeFinishedPlugIn>()!).Verify(view => view!.TradeFinishedAsync(TradeResult.Success), Times.Once);
}
/// <summary>
/// Tests a trade of items.
/// </summary>
[Test]
public async ValueTask TradeItemsAsync()
{
var trader1 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var trader2 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var tradeRequestAction = new TradeRequestAction();
var tradeResponseAction = new TradeAcceptAction();
var item1 = this.GetItem();
var item2 = this.GetItem();
await trader1.Inventory!.AddItemAsync(20, item1).ConfigureAwait(false);
await trader1.Inventory.AddItemAsync(21, item2).ConfigureAwait(false);
await tradeRequestAction.RequestTradeAsync(trader1, trader2).ConfigureAwait(false);
await tradeResponseAction.HandleTradeAcceptAsync(trader2, true).ConfigureAwait(false);
var itemMoveAction = new MoveItemAction();
await itemMoveAction.MoveItemAsync(trader1, 20, Storages.Inventory, 0, Storages.Trade).ConfigureAwait(false);
await itemMoveAction.MoveItemAsync(trader1, 21, Storages.Inventory, 2, Storages.Trade).ConfigureAwait(false);
Assert.That(trader1.TemporaryStorage!.Items.First(), Is.SameAs(item1));
var tradeButtonHandler = new TradeButtonAction();
await tradeButtonHandler.TradeButtonChangedAsync(trader1, TradeButtonState.Checked).ConfigureAwait(false);
await tradeButtonHandler.TradeButtonChangedAsync(trader2, TradeButtonState.Checked).ConfigureAwait(false);
Assert.That(trader1.Inventory.ItemStorage.Items, Is.Empty);
Assert.That(trader2.Inventory!.ItemStorage.Items.First(), Is.SameAs(item1));
}
/// <summary>
/// Tests a trade of items, when it failes due to missing inventory space.
/// </summary>
[Test]
public async ValueTask TradeFailedItemsNotFitAsync()
{
var trader1 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var trader2 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var tradeRequestAction = new TradeRequestAction();
var tradeResponseAction = new TradeAcceptAction();
// Fill up inventory of the receiving player
for (byte i = (byte)(InventoryConstants.LastEquippableItemSlotIndex + 1); i < 64 + InventoryConstants.LastEquippableItemSlotIndex; i++)
{
var item = this.GetItem();
await trader2.Inventory!.AddItemAsync(i, item).ConfigureAwait(false);
}
// Create items which should be traded.
var item1 = this.GetItem();
var item2 = this.GetItem();
await trader1.Inventory!.AddItemAsync(20, item1).ConfigureAwait(false);
await trader1.Inventory.AddItemAsync(21, item2).ConfigureAwait(false);
// Set up the trade
await tradeRequestAction.RequestTradeAsync(trader1, trader2).ConfigureAwait(false);
await tradeResponseAction.HandleTradeAcceptAsync(trader2, true).ConfigureAwait(false);
var itemMoveAction = new MoveItemAction();
await itemMoveAction.MoveItemAsync(trader1, 20, Storages.Inventory, 0, Storages.Trade).ConfigureAwait(false);
await itemMoveAction.MoveItemAsync(trader1, 21, Storages.Inventory, 2, Storages.Trade).ConfigureAwait(false);
Assert.That(trader1.TemporaryStorage!.Items.First(), Is.SameAs(item1));
// Accept the trade on both ends
var tradeButtonHandler = new TradeButtonAction();
await tradeButtonHandler.TradeButtonChangedAsync(trader1, TradeButtonState.Checked).ConfigureAwait(false);
await tradeButtonHandler.TradeButtonChangedAsync(trader2, TradeButtonState.Checked).ConfigureAwait(false);
// Check result
Assert.That(trader1.Inventory.ItemStorage.Items, Is.Not.Empty);
Assert.That(trader1.Inventory!.ItemStorage.Items.First(), Is.SameAs(item1));
Assert.That(trader1.Inventory!.ItemStorage.Items.Last(), Is.SameAs(item2));
}
/// <summary>
/// Tests that a trade cancellation does not abort restoring all valid items when one backup item is invalid.
/// </summary>
[Test]
public async ValueTask TradeCancelIgnoresInvalidBackupItemsAsync()
{
var trader1 = this.CreateTrader(PlayerState.TradeOpened);
var trader2 = this.CreateTrader(PlayerState.TradeOpened);
Mock.Get(trader1).Setup(t => t.Logger).Returns(NullLogger.Instance);
Mock.Get(trader2).Setup(t => t.Logger).Returns(NullLogger.Instance);
trader1.TradingPartner = trader2;
trader2.TradingPartner = trader1;
var brokenItem = new Mock<Item>();
brokenItem.SetupAllProperties();
brokenItem.Object.ItemSlot = 20;
var validItem = new Mock<Item>();
validItem.SetupAllProperties();
validItem.Object.ItemSlot = 21;
trader1.BackupInventory = new BackupItemStorage(trader1.Inventory!.ItemStorage)
{
Items = new List<Item> { brokenItem.Object, validItem.Object },
};
var inventoryMock = Mock.Get(trader1.Inventory!);
inventoryMock.Setup(i => i.AddItemAsync(20, brokenItem.Object)).Throws(new InvalidOperationException("broken test item"));
inventoryMock.Setup(i => i.AddItemAsync(21, validItem.Object)).Returns(new ValueTask<bool>(true));
var cancelAction = new TradeCancelAction();
await cancelAction.CancelTradeAsync(trader1).ConfigureAwait(false);
Assert.That(trader1.PlayerState.CurrentState, Is.EqualTo(PlayerState.EnteredWorld));
Assert.That(trader2.PlayerState.CurrentState, Is.EqualTo(PlayerState.EnteredWorld));
inventoryMock.Verify(i => i.AddItemAsync(21, validItem.Object), Times.Once);
Assert.That(trader1.BackupInventory, Is.Null);
}
private Item GetItem()
{
var item = new Mock<Item>();
item.SetupAllProperties();
item.Object.Definition = new ItemDefinition { Width = 1, Height = 1 };
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
return item.Object;
}
private ITrader CreateTrader(State playerState)
{
var trader = new Mock<ITrader>();
trader.SetupAllProperties();
trader.Setup(t => t.PlayerState).Returns(new StateMachine(playerState));
var inventory = new Mock<IInventoryStorage>();
var itemStorage = new Mock<ItemStorage>();
itemStorage.Setup(i => i.Items).Returns(new List<Item>());
inventory.Setup(i => i.ItemStorage).Returns(itemStorage.Object);
trader.Setup(t => t.Inventory).Returns(inventory.Object);
trader.Object.BackupInventory = new BackupItemStorage(itemStorage.Object) { Items = new List<Item>() };
var temporaryStorage = new Mock<IStorage>();
temporaryStorage.Setup(t => t.Items).Returns(new List<Item>());
trader.Setup(t => t.TemporaryStorage).Returns(temporaryStorage.Object);
trader.Setup(t => t.ViewPlugIns).Returns(new MockViewPlugInContainer());
var contextMock = new Mock<IPlayerContext>();
trader.Setup(t => t.PersistenceContext).Returns(contextMock.Object);
return trader.Object;
}
//// TODO: Test fail scenarios
}

View File

@@ -0,0 +1,237 @@
// <copyright file="ViewPlugInContainerTest.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 System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameServer;
using MUnique.OpenMU.GameServer.RemoteView;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Unit tests for the <see cref="ViewPlugInContainer"/>.
/// </summary>
[TestFixture]
public class ViewPlugInContainerTest
{
private static readonly ClientVersion Season6E3English = new(6, 3, ClientLanguage.English);
private static readonly ClientVersion Season9E2English = new(9, 2, ClientLanguage.English);
/// <summary>
/// A test interface for our test plugin implementations.
/// </summary>
public interface ISomeViewPlugIn : IViewPlugIn
{
}
/// <summary>
/// Tests if the the plug in of correct version is selected when the plugin for the exact version is available.
/// </summary>
[Test]
public void SelectPlugInOfCorrectVersionWhenExactVersionIsAvailable()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<ISomeViewPlugIn, Season1PlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season9PlugIn>();
var containerForSeason6 = new ViewPlugInContainer(this.CreatePlayer(manager), Season6E3English, manager);
Assert.That(containerForSeason6.GetPlugIn<ISomeViewPlugIn>()!.GetType(), Is.EqualTo(typeof(Season6PlugIn)));
}
/// <summary>
/// Tests if the the plug in of correct version is selected when only plugins for lower versions are available.
/// </summary>
[Test]
public void SelectPlugInOfCorrectVersionWhenLowerVersionsAreAvailable()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<ISomeViewPlugIn, InvariantSeasonPlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season1PlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugIn>();
var containerForSeason9 = new ViewPlugInContainer(this.CreatePlayer(manager), Season9E2English, manager);
Assert.That(containerForSeason9.GetPlugIn<ISomeViewPlugIn>()!.GetType(), Is.EqualTo(typeof(Season6PlugIn)));
}
/// <summary>
/// Tests if plugins of the correct language are selected.
/// </summary>
[Test]
public void SelectPlugInOfCorrectLanguage()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugInOfSomeOtherLanguage>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugIn>();
var containerForSeason6English = new ViewPlugInContainer(this.CreatePlayer(manager), Season6E3English, manager);
Assert.That(containerForSeason6English.GetPlugIn<ISomeViewPlugIn>()!.GetType(), Is.EqualTo(typeof(Season6PlugIn)));
}
/// <summary>
/// Tests if plugins of invariant language and version are selected.
/// </summary>
[Test]
public void SelectInvariantPlugIn()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<ISomeViewPlugIn, InvariantSeasonPlugIn>();
var containerForSeason6English = new ViewPlugInContainer(this.CreatePlayer(manager), Season6E3English, manager);
Assert.That(containerForSeason6English.GetPlugIn<ISomeViewPlugIn>()!.GetType(), Is.EqualTo(typeof(InvariantSeasonPlugIn)));
}
/// <summary>
/// Tests if another plugin is getting 'effective' when the currently effective plugin gets deactivated.
/// </summary>
[Test]
public void SelectPlugInAfterDeactivation()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<ISomeViewPlugIn, InvariantSeasonPlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season1PlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugIn>();
var containerForSeason9 = new ViewPlugInContainer(this.CreatePlayer(manager), Season9E2English, manager);
manager.DeactivatePlugIn<Season6PlugIn>();
Assert.That(containerForSeason9.GetPlugIn<ISomeViewPlugIn>()!.GetType(), Is.EqualTo(typeof(Season1PlugIn)));
}
/// <summary>
/// Tests if the language specific plugin has priority over the invariant one.
/// </summary>
[Test]
public void SelectLanguageSpecificOverInvariant()
{
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugIn>();
manager.RegisterPlugIn<ISomeViewPlugIn, Season6PlugInInvariant>();
var containerForSeason9 = new ViewPlugInContainer(this.CreatePlayer(manager), Season9E2English, manager);
Assert.That(containerForSeason9.GetPlugIn<ISomeViewPlugIn>()!.GetType(), Is.EqualTo(typeof(Season6PlugIn)));
}
private RemotePlayer CreatePlayer(PlugInManager plugInManager)
{
var gameContext = new Mock<IGameServerContext>();
gameContext.Setup(c => c.PersistenceContextProvider).Returns(new Mock<IPersistenceContextProvider>().Object);
gameContext.Setup(c => c.Configuration).Returns(new GameConfiguration());
gameContext.Setup(c => c.PlugInManager).Returns(plugInManager);
gameContext.Setup(c => c.LoggerFactory).Returns(new NullLoggerFactory());
return new RemotePlayer(gameContext.Object, new Mock<IConnection>().Object, default);
}
/// <summary>
/// A plugin which is version/language invariant.
/// </summary>
[PlugIn]
[Display(Name = "Season Invariant Test PlugIn")]
[Guid("96A3FED8-0112-4CFC-A717-70EEEEBE859A")]
public class InvariantSeasonPlugIn : ISomeViewPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="InvariantSeasonPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public InvariantSeasonPlugIn(RemotePlayer player)
{
}
}
/// <summary>
/// A test plugin for season 1.
/// </summary>
[PlugIn]
[Display(Name = "Season 1 Test PlugIn")]
[Guid("8CA21647-85D5-43BB-A8F9-3543D0E02176")]
[MinimumClient(1, 0, ClientLanguage.English)]
public class Season1PlugIn : ISomeViewPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="Season1PlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public Season1PlugIn(RemotePlayer player)
{
}
}
/// <summary>
/// A test plugin for season 6.
/// </summary>
[PlugIn]
[Display(Name = "Season 6 Test PlugIn")]
[Guid("7C029691-BB22-4B5D-BE96-924537E43EB2")]
[MinimumClient(6, 3, ClientLanguage.English)]
public class Season6PlugIn : ISomeViewPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="Season6PlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public Season6PlugIn(RemotePlayer player)
{
}
}
/// <summary>
/// A test plugin for season 6, with invariant language.
/// </summary>
[PlugIn]
[Display(Name = "Season 6 Test PlugIn, Invariant language")]
[Guid("D58A6AC6-A804-4321-9422-0911EDC82867")]
[MinimumClient(6, 3, ClientLanguage.Invariant)]
public class Season6PlugInInvariant : ISomeViewPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="Season6PlugInInvariant"/> class.
/// </summary>
/// <param name="player">The player.</param>
public Season6PlugInInvariant(RemotePlayer player)
{
}
}
/// <summary>
/// A test plugin for season 6, but for another client language.
/// </summary>
[PlugIn]
[Display(Name = "Season 6 Test PlugIn")]
[Guid("05C47D9E-F0A0-48B3-9FFF-22CF43B20494")]
[MinimumClient(6, 3, (ClientLanguage)42)]
public class Season6PlugInOfSomeOtherLanguage : ISomeViewPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="Season6PlugInOfSomeOtherLanguage"/> class.
/// </summary>
/// <param name="player">The player.</param>
public Season6PlugInOfSomeOtherLanguage(RemotePlayer player)
{
}
}
/// <summary>
/// Test plugin for season 9.
/// </summary>
[PlugIn]
[Display(Name = "Season 9 Test PlugIn")]
[Guid("82AC1C9A-F3D0-4196-A3CD-6CB36AA2D914")]
[MinimumClient(9, 2, ClientLanguage.English)]
public class Season9PlugIn : ISomeViewPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="Season9PlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public Season9PlugIn(RemotePlayer player)
{
}
}
}