baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
39
tests/MUnique.OpenMU.Tests/AppearanceSerializerTest.cs
Normal file
39
tests/MUnique.OpenMU.Tests/AppearanceSerializerTest.cs
Normal 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 } };
|
||||
}
|
||||
}
|
||||
72
tests/MUnique.OpenMU.Tests/CharacterMoveTest.cs
Normal file
72
tests/MUnique.OpenMU.Tests/CharacterMoveTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
147
tests/MUnique.OpenMU.Tests/ClientAttributeTest.cs
Normal file
147
tests/MUnique.OpenMU.Tests/ClientAttributeTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
161
tests/MUnique.OpenMU.Tests/DropGeneratorTest.cs
Normal file
161
tests/MUnique.OpenMU.Tests/DropGeneratorTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
69
tests/MUnique.OpenMU.Tests/DropItemGroupExtensions.cs
Normal file
69
tests/MUnique.OpenMU.Tests/DropItemGroupExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
302
tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs
Normal file
302
tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
229
tests/MUnique.OpenMU.Tests/FriendServerTest.cs
Normal file
229
tests/MUnique.OpenMU.Tests/FriendServerTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
165
tests/MUnique.OpenMU.Tests/FrustumBasedTargetFilterTest.cs
Normal file
165
tests/MUnique.OpenMU.Tests/FrustumBasedTargetFilterTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
51
tests/MUnique.OpenMU.Tests/GameContextTestHelper.cs
Normal file
51
tests/MUnique.OpenMU.Tests/GameContextTestHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
182
tests/MUnique.OpenMU.Tests/GameMapTest.cs
Normal file
182
tests/MUnique.OpenMU.Tests/GameMapTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
177
tests/MUnique.OpenMU.Tests/GuildActionTest.cs
Normal file
177
tests/MUnique.OpenMU.Tests/GuildActionTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
601
tests/MUnique.OpenMU.Tests/GuildAllianceTest.cs
Normal file
601
tests/MUnique.OpenMU.Tests/GuildAllianceTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
87
tests/MUnique.OpenMU.Tests/GuildServerTest.cs
Normal file
87
tests/MUnique.OpenMU.Tests/GuildServerTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
93
tests/MUnique.OpenMU.Tests/GuildTestBase.cs
Normal file
93
tests/MUnique.OpenMU.Tests/GuildTestBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
418
tests/MUnique.OpenMU.Tests/ItemConsumptionTest.cs
Normal file
418
tests/MUnique.OpenMU.Tests/ItemConsumptionTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
440
tests/MUnique.OpenMU.Tests/ItemPriceCalculatorTest.cs
Normal file
440
tests/MUnique.OpenMU.Tests/ItemPriceCalculatorTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
176
tests/MUnique.OpenMU.Tests/ItemRequirementCalculationTest.cs
Normal file
176
tests/MUnique.OpenMU.Tests/ItemRequirementCalculationTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
315
tests/MUnique.OpenMU.Tests/ItemSerializerTests.cs
Normal file
315
tests/MUnique.OpenMU.Tests/ItemSerializerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
431
tests/MUnique.OpenMU.Tests/LocalizedStringTests.cs
Normal file
431
tests/MUnique.OpenMU.Tests/LocalizedStringTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
54
tests/MUnique.OpenMU.Tests/MUnique.OpenMU.Tests.csproj
Normal file
54
tests/MUnique.OpenMU.Tests/MUnique.OpenMU.Tests.csproj
Normal 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>
|
||||
255
tests/MUnique.OpenMU.Tests/MasterSystemTest.cs
Normal file
255
tests/MUnique.OpenMU.Tests/MasterSystemTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
30
tests/MUnique.OpenMU.Tests/MockViewPlugInContainer.cs
Normal file
30
tests/MUnique.OpenMU.Tests/MockViewPlugInContainer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
137
tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs
Normal file
137
tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
132
tests/MUnique.OpenMU.Tests/MonsterAttributeReloadTests.cs
Normal file
132
tests/MUnique.OpenMU.Tests/MonsterAttributeReloadTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
307
tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs
Normal file
307
tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
89
tests/MUnique.OpenMU.Tests/ObserverToWorldAdapterTest.cs
Normal file
89
tests/MUnique.OpenMU.Tests/ObserverToWorldAdapterTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
77
tests/MUnique.OpenMU.Tests/Offline/BuffHandlerTests.cs
Normal file
77
tests/MUnique.OpenMU.Tests/Offline/BuffHandlerTests.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
160
tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs
Normal file
160
tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
tests/MUnique.OpenMU.Tests/Offline/HealingHandlerTests.cs
Normal file
103
tests/MUnique.OpenMU.Tests/Offline/HealingHandlerTests.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
134
tests/MUnique.OpenMU.Tests/Offline/ItemPickupHandlerTests.cs
Normal file
134
tests/MUnique.OpenMU.Tests/Offline/ItemPickupHandlerTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
61
tests/MUnique.OpenMU.Tests/Offline/MovementHandlerTests.cs
Normal file
61
tests/MUnique.OpenMU.Tests/Offline/MovementHandlerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
146
tests/MUnique.OpenMU.Tests/Offline/OfflinePlayerManagerTests.cs
Normal file
146
tests/MUnique.OpenMU.Tests/Offline/OfflinePlayerManagerTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
63
tests/MUnique.OpenMU.Tests/Offline/OfflinePlayerTests.cs
Normal file
63
tests/MUnique.OpenMU.Tests/Offline/OfflinePlayerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
117
tests/MUnique.OpenMU.Tests/Offline/PetHandlerTests.cs
Normal file
117
tests/MUnique.OpenMU.Tests/Offline/PetHandlerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
221
tests/MUnique.OpenMU.Tests/Offline/RepairHandlerTests.cs
Normal file
221
tests/MUnique.OpenMU.Tests/Offline/RepairHandlerTests.cs
Normal 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 <= 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 <= 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
215
tests/MUnique.OpenMU.Tests/PKClearChatCommandPlugInTest.cs
Normal file
215
tests/MUnique.OpenMU.Tests/PKClearChatCommandPlugInTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
251
tests/MUnique.OpenMU.Tests/PacketHandlerPlugInContainerTest.cs
Normal file
251
tests/MUnique.OpenMU.Tests/PacketHandlerPlugInContainerTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
205
tests/MUnique.OpenMU.Tests/Party/PartyManagerTest.cs
Normal file
205
tests/MUnique.OpenMU.Tests/Party/PartyManagerTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
408
tests/MUnique.OpenMU.Tests/Party/PartyTest.cs
Normal file
408
tests/MUnique.OpenMU.Tests/Party/PartyTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
172
tests/MUnique.OpenMU.Tests/PlayerTestHelper.cs
Normal file
172
tests/MUnique.OpenMU.Tests/PlayerTestHelper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
67
tests/MUnique.OpenMU.Tests/PointExtensionsTest.cs
Normal file
67
tests/MUnique.OpenMU.Tests/PointExtensionsTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
383
tests/MUnique.OpenMU.Tests/PowerUpFactoryTest.cs
Normal file
383
tests/MUnique.OpenMU.Tests/PowerUpFactoryTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tests/MUnique.OpenMU.Tests/Properties/AssemblyInfo.cs
Normal file
10
tests/MUnique.OpenMU.Tests/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
191
tests/MUnique.OpenMU.Tests/ResetCharacterActionTest.cs
Normal file
191
tests/MUnique.OpenMU.Tests/ResetCharacterActionTest.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
142
tests/MUnique.OpenMU.Tests/ResetProgressionCalculatorTest.cs
Normal file
142
tests/MUnique.OpenMU.Tests/ResetProgressionCalculatorTest.cs
Normal 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 },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
37
tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs
Normal file
37
tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
107
tests/MUnique.OpenMU.Tests/SkillListTest.cs
Normal file
107
tests/MUnique.OpenMU.Tests/SkillListTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
513
tests/MUnique.OpenMU.Tests/SpeedHackAntiCheatTests.cs
Normal file
513
tests/MUnique.OpenMU.Tests/SpeedHackAntiCheatTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
134
tests/MUnique.OpenMU.Tests/StateMachineTest.cs
Normal file
134
tests/MUnique.OpenMU.Tests/StateMachineTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
139
tests/MUnique.OpenMU.Tests/StorageTest.cs
Normal file
139
tests/MUnique.OpenMU.Tests/StorageTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
254
tests/MUnique.OpenMU.Tests/TradeTest.cs
Normal file
254
tests/MUnique.OpenMU.Tests/TradeTest.cs
Normal 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
|
||||
}
|
||||
237
tests/MUnique.OpenMU.Tests/ViewPlugInContainerTest.cs
Normal file
237
tests/MUnique.OpenMU.Tests/ViewPlugInContainerTest.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user