baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
3
tests/Directory.Packages.props
Normal file
3
tests/Directory.Packages.props
Normal file
@@ -0,0 +1,3 @@
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props, $(MSBuildThisFileDirectory)../src))" />
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
// <copyright file="AttributeDefinitionTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="AttributeDefinition"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class AttributeDefinitionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if different instances with the same id are treated as being equal.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Equality()
|
||||
{
|
||||
var attributeId = new Guid("86A31E67-8696-43C5-A9FE-CA85E1E07017");
|
||||
var definition1 = new AttributeDefinition(attributeId, "foo", "bar");
|
||||
var definition2 = new AttributeDefinition(attributeId, "test", "123");
|
||||
Assert.That(definition1 == definition2, Is.True);
|
||||
Assert.That(definition1, Is.EqualTo(definition2));
|
||||
Assert.That(definition1.GetHashCode(), Is.EqualTo(definition2.GetHashCode()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// <copyright file="AttributeRelationshipElementTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="AttributeRelationshipElement"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class AttributeRelationshipElementTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if a relationship between two elements with the <see cref="InputOperator.Add"/> is handled correctly.
|
||||
/// Both element values should be summed up, and according to the <see cref="InputOperator.Add"/> the inputOperand should be added on top.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void InputOperatorAdd()
|
||||
{
|
||||
const int element1Value = 1;
|
||||
const int element2Value = 2;
|
||||
const int inputOperand = 10;
|
||||
var element1 = new SimpleElement { Value = element1Value };
|
||||
var element2 = new SimpleElement { Value = element2Value };
|
||||
var operandElement = new ConstantElement(inputOperand);
|
||||
var relationshipElement = new AttributeRelationshipElement(new[] { element1, element2 }, operandElement, InputOperator.Add);
|
||||
Assert.That(relationshipElement.Value, Is.EqualTo(element1Value + element2Value + inputOperand));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a relationship between two elements with the <see cref="InputOperator.Multiply"/> is handled correctly.
|
||||
/// Both element values should be summed up, and according to the <see cref="InputOperator.Multiply"/> the sum should be multiplied with the inputOperand.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void InputOperatorMultiply()
|
||||
{
|
||||
const int element1Value = 1;
|
||||
const int element2Value = 2;
|
||||
const int inputOperand = 10;
|
||||
var element1 = new SimpleElement { Value = element1Value };
|
||||
var element2 = new SimpleElement { Value = element2Value };
|
||||
var operandElement = new ConstantElement(inputOperand);
|
||||
var relationshipElement = new AttributeRelationshipElement(new[] { element1, element2 }, operandElement, InputOperator.Multiply);
|
||||
Assert.That(relationshipElement.Value, Is.EqualTo((element1Value + element2Value) * inputOperand));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a relationship between two elements with the <see cref="InputOperator.Exponentiate"/> is handled correctly.
|
||||
/// Both element values should be summed up, and according to the <see cref="InputOperator.Exponentiate"/> the sum should be raised by the power of inputOperand.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void InputOperatorPower()
|
||||
{
|
||||
const int element1Value = 1;
|
||||
const int element2Value = 2;
|
||||
const int inputOperand = 10;
|
||||
var element1 = new SimpleElement { Value = element1Value };
|
||||
var element2 = new SimpleElement { Value = element2Value };
|
||||
var operandElement = new ConstantElement(inputOperand);
|
||||
var relationshipElement = new AttributeRelationshipElement(new[] { element1, element2 }, operandElement, InputOperator.Exponentiate);
|
||||
Assert.That(relationshipElement.Value, Is.EqualTo(Math.Pow(element1Value + element2Value, inputOperand)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a relationship between two elements with the <see cref="InputOperator.ExponentiateByAttribute"/> is handled correctly.
|
||||
/// Both element values should be summed up, and according to the <see cref="InputOperator.ExponentiateByAttribute"/> the input operand should be raised by the power of the sum.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void InputOperatorPowerByAttribute()
|
||||
{
|
||||
const int element1Value = 1;
|
||||
const int element2Value = 2;
|
||||
const int inputOperand = 10;
|
||||
var element1 = new SimpleElement { Value = element1Value };
|
||||
var element2 = new SimpleElement { Value = element2Value };
|
||||
var operandElement = new ConstantElement(inputOperand);
|
||||
var relationshipElement = new AttributeRelationshipElement(new[] { element1, element2 }, operandElement, InputOperator.ExponentiateByAttribute);
|
||||
Assert.That(relationshipElement.Value, Is.EqualTo(Math.Pow(inputOperand, element1Value + element2Value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="AttributeRelationshipElement.Value"/> is updated correctly when one of the elements value changed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangesWhenElementChanges()
|
||||
{
|
||||
const int element1Value = 1;
|
||||
const int element2Value = 2;
|
||||
const int inputOperand = 10;
|
||||
var element1 = new SimpleElement { Value = element1Value };
|
||||
var element2 = new SimpleElement { Value = element2Value };
|
||||
var operandElement = new ConstantElement(inputOperand);
|
||||
var relationshipElement = new AttributeRelationshipElement(new[] { element1, element2 }, operandElement, InputOperator.Add);
|
||||
Assert.That(relationshipElement.Value, Is.EqualTo(element1Value + element2Value + inputOperand));
|
||||
const int element1NewValue = 2;
|
||||
element1.Value = element1NewValue;
|
||||
Assert.That(relationshipElement.Value, Is.EqualTo(element1NewValue + element2Value + inputOperand));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="SimpleElement.ValueChanged"/> is called when one of the elements value changed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangedEventWhenElementChanges()
|
||||
{
|
||||
const int element1Value = 1;
|
||||
const int element2Value = 2;
|
||||
const int inputOperand = 10;
|
||||
var element1 = new SimpleElement { Value = element1Value };
|
||||
var element2 = new SimpleElement { Value = element2Value };
|
||||
var operandElement = new ConstantElement(inputOperand);
|
||||
var relationshipElement = new AttributeRelationshipElement(new[] { element1, element2 }, operandElement, InputOperator.Add);
|
||||
var eventCalled = false;
|
||||
relationshipElement.ValueChanged += (sender, e) => eventCalled = true;
|
||||
element1.Value = 2;
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// <copyright file="AttributeSystemTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="AttributeSystem"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class AttributeSystemTests
|
||||
{
|
||||
private readonly AttributeDefinition _attributeA = new(Guid.NewGuid(), "A", "The A attribute");
|
||||
private readonly AttributeDefinition _attributeB = new(Guid.NewGuid(), "B", "The B attribute");
|
||||
private readonly AttributeDefinition _attributeC = new(Guid.NewGuid(), "C", "The C attribute");
|
||||
private readonly AttributeDefinition _attributeD = new(Guid.NewGuid(), "D", "The D attribute");
|
||||
private readonly AttributeDefinition _attributeAplusB = new(Guid.NewGuid(), "A+B", "The A+B attribute");
|
||||
private readonly AttributeDefinition _attributeAtimesB = new(Guid.NewGuid(), "A*B", "The A*B attribute");
|
||||
private readonly AttributeDefinition _attributeAchained = new(Guid.NewGuid(), "A'", "The chained A attribute");
|
||||
|
||||
private List<IAttribute> _statAttributes = null!;
|
||||
private List<IAttribute> _baseAttributes = null!;
|
||||
private List<AttributeRelationship> _relationShips = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Setups each test case.
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
this._statAttributes = new List<IAttribute>();
|
||||
this._baseAttributes = new List<IAttribute>();
|
||||
this._relationShips = new List<AttributeRelationship>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a <see cref="StatAttribute"/> is added correctly and it's value is returned.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void StatAttributeValue()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeA, attributeAValue));
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[this._attributeA];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a attribute is added correctly and it's value is returned.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void BaseAttributeValue()
|
||||
{
|
||||
const int attributeAValue = 9999;
|
||||
this._baseAttributes.Add(new ConstValueAttribute(attributeAValue, this._attributeA));
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[this._attributeA];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a <see cref="AttributeRelationship"/> is added correctly and the combined (in this case a chained) attribute does return the right value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChainedAttribute()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeA, attributeAValue));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAchained, 1, this._attributeA));
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[this._attributeAchained];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a <see cref="AttributeRelationship"/> is added correctly and the multiplied attribute does return the right multiplied value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MultipliedAttribute()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
const int multiplier = 3;
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeA, attributeAValue));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAchained, multiplier, this._attributeA));
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[this._attributeAchained];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue * multiplier));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a combination of <see cref="AttributeRelationship"/>s is added and handled correctly.
|
||||
/// Two attributes are combined to one target attribute.
|
||||
/// It tests if the resulting value is calculated correctly.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CombinedRelationship()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
const int attributeBValue = 4938;
|
||||
const int attributeBMultiplier = 2;
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeA, attributeAValue));
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeB, attributeBValue));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAplusB, 1, this._attributeA));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAplusB, attributeBMultiplier, this._attributeB));
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[this._attributeAplusB];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue + (attributeBValue * attributeBMultiplier)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a combination of <see cref="AttributeRelationship"/>s is added and handled correctly.
|
||||
/// Two attributes are combined to one target attribute.
|
||||
/// It tests if the resulting value is calculated correctly after one of the depending attributes changed their value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CombinedRelationshipChangedValue()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
const int attributeBValue = 4938;
|
||||
const int attributeBMultiplier = 2;
|
||||
var statAttributeA = new StatAttribute(this._attributeA, attributeAValue);
|
||||
this._statAttributes.Add(statAttributeA);
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeB, attributeBValue));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAplusB, 1, this._attributeA));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAplusB, attributeBMultiplier, this._attributeB));
|
||||
var system = this.CreateAttributeSystem();
|
||||
const int attributeAnewValue = 1000;
|
||||
statAttributeA.Value = attributeAnewValue;
|
||||
var value = system[this._attributeAplusB];
|
||||
Assert.That(value, Is.EqualTo(attributeAnewValue + (attributeBValue * attributeBMultiplier)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if adding additional elements results in a updated correct value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CombinedRelationshipAddedElement()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
const int attributeBValue = 4938;
|
||||
const int attributeBMultiplier = 2;
|
||||
var statAttributeA = new StatAttribute(this._attributeA, attributeAValue);
|
||||
this._statAttributes.Add(statAttributeA);
|
||||
this._statAttributes.Add(new StatAttribute(this._attributeB, attributeBValue));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAplusB, 1, this._attributeA));
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAplusB, attributeBMultiplier, this._attributeB));
|
||||
var system = this.CreateAttributeSystem();
|
||||
const int addedAttributeValue = 10;
|
||||
system.AddElement(new ConstValueAttribute(addedAttributeValue, this._attributeAplusB), this._attributeAplusB);
|
||||
var value = system[this._attributeAplusB];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue + (attributeBValue * attributeBMultiplier) + addedAttributeValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if using another attribute as operand results in a correct value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MultipliedAttributes()
|
||||
{
|
||||
const int attributeAValue = 1234;
|
||||
const int attributeBValue = 2;
|
||||
|
||||
var statAttributeA = new StatAttribute(this._attributeA, attributeAValue);
|
||||
var statAttributeB = new StatAttribute(this._attributeB, attributeBValue);
|
||||
this._statAttributes.Add(statAttributeA);
|
||||
this._statAttributes.Add(statAttributeB);
|
||||
|
||||
this._relationShips.Add(new AttributeRelationship(this._attributeAtimesB, this._attributeB, this._attributeA));
|
||||
var system = this.CreateAttributeSystem();
|
||||
|
||||
var value = system[this._attributeAtimesB];
|
||||
Assert.That(value, Is.EqualTo(attributeAValue * attributeBValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if using another attribute as operand results in a correct value.
|
||||
/// </summary>
|
||||
[TestCase(true, 2)]
|
||||
[TestCase(false, 0)]
|
||||
public void ConditionalAttributes(bool conditionMet, float expected)
|
||||
{
|
||||
const int bonusValue = 2;
|
||||
var targetAttribute = this._attributeAplusB;
|
||||
var bonusIfConditionMet = new StatAttribute(this._attributeB, bonusValue);
|
||||
|
||||
var conditionalAttribute = new StatAttribute(this._attributeC, conditionMet ? 1 : 0);
|
||||
|
||||
this._statAttributes.Add(bonusIfConditionMet);
|
||||
this._statAttributes.Add(conditionalAttribute);
|
||||
|
||||
this._relationShips.Add(new AttributeRelationship(targetAttribute, conditionalAttribute.Definition, bonusIfConditionMet.Definition));
|
||||
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[targetAttribute];
|
||||
Assert.That(value, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the use case of multiplying a base value with a conditional bonus multiplier.
|
||||
/// This is how Stats.DefenseIncreaseWithEquippedShield is intended to work.
|
||||
/// </summary>
|
||||
[TestCase(true, 105)]
|
||||
[TestCase(false, 100)]
|
||||
public void ConditionalAttributes_Multiply(bool conditionMet, float expected)
|
||||
{
|
||||
const float bonusMultiplier = 0.05f;
|
||||
const float baseValue = 100f;
|
||||
var targetAttribute = this._attributeAplusB;
|
||||
var baseAttribute = new StatAttribute(this._attributeA, baseValue);
|
||||
var bonusIfConditionMet = new StatAttribute(this._attributeB, bonusMultiplier);
|
||||
var conditionalAttribute = new StatAttribute(this._attributeC, conditionMet ? 1 : 0);
|
||||
|
||||
var tempAttribute = this._attributeD;
|
||||
|
||||
this._statAttributes.Add(baseAttribute);
|
||||
this._statAttributes.Add(bonusIfConditionMet);
|
||||
this._statAttributes.Add(conditionalAttribute);
|
||||
|
||||
// First, we copy our base value to the target
|
||||
this._relationShips.Add(new AttributeRelationship(targetAttribute, 1, baseAttribute.Definition));
|
||||
|
||||
// Then, we calculate the bonus into a temporary attribute, depending on the condition
|
||||
this._relationShips.Add(new AttributeRelationship(tempAttribute, conditionalAttribute.Definition, bonusIfConditionMet.Definition));
|
||||
|
||||
// Finally, we apply the temporary attribute as multiplier for the base value and add it to the target
|
||||
this._relationShips.Add(new AttributeRelationship(targetAttribute, tempAttribute, baseAttribute.Definition));
|
||||
|
||||
var system = this.CreateAttributeSystem();
|
||||
var value = system[targetAttribute];
|
||||
Assert.That(value, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a changed maximum value of an attribute is considered when requesting it from the AttributeSystem.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MaximumValueIsRespectedFromStoredDefinition()
|
||||
{
|
||||
// Create an attribute definition with a maximum value
|
||||
var attackSpeedDefinition = new AttributeDefinition(Guid.NewGuid(), "AttackSpeed", "Attack speed attribute")
|
||||
{
|
||||
MaximumValue = 300,
|
||||
};
|
||||
|
||||
// Add a stat attribute with a very high value (exceeding the maximum)
|
||||
const int highValue = 5000;
|
||||
this._statAttributes.Add(new StatAttribute(attackSpeedDefinition, highValue));
|
||||
|
||||
var system = this.CreateAttributeSystem();
|
||||
|
||||
// Create a different instance of the attribute definition (simulating what happens when
|
||||
// the definition is retrieved from a different source, like a database)
|
||||
var differentInstanceOfDefinition = new AttributeDefinition(attackSpeedDefinition.Id, "AttackSpeed", "Attack speed attribute");
|
||||
|
||||
// The system should return the maximum value from the stored definition, not exceed it
|
||||
var value = system[differentInstanceOfDefinition];
|
||||
Assert.That(value, Is.EqualTo(300));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a changed maximum value is respected for a ComposableAttribute when requesting it from the AttributeSystem.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MaximumValueIsRespectedFromStoredDefinitionForComposableAttribute()
|
||||
{
|
||||
// Create an attribute definition with a maximum value
|
||||
var attackSpeedDefinition = new AttributeDefinition(Guid.NewGuid(), "AttackSpeed", "Attack speed attribute")
|
||||
{
|
||||
MaximumValue = 300,
|
||||
};
|
||||
|
||||
// Add a base attribute which will create a ComposableAttribute
|
||||
const int highValue = 5000;
|
||||
this._baseAttributes.Add(new ConstValueAttribute(highValue, attackSpeedDefinition));
|
||||
|
||||
var system = this.CreateAttributeSystem();
|
||||
|
||||
// Create a different instance of the attribute definition
|
||||
var differentInstanceOfDefinition = new AttributeDefinition(attackSpeedDefinition.Id, "AttackSpeed", "Attack speed attribute");
|
||||
|
||||
// The system should return the maximum value from the stored definition, not exceed it
|
||||
var value = system[differentInstanceOfDefinition];
|
||||
Assert.That(value, Is.EqualTo(300));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the attribute system for testing, initialized with <see cref="_statAttributes"/>, <see cref="_baseAttributes"/> and <see cref="_relationShips"/>.
|
||||
/// </summary>
|
||||
/// <returns>The created attribute system, initialized with <see cref="_statAttributes"/>, <see cref="_baseAttributes"/> and <see cref="_relationShips"/>.</returns>
|
||||
private AttributeSystem CreateAttributeSystem()
|
||||
{
|
||||
return new(this._statAttributes, this._baseAttributes, this._relationShips);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// <copyright file="ComposableAttributeTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="ComposableAttribute"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ComposableAttributeTests
|
||||
{
|
||||
private ComposableAttribute _composableAttribute = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up each test case.
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var attributeDefinition = new AttributeDefinition(new Guid("52263EA9-F309-475D-B10B-352D3BFD7650"), "Test attribute", "Test attribute");
|
||||
this._composableAttribute = new ComposableAttribute(attributeDefinition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value is 0 after creation.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueIsNullAfterCreation()
|
||||
{
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value is updated after adding an element.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueAfterAddedElement()
|
||||
{
|
||||
var element = new ConstantElement(4711);
|
||||
this._composableAttribute.AddElement(element);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(element.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value of multiple elements is combined in <see cref="ComposableAttribute.Value"/> by using <see cref="AggregateType.AddRaw"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueOfMultipleRawElements()
|
||||
{
|
||||
var element1 = new ConstantElement(3000);
|
||||
var element2 = new ConstantElement(5000);
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(element1.Value + element2.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value of multiple elements is combined in <see cref="ComposableAttribute.Value"/>
|
||||
/// by using <see cref="AggregateType.AddRaw"/> in the first element and
|
||||
/// by using <see cref="AggregateType.Multiplicate"/> in the second element.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueWithRawAndMultiplierElements()
|
||||
{
|
||||
var element1 = new ConstantElement(3000);
|
||||
var element2 = new SimpleElement { Value = 5, AggregateType = AggregateType.Multiplicate };
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(element1.Value * element2.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value of multiple elements is combined in <see cref="ComposableAttribute.Value"/>
|
||||
/// by using <see cref="AggregateType.AddRaw"/> in the first element,
|
||||
/// by using <see cref="AggregateType.Multiplicate"/> in the second element and
|
||||
/// by using <see cref="AggregateType.AddFinal"/> in the last element.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueWithRawMultiplierAndFinalElements()
|
||||
{
|
||||
var element1 = new ConstantElement(3000);
|
||||
var element2 = new SimpleElement { Value = 5, AggregateType = AggregateType.Multiplicate };
|
||||
var element3 = new SimpleElement { Value = 1000, AggregateType = AggregateType.AddFinal };
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
this._composableAttribute.AddElement(element3);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo((element1.Value * element2.Value) + element3.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value of multiple elements is combined in <see cref="ComposableAttribute.Value"/>
|
||||
/// by using one <see cref="AggregateType.AddRaw"/> element and
|
||||
/// by using several <see cref="AggregateType.Maximum"/> elements.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueWithRawAndMultipleMaximumElements()
|
||||
{
|
||||
var element1 = new ConstantElement(3000);
|
||||
var element2 = new SimpleElement { Value = 5, AggregateType = AggregateType.Maximum };
|
||||
var element3 = new SimpleElement { Value = 1000, AggregateType = AggregateType.Maximum };
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
this._composableAttribute.AddElement(element3);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(element1.Value + Math.Max(element2.Value, element3.Value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value of multiple elements is combined in <see cref="ComposableAttribute.Value"/>
|
||||
/// by using <see cref="AggregateType.Multiplicate"/> elements exclusively.
|
||||
/// A <see cref="AggregateType.AddRaw"/> element of value 1 should be assumed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueWithMultiplierElementsOnly()
|
||||
{
|
||||
var element1 = new SimpleElement { Value = 5, AggregateType = AggregateType.Multiplicate };
|
||||
var element2 = new SimpleElement { Value = 1000, AggregateType = AggregateType.Multiplicate };
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(element1.Value * element2.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value of multiple elements is combined in <see cref="ComposableAttribute.Value"/>
|
||||
/// by using <see cref="AggregateType.Multiplicate"/> in the first element and
|
||||
/// by using <see cref="AggregateType.AddFinal"/> in the second element.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueWithMultiplierAndFinalElements()
|
||||
{
|
||||
var element1 = new SimpleElement { Value = 5, AggregateType = AggregateType.Multiplicate };
|
||||
var element2 = new SimpleElement { Value = 1000, AggregateType = AggregateType.AddFinal };
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(1000));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the updated correctly after an element got removed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueCorrectAfterElementRemoved()
|
||||
{
|
||||
var element1 = new ConstantElement(3000);
|
||||
var element2 = new SimpleElement { Value = 5, AggregateType = AggregateType.Multiplicate };
|
||||
var element3 = new SimpleElement { Value = 1000, AggregateType = AggregateType.AddFinal };
|
||||
this._composableAttribute.AddElement(element1);
|
||||
this._composableAttribute.AddElement(element2);
|
||||
this._composableAttribute.AddElement(element3);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo((element1.Value * element2.Value) + element3.Value));
|
||||
this._composableAttribute.RemoveElement(element2);
|
||||
Assert.That(this._composableAttribute.Value, Is.EqualTo(element1.Value + element3.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="BaseAttribute.ValueChanged"/> is called when the depending element value changed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangedEvent()
|
||||
{
|
||||
var element = new SimpleElement { Value = 5 };
|
||||
this._composableAttribute.AddElement(element);
|
||||
|
||||
var eventCalled = false;
|
||||
this._composableAttribute.ValueChanged += (_, _) => eventCalled = true;
|
||||
element.Value = 6;
|
||||
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="BaseAttribute.ValueChanged"/> is called when a new depending element was added.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangedEventWhenElementAdded()
|
||||
{
|
||||
var element = new SimpleElement { Value = 5 };
|
||||
bool eventCalled = false;
|
||||
this._composableAttribute.ValueChanged += (_, _) => eventCalled = true;
|
||||
this._composableAttribute.AddElement(element);
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="BaseAttribute.ValueChanged"/> is called when a new depending element was removed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangedEventWhenElementRemoved()
|
||||
{
|
||||
var element = new SimpleElement { Value = 5 };
|
||||
this._composableAttribute.AddElement(element);
|
||||
|
||||
bool eventCalled = false;
|
||||
this._composableAttribute.ValueChanged += (_, _) => eventCalled = true;
|
||||
this._composableAttribute.RemoveElement(element);
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// <copyright file="ConstValueAttributeTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="ConstValueAttribute"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ConstValueAttributeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if the value of the attribute is as defined in the constructor.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueAsDefined()
|
||||
{
|
||||
const int constantValue = 999;
|
||||
var element = new ConstValueAttribute(constantValue, new AttributeDefinition());
|
||||
Assert.That(element.Value, Is.EqualTo(constantValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="ConstValueAttribute.AggregateType"/> is <see cref="AggregateType.AddRaw"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AggregateTypeIsRaw()
|
||||
{
|
||||
var element = new ConstValueAttribute(999, new AttributeDefinition());
|
||||
Assert.That(element.AggregateType, Is.EqualTo(AggregateType.AddRaw));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <copyright file="ConstantElementTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="ConstantElement"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ConstantElementTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if the value of the element is as defined in the constructor.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueAsDefined()
|
||||
{
|
||||
const int constantValue = 999;
|
||||
var element = new ConstantElement(constantValue);
|
||||
Assert.That(element.Value, Is.EqualTo(constantValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="ConstantElement.AggregateType"/> is <see cref="AggregateType.AddRaw"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AggregateTypeIsRaw()
|
||||
{
|
||||
const int constantValue = 999;
|
||||
var element = new ConstantElement(constantValue);
|
||||
Assert.That(element.AggregateType, Is.EqualTo(AggregateType.AddRaw));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<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.AttributeSystem.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.AttributeSystem.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="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\AttributeSystem\MUnique.OpenMU.AttributeSystem.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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.AttributeSystem.Test")]
|
||||
@@ -0,0 +1,61 @@
|
||||
// <copyright file="SimpleElementTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AttributeSystem.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="SimpleElement"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class SimpleElementTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if the value is 0 after creation.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueIsNullAfterCreation()
|
||||
{
|
||||
var element = new SimpleElement();
|
||||
Assert.That(element.Value, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the value returns the same as what has been set before.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueIsSet()
|
||||
{
|
||||
const int elementValue = 4711;
|
||||
var element = new SimpleElement { Value = elementValue };
|
||||
Assert.That(element.Value, Is.EqualTo(elementValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="SimpleElement.ValueChanged"/> is called when the <see cref="SimpleElement.Value"/> has been changed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangedEventWhenValueChanges()
|
||||
{
|
||||
var element = new SimpleElement();
|
||||
bool eventCalled = false;
|
||||
element.ValueChanged += (sender, e) => eventCalled = true;
|
||||
element.Value = 6;
|
||||
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the <see cref="SimpleElement.ValueChanged"/> is called when the <see cref="SimpleElement.AggregateType"/> has been changed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ValueChangedEventWhenAggregateTypeChanges()
|
||||
{
|
||||
var element = new SimpleElement { Value = 0 };
|
||||
bool eventCalled = false;
|
||||
element.ValueChanged += (sender, e) => eventCalled = true;
|
||||
element.AggregateType = AggregateType.AddFinal;
|
||||
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
}
|
||||
302
tests/MUnique.OpenMU.ChatServer.Tests/ChatClientTests.cs
Normal file
302
tests/MUnique.OpenMU.ChatServer.Tests/ChatClientTests.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
// <copyright file="ChatClientTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.Tests;
|
||||
|
||||
using System.Buffers;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="ChatClient"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ChatClientTests
|
||||
{
|
||||
private const string ChatServerHost = "";
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the client joined the room when the authentication was successful.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task AuthenticationSuccessAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe = new DuplexPipe();
|
||||
var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var client = new ChatClient(connection, manager, new NullLogger<ChatClient>());
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
|
||||
var authenticationPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
Assert.That(room.ConnectedClients, Contains.Item(client));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the authentication fails by providing the wrong token.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task AuthenticationFailedByWrongTokenAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe = new DuplexPipe();
|
||||
var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var client = new ChatClient(connection, manager, new NullLogger<ChatClient>());
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450674"));
|
||||
var authenticationPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
Assert.That(room.ConnectedClients.Contains(client), Is.False);
|
||||
Assert.That(connection.Connected, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests what happens if two clients try to authenticate with the same token. The first connection should be connected and authenticated, the second disconnected.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task AuthenticationFailedForSecondConnectionAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
var authenticationPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
var duplexPipe1 = new DuplexPipe();
|
||||
var connection1 = new Connection(duplexPipe1, null, null, new NullLogger<Connection>());
|
||||
var client1 = new ChatClient(connection1, manager, new NullLogger<ChatClient>());
|
||||
await duplexPipe1.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
var duplexPipe2 = new DuplexPipe();
|
||||
var connection2 = new Connection(duplexPipe2, null, null, new NullLogger<Connection>());
|
||||
var client2 = new ChatClient(connection2, manager, new NullLogger<ChatClient>());
|
||||
var disconnectedRaised = false;
|
||||
client2.Disconnected += (_, _) => disconnectedRaised = true;
|
||||
|
||||
await duplexPipe2.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
Assert.That(room.ConnectedClients, Has.Count.EqualTo(1));
|
||||
Assert.That(room.ConnectedClients, Contains.Item(client1));
|
||||
Assert.That(connection2.Connected, Is.False);
|
||||
Assert.That(disconnectedRaised, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a successful authentication sets the <see cref="IChatClient.Nickname"/>.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task SetNicknameAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe = new DuplexPipe();
|
||||
var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var client = new ChatClient(connection, manager, new NullLogger<ChatClient>());
|
||||
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
var authenticationPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
Assert.That(client.Nickname, Is.EqualTo("Bob"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a successful authentication sets the <see cref="IChatClient.Index"/>.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task SetClientIndexAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe = new DuplexPipe();
|
||||
var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var client = new ChatClient(connection, manager, new NullLogger<ChatClient>());
|
||||
|
||||
var authInfo = new ChatServerAuthenticationInfo(3, roomId, "Bob", ChatServerHost, "128450673");
|
||||
room!.RegisterClient(authInfo);
|
||||
|
||||
var authenticationPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
Assert.That(client.Index, Is.EqualTo(authInfo.Index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a message sent to the client gets "encrypted" properly by the XOR-3 key.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task SentMessageEncryptedProperlyAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var duplexPipe = new DuplexPipe();
|
||||
var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var client = new ChatClient(connection, manager, new NullLogger<ChatClient>());
|
||||
var expectedPacket = new byte[] { 0xC1, 0x0B, 0x04, 0x01, 0x06, 0xBD, 0x8E, 0xEA, 0xBD, 0x8E, 0xEA };
|
||||
await client.SendMessageAsync(1, "AAAAAA").ConfigureAwait(false);
|
||||
var sendResult = await duplexPipe.SendPipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
var sentPacket = sendResult.Buffer.ToArray();
|
||||
Assert.That(sentPacket, Is.EqualTo(expectedPacket));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the room client list is sent property to the client. It should contain the joined client.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task RoomClientListSentAfterJoinAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe = new DuplexPipe();
|
||||
var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var client = new ChatClient(connection, manager, new NullLogger<ChatClient>());
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
|
||||
var authenticationPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket).ConfigureAwait(false);
|
||||
|
||||
var expectedPacket = new byte[] { 0xC2, 0x00, 0x13, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x42, 0x6F, 0x62, 0, 0, 0, 0, 0, 0, 0 };
|
||||
var readResult = await duplexPipe.SendPipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
var result = readResult.Buffer.ToArray();
|
||||
Assert.That(result, Is.EquivalentTo(expectedPacket));
|
||||
Assert.That(client.Nickname, Is.EqualTo("Bob"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the room client list is sent to the client which joins as second. It should contain both clients.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task RoomClientListSentForSecondClientAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe1 = new DuplexPipe();
|
||||
var connection1 = new Connection(duplexPipe1, null, null, new NullLogger<Connection>());
|
||||
var client1 = new ChatClient(connection1, manager, new NullLogger<ChatClient>());
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
room.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Alice", ChatServerHost, "94371960"));
|
||||
|
||||
var authenticationPacket1 = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe1.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket1).ConfigureAwait(false);
|
||||
|
||||
var duplexPipe2 = new DuplexPipe();
|
||||
var connection2 = new Connection(duplexPipe2, null, null, new NullLogger<Connection>());
|
||||
var client2 = new ChatClient(connection2, manager, new NullLogger<ChatClient>());
|
||||
var authenticationPacket2 = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xC5, 0xFB, 0x98, 0xCB, 0xFE, 0x92, 0xCA, 0xFF, 0xAB, 0xFC };
|
||||
await duplexPipe2.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket2).ConfigureAwait(false);
|
||||
|
||||
var expectedPacket = new byte[]
|
||||
{
|
||||
0xC2, 0x00, 0x1E, 0x02, 0x00, 0x00, 0x02, 0x00,
|
||||
0x00, 0x42, 0x6F, 0x62, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x01, 0x41, 0x6C, 0x69, 0x63, 0x65, 0, 0, 0, 0, 0,
|
||||
};
|
||||
|
||||
await duplexPipe2.SendPipe.Writer.WaitForFlushAsync().ConfigureAwait(false);
|
||||
|
||||
var readResult = await duplexPipe2.SendPipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
var result = readResult.Buffer.ToArray();
|
||||
Assert.That(result, Is.EquivalentTo(expectedPacket));
|
||||
Assert.That(client1.Nickname, Is.EqualTo("Bob"));
|
||||
Assert.That(client2.Nickname, Is.EqualTo("Alice"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the first client gets notified correctly when the second client joins the chat room.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task ClientJoinedPacketSentAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var duplexPipe1 = new DuplexPipe();
|
||||
var connection1 = new Connection(duplexPipe1, null, null, new NullLogger<Connection>());
|
||||
var client1 = new ChatClient(connection1, manager, new NullLogger<ChatClient>());
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
room.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Alice", ChatServerHost, "94371960"));
|
||||
|
||||
var authenticationPacket1 = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await duplexPipe1.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket1).ConfigureAwait(false);
|
||||
|
||||
var duplexPipe2 = new DuplexPipe();
|
||||
var connection2 = new Connection(duplexPipe2, null, null, new NullLogger<Connection>());
|
||||
var client2 = new ChatClient(connection2, manager, new NullLogger<ChatClient>());
|
||||
var authenticationPacket2 = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xC5, 0xFB, 0x98, 0xCB, 0xFE, 0x92, 0xCA, 0xFF, 0xAB, 0xFC };
|
||||
await duplexPipe2.ReceivePipe.Writer.WriteAndWaitForFlushAsync(authenticationPacket2).ConfigureAwait(false);
|
||||
|
||||
var expectedPacket = new byte[]
|
||||
{
|
||||
0xC1, 0x0F, 0x01, 0x00, 0x01, 0x41, 0x6C, 0x69, 0x63, 0x65, 0, 0, 0, 0, 0,
|
||||
}.AsString();
|
||||
|
||||
var readResult = await duplexPipe1.SendPipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
var received = readResult.Buffer.ToArray().AsString();
|
||||
|
||||
Assert.That(received, Contains.Substring(expectedPacket));
|
||||
Assert.That(client1.Nickname, Is.EqualTo("Bob"));
|
||||
Assert.That(client2.Nickname, Is.EqualTo("Alice"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a call to <see cref="ChatClient.LogOffAsync" /> removes it from the chatroom,
|
||||
/// and the other remaining client gets notified about it.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task ClientLoggedOffAsync()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
var bobsPipe = new DuplexPipe();
|
||||
var bobsClient = new ChatClient(new Connection(bobsPipe, null, null, new NullLogger<Connection>()), manager, new NullLogger<ChatClient>());
|
||||
room!.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Bob", ChatServerHost, "128450673"));
|
||||
room.RegisterClient(new ChatServerAuthenticationInfo(room.GetNextClientIndex(), roomId, "Alice", ChatServerHost, "94371960"));
|
||||
|
||||
var bobsAuthPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xCD, 0xFD, 0x93, 0xC8, 0xFA, 0x9B, 0xCA, 0xF8, 0x98, 0xFC };
|
||||
await bobsPipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(bobsAuthPacket).ConfigureAwait(false);
|
||||
await bobsPipe.ReceivePipe.Writer.WaitForFlushAsync().ConfigureAwait(false);
|
||||
|
||||
var alicePipe = new DuplexPipe();
|
||||
var aliceConnection = new Connection(alicePipe, null, null, new NullLogger<Connection>());
|
||||
var aliceClient = new ChatClient(aliceConnection, manager, new NullLogger<ChatClient>());
|
||||
var aliceAuthPacket = new byte[] { 0xC1, 0x10, 0x00, 0x00, (byte)roomId, (byte)(roomId >> 8), 0xC5, 0xFB, 0x98, 0xCB, 0xFE, 0x92, 0xCA, 0xFF, 0xAB, 0xFC };
|
||||
await alicePipe.ReceivePipe.Writer.WriteAndWaitForFlushAsync(aliceAuthPacket).ConfigureAwait(false);
|
||||
|
||||
await bobsClient.LogOffAsync().ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
|
||||
var expectedPacket = new byte[]
|
||||
{
|
||||
0xC1, 0x0F, 0x01, 0x01, 0x00, 0x42, 0x6F, 0x62, 0, 0, 0, 0, 0, 0, 0,
|
||||
}.AsString();
|
||||
|
||||
await alicePipe.SendPipe.Writer.WaitForFlushAsync().ConfigureAwait(false);
|
||||
|
||||
var readResult = await alicePipe.SendPipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
var packets = readResult.Buffer.ToArray().AsString();
|
||||
Assert.That(packets, Contains.Substring(expectedPacket));
|
||||
Assert.That(room.ConnectedClients, Has.Count.EqualTo(1));
|
||||
Assert.That(room.ConnectedClients, Contains.Item(aliceClient));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// <copyright file="ChatRoomManagerTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="ChatRoomManager"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ChatRoomManagerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if the returned roomId by <see cref="ChatRoomManager.CreateChatRoom"/> actually returns a roomId with which the room can be retrieved by <see cref="ChatRoomManager.GetChatRoom"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RoomCreation()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId = manager.CreateChatRoom();
|
||||
var room = manager.GetChatRoom(roomId);
|
||||
Assert.That(room, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if recurring calls to <see cref="ChatRoomManager.CreateChatRoom"/> return different room ids.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RoomCreationUniqueIds()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var roomId1 = manager.CreateChatRoom();
|
||||
var roomId2 = manager.CreateChatRoom();
|
||||
|
||||
Assert.That(roomId1, Is.Not.EqualTo(roomId2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a call to <see cref="ChatRoomManager.GetChatRoom"/> with a random room id does not return a room.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetChatRoomNull()
|
||||
{
|
||||
var manager = new ChatRoomManager(new NullLoggerFactory());
|
||||
var room = manager.GetChatRoom(9999);
|
||||
Assert.That(room, Is.Null);
|
||||
}
|
||||
}
|
||||
270
tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs
Normal file
270
tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
// <copyright file="ChatRoomTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using MUnique.OpenMU.ChatServer;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="ChatRoom"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ChatRoomTests
|
||||
{
|
||||
private const string ChatServerHost = "";
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a new room returns the specified room id, which was given to the constructor.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RoomId()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
Assert.That(room.RoomId, Is.EqualTo(roomId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to register a client with a different room id. This should fail with an <see cref="ArgumentException"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterClientWithDifferentRoomId()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId - 1, "Bob", ChatServerHost, "123456789");
|
||||
Assert.Throws<ArgumentException>(() => room.RegisterClient(authenticationInfo));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to register a client with a correct room id.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterClientWithCorrectRoomId()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
Assert.DoesNotThrow(() => room.RegisterClient(authenticationInfo));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to join a null client. This should fail with an <see cref="ArgumentNullException"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask TryJoinNullClientAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo);
|
||||
Assert.That(async () => await room.TryJoinAsync(null!), Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to join the room with an unauthenticated client. This should fail.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask TryJoinWithUnauthenticatedClientAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo);
|
||||
var chatClient = new Mock<IChatClient>();
|
||||
Assert.That(await room.TryJoinAsync(chatClient.Object).ConfigureAwait(false), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to join the room with a client with a wrong authentication token. This should fail.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask TryJoinWithAuthenticatedClientButWrongTokenAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo);
|
||||
var chatClient = new Mock<IChatClient>();
|
||||
chatClient.Setup(c => c.AuthenticationToken).Returns("987654321");
|
||||
Assert.That(await room.TryJoinAsync(chatClient.Object).ConfigureAwait(false), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to join the room with an authenticated client. This should be successful.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask TryJoinWithAuthenticatedClientAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo);
|
||||
var chatClient = new Mock<IChatClient>();
|
||||
chatClient.Setup(c => c.AuthenticationToken).Returns(authenticationInfo.AuthenticationToken);
|
||||
Assert.That(await room.TryJoinAsync(chatClient.Object).ConfigureAwait(false), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="IChatClient.SendChatRoomClientListAsync"/> is called after a client successfully joined a room.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask ChatRoomClientListSentAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo);
|
||||
var chatClient = new Mock<IChatClient>();
|
||||
chatClient.Setup(c => c.AuthenticationToken).Returns(authenticationInfo.AuthenticationToken);
|
||||
await room.TryJoinAsync(chatClient.Object).ConfigureAwait(false);
|
||||
chatClient.Verify(c => c.SendChatRoomClientListAsync(room.ConnectedClients), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="ChatRoom.ConnectedClients"/> returns the successfully authenticated and joined client.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask ConnectedClientsAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo);
|
||||
var chatClient = new Mock<IChatClient>();
|
||||
chatClient.Setup(c => c.AuthenticationToken).Returns(authenticationInfo.AuthenticationToken);
|
||||
await room.TryJoinAsync(chatClient.Object).ConfigureAwait(false);
|
||||
Assert.That(room.ConnectedClients, Has.Count.EqualTo(1));
|
||||
Assert.That(room.ConnectedClients, Contains.Item(chatClient.Object));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="IChatClient.SendChatRoomClientUpdateAsync"/> is called as soon as another client joined the room.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask SendJoinedMessageAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId0 = room.GetNextClientIndex();
|
||||
var clientId1 = room.GetNextClientIndex();
|
||||
var authenticationInfo0 = new ChatServerAuthenticationInfo(clientId0, roomId, "Alice", ChatServerHost, "99999");
|
||||
var authenticationInfo1 = new ChatServerAuthenticationInfo(clientId1, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo0);
|
||||
room.RegisterClient(authenticationInfo1);
|
||||
|
||||
var chatClient0 = new Mock<IChatClient>();
|
||||
chatClient0.SetupAllProperties();
|
||||
chatClient0.Setup(c => c.AuthenticationToken).Returns(authenticationInfo0.AuthenticationToken);
|
||||
var chatClient1 = new Mock<IChatClient>();
|
||||
chatClient1.SetupAllProperties();
|
||||
chatClient1.Setup(c => c.AuthenticationToken).Returns(authenticationInfo1.AuthenticationToken);
|
||||
|
||||
await room.TryJoinAsync(chatClient0.Object).ConfigureAwait(false);
|
||||
await room.TryJoinAsync(chatClient1.Object).ConfigureAwait(false);
|
||||
|
||||
chatClient0.Verify(c => c.SendChatRoomClientUpdateAsync(clientId1, authenticationInfo1.ClientName, ChatRoomClientUpdateType.Joined), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="IChatClient.SendChatRoomClientUpdateAsync"/> is called as soon as another client left the room.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask SendLeftMessageAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId0 = room.GetNextClientIndex();
|
||||
var clientId1 = room.GetNextClientIndex();
|
||||
var authenticationInfo0 = new ChatServerAuthenticationInfo(clientId0, roomId, "Alice", ChatServerHost, "99999");
|
||||
var authenticationInfo1 = new ChatServerAuthenticationInfo(clientId1, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo0);
|
||||
room.RegisterClient(authenticationInfo1);
|
||||
|
||||
var chatClient0 = new Mock<IChatClient>();
|
||||
chatClient0.SetupAllProperties();
|
||||
chatClient0.Setup(c => c.AuthenticationToken).Returns(authenticationInfo0.AuthenticationToken);
|
||||
|
||||
var chatClient1 = new Mock<IChatClient>();
|
||||
chatClient1.SetupAllProperties();
|
||||
chatClient1.Setup(c => c.AuthenticationToken).Returns(authenticationInfo1.AuthenticationToken);
|
||||
|
||||
await room.TryJoinAsync(chatClient0.Object).ConfigureAwait(false);
|
||||
await room.TryJoinAsync(chatClient1.Object).ConfigureAwait(false);
|
||||
|
||||
await room.LeaveAsync(chatClient0.Object).ConfigureAwait(false);
|
||||
chatClient1.Verify(c => c.SendChatRoomClientUpdateAsync(clientId0, authenticationInfo0.ClientName, ChatRoomClientUpdateType.Left), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="IChatClient.SendMessageAsync"/> is called as soon as a message is sent through the room.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask SendMessageAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
const string chatMessage = "foobar1234567890";
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId0 = room.GetNextClientIndex();
|
||||
var clientId1 = room.GetNextClientIndex();
|
||||
var authenticationInfo0 = new ChatServerAuthenticationInfo(clientId0, roomId, "Alice", ChatServerHost, "99999");
|
||||
var authenticationInfo1 = new ChatServerAuthenticationInfo(clientId1, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo0);
|
||||
room.RegisterClient(authenticationInfo1);
|
||||
|
||||
var chatClient0 = new Mock<IChatClient>();
|
||||
chatClient0.Setup(c => c.AuthenticationToken).Returns(authenticationInfo0.AuthenticationToken);
|
||||
|
||||
var chatClient1 = new Mock<IChatClient>();
|
||||
chatClient1.Setup(c => c.AuthenticationToken).Returns(authenticationInfo1.AuthenticationToken);
|
||||
|
||||
await room.TryJoinAsync(chatClient0.Object).ConfigureAwait(false);
|
||||
await room.TryJoinAsync(chatClient1.Object).ConfigureAwait(false);
|
||||
|
||||
await room.SendMessageAsync(clientId1, chatMessage).ConfigureAwait(false);
|
||||
chatClient0.Verify(c => c.SendMessageAsync(clientId1, chatMessage), Times.Once);
|
||||
chatClient1.Verify(c => c.SendMessageAsync(clientId1, chatMessage), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="ChatRoom.RoomClosed"/> is fired as soon as all connected clients left the room.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask RoomClosedEventAsync()
|
||||
{
|
||||
const ushort roomId = 4711;
|
||||
var room = new ChatRoom(roomId, new NullLogger<ChatRoom>());
|
||||
var clientId0 = room.GetNextClientIndex();
|
||||
var clientId1 = room.GetNextClientIndex();
|
||||
var authenticationInfo0 = new ChatServerAuthenticationInfo(clientId0, roomId, "Alice", ChatServerHost, "99999");
|
||||
var authenticationInfo1 = new ChatServerAuthenticationInfo(clientId1, roomId, "Bob", ChatServerHost, "123456789");
|
||||
room.RegisterClient(authenticationInfo0);
|
||||
room.RegisterClient(authenticationInfo1);
|
||||
|
||||
var chatClient0 = new Mock<IChatClient>();
|
||||
chatClient0.Setup(c => c.AuthenticationToken).Returns(authenticationInfo0.AuthenticationToken);
|
||||
|
||||
var chatClient1 = new Mock<IChatClient>();
|
||||
chatClient1.Setup(c => c.AuthenticationToken).Returns(authenticationInfo1.AuthenticationToken);
|
||||
|
||||
var eventCalled = false;
|
||||
room.RoomClosed += (sender, e) => eventCalled = true;
|
||||
await room.TryJoinAsync(chatClient0.Object).ConfigureAwait(false);
|
||||
await room.TryJoinAsync(chatClient1.Object).ConfigureAwait(false);
|
||||
await room.LeaveAsync(chatClient0.Object).ConfigureAwait(false);
|
||||
await room.LeaveAsync(chatClient1.Object).ConfigureAwait(false);
|
||||
Assert.That(eventCalled, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<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.ChatServer.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.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\ChatServer\MUnique.OpenMU.ChatServer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
38
tests/MUnique.OpenMU.ChatServer.Tests/PipeWriterExtension.cs
Normal file
38
tests/MUnique.OpenMU.ChatServer.Tests/PipeWriterExtension.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// <copyright file="PipeWriterExtension.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.Tests;
|
||||
|
||||
using System.IO.Pipelines;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="PipeWriter"/>, helpful for testing.
|
||||
/// </summary>
|
||||
internal static class PipeWriterExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Flushes and waits until the written bytes are flushed.
|
||||
/// </summary>
|
||||
/// <param name="pipeWriter">The pipe writer.</param>
|
||||
/// <param name="data">The data which should be written.</param>
|
||||
public static async Task WriteAndWaitForFlushAsync(this PipeWriter pipeWriter, ReadOnlyMemory<byte> data)
|
||||
{
|
||||
await pipeWriter.WriteAsync(data).ConfigureAwait(false);
|
||||
await pipeWriter.WaitForFlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits until all <see cref="PipeWriter.UnflushedBytes"/> are flushed.
|
||||
/// </summary>
|
||||
/// <param name="pipeWriter">The pipe writer.</param>
|
||||
public static async Task WaitForFlushAsync(this PipeWriter pipeWriter)
|
||||
{
|
||||
do
|
||||
{
|
||||
await Task.Delay(200).ConfigureAwait(false);
|
||||
}
|
||||
while (pipeWriter.UnflushedBytes > 0);
|
||||
await Task.Delay(200).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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.ChatServer.Tests")]
|
||||
@@ -0,0 +1,42 @@
|
||||
// <copyright file="DropGeneratorBenchmarks.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for the <see cref="DefaultDropGenerator"/>.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[ThreadingDiagnoser]
|
||||
[InvocationCount(100)]
|
||||
public class DropGeneratorBenchmarks
|
||||
{
|
||||
private DefaultDropGenerator _generator = null!;
|
||||
private MonsterDefinition _monster = null!;
|
||||
private Player _player = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Global setup for the benchmarks.
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public async Task Setup()
|
||||
{
|
||||
var config = GameConfigurationTestHelper.Create();
|
||||
var randomizer = RandomizerTestHelper.Create();
|
||||
_generator = new DefaultDropGenerator(config, randomizer);
|
||||
_monster = MonsterTestHelper.Create(10, 1);
|
||||
_player = await PlayerTestHelper.CreatePlayerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks the drop generation with a single player and monster.
|
||||
/// </summary>
|
||||
/// <returns>A value task.</returns>
|
||||
[Benchmark]
|
||||
public async ValueTask GenerateItemDropsAsync()
|
||||
=> await _generator.GenerateItemDropsAsync(_monster, 1000, _player);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// <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.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
using Moq;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="DropItemGroup"/>.
|
||||
/// </summary>
|
||||
public static class DropItemGroupExtensions
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// <copyright file="GameConfigurationTestHelper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
using Moq;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for creating test game configurations.
|
||||
/// </summary>
|
||||
public static class GameConfigurationTestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mock game configuration for drop generation testing.
|
||||
/// </summary>
|
||||
/// <returns>A mock game configuration.</returns>
|
||||
public static GameConfiguration Create()
|
||||
{
|
||||
var gameConfiguration = new Mock<GameConfiguration>();
|
||||
gameConfiguration.SetupAllProperties();
|
||||
gameConfiguration.Object.ExcellentItemDropLevelDelta = 50;
|
||||
gameConfiguration.Object.MaximumItemOptionLevelDrop = 3;
|
||||
|
||||
var items = CreateItems();
|
||||
gameConfiguration.Setup(c => c.Items).Returns(items);
|
||||
return gameConfiguration.Object;
|
||||
}
|
||||
|
||||
private static IList<ItemDefinition> CreateItems()
|
||||
{
|
||||
var items = new List<ItemDefinition>();
|
||||
var random = new Random(42);
|
||||
|
||||
for (byte dropLevel = 0; dropLevel <= 200; dropLevel++)
|
||||
{
|
||||
int itemsAtThisLevel = random.Next(5, 20);
|
||||
for (int i = 0; i < itemsAtThisLevel; i++)
|
||||
{
|
||||
var item = new Mock<ItemDefinition>();
|
||||
item.SetupAllProperties();
|
||||
item.Setup(d => d.PossibleItemSetGroups).Returns(new List<ItemSetGroup>());
|
||||
item.Setup(d => d.PossibleItemOptions).Returns(CreateItemOptions(dropLevel));
|
||||
item.Setup(d => d.Requirements).Returns(new List<AttributeRequirement>());
|
||||
item.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
|
||||
item.Setup(d => d.DropItems).Returns(new List<ItemDropItemGroup>());
|
||||
item.Setup(d => d.QualifiedCharacters).Returns(new List<CharacterClass>());
|
||||
|
||||
item.Object.DropsFromMonsters = true;
|
||||
item.Object.DropLevel = dropLevel;
|
||||
item.Object.Width = (byte)(i % 4 + 1);
|
||||
item.Object.Height = (byte)(i % 4 + 1);
|
||||
item.Object.MaximumItemLevel = 13;
|
||||
item.Object.MaximumSockets = dropLevel > 100 ? 5 : 0;
|
||||
item.Object.Group = (byte)(dropLevel % 16);
|
||||
item.Object.Number = (short)(i % 256);
|
||||
|
||||
items.Add(item.Object);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static IList<ItemOptionDefinition> CreateItemOptions(byte dropLevel)
|
||||
{
|
||||
var options = new List<ItemOptionDefinition>();
|
||||
|
||||
if (dropLevel > 30)
|
||||
{
|
||||
var excellentOption = new Mock<ItemOptionDefinition>();
|
||||
excellentOption.SetupAllProperties();
|
||||
excellentOption.Setup(o => o.PossibleOptions).Returns(CreateExcellentOptions());
|
||||
excellentOption.Object.AddsRandomly = true;
|
||||
excellentOption.Object.AddChance = 100;
|
||||
excellentOption.Object.MaximumOptionsPerItem = 6;
|
||||
options.Add(excellentOption.Object);
|
||||
}
|
||||
|
||||
if (dropLevel > 50)
|
||||
{
|
||||
var luckOption = new Mock<ItemOptionDefinition>();
|
||||
luckOption.SetupAllProperties();
|
||||
luckOption.Setup(o => o.PossibleOptions).Returns(new List<IncreasableItemOption>());
|
||||
luckOption.Object.AddsRandomly = true;
|
||||
luckOption.Object.AddChance = 100;
|
||||
luckOption.Object.MaximumOptionsPerItem = 1;
|
||||
options.Add(luckOption.Object);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static IList<IncreasableItemOption> CreateExcellentOptions()
|
||||
{
|
||||
var options = new List<IncreasableItemOption>();
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var option = new Mock<IncreasableItemOption>();
|
||||
option.SetupAllProperties();
|
||||
option.Setup(o => o.LevelDependentOptions).Returns(new List<ItemOptionOfLevel>());
|
||||
options.Add(option.Object);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@@ -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.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// <copyright file="MonsterTestHelper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
using Moq;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for creating test monsters.
|
||||
/// </summary>
|
||||
public static class MonsterTestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mock monster definition.
|
||||
/// </summary>
|
||||
/// <param name="numberOfDrops">The maximum number of item drops.</param>
|
||||
/// <param name="level">The monster level.</param>
|
||||
/// <returns>A mock monster definition.</returns>
|
||||
public static MonsterDefinition Create(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 });
|
||||
|
||||
monster.Object.DropItemGroups.AddBasicDropItemGroups();
|
||||
|
||||
return monster.Object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// <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.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
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.Attributes;
|
||||
|
||||
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>());
|
||||
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);
|
||||
|
||||
player.Attributes!.AddElement(new SimpleElement(200.0f, AggregateType.AddRaw), Stats.TotalLevel);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
private class TestPlayer : Player
|
||||
{
|
||||
public TestPlayer(IGameContext gameContext)
|
||||
: base(gameContext)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ICustomPlugInContainer<IViewPlugIn> CreateViewPlugInContainer()
|
||||
{
|
||||
return new MockViewPlugInContainer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// <copyright file="RandomizerTestHelper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
using Moq;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for creating test randomizers.
|
||||
/// </summary>
|
||||
public static class RandomizerTestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mock randomizer with random behavior.
|
||||
/// </summary>
|
||||
/// <returns>A mock randomizer.</returns>
|
||||
public static IRandomizer Create()
|
||||
{
|
||||
var randomizer = new Mock<IRandomizer>();
|
||||
var random = new Random();
|
||||
randomizer.Setup(r => r.NextInt(It.IsAny<int>(), It.IsAny<int>())).Returns((int min, int max) => random.Next(min, max));
|
||||
randomizer.Setup(r => r.NextDouble()).Returns(() => random.NextDouble());
|
||||
randomizer.Setup(r => r.NextRandomBool(It.IsAny<int>())).Returns((int chance) => random.Next(100) < chance);
|
||||
return randomizer.Object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<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>
|
||||
<ApplicationIcon />
|
||||
<OutputType>Exe</OutputType>
|
||||
<StartupObject />
|
||||
<AssemblyName>MUnique.OpenMU.GameLogic.Benchmarks</AssemblyName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.GameLogic.Benchmarks.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.GameLogic.Benchmarks.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\src\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
|
||||
<Compile Include="..\..\src\SharedGlobalUsings.cs" Link="SharedGlobalUsings.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\src\stylecop.json" Link="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="Nito.AsyncEx.Coordination" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\GameLogic\MUnique.OpenMU.GameLogic.csproj" />
|
||||
<ProjectReference Include="..\..\src\Persistence\InMemory\MUnique.OpenMU.Persistence.InMemory.csproj" />
|
||||
<ProjectReference Include="..\..\src\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27703.2047
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MUnique.OpenMU.GameLogic.Benchmarks", "MUnique.OpenMU.GameLogic.Benchmarks.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B2012905-0E9A-4059-9E4D-AD78A91273F2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
100
tests/MUnique.OpenMU.GameLogic.Benchmarks/PartyBenchmarks.cs
Normal file
100
tests/MUnique.OpenMU.GameLogic.Benchmarks/PartyBenchmarks.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
// <copyright file="PartyBenchmarks.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Benchmarks;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.GameLogic.Benchmarks.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for <see cref="Party"/> class methods related to XP distribution and item drops.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[ThreadingDiagnoser]
|
||||
[InvocationCount(100)]
|
||||
public class PartyBenchmarks
|
||||
{
|
||||
private Party _party = null!;
|
||||
private Player _killer = null!;
|
||||
private IAttackable _killedObject = null!;
|
||||
private List<Player> _players = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the benchmark by creating a party with 5 players.
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public async Task Setup()
|
||||
{
|
||||
var partyManager = new PartyManager(5, new NullLogger<Party>());
|
||||
_party = new Party(partyManager, 5, new NullLogger<Party>());
|
||||
|
||||
_players = new List<Player>();
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync();
|
||||
await player.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false);
|
||||
if (player.Attributes is { } attrs)
|
||||
{
|
||||
attrs[Stats.Level] = (short)(100 + i);
|
||||
}
|
||||
|
||||
_players.Add(player);
|
||||
await _party.AddAsync(player).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_killer = _players[0];
|
||||
foreach (var player in _players)
|
||||
{
|
||||
_killer.Observers.Add(player);
|
||||
}
|
||||
|
||||
_killedObject = CreateMockAttackable(50);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks the <see cref="Party.DistributeExperienceAfterKillAsync"/> method.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask DistributeExperienceAfterKillAsync()
|
||||
{
|
||||
await _party.DistributeExperienceAfterKillAsync(_killedObject, _killer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks the <see cref="Party.DistributeMoneyAfterKillAsync"/> method.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask DistributeMoneyAfterKillAsync()
|
||||
{
|
||||
await _party.DistributeMoneyAfterKillAsync(_killedObject, _killer, 10000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks the <see cref="Party.GetQuestDropItemGroupsAsync"/> method.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask GetQuestDropItemGroupsAsync()
|
||||
{
|
||||
await _party.GetQuestDropItemGroupsAsync(_killer);
|
||||
}
|
||||
|
||||
private static IAttackable CreateMockAttackable(float level)
|
||||
{
|
||||
var attributes = new Mock<IAttributeSystem>();
|
||||
attributes.Setup(a => a[Stats.Level]).Returns(level);
|
||||
|
||||
var result = new Mock<IAttackable>();
|
||||
result.SetupAllProperties();
|
||||
result.SetupGet(a => a.Attributes).Returns(attributes.Object);
|
||||
|
||||
GameMap? nullMap = null;
|
||||
result.SetupGet(a => a.CurrentMap).Returns(nullMap);
|
||||
|
||||
return result.Object;
|
||||
}
|
||||
}
|
||||
29
tests/MUnique.OpenMU.GameLogic.Benchmarks/Program.cs
Normal file
29
tests/MUnique.OpenMU.GameLogic.Benchmarks/Program.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#pragma warning disable SA1200
|
||||
|
||||
global using BenchmarkDotNet.Attributes;
|
||||
global using BenchmarkDotNet.Jobs;
|
||||
global using BenchmarkDotNet.Running;
|
||||
|
||||
#pragma warning restore SA1200
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// The class of the entry point of the benchmark.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry point of the benchmark.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BenchmarkRunner.Run<DropGeneratorBenchmarks>();
|
||||
BenchmarkRunner.Run<PartyBenchmarks>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// <copyright file="EncryptionBenchmarks.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Benchmarks;
|
||||
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using MUnique.OpenMU.Network.SimpleModulus;
|
||||
using MUnique.OpenMU.Network.Xor;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for the simple modulus encryption.
|
||||
/// </summary>
|
||||
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
|
||||
[MemoryDiagnoser]
|
||||
[InvocationCount(100)]
|
||||
public class EncryptionBenchmarks
|
||||
{
|
||||
/// <summary>
|
||||
/// The packet count for each benchmark run.
|
||||
/// </summary>
|
||||
private const int PacketCount = 100000;
|
||||
|
||||
/// <summary>
|
||||
/// That's a 185 bytes unencrypted C3 packet. Encrypted it takes 255 bytes.
|
||||
/// </summary>
|
||||
private readonly byte[] _c3Packet = Convert.FromBase64String("w7kxFgK8hYpGGLgdXe7ZpTZViB+r3sRI3YSqZs7/Mh5Vmh2mXqs+3dqkvURmXrL57ASs+FkJz/236Tl9ER67R+WZyMLRMkeLF6tEBiB/4X7SsXrKUznES8of73RxwMy76HZezJbvJ7m9IOGuxcjcNwe6q1+k8fOs1Hz3sULSGlbfiB6qIBXo4onADTNYFoYCQrdtthVsF/aDsvcZ93V36gaKzzyqMhby0sjV4+TAU7719W6LZWNAcnA=");
|
||||
|
||||
private readonly byte[] _c1Packet = Convert.FromBase64String("wf8AudHEjjSP53H6Rkp3oXj7B9z+rVDR2f0Is4bvsIsUL3RM/aTDB2FX9YG3Hkboy1Z1JThot558MeDTvNuunzfl5RbWK6TTOP97prjPGbq3IOcweopTq3fVz8vD8EuFqVVJ0jgvEZ+xoe047RHmrRgmG5zzfSWtkTmeAVzZD0i09f1jhUeBiA5HfticGr5m7iGzndSvkSwvm0D/kRBD15GlhPgTgyfQpJONrP5NEHd7NxI6JnJzBWPQM+kHgvb+BKdH95bFUmv54vlBIeUt4ovIg1r9CLEfMX+UQk89yCKcj6dXBRjgteSmQUN5MuN9o1FePv6cAPv2KMUXMBAc");
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks the performance of the <see cref="SimpleModulusEncryptionAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>The value task.</returns>
|
||||
[Benchmark]
|
||||
public async ValueTask SimpleModulusEncryptionAsync()
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
var pipelinedEncryptor = new PipelinedSimpleModulusEncryptor(pipe.Writer);
|
||||
var readBuffer = new byte[256];
|
||||
for (int i = 0; i < PacketCount; i++)
|
||||
{
|
||||
await pipelinedEncryptor.Writer.WriteAsync(this._c3Packet).ConfigureAwait(false);
|
||||
await pipelinedEncryptor.Writer.FlushAsync().ConfigureAwait(false);
|
||||
var readResult = await pipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
readResult.Buffer.CopyTo(readBuffer);
|
||||
//// In the server, I would process the readBuffer here
|
||||
pipe.Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
|
||||
}
|
||||
|
||||
await pipelinedEncryptor.Writer.CompleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks the performance of the <see cref="MUnique.OpenMU.Network.Xor.PipelinedXor32Encryptor"/>.
|
||||
/// </summary>
|
||||
/// <returns>The value task.</returns>
|
||||
[Benchmark]
|
||||
public async ValueTask Xor32EncryptionAsync()
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
var pipelinedEncryptor = new PipelinedXor32Encryptor(pipe.Writer);
|
||||
var readBuffer = new byte[256];
|
||||
for (int i = 0; i < PacketCount; i++)
|
||||
{
|
||||
await pipelinedEncryptor.Writer.WriteAsync(this._c1Packet).ConfigureAwait(false);
|
||||
await pipelinedEncryptor.Writer.FlushAsync().ConfigureAwait(false);
|
||||
var readResult = await pipe.Reader.ReadAsync().ConfigureAwait(false);
|
||||
readResult.Buffer.CopyTo(readBuffer);
|
||||
//// In the client/server, I would process the readBuffer here
|
||||
pipe.Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
|
||||
}
|
||||
|
||||
await pipelinedEncryptor.Writer.CompleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<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>
|
||||
<ApplicationIcon />
|
||||
<OutputType>Exe</OutputType>
|
||||
<StartupObject />
|
||||
<AssemblyName>MUnique.OpenMU.Network.Benchmarks</AssemblyName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.Network.Benchmarks.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.Network.Benchmarks.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\src\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
|
||||
<Compile Include="..\..\src\SharedGlobalUsings.cs" Link="SharedGlobalUsings.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\src\stylecop.json" Link="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
<PackageReference Include="Nito.AsyncEx.Coordination" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Network\MUnique.OpenMU.Network.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27703.2047
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MUnique.OpenMU.Network.Benchmarks", "MUnique.OpenMU.Network.Benchmarks.csproj", "{CB2197B1-D4D1-42CD-88B8-0BDEA7C83040}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{CB2197B1-D4D1-42CD-88B8-0BDEA7C83040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB2197B1-D4D1-42CD-88B8-0BDEA7C83040}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB2197B1-D4D1-42CD-88B8-0BDEA7C83040}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB2197B1-D4D1-42CD-88B8-0BDEA7C83040}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B2012905-0E9A-4059-9E4D-AD78A91273F1}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
70
tests/MUnique.OpenMU.Network.Benchmarks/PacketSending.cs
Normal file
70
tests/MUnique.OpenMU.Network.Benchmarks/PacketSending.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
// <copyright file="PacketSending.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.Network.Benchmarks;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// A benchmark of sending network packets.
|
||||
/// The memory allocation should be the same.
|
||||
/// </summary>
|
||||
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
|
||||
[MemoryDiagnoser]
|
||||
[InvocationCount(100)]
|
||||
public class PacketSending
|
||||
{
|
||||
private readonly byte[] _c1Packet = Convert.FromBase64String("wf8AudHEjjSP53H6Rkp3oXj7B9z+rVDR2f0Is4bvsIsUL3RM/aTDB2FX9YG3Hkboy1Z1JThot558MeDTvNuunzfl5RbWK6TTOP97prjPGbq3IOcweopTq3fVz8vD8EuFqVVJ0jgvEZ+xoe047RHmrRgmG5zzfSWtkTmeAVzZD0i09f1jhUeBiA5HfticGr5m7iGzndSvkSwvm0D/kRBD15GlhPgTgyfQpJONrP5NEHd7NxI6JnJzBWPQM+kHgvb+BKdH95bFUmv54vlBIeUt4ovIg1r9CLEfMX+UQk89yCKcj6dXBRjgteSmQUN5MuN9o1FePv6cAPv2KMUXMBAc");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the packet count which will be tested.
|
||||
/// </summary>
|
||||
[Params(100, 500, 1000)]
|
||||
public int PacketCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends packets at the connection with Spans.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask SendSpanAsync()
|
||||
{
|
||||
CancellationToken cancellationToken = default;
|
||||
var duplexPipe = new DuplexPipe();
|
||||
using var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
for (int i = 0; i < this.PacketCount && !cancellationToken.IsCancellationRequested; i++)
|
||||
{
|
||||
using var l = await connection.OutputLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
Write();
|
||||
await connection.Output.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
void Write()
|
||||
{
|
||||
var span = connection.Output.GetSpan(this._c1Packet.Length);
|
||||
this._c1Packet.CopyTo(span);
|
||||
connection.Output.Advance(this._c1Packet.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends packets at the connection with Spans.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask SendSpanInlineAsync()
|
||||
{
|
||||
CancellationToken cancellationToken = default;
|
||||
var duplexPipe = new DuplexPipe();
|
||||
using var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
for (int i = 0; i < this.PacketCount && !cancellationToken.IsCancellationRequested; i++)
|
||||
{
|
||||
using var l = await connection.OutputLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._c1Packet.CopyTo(connection.Output.GetSpan(this._c1Packet.Length));
|
||||
connection.Output.Advance(this._c1Packet.Length);
|
||||
await connection.Output.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
tests/MUnique.OpenMU.Network.Benchmarks/Program.cs
Normal file
29
tests/MUnique.OpenMU.Network.Benchmarks/Program.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#pragma warning disable SA1200
|
||||
|
||||
global using BenchmarkDotNet.Attributes;
|
||||
global using BenchmarkDotNet.Jobs;
|
||||
global using BenchmarkDotNet.Running;
|
||||
|
||||
#pragma warning restore SA1200
|
||||
|
||||
namespace MUnique.OpenMU.Network.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// The class of the entry point of the benchmark.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry point of the benchmark.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BenchmarkRunner.Run<EncryptionBenchmarks>();
|
||||
BenchmarkRunner.Run<PacketSending>();
|
||||
}
|
||||
}
|
||||
@@ -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.Network.Benchmarks")]
|
||||
@@ -0,0 +1,150 @@
|
||||
// <copyright file="ChatServerPacketTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This source code was auto-generated by an XSL transformation.
|
||||
// Do not change this file. Instead, change the XML data which contains
|
||||
// the packet definitions and re-run the transformation (rebuild this project).
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace MUnique.OpenMU.Network.Packets.Tests.ChatServer;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using MUnique.OpenMU.Network.Packets.ChatServer;
|
||||
|
||||
/// <summary>
|
||||
/// Auto-generated tests for packet structures to validate packet definitions.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PacketStructureTests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for Authenticate.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Authenticate_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 16;
|
||||
var actualLength = AuthenticateRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'RoomId' boundary
|
||||
Assert.That(4 + 2, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'RoomId' exceeds packet boundary");
|
||||
|
||||
// Validate field 'Token' boundary
|
||||
Assert.That(6 + 10, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'Token' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ChatRoomClientJoined.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChatRoomClientJoined_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 15;
|
||||
var actualLength = ChatRoomClientJoinedRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'ClientIndex' boundary
|
||||
Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'ClientIndex' exceeds packet boundary");
|
||||
|
||||
// Validate field 'Name' boundary
|
||||
Assert.That(5 + 10, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'Name' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for LeaveChatRoom.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LeaveChatRoom_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 3;
|
||||
var actualLength = LeaveChatRoomRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ChatRoomClientLeft.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChatRoomClientLeft_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 15;
|
||||
var actualLength = ChatRoomClientLeftRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'ClientIndex' boundary
|
||||
Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'ClientIndex' exceeds packet boundary");
|
||||
|
||||
// Validate field 'Name' boundary
|
||||
Assert.That(5 + 10, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'Name' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ChatRoomClients.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChatRoomClients_PacketSizeValidation()
|
||||
{
|
||||
// Basic packet validation
|
||||
// Validate header type and field boundaries
|
||||
|
||||
// Field 'ClientCount' starts at index 6 with size 1
|
||||
Assert.That(6, Is.GreaterThanOrEqualTo(0),
|
||||
"Field 'ClientCount' has invalid negative index");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ChatMessage.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChatMessage_PacketSizeValidation()
|
||||
{
|
||||
// Variable-length packet validation
|
||||
// Test GetRequiredSize method with sample data
|
||||
const int testBinaryLength = 10;
|
||||
var calculatedSize = ChatMessageRef.GetRequiredSize(testBinaryLength);
|
||||
var expectedMinSize = testBinaryLength + 5;
|
||||
|
||||
Assert.That(calculatedSize, Is.GreaterThanOrEqualTo(expectedMinSize),
|
||||
"GetRequiredSize calculation incorrect for binary field");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for KeepAlive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void KeepAlive_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 3;
|
||||
var actualLength = KeepAliveRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,210 @@
|
||||
// <copyright file="ConnectServerPacketTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This source code was auto-generated by an XSL transformation.
|
||||
// Do not change this file. Instead, change the XML data which contains
|
||||
// the packet definitions and re-run the transformation (rebuild this project).
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace MUnique.OpenMU.Network.Packets.Tests.ConnectServer;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using MUnique.OpenMU.Network.Packets.ConnectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Auto-generated tests for packet structures to validate packet definitions.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PacketStructureTests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ConnectionInfoRequest075.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConnectionInfoRequest075_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 5;
|
||||
var actualLength = ConnectionInfoRequest075Ref.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'ServerId' boundary
|
||||
Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'ServerId' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ConnectionInfoRequest.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConnectionInfoRequest_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 6;
|
||||
var actualLength = ConnectionInfoRequestRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'ServerId' boundary
|
||||
Assert.That(4 + 2, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'ServerId' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ConnectionInfo.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConnectionInfo_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 22;
|
||||
var actualLength = ConnectionInfoRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'IpAddress' boundary
|
||||
Assert.That(4 + 16, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'IpAddress' exceeds packet boundary");
|
||||
|
||||
// Validate field 'Port' boundary
|
||||
Assert.That(20 + 2, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'Port' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ServerListRequest.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ServerListRequest_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 4;
|
||||
var actualLength = ServerListRequestRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ServerListResponse.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ServerListResponse_PacketSizeValidation()
|
||||
{
|
||||
// Basic packet validation
|
||||
// Validate header type and field boundaries
|
||||
|
||||
// Field 'ServerCount' starts at index 5 with size 2
|
||||
Assert.That(5, Is.GreaterThanOrEqualTo(0),
|
||||
"Field 'ServerCount' has invalid negative index");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ServerListRequestOld.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ServerListRequestOld_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 4;
|
||||
var actualLength = ServerListRequestOldRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ServerListResponseOld.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ServerListResponseOld_PacketSizeValidation()
|
||||
{
|
||||
// Basic packet validation
|
||||
// Validate header type and field boundaries
|
||||
|
||||
// Field 'ServerCount' starts at index 5 with size 1
|
||||
Assert.That(5, Is.GreaterThanOrEqualTo(0),
|
||||
"Field 'ServerCount' has invalid negative index");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for Hello.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Hello_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 4;
|
||||
var actualLength = HelloRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for PatchCheckRequest.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PatchCheckRequest_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 6;
|
||||
var actualLength = PatchCheckRequestRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'MajorVersion' boundary
|
||||
Assert.That(3 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'MajorVersion' exceeds packet boundary");
|
||||
|
||||
// Validate field 'MinorVersion' boundary
|
||||
Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'MinorVersion' exceeds packet boundary");
|
||||
|
||||
// Validate field 'PatchVersion' boundary
|
||||
Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'PatchVersion' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for PatchVersionOkay.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PatchVersionOkay_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 4;
|
||||
var actualLength = PatchVersionOkayRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ClientNeedsPatch.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ClientNeedsPatch_PacketSizeValidation()
|
||||
{
|
||||
// Variable-length packet validation
|
||||
// Test GetRequiredSize method with sample data
|
||||
const string testString = "TestData";
|
||||
var calculatedSize = ClientNeedsPatchRef.GetRequiredSize(testString);
|
||||
var expectedMinSize = Encoding.UTF8.GetByteCount(testString) + 1 + 6;
|
||||
|
||||
Assert.That(calculatedSize, Is.GreaterThanOrEqualTo(expectedMinSize),
|
||||
"GetRequiredSize calculation incorrect for string field");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<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.Network.Packets.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.Network.Packets.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="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Network\Packets\MUnique.OpenMU.Network.Packets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(ci)'!='true'">
|
||||
<!-- Generate test files from XML packet definitions -->
|
||||
<XslTransformation OutputPaths="ClientToServerPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ClientToServer\ClientToServerPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="<Parameter Name='resultFileName' Value='ClientToServerPacketTests.cs'/><Parameter Name='subNamespace' Value='ClientToServer'/>" />
|
||||
<XslTransformation OutputPaths="ServerToClientPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ServerToClient\ServerToClientPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="<Parameter Name='resultFileName' Value='ServerToClientPacketTests.cs'/><Parameter Name='subNamespace' Value='ServerToClient'/>" />
|
||||
<XslTransformation OutputPaths="ChatServerPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ChatServer\ChatServerPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="<Parameter Name='resultFileName' Value='ChatServerPacketTests.cs'/><Parameter Name='subNamespace' Value='ChatServer'/>" />
|
||||
<XslTransformation OutputPaths="ConnectServerPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ConnectServer\ConnectServerPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="<Parameter Name='resultFileName' Value='ConnectServerPacketTests.cs'/><Parameter Name='subNamespace' Value='ConnectServer'/>" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
76
tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs
Normal file
76
tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="ConnectionTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Tests;
|
||||
|
||||
using System.IO.Pipelines;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="Connection"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ConnectionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if the connection is disconnected after a malformed packet was received.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task DisconnectedByMalformedPacketReceivedAsync()
|
||||
{
|
||||
var malformedData = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
|
||||
var duplexPipe = new DuplexPipe();
|
||||
using var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
var disconnected = false;
|
||||
connection.Disconnected += async () => disconnected = true;
|
||||
_ = connection.BeginReceiveAsync();
|
||||
try
|
||||
{
|
||||
await duplexPipe.ReceivePipe.Writer.WriteAsync(malformedData).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// we need to swallow the exception for this test, so we can check the connected flag afterwards.
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10 && !disconnected; i++)
|
||||
{
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Assert.That(connection.Connected, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the reader (e.g. SocketConnection) gets an exception when it reads a malformed packet which leads to an exception.
|
||||
/// The consumer (e.g. SocketConnection) will take care to call <see cref="PipeReader.Complete"/> or <see cref="PipeReader.CompleteAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task ExceptionWhenFailingToEncryptSentPacketAsync()
|
||||
{
|
||||
var malformedData = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
|
||||
var duplexPipe = new DuplexPipe();
|
||||
using var connection = new Connection(duplexPipe, null, new Xor.PipelinedXor32Encryptor(duplexPipe.Output), new NullLogger<Connection>());
|
||||
|
||||
_ = connection.BeginReceiveAsync();
|
||||
await connection.Output.WriteAsync(malformedData).ConfigureAwait(false);
|
||||
|
||||
Assert.That(
|
||||
async () => await duplexPipe.SendPipe.Reader.ReadAsync().ConfigureAwait(false),
|
||||
Throws.TypeOf<InvalidPacketHeaderException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the connection is initially connected.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void InitiallyConnected()
|
||||
{
|
||||
var duplexPipe = new DuplexPipe();
|
||||
using var connection = new Connection(duplexPipe, null, null, new NullLogger<Connection>());
|
||||
Assert.That(connection.Connected, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// <copyright file="InvalidPacketHeaderExceptionTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Tests;
|
||||
|
||||
using System.IO.Pipelines;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="InvalidPacketHeaderException"/> are thrown when malformed data is read by a <see cref="PacketPipeReaderBase"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class InvalidPacketHeaderExceptionTest
|
||||
{
|
||||
private readonly byte[] _malformedData = { 0xC1, 0x03, 0xFF, 0x00, 0x00, 0x00 };
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the exception is thrown.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task ThrownAsync()
|
||||
{
|
||||
await this.TestExceptionAsync(e => { }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="InvalidPacketHeaderException.Header"/> is assigned correctly.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task TestHeaderAsync()
|
||||
{
|
||||
await this.TestExceptionAsync(e => Assert.That(e.Header, Is.EquivalentTo(new byte[] { 0x00, 0x00, 0x00 }))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="InvalidPacketHeaderException.Position"/> is assigned correctly.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task TestPositionAsync()
|
||||
{
|
||||
await this.TestExceptionAsync(e => Assert.That(e.Position, Is.EqualTo(3))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if <see cref="InvalidPacketHeaderException.BufferContent"/> is assigned correctly.
|
||||
/// </summary>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task TestBufferContentAsync()
|
||||
{
|
||||
await this.TestExceptionAsync(e => Assert.That(e.BufferContent, Is.EquivalentTo(this._malformedData))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask TestExceptionAsync(Action<InvalidPacketHeaderException> check)
|
||||
{
|
||||
bool thrown = false;
|
||||
var duplexPipe = new DuplexPipe(new PipeOptions(pauseWriterThreshold: 1, resumeWriterThreshold: 1));
|
||||
using var connection = new Connection(duplexPipe, null, new Xor.PipelinedXor32Encryptor(duplexPipe.Output), new NullLogger<Connection>());
|
||||
_ = connection.BeginReceiveAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_ = await duplexPipe.ReceivePipe.Writer.WriteAsync(this._malformedData).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidPacketHeaderException e)
|
||||
{
|
||||
thrown = true;
|
||||
check(e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.Fail($"Wrong exception type {e}", e);
|
||||
}
|
||||
|
||||
Assert.That(thrown);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// <copyright file="IpAddressResolverFactoryTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Tests;
|
||||
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="IpAddressResolverFactory"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public class IpAddressResolverFactoryTests
|
||||
{
|
||||
private readonly IPAddress _expectedLoopbackAddress = IPAddress.Parse("127.127.127.127");
|
||||
|
||||
private string? _originalResolveIpEnvironmentVariable;
|
||||
private string? _originalAspNetCoreEnvironmentVariable;
|
||||
|
||||
/// <summary>
|
||||
/// Captures and clears the environment variables which influence resolver determination.
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
this._originalResolveIpEnvironmentVariable = Environment.GetEnvironmentVariable("RESOLVE_IP");
|
||||
this._originalAspNetCoreEnvironmentVariable = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
Environment.SetEnvironmentVariable("RESOLVE_IP", null);
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the environment variables which were changed during a test.
|
||||
/// </summary>
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("RESOLVE_IP", this._originalResolveIpEnvironmentVariable);
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", this._originalAspNetCoreEnvironmentVariable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if runtime reconfiguration is ignored when the resolver was configured by startup parameters.
|
||||
/// </summary>
|
||||
/// <returns>The asynchronous operation.</returns>
|
||||
[Test]
|
||||
public async Task ResolverConfiguredByStartupParameterCannotBeOverriddenAsync()
|
||||
{
|
||||
var resolver = (ConfigurableIpResolver)IpAddressResolverFactory.CreateIpResolver(new[] { "-resolveIP:loopback" }, (IpResolverType.Public, null), new NullLoggerFactory());
|
||||
|
||||
resolver.Configure(IpResolverType.Custom, "1.2.3.4");
|
||||
var resolvedAddress = await resolver.ResolveIPv4Async().ConfigureAwait(false);
|
||||
|
||||
Assert.That(resolvedAddress, Is.EqualTo(this._expectedLoopbackAddress));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if runtime reconfiguration is ignored when the resolver was configured by environment variable.
|
||||
/// </summary>
|
||||
/// <returns>The asynchronous operation.</returns>
|
||||
[Test]
|
||||
public async Task ResolverConfiguredByEnvironmentVariableCannotBeOverriddenAsync()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("RESOLVE_IP", "loopback");
|
||||
var resolver = (ConfigurableIpResolver)IpAddressResolverFactory.CreateIpResolver(Array.Empty<string>(), (IpResolverType.Public, null), new NullLoggerFactory());
|
||||
|
||||
resolver.Configure(IpResolverType.Custom, "1.2.3.4");
|
||||
var resolvedAddress = await resolver.ResolveIPv4Async().ConfigureAwait(false);
|
||||
|
||||
Assert.That(resolvedAddress, Is.EqualTo(this._expectedLoopbackAddress));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if runtime reconfiguration still works when the resolver was configured by persisted settings.
|
||||
/// </summary>
|
||||
/// <returns>The asynchronous operation.</returns>
|
||||
[Test]
|
||||
public async Task ResolverConfiguredByPersistedSettingsCanBeOverriddenAsync()
|
||||
{
|
||||
var resolver = (ConfigurableIpResolver)IpAddressResolverFactory.CreateIpResolver(Array.Empty<string>(), (IpResolverType.Loopback, null), new NullLoggerFactory());
|
||||
|
||||
resolver.Configure(IpResolverType.Custom, "1.2.3.4");
|
||||
var resolvedAddress = await resolver.ResolveIPv4Async().ConfigureAwait(false);
|
||||
|
||||
Assert.That(resolvedAddress, Is.EqualTo(IPAddress.Parse("1.2.3.4")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<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.Network.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.Network.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="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Network\MUnique.OpenMU.Network.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
41
tests/MUnique.OpenMU.Network.Tests/PacketTwisterTest.cs
Normal file
41
tests/MUnique.OpenMU.Network.Tests/PacketTwisterTest.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="PacketTwisterTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Tests;
|
||||
|
||||
using MUnique.OpenMU.Network.PacketTwister;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="PacketTwistRunner"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PacketTwisterTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests encryption and decryption using the <see cref="PacketTwistRunner"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void EncryptDecryptWithPacketTwister()
|
||||
{
|
||||
var decrypted = Convert.FromBase64String("w7gAudHEjjSP53H6Rkp3oXj7B9z+rVDR2f0Is4bvsIsUL3RM/aTDB2FX9YG3Hkboy1Z1JThot558MeDTvNuunzfl5RbWK6TTOP97prjPGbq3IOcweopTq3fVz8vD8EuFqVVJ0jgvEZ+xoe047RHmrRgmG5zzfSWtkTmeAVzZD0i09f1jhUeBiA5HfticGr5m7iGzndSvkSwvm0D/kRBD15GlhPgTgyfQpJONrP5NEHd7NxI6JnJzBQ==");
|
||||
var packetTwister = new PacketTwister.PacketTwistRunner();
|
||||
for (byte packetType = 0; packetType < byte.MaxValue; packetType++)
|
||||
{
|
||||
decrypted[2] = packetType;
|
||||
var result = decrypted.ToArray();
|
||||
packetTwister.Encrypt(result);
|
||||
packetTwister.Decrypt(result);
|
||||
CompareArrays(decrypted, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CompareArrays(byte[] expected, byte[] actual)
|
||||
{
|
||||
Assert.That(actual.Length, Is.EqualTo(expected.Length));
|
||||
for (int i = 0; i < actual.Length; i++)
|
||||
{
|
||||
Assert.That(actual[i], Is.EqualTo(expected[i]), "index {0}, packet type {1}", i, expected[expected.GetPacketHeaderSize()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
160
tests/MUnique.OpenMU.Network.Tests/PipelinedDecryptorTests.cs
Normal file
160
tests/MUnique.OpenMU.Network.Tests/PipelinedDecryptorTests.cs
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
|
||||
// <copyright file="PipelinedEncryptDecryptCycleTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Tests;
|
||||
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using MUnique.OpenMU.Network.SimpleModulus;
|
||||
using MUnique.OpenMU.Network.Xor;
|
||||
|
||||
/// <summary>
|
||||
/// Tests the cycle of encrypting and decrypting a packet purely due pipes.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PipelinedEncryptDecryptCycleTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests the encryption and decryption cycle of C3-packets from client to server.
|
||||
/// These packets get encrypted first by <see cref="PipelinedXor32Encryptor"/>, then by <see cref="PipelinedSimpleModulusEncryptor"/> using client-side keys.
|
||||
/// Then it gets decrypted by the <see cref="PipelinedSimpleModulusDecryptor"/> using server-side keys and finally by the <see cref="PipelinedXor32Decryptor"/>.
|
||||
/// </summary>
|
||||
/// <returns>The task.</returns>
|
||||
[Test]
|
||||
public async Task ClientToServerC3Async()
|
||||
{
|
||||
var packet = Convert.FromBase64String("w7kxFgK8hYpGGLgdXe7ZpTZViB+r3sRI3YSqZs7/Mh5Vmh2mXqs+3dqkvURmXrL57ASs+FkJz/236Tl9ER67R+WZyMLRMkeLF6tEBiB/4X7SsXrKUznES8of73RxwMy76HZezJbvJ7m9IOGuxcjcNwe6q1+k8fOs1Hz3sULSGlbfiB6qIBXo4onADTNYFoYCQrdtthVsF/aDsvcZ93V36gaKzzyqMhby0sjV4+TAU7719W6LZWNAcnA=");
|
||||
await this.EncryptDecryptFromClientToServerAsync(packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption of a packet where the final block doesn't have maximum size.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The test uses a real ping packet which was captured from a real game client.
|
||||
/// </remarks>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task ClientToServerC3WithNonMaximalFinalBlockSizeAsync()
|
||||
{
|
||||
var packet = new byte[] { 195, 12, 14, 0, 1, 51, 254, 39, 0, 0, 0, 0 };
|
||||
await this.EncryptDecryptFromClientToServerAsync(packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption of a C3 packet where the size is lower than the maximum block size.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The test uses a real ping packet which was captured from a real game client.
|
||||
/// </remarks>
|
||||
/// <returns>The async task.</returns>
|
||||
[Test]
|
||||
public async Task ClientToServerC3WithSmallPacketAsync()
|
||||
{
|
||||
var packet = new byte[] { 195, 5, 14, 0, 1 };
|
||||
await this.EncryptDecryptFromClientToServerAsync(packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption and decryption cycle of C1-packets from client to server.
|
||||
/// These packets get encrypted first by <see cref="PipelinedXor32Encryptor"/>, then <see cref="PipelinedSimpleModulusEncryptor"/> just forwards then as-is.
|
||||
/// Then the <see cref="PipelinedSimpleModulusDecryptor"/> forwards them as well and finally it gets decrypted by the <see cref="PipelinedXor32Decryptor"/>.
|
||||
/// </summary>
|
||||
/// <returns>The task.</returns>
|
||||
[Test]
|
||||
public async Task ClientToServerC1Async()
|
||||
{
|
||||
var packet = new byte[] { 0xC1, 0x06, 0x11, 0x01, 0x02, 0x03 };
|
||||
await this.EncryptDecryptFromClientToServerAsync(packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption and decryption cycle of C3-packets from server to client.
|
||||
/// These packets get encrypted first by the <see cref="PipelinedSimpleModulusEncryptor"/> using server-side keys.
|
||||
/// On the client side it gets decrypted by the <see cref="PipelinedSimpleModulusDecryptor"/> using client-side keys.
|
||||
/// </summary>
|
||||
/// <returns>The task.</returns>
|
||||
[Test]
|
||||
public async Task ServerToClientC3Async()
|
||||
{
|
||||
var packet = Convert.FromBase64String("w7kxFgK8hYpGGLgdXe7ZpTZViB+r3sRI3YSqZs7/Mh5Vmh2mXqs+3dqkvURmXrL57ASs+FkJz/236Tl9ER67R+WZyMLRMkeLF6tEBiB/4X7SsXrKUznES8of73RxwMy76HZezJbvJ7m9IOGuxcjcNwe6q1+k8fOs1Hz3sULSGlbfiB6qIBXo4onADTNYFoYCQrdtthVsF/aDsvcZ93V36gaKzzyqMhby0sjV4+TAU7719W6LZWNAcnA=");
|
||||
await this.EncryptDecryptFromServerToClientAsync(packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption and decryption cycle of C1-packets from server to client.
|
||||
/// These packets are not encrypted at all, so all involved simple modulus encryptor/decryptors just forward them.
|
||||
/// </summary>
|
||||
/// <returns>The task.</returns>
|
||||
[Test]
|
||||
public async Task ServerToClientC1Async()
|
||||
{
|
||||
var packet = new byte[] { 0xC1, 0x06, 0x11, 0x01, 0x02, 0x03 };
|
||||
await this.EncryptDecryptFromServerToClientAsync(packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption-decryption cycle for the packet from server to client. The specified packet must be the same after the packet has passed this cycle.
|
||||
/// Packets from server to client are never encrypted by Xor32, so these encryptor/decryptors are not involved here.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet.</param>
|
||||
/// <returns>The task.</returns>
|
||||
private async Task EncryptDecryptFromServerToClientAsync(byte[] packet)
|
||||
{
|
||||
// this pipe connects the encryptor with the decryptor. You can imagine this as the server-to-client pipe of a network socket, for example.
|
||||
var pipe = new Pipe();
|
||||
|
||||
var encryptor = new PipelinedSimpleModulusEncryptor(pipe.Writer);
|
||||
var decryptor = new PipelinedSimpleModulusDecryptor(pipe.Reader, PipelinedSimpleModulusDecryptor.DefaultClientKey);
|
||||
encryptor.Writer.Write(packet);
|
||||
await encryptor.Writer.FlushAsync().ConfigureAwait(false);
|
||||
var readResult = await decryptor.Reader.ReadAsync().ConfigureAwait(false);
|
||||
|
||||
var result = readResult.Buffer.ToArray();
|
||||
Assert.That(result, Is.EquivalentTo(packet));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the encryption-decryption cycle for the packet. The specified packet must be the same after the packet has passed this cycle.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet.</param>
|
||||
/// <returns>The async task.</returns>
|
||||
private async Task EncryptDecryptFromClientToServerAsync(byte[] packet)
|
||||
{
|
||||
// this pipe connects the encryptor with the decryptor. You can imagine this as the client-to-server pipe of a network socket, for example.
|
||||
var pipe = new Pipe();
|
||||
|
||||
var encryptor = new PipelinedXor32Encryptor(new PipelinedSimpleModulusEncryptor(pipe.Writer, PipelinedSimpleModulusEncryptor.DefaultClientKey).Writer);
|
||||
var decryptor = new PipelinedXor32Decryptor(new PipelinedSimpleModulusDecryptor(pipe.Reader).Reader);
|
||||
encryptor.Writer.Write(packet);
|
||||
await encryptor.Writer.FlushAsync().ConfigureAwait(false);
|
||||
var readResult = await decryptor.Reader.ReadAsync().ConfigureAwait(false);
|
||||
|
||||
var result = readResult.Buffer.ToArray();
|
||||
Assert.That(result, Is.EquivalentTo(packet));
|
||||
}
|
||||
}
|
||||
107
tests/MUnique.OpenMU.Network.Tests/PipelinedEncryptorTests.cs
Normal file
107
tests/MUnique.OpenMU.Network.Tests/PipelinedEncryptorTests.cs
Normal file
File diff suppressed because one or more lines are too long
@@ -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.Network.Tests")]
|
||||
162
tests/MUnique.OpenMU.Network.Tests/SocketConnectionTest.cs
Normal file
162
tests/MUnique.OpenMU.Network.Tests/SocketConnectionTest.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
// <copyright file="SocketConnectionTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Network.Tests;
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Pipelines.Sockets.Unofficial;
|
||||
|
||||
/// <summary>
|
||||
/// Test of the async connection implementation.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Ignore("It's using real sockets")]
|
||||
public class SocketConnectionTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests the receive function with a pipelined connection object.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestReceivePipelined()
|
||||
{
|
||||
this.TestReceivePipelined(socket => new Connection(SocketConnection.Create(socket), null, null, new NullLogger<Connection>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the receive function with a pipelined connection object with encryptor/decryptor.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestReceivePipelinedWithEncryption()
|
||||
{
|
||||
this.TestReceivePipelined(socket =>
|
||||
{
|
||||
var socketConnection = SocketConnection.Create(socket);
|
||||
return new Connection(socketConnection, new PipelinedDecryptor(socketConnection.Input), new PipelinedEncryptor(socketConnection.Output), new NullLogger<Connection>());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the connection is disconnected after sending invalid data.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestDisconnectOnInvalidHeaderSentAsync()
|
||||
{
|
||||
IConnection? connection = null;
|
||||
var server = new TcpListener(IPAddress.Any, 5000);
|
||||
server.Start();
|
||||
try
|
||||
{
|
||||
server.BeginAcceptSocket(
|
||||
asyncResult =>
|
||||
{
|
||||
var clientSocket = server.EndAcceptSocket(asyncResult);
|
||||
var socketConnection = SocketConnection.Create(clientSocket);
|
||||
connection = new Connection(socketConnection, new PipelinedDecryptor(socketConnection.Input), new PipelinedEncryptor(socketConnection.Output), new NullLogger<Connection>());
|
||||
}, null);
|
||||
|
||||
using var client = new TcpClient("127.0.0.1", 5000);
|
||||
while (connection == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
#pragma warning disable 4014
|
||||
connection.BeginReceiveAsync();
|
||||
#pragma warning restore 4014
|
||||
|
||||
var packet = new byte[22222];
|
||||
packet[0] = 0xDE;
|
||||
packet[1] = 0xAD;
|
||||
packet[2] = 0xBE;
|
||||
packet[3] = 0xAF;
|
||||
await connection.Output.WriteAsync(packet).ConfigureAwait(false);
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
|
||||
Assert.That(connection.Connected, Is.False);
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the connection is disconnected after receiving invalid data.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDisconnectOnInvalidHeaderReceived()
|
||||
{
|
||||
var server = new TcpListener(IPAddress.Any, 5000);
|
||||
server.Start();
|
||||
try
|
||||
{
|
||||
server.BeginAcceptSocket(
|
||||
asyncResult =>
|
||||
{
|
||||
var clientSocket = server.EndAcceptSocket(asyncResult);
|
||||
var packet = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0, 0, 0 };
|
||||
clientSocket.BeginSend(packet, 0, packet.Length, SocketFlags.None, null, null);
|
||||
}, null);
|
||||
|
||||
using var client = new TcpClient("127.0.0.1", 5000);
|
||||
var socketConnection = SocketConnection.Create(client.Client);
|
||||
var connection = new Connection(socketConnection, new PipelinedDecryptor(socketConnection.Input), new PipelinedEncryptor(socketConnection.Output), new NullLogger<Connection>());
|
||||
|
||||
_ = connection.BeginReceiveAsync();
|
||||
|
||||
Thread.Sleep(100);
|
||||
Assert.That(connection.Connected, Is.False);
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the receiving of data with any <see cref="IConnection"/> implementation.
|
||||
/// </summary>
|
||||
/// <param name="connectionCreator">The connection creator.</param>
|
||||
private void TestReceivePipelined(Func<Socket, IConnection> connectionCreator)
|
||||
{
|
||||
const int maximumPacketCount = 1000;
|
||||
|
||||
IConnection? connection = null;
|
||||
var server = new TcpListener(IPAddress.Any, 5000);
|
||||
server.Start();
|
||||
server.BeginAcceptSocket(
|
||||
asyncResult =>
|
||||
{
|
||||
var clientSocket = server.EndAcceptSocket(asyncResult);
|
||||
connection = connectionCreator(clientSocket);
|
||||
}, null);
|
||||
using (var client = new TcpClient("127.0.0.1", 5000))
|
||||
{
|
||||
while (connection == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
int packetCount = 0;
|
||||
connection.PacketReceived += async p => Interlocked.Increment(ref packetCount);
|
||||
_ = connection.BeginReceiveAsync();
|
||||
|
||||
var packet = new byte[] { 0xC1, 10, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
for (int i = 0; i < maximumPacketCount; i++)
|
||||
{
|
||||
client.Client.BeginSend(packet, 0, packet.Length, SocketFlags.None, null, null);
|
||||
}
|
||||
|
||||
while (packetCount < maximumPacketCount)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
server.Stop();
|
||||
}
|
||||
}
|
||||
96
tests/MUnique.OpenMU.Pathfinding.Tests/BinaryMinHeapTest.cs
Normal file
96
tests/MUnique.OpenMU.Pathfinding.Tests/BinaryMinHeapTest.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// <copyright file="BinaryMinHeapTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.Tests;
|
||||
|
||||
using System.Diagnostics;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Tests the <see cref="BinaryMinHeap{T}"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
internal class BinaryMinHeapTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if the heap pops the node with the lowest cost.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MinHeapWithNodes()
|
||||
{
|
||||
var heap = new BinaryMinHeap<Node>(new NodeComparer());
|
||||
foreach (var number in Enumerable.Range(1, 10).Reverse())
|
||||
{
|
||||
heap.Push(new Node { PredictedTotalCost = number });
|
||||
}
|
||||
|
||||
var minimumNumber = heap.Pop().PredictedTotalCost;
|
||||
Assert.That(minimumNumber, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the heap pops the lowest value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MinHeapWithNumbers()
|
||||
{
|
||||
var heap = new BinaryMinHeap<int>();
|
||||
foreach (var number in Enumerable.Range(1, 10).Reverse())
|
||||
{
|
||||
heap.Push(number);
|
||||
}
|
||||
|
||||
var minimumNumber = heap.Pop();
|
||||
Assert.That(minimumNumber, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the performance between <see cref="BinaryMinHeap{T}"/> and <see cref="IndexedLinkedList{T}"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PerformanceComparison()
|
||||
{
|
||||
var simpleHeap = new BinaryMinHeap<Node>(new NodeComparer());
|
||||
var indexedHeap = new IndexedLinkedList<Node>(new NodeComparer(), new NodeIndexer());
|
||||
var count = 1000;
|
||||
this.PushNodes(simpleHeap, count);
|
||||
this.PushNodes(indexedHeap, count);
|
||||
var pushSimple = this.PushNodes(simpleHeap, count);
|
||||
var pushIndexed = this.PushNodes(indexedHeap, count);
|
||||
this.PopNodes(simpleHeap, count);
|
||||
this.PopNodes(indexedHeap, count);
|
||||
var popSimple = this.PopNodes(simpleHeap, count);
|
||||
var popIndexed = this.PopNodes(indexedHeap, count);
|
||||
|
||||
Assert.Pass($"(PUSH) BinaryMinHeap: {pushSimple.Elapsed}, IndexedLinkedList: {pushIndexed.Elapsed}\r\n(POP) BinaryMinHeap: {popSimple.Elapsed}, IndexedLinkedList: {popIndexed.Elapsed}");
|
||||
}
|
||||
|
||||
private Stopwatch PopNodes(IPriorityQueue<Node> queue, int count)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
queue.Pop();
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
return stopwatch;
|
||||
}
|
||||
|
||||
private Stopwatch PushNodes(IPriorityQueue<Node> queue, int count)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
var randomizer = new Random(1);
|
||||
foreach (var unused in Enumerable.Range(1, count))
|
||||
{
|
||||
var node = new Node { PredictedTotalCost = randomizer.Next(1, count) };
|
||||
queue.Push(node);
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
return stopwatch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<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.Pathfinding.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.Pathfinding.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="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Pathfinding\MUnique.OpenMU.Pathfinding.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
27
tests/MUnique.OpenMU.Pathfinding.Tests/NodeComparerTest.cs
Normal file
27
tests/MUnique.OpenMU.Pathfinding.Tests/NodeComparerTest.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="NodeComparerTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.Tests;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Tests the <see cref="NodeComparer"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
internal class NodeComparerTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if <see cref="NodeComparer.Compare(Node, Node)"/> does return the
|
||||
/// correct value when comparing two different nodes with different costs.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Compare()
|
||||
{
|
||||
var node1 = new Node { PredictedTotalCost = 1 };
|
||||
var node2 = new Node { PredictedTotalCost = 2 };
|
||||
var comparer = new NodeComparer();
|
||||
Assert.That(comparer.Compare(node1, node2), Is.Negative);
|
||||
}
|
||||
}
|
||||
121
tests/MUnique.OpenMU.Pathfinding.Tests/PathFinderTest.cs
Normal file
121
tests/MUnique.OpenMU.Pathfinding.Tests/PathFinderTest.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
// <copyright file="PathFinderTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.Tests;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the pathfinder.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PathFinderTest
|
||||
{
|
||||
private IPathFinder _pathFinder = null!;
|
||||
private byte[,] _grid = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the path finder with a basic, unrestricted grid.
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
this._grid = new byte[0x100, 0x100];
|
||||
for (int x = 100; x < 200; x++)
|
||||
{
|
||||
for (int y = 100; y < 200; y++)
|
||||
{
|
||||
this._grid[x, y] = 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Safezone:
|
||||
for (int x = 50; x < 100; x++)
|
||||
{
|
||||
for (int y = 50; y < 100; y++)
|
||||
{
|
||||
this._grid[x, y] = 0b1000_0001;
|
||||
}
|
||||
}
|
||||
|
||||
this._pathFinder = new PathFinder(new ScopedGridNetwork());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the straight path.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestStraightPath()
|
||||
{
|
||||
var start = new Point(110, 100);
|
||||
var end = new Point(115, 100);
|
||||
var result = this._pathFinder.FindPath(start, end, this._grid, false);
|
||||
Assert.That(result, Is.Not.Null);
|
||||
var lastNode = result!.LastOrDefault();
|
||||
Assert.That(lastNode, Is.Not.Null);
|
||||
Assert.That(lastNode.X, Is.EqualTo(end.X));
|
||||
Assert.That(lastNode.Y, Is.EqualTo(end.Y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the straight path.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestStraightPath_InSafezone()
|
||||
{
|
||||
var start = new Point(51, 60);
|
||||
var end = new Point(60, 60);
|
||||
var result = this._pathFinder.FindPath(start, end, this._grid, true);
|
||||
Assert.That(result, Is.Not.Null);
|
||||
var lastNode = result!.LastOrDefault();
|
||||
Assert.That(lastNode, Is.Not.Null);
|
||||
Assert.That(lastNode.X, Is.EqualTo(end.X));
|
||||
Assert.That(lastNode.Y, Is.EqualTo(end.Y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the straight path.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestStraightPath_InSafezone_ButNotIncluded()
|
||||
{
|
||||
var start = new Point(51, 60);
|
||||
var end = new Point(60, 60);
|
||||
var result = this._pathFinder.FindPath(start, end, this._grid, false);
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the diagonal path.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDiagonalPath()
|
||||
{
|
||||
var start = new Point(100, 100);
|
||||
var end = new Point(110, 110);
|
||||
var result = this._pathFinder.FindPath(start, end, this._grid, false);
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result!.Count, Is.EqualTo(10));
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
Assert.That(result[i - 1].X, Is.EqualTo(start.X + i));
|
||||
Assert.That(result[i - 1].Y, Is.EqualTo(start.Y + i));
|
||||
}
|
||||
|
||||
Assert.That(result.Last().X, Is.EqualTo(end.X));
|
||||
Assert.That(result.Last().Y, Is.EqualTo(end.Y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if no path can be found if the end is on an unreachable coordinate.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestNoPathFound()
|
||||
{
|
||||
var start = new Point(110, 100);
|
||||
var end = new Point(115, 99);
|
||||
var result = this._pathFinder.FindPath(start, end, this._grid, false);
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
}
|
||||
@@ -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.Pathfinding.Tests")]
|
||||
@@ -0,0 +1,108 @@
|
||||
// <copyright file="JsonQueryBuilderTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.Initialization.Tests;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using Account = MUnique.OpenMU.Persistence.EntityFramework.Model.Account;
|
||||
using GameConfiguration = MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="JsonQueryBuilder"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
internal class JsonQueryBuilderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets up this instance.
|
||||
/// </summary>
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
ConnectionConfigurator.Initialize(new ConfigFileDatabaseConnectionStringProvider());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the json query builder for the <see cref="GameConfiguration"/> type.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void JsonQueryBuilderGameConfiguration()
|
||||
{
|
||||
using var installationContext = new ConfigurationContext();
|
||||
var type = installationContext.Model.GetEntityTypes().FirstOrDefault(t => t.ClrType == typeof(GameConfiguration));
|
||||
string result;
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
try
|
||||
{
|
||||
var builder = new GameConfigurationJsonQueryBuilder();
|
||||
result = builder.BuildJsonQueryForEntity(type!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
}
|
||||
|
||||
result = $"-- Json query created in {stopwatch.ElapsedMilliseconds} ms:{Environment.NewLine}" + result;
|
||||
//// File.WriteAllText(@"C:\temp\json_GameConfiguration.txt", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the json query builder for the <see cref="Account"/> type.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void JsonQueryBuilderAccount()
|
||||
{
|
||||
using var installationContext = new ConfigurationContext();
|
||||
var type = installationContext.Model.GetEntityTypes().FirstOrDefault(t => t.ClrType == typeof(Account));
|
||||
string result;
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
try
|
||||
{
|
||||
var builder = new JsonQueryBuilder();
|
||||
result = builder.BuildJsonQueryForEntity(type!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
}
|
||||
|
||||
result = $"-- Json query created in {stopwatch.ElapsedMilliseconds} ms:{Environment.NewLine}" + result;
|
||||
//// File.WriteAllText(@"C:\temp\json_Account.txt", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the <see cref="GameConfiguration"/> using the <see cref="JsonQueryBuilder"/> and the <see cref="JsonObjectLoader"/>.
|
||||
/// It always fails, because it reports the taken time.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Ignore("It hits the database.")]
|
||||
public async Task LoadConfigByJsonAsync()
|
||||
{
|
||||
await using var installationContext = new ConfigurationContext();
|
||||
installationContext.Database.OpenConnection();
|
||||
var builder = new GameConfigurationJsonObjectLoader();
|
||||
IEnumerable<GameConfiguration> result;
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
try
|
||||
{
|
||||
result = await builder.LoadAllObjectsAsync<EntityFramework.Model.GameConfiguration>(installationContext).ConfigureAwait(false);
|
||||
result = result.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
}
|
||||
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.Not.EqualTo(0));
|
||||
Assert.That(stopwatch.ElapsedMilliseconds, Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
@@ -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.Persistence.Initialization.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.Persistence.Initialization.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>
|
||||
<Content Include="..\..\src\Persistence\EntityFramework\ConnectionSettings.xml" Link="ConnectionSettings.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<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\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" />
|
||||
<ProjectReference Include="..\..\src\Persistence\EntityFramework\MUnique.OpenMU.Persistence.EntityFramework.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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.Persistence.Initialization.EntityFramework")]
|
||||
@@ -0,0 +1,185 @@
|
||||
// <copyright file="TestInitializationWithEfCore.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.Initialization.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MUnique.OpenMU.DataModel;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.Persistence.Initialization.Updates;
|
||||
using MUnique.OpenMU.Persistence.InMemory;
|
||||
|
||||
/// <summary>
|
||||
/// The main program class.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
internal class TestInitializationWithEfCore
|
||||
{
|
||||
private const byte IcarusMapNumber = 10;
|
||||
private static readonly Guid FeatherDropGroupId = new(0x200, IcarusMapNumber, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
private static readonly Guid CrestDropGroupId = new(0x200, IcarusMapNumber, 2, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Tests the data initialization using the entity framework core.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Ignore("This is not a real test which should run automatically.")]
|
||||
public async Task SetupDatabaseAndTestLoadingDataAsync()
|
||||
{
|
||||
var manager = new PersistenceContextProvider(new NullLoggerFactory(), null);
|
||||
using var update = await manager.ReCreateDatabaseAsync().ConfigureAwait(false);
|
||||
await this.TestDataInitializationAsync(new PersistenceContextProvider(new NullLoggerFactory(), null)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the data initialization using the in-memory persistence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestDataInitializationInMemoryAsync()
|
||||
{
|
||||
await this.TestDataInitializationAsync(new InMemoryPersistenceContextProvider()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the data initialization using the in-memory persistence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSeason6DataAsync()
|
||||
{
|
||||
var contextProvider = new InMemoryPersistenceContextProvider();
|
||||
var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
||||
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
||||
await this.AssertIcarusFeatherAndCrestDropGroupsAsync(contextProvider).ConfigureAwait(false);
|
||||
await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that applying the update for Crest of Monarch in Season 6 is idempotent.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSeason6CrestOfMonarchUpdatePlugInAsync()
|
||||
{
|
||||
var contextProvider = new InMemoryPersistenceContextProvider();
|
||||
var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
||||
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
||||
|
||||
using var context = contextProvider.CreateNewContext();
|
||||
var gameConfiguration = (await context.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
||||
var map = gameConfiguration.Maps.First(m => m.Number == IcarusMapNumber && m.Discriminator == 0);
|
||||
|
||||
if (gameConfiguration.DropItemGroups.FirstOrDefault(group => group.GetId() == CrestDropGroupId) is { } existingCrestGroup)
|
||||
{
|
||||
map.DropItemGroups.Remove(existingCrestGroup);
|
||||
gameConfiguration.DropItemGroups.Remove(existingCrestGroup);
|
||||
}
|
||||
|
||||
var update = new AddCrestOfMonarchDropGroupUpdateSeason6();
|
||||
await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false);
|
||||
await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false);
|
||||
|
||||
var groups = gameConfiguration.DropItemGroups.Where(group => group.GetId() == CrestDropGroupId).ToList();
|
||||
Assert.That(groups, Has.Count.EqualTo(1));
|
||||
Assert.That(map.DropItemGroups.Count(group => group.GetId() == CrestDropGroupId), Is.EqualTo(1));
|
||||
Assert.That(groups[0].Chance, Is.EqualTo(0.001));
|
||||
Assert.That(groups[0].MinimumMonsterLevel, Is.EqualTo((byte)82));
|
||||
Assert.That(groups[0].ItemLevel, Is.EqualTo((byte)1));
|
||||
Assert.That(groups[0].PossibleItems, Has.Count.EqualTo(1));
|
||||
Assert.That(groups[0].PossibleItems.Single().Group, Is.EqualTo((byte)13));
|
||||
Assert.That(groups[0].PossibleItems.Single().Number, Is.EqualTo((short)14));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the data initialization using the in-memory persistence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Test075DataAsync()
|
||||
{
|
||||
var contextProvider = new InMemoryPersistenceContextProvider();
|
||||
var dataInitialization = new Version075.DataInitialization(contextProvider, new NullLoggerFactory());
|
||||
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
||||
await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the data initialization using the in-memory persistence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Test095dDataAsync()
|
||||
{
|
||||
var contextProvider = new InMemoryPersistenceContextProvider();
|
||||
var dataInitialization = new Version095d.DataInitialization(contextProvider, new NullLoggerFactory());
|
||||
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
||||
await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task TestDataInitializationAsync(IPersistenceContextProvider contextProvider)
|
||||
{
|
||||
var initialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
||||
await initialization.CreateInitialDataAsync(3, true).ConfigureAwait(false);
|
||||
|
||||
// Loading game configuration
|
||||
using var context = contextProvider.CreateNewConfigurationContext();
|
||||
var gameConfiguraton = (await context.GetAsync<DataModel.Configuration.GameConfiguration>().ConfigureAwait(false)).FirstOrDefault();
|
||||
Assert.That(gameConfiguraton, Is.Not.Null);
|
||||
|
||||
// Testing loading of an account
|
||||
using var accountContext = contextProvider.CreateNewPlayerContext(gameConfiguraton!);
|
||||
var account1 = await accountContext.GetAccountByLoginNameAsync("test1", "test1").ConfigureAwait(false);
|
||||
Assert.That(account1, Is.Not.Null);
|
||||
Assert.That(account1!.LoginName, Is.EqualTo("test1"));
|
||||
}
|
||||
|
||||
private async Task TestIfItemsFitIntoInventoriesAsync(IPersistenceContextProvider contextProvider)
|
||||
{
|
||||
using var configContext = contextProvider.CreateNewConfigurationContext();
|
||||
var config = (await configContext.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
||||
|
||||
using var context = contextProvider.CreateNewPlayerContext(config);
|
||||
var characters = (await context.GetAccountsOrderedByLoginNameAsync(0, 100).ConfigureAwait(false)).SelectMany(a => a.Characters).ToList();
|
||||
Assert.That(characters, Is.Not.Empty);
|
||||
byte inventorySize = (byte)(InventoryConstants.EquippableSlotsCount + 64);
|
||||
foreach (var character in characters)
|
||||
{
|
||||
try
|
||||
{
|
||||
var storage = character.Inventory!;
|
||||
var inventory = new Storage(inventorySize, InventoryConstants.EquippableSlotsCount, 0, storage);
|
||||
Assert.That(inventory.Items.Count(), Is.EqualTo(storage.Items.Count));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Warn($"{ex.Message} Character: {character.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AssertIcarusFeatherAndCrestDropGroupsAsync(IPersistenceContextProvider contextProvider)
|
||||
{
|
||||
using var context = contextProvider.CreateNewConfigurationContext();
|
||||
var gameConfiguration = (await context.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
||||
var map = gameConfiguration.Maps.First(m => m.Number == IcarusMapNumber && m.Discriminator == 0);
|
||||
|
||||
var featherGroup = gameConfiguration.DropItemGroups.Single(group => group.GetId() == FeatherDropGroupId);
|
||||
var crestGroup = gameConfiguration.DropItemGroups.Single(group => group.GetId() == CrestDropGroupId);
|
||||
|
||||
Assert.That(map.DropItemGroups.Count(group => group.GetId() == FeatherDropGroupId), Is.EqualTo(1));
|
||||
Assert.That(map.DropItemGroups.Count(group => group.GetId() == CrestDropGroupId), Is.EqualTo(1));
|
||||
|
||||
Assert.That(featherGroup.Chance, Is.EqualTo(0.001));
|
||||
Assert.That(featherGroup.MinimumMonsterLevel, Is.EqualTo((byte)82));
|
||||
Assert.That(featherGroup.ItemLevel, Is.Null);
|
||||
Assert.That(featherGroup.PossibleItems, Has.Count.EqualTo(1));
|
||||
Assert.That(featherGroup.PossibleItems.Single().Group, Is.EqualTo((byte)13));
|
||||
Assert.That(featherGroup.PossibleItems.Single().Number, Is.EqualTo((short)14));
|
||||
|
||||
Assert.That(crestGroup.Chance, Is.EqualTo(0.001));
|
||||
Assert.That(crestGroup.MinimumMonsterLevel, Is.EqualTo((byte)82));
|
||||
Assert.That(crestGroup.ItemLevel, Is.EqualTo((byte)1));
|
||||
Assert.That(crestGroup.PossibleItems, Has.Count.EqualTo(1));
|
||||
Assert.That(crestGroup.PossibleItems.Single().Group, Is.EqualTo((byte)13));
|
||||
Assert.That(crestGroup.PossibleItems.Single().Number, Is.EqualTo((short)14));
|
||||
}
|
||||
}
|
||||
128
tests/MUnique.OpenMU.PlugIns.Tests/CustomPlugInContainerTest.cs
Normal file
128
tests/MUnique.OpenMU.PlugIns.Tests/CustomPlugInContainerTest.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
// <copyright file="CustomPlugInContainerTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="PlugInManager"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class CustomPlugInContainerTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if creating the plugin container with an interface without a <see cref="CustomPlugInContainerAttribute"/> throws an exception.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CreatingContainerWithNonMarkedTypeThrowsException()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
var mock = new Mock<CustomPlugInContainerBase<ITestCustomPlugIn>>(manager);
|
||||
var exception = Assert.Throws<TargetInvocationException>(() =>
|
||||
{
|
||||
_ = mock.Object;
|
||||
});
|
||||
|
||||
Assert.That(exception?.InnerException, Is.InstanceOf<ArgumentException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a plugin can be retrieved from the custom container, when the plugin has been registered after the container was created.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetPlugInFromCustomContainerWithRegisteredPlugInAfterRegistration()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
var container = new CustomTestPlugInContainer(manager);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn>();
|
||||
|
||||
var plugIn = container.GetPlugIn<ITestCustomPlugIn>();
|
||||
Assert.That(plugIn, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a plugin can be retrieved from the custom container, when the plugin has been registered before the container was created.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetPlugInFromCustomContainerWithInitiallyRegisteredPlugIn()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn>();
|
||||
var container = new CustomTestPlugInContainer(manager);
|
||||
var plugIn = container.GetPlugIn<ITestCustomPlugIn>();
|
||||
Assert.That(plugIn, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a plugin can't be retrieved from the custom container when the plugin has been deactivated before.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DontGetPlugInFromCustomContainerAfterDeactivation()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn>();
|
||||
var container = new CustomTestPlugInContainer(manager);
|
||||
manager.DeactivatePlugIn<TestCustomPlugIn>();
|
||||
var plugIn = container.GetPlugIn<ITestCustomPlugIn>();
|
||||
Assert.That(plugIn, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a plugin can't be retrieved from the custom container when the plugin isn't suitable for the container and therefore not effective.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DontGetPlugInFromCustomContainerIfItDoesntSuit()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
var container = new CustomTestPlugInContainer(manager) { CreateNewPlugIns = false };
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn>();
|
||||
var plugIn = container.GetPlugIn<ITestCustomPlugIn>();
|
||||
Assert.That(plugIn, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the custom container replaces the plugin implementation if a new (more suitable) plugin is registered.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ReplacePlugInAtCustomContainer()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn>();
|
||||
var container = new CustomTestPlugInContainer(manager);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn2>();
|
||||
var plugIn = container.GetPlugIn<ITestCustomPlugIn>();
|
||||
Assert.That(plugIn, Is.InstanceOf<TestCustomPlugIn2>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a plugin which got deactivated by another plugin, gets reactivated as soon as the other plugin gets deactivated.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ReactivatePlugInAtCustomContainer()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn>();
|
||||
var container = new CustomTestPlugInContainer(manager);
|
||||
manager.RegisterPlugIn<ITestCustomPlugIn, TestCustomPlugIn2>();
|
||||
manager.DeactivatePlugIn<TestCustomPlugIn2>();
|
||||
var plugIn = container.GetPlugIn<ITestCustomPlugIn>();
|
||||
Assert.That(plugIn, Is.InstanceOf<TestCustomPlugIn>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a plugin can be retrieved with both of its implemented interfaces.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetPlugInFromCustomContainerWithAllImplementedInterfaces()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
|
||||
var container = new CustomTestPlugInContainer(manager);
|
||||
container.AddPlugIn(new TestCustomPlugIn2(), true);
|
||||
Assert.That(container.GetPlugIn<ITestCustomPlugIn>(), Is.Not.Null);
|
||||
Assert.That(container.GetPlugIn<IAnotherCustomPlugIn>(), Is.Not.Null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// <copyright file="CustomTestPlugInContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A test implementation of a <see cref="CustomPlugInContainerBase{TPlugIn}"/>.
|
||||
/// </summary>
|
||||
public class CustomTestPlugInContainer : CustomPlugInContainerBase<ICustomTestPlugInContainer>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomTestPlugInContainer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The plugin manager which manages this instance.</param>
|
||||
public CustomTestPlugInContainer(PlugInManager manager)
|
||||
: base(manager)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance creates new plug ins in <see cref="CreatePlugInIfSuitable"/>.
|
||||
/// </summary>
|
||||
public bool CreateNewPlugIns { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void CreatePlugInIfSuitable(Type plugInType)
|
||||
{
|
||||
if (this.CreateNewPlugIns)
|
||||
{
|
||||
this.AddPlugIn((ITestCustomPlugIn)Activator.CreateInstance(plugInType)!, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ICustomTestPlugInContainer? DetermineEffectivePlugIn(Type interfaceType)
|
||||
{
|
||||
return this.ActivePlugIns.FirstOrDefault(interfaceType.IsInstanceOfType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsNewPlugInReplacingOld(ICustomTestPlugInContainer currentEffectivePlugIn, ICustomTestPlugInContainer activatedPlugIn)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
63
tests/MUnique.OpenMU.PlugIns.Tests/ExamplePlugIn.cs
Normal file
63
tests/MUnique.OpenMU.PlugIns.Tests/ExamplePlugIn.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
// <copyright file="ExamplePlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of the <see cref="IExamplePlugIn"/> which tells us if it got executed.
|
||||
/// </summary>
|
||||
/// <seealso cref="IExamplePlugIn" />
|
||||
[Guid("9FCA692F-2BD5-4310-8755-E20761F94180")]
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(ExamplePlugIn), Description = "Just an example plugin.")]
|
||||
internal class ExamplePlugIn : IExamplePlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin instance was executed in the test.
|
||||
/// </summary>
|
||||
public bool WasExecuted { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DoStuff(Player player, string text, MyEventArgs args)
|
||||
{
|
||||
this.WasExecuted = true;
|
||||
args.WasExecuted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A plugin of a nested type.
|
||||
/// </summary>
|
||||
/// <seealso cref="IExamplePlugIn" />
|
||||
[Guid("B6D7E11D-E99D-4466-BAE1-87B043ED345D")]
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(NestedPlugIn), Description = "A nested example plugin.")]
|
||||
internal class NestedPlugIn : IExamplePlugIn
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void DoStuff(Player player, string text, MyEventArgs args)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A plugin of a nested type which doesn't have a guid.
|
||||
/// </summary>
|
||||
/// <seealso cref="IExamplePlugIn" />
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(NestedPlugIn), Description = "A nested example plugin without Guid.")]
|
||||
internal class NestedWithoutGuid : IExamplePlugIn
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void DoStuff(Player player, string text, MyEventArgs args)
|
||||
{
|
||||
// does nothing, too
|
||||
}
|
||||
}
|
||||
}
|
||||
40
tests/MUnique.OpenMU.PlugIns.Tests/ExampleStrategyPlugIn.cs
Normal file
40
tests/MUnique.OpenMU.PlugIns.Tests/ExampleStrategyPlugIn.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// <copyright file="ExampleStrategyPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// A test strategy plugin.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.PlugIns.Tests.IExampleStrategyPlugIn" />
|
||||
[Guid("69A6FCD1-E828-4841-BE91-E064231ED7B9")]
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(ExampleStrategyPlugIn), Description = "A test strategy plugin.")]
|
||||
public class ExampleStrategyPlugIn : IExampleStrategyPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the command key which is handled by this strategy plugin type.
|
||||
/// </summary>
|
||||
public static string CommandKey => "/mytest";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => CommandKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handled command.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The handled command.
|
||||
/// </value>
|
||||
public string? HandledCommand { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void HandleCommand(string command)
|
||||
{
|
||||
this.HandledCommand = command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// <copyright file="ICustomTestPlugInContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// A common interface for all plugins managed by the <see cref="CustomTestPlugInContainer"/>.
|
||||
/// </summary>
|
||||
[CustomPlugInContainer("test custom container interface", "")]
|
||||
[Guid("AD127356-FF4D-47EE-9E36-52DB0C2881B6")]
|
||||
public interface ICustomTestPlugInContainer
|
||||
{
|
||||
}
|
||||
24
tests/MUnique.OpenMU.PlugIns.Tests/IExamplePlugIn.cs
Normal file
24
tests/MUnique.OpenMU.PlugIns.Tests/IExamplePlugIn.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// <copyright file="IExamplePlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Example interface for a plugin.
|
||||
/// </summary>
|
||||
[Guid("34AEED37-9D62-4AE1-9320-91BB620B39C2")]
|
||||
[PlugInPoint("Example PlugIn Point", "This plugin point is an example.")]
|
||||
public interface IExamplePlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Does some stuff.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="args">The <see cref="MyEventArgs"/> instance containing the event data.</param>
|
||||
void DoStuff(Player player, string text, MyEventArgs args);
|
||||
}
|
||||
21
tests/MUnique.OpenMU.PlugIns.Tests/IExampleStrategyPlugIn.cs
Normal file
21
tests/MUnique.OpenMU.PlugIns.Tests/IExampleStrategyPlugIn.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="IExampleStrategyPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an example strategy plugin.
|
||||
/// </summary>
|
||||
[Guid("1E68B14C-9156-448A-A6AB-90E423A8E91C")]
|
||||
[PlugInPoint("Strategy Plugin Test Interface", "A strategy plugin test interface")]
|
||||
public interface IExampleStrategyPlugIn : IStrategyPlugIn<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command.</param>
|
||||
void HandleCommand(string command);
|
||||
}
|
||||
19
tests/MUnique.OpenMU.PlugIns.Tests/ITestCustomPlugIn.cs
Normal file
19
tests/MUnique.OpenMU.PlugIns.Tests/ITestCustomPlugIn.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// <copyright file="ITestCustomPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A plugin interface for specific implementations for plugins managed by the <see cref="CustomTestPlugInContainer"/>.
|
||||
/// </summary>
|
||||
public interface ITestCustomPlugIn : ICustomTestPlugInContainer
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A plugin interface for specific implementations for plugins managed by the <see cref="CustomTestPlugInContainer"/>.
|
||||
/// </summary>
|
||||
public interface IAnotherCustomPlugIn : ICustomTestPlugInContainer
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<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.PlugIns.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.PlugIns.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="nunit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\GameLogic\MUnique.OpenMU.GameLogic.csproj" />
|
||||
<ProjectReference Include="..\MUnique.OpenMU.Tests\MUnique.OpenMU.Tests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
24
tests/MUnique.OpenMU.PlugIns.Tests/MyEventArgs.cs
Normal file
24
tests/MUnique.OpenMU.PlugIns.Tests/MyEventArgs.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// <copyright file="MyEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for <see cref="IExamplePlugIn.DoStuff"/> which tell us, if the plugin got executed.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.ComponentModel.CancelEventArgs" />
|
||||
public class MyEventArgs : CancelEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the plugin instance was executed in the test.
|
||||
/// </summary>
|
||||
public bool WasExecuted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a text.
|
||||
/// </summary>
|
||||
public string? Text { get; set; }
|
||||
}
|
||||
331
tests/MUnique.OpenMU.PlugIns.Tests/PlugInManagerTest.cs
Normal file
331
tests/MUnique.OpenMU.PlugIns.Tests/PlugInManagerTest.cs
Normal file
@@ -0,0 +1,331 @@
|
||||
// <copyright file="PlugInManagerTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel.Design;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MUnique.OpenMU.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="PlugInManager"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PlugInManagerTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests if registering a plugin type creates a proxy for it.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisteringPlugInCreatesProxy()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.RegisterPlugIn<IExamplePlugIn, ExamplePlugIn>();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
Assert.That(point, Is.InstanceOf<IExamplePlugIn>());
|
||||
Assert.That(point, Is.InstanceOf<IPlugInContainer<IExamplePlugIn>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if registered plugins are active by default.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask RegisteredPlugInsActiveByDefaultAsync()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var plugIn = new ExamplePlugIn();
|
||||
manager.RegisterPlugInAtPlugInPoint<IExamplePlugIn>(plugIn);
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(plugIn.WasExecuted, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if plugins can be deactivated and are not executed if they are.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask DeactivatingPlugInsAsync()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var plugIn = new ExamplePlugIn();
|
||||
manager.RegisterPlugInAtPlugInPoint<IExamplePlugIn>(plugIn);
|
||||
manager.DeactivatePlugIn<ExamplePlugIn>();
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(plugIn.WasExecuted, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if deactivating a deactivated plugin doesn't cause issues.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask DeactivatingDeactivatedPlugInAsync()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var plugIn = new ExamplePlugIn();
|
||||
manager.RegisterPlugInAtPlugInPoint<IExamplePlugIn>(plugIn);
|
||||
manager.DeactivatePlugIn<ExamplePlugIn>();
|
||||
manager.DeactivatePlugIn<ExamplePlugIn>();
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(plugIn.WasExecuted, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if activating an activated plugin doesn't cause issues.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask ActivatingActivatedPlugInAsync()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var plugIn = new ExamplePlugIn();
|
||||
manager.RegisterPlugInAtPlugInPoint<IExamplePlugIn>(plugIn);
|
||||
manager.ActivatePlugIn<ExamplePlugIn>();
|
||||
manager.ActivatePlugIn<ExamplePlugIn>();
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(plugIn.WasExecuted, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if deactivating a plugin doesn't affect another plugin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask DeactivatingOnePlugInDoesntAffectOthersAsync()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var plugIn = new ExamplePlugIn();
|
||||
manager.RegisterPlugInAtPlugInPoint<IExamplePlugIn>(plugIn);
|
||||
manager.RegisterPlugIn<IExamplePlugIn, ExamplePlugIn.NestedPlugIn>();
|
||||
manager.DeactivatePlugIn<ExamplePlugIn.NestedPlugIn>();
|
||||
manager.ActivatePlugIn<ExamplePlugIn.NestedPlugIn>();
|
||||
manager.DeactivatePlugIn<ExamplePlugIn.NestedPlugIn>();
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(plugIn.WasExecuted, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the plugins are in the correct active-state if they got created with a <see cref="PlugInConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <param name="active">If set to <c>true</c>, the <see cref="PlugInConfiguration"/> is configured to be active.</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async ValueTask CreatedAndActiveByConfigurationAsync(bool active)
|
||||
{
|
||||
var configuration = new PlugInConfiguration
|
||||
{
|
||||
TypeId = typeof(ExamplePlugIn).GUID,
|
||||
IsActive = active,
|
||||
};
|
||||
var manager = new PlugInManager(new List<PlugInConfiguration> { configuration }, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(args.WasExecuted, Is.EqualTo(active));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a custom plugin in a non-existing assembly is not created and throws no errors.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CustomPlugInByExternalAssemblyNotFoundDoesntThrowError()
|
||||
{
|
||||
var configuration = new PlugInConfiguration
|
||||
{
|
||||
TypeId = new Guid("D88B1ACA-42B7-4A89-B3E0-3C97AA4C8578"),
|
||||
IsActive = true,
|
||||
ExternalAssemblyName = "DoesNotExist.dll",
|
||||
};
|
||||
_ = new PlugInManager(new List<PlugInConfiguration> { configuration }, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if an unknown plugin in the configuration doesn't cause exceptions.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UnknownPlugInByConfigurationDoesntThrowError()
|
||||
{
|
||||
var configuration = new PlugInConfiguration
|
||||
{
|
||||
TypeId = new Guid("A9BDA3E2-4EB6-45C3-B234-37C1819C0CB6"),
|
||||
IsActive = true,
|
||||
};
|
||||
_ = new PlugInManager(new List<PlugInConfiguration> { configuration }, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if activating an unknown plugin doesn't cause exceptions.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ActivatingUnknownPlugInDoesNotThrowError()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.ActivatePlugIn(new Guid("4C38A813-F9BF-428A-8EA1-A6C90A87E583"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if deactivating an unknown plugin doesn't cause exceptions.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DeactivatingUnknownPlugInDoesNotThrowError()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.ActivatePlugIn(new Guid("4C38A813-F9BF-428A-8EA1-A6C90A87E583"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if plugins can be activated and are executed if they are.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask ActivatingPlugInsAsync()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
var plugIn = new ExamplePlugIn();
|
||||
manager.RegisterPlugInAtPlugInPoint<IExamplePlugIn>(plugIn);
|
||||
manager.DeactivatePlugIn<ExamplePlugIn>();
|
||||
manager.ActivatePlugIn<ExamplePlugIn>();
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
|
||||
var point = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
point!.DoStuff(player, command, args);
|
||||
Assert.That(plugIn.WasExecuted, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the automatic discovery of plugins of the loaded assemblies.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AutoDiscovery()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.DiscoverAndRegisterPlugIns();
|
||||
var examplePlugInPoint = manager.GetPlugInPoint<IExamplePlugIn>();
|
||||
Assert.That(examplePlugInPoint, Is.InstanceOf<IExamplePlugIn>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if registering a plug in without a unique identifier throws an error.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisteringPlugInWithoutGuidThrowsError()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
Assert.Throws<ArgumentException>(() => manager.RegisterPlugIn<IExamplePlugIn, ExamplePlugIn.NestedWithoutGuid>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the strategy provider is created for registered strategy plug in.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void StrategyProviderCreatedForRegisteredStrategyPlugIn()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.RegisterPlugIn<IExampleStrategyPlugIn, ExampleStrategyPlugIn>();
|
||||
|
||||
var strategyProvider = manager.GetStrategyProvider<string, IExampleStrategyPlugIn>();
|
||||
Assert.That(strategyProvider, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the strategy provider is not created when there is no registered strategy plug in yet.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void StrategyProviderNotCreatedWithoutRegisteredStrategyPlugIn()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
|
||||
var strategyProvider = manager.GetStrategyProvider<string, IExampleStrategyPlugIn>();
|
||||
Assert.That(strategyProvider, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the registered strategy plug in is available.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisteredStrategyPlugInAvailable()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.RegisterPlugIn<IExampleStrategyPlugIn, ExampleStrategyPlugIn>();
|
||||
|
||||
var strategy = manager.GetStrategy<IExampleStrategyPlugIn>(ExampleStrategyPlugIn.CommandKey);
|
||||
Assert.That(strategy, Is.Not.Null);
|
||||
Assert.That(strategy, Is.TypeOf<ExampleStrategyPlugIn>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the registered, but deactivated strategy plug in is not available.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DeactivatedStrategyPlugInNotAvailable()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.RegisterPlugIn<IExampleStrategyPlugIn, ExampleStrategyPlugIn>();
|
||||
manager.DeactivatePlugIn<ExampleStrategyPlugIn>();
|
||||
var strategy = manager.GetStrategy<IExampleStrategyPlugIn>(ExampleStrategyPlugIn.CommandKey);
|
||||
Assert.That(strategy, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if registering an already registered strategy plug in does not throw an error.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisteringRegisteredStrategyPlugInDoesntThrowError()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.RegisterPlugIn<IExampleStrategyPlugIn, ExampleStrategyPlugIn>();
|
||||
manager.RegisterPlugIn<IExampleStrategyPlugIn, ExampleStrategyPlugIn>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if no plug in point is created and returned for strategy plug ins.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void NoPlugInPointForStrategyPlugIn()
|
||||
{
|
||||
var manager = new PlugInManager(null, new NullLoggerFactory(), this.CreateServiceProvider(), null);
|
||||
manager.RegisterPlugIn<IExampleStrategyPlugIn, ExampleStrategyPlugIn>();
|
||||
Assert.That(manager.GetPlugInPoint<IExampleStrategyPlugIn>(), Is.Null);
|
||||
}
|
||||
|
||||
private IServiceProvider CreateServiceProvider()
|
||||
{
|
||||
var provider = new ServiceContainer();
|
||||
provider.AddService(typeof(ILoggerFactory), new NullLoggerFactory());
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// <copyright file="PlugInProxyTypeGeneratorTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel;
|
||||
using Nito.AsyncEx;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using MUnique.OpenMU.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="PlugInProxyTypeGenerator"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PlugInProxyTypeGeneratorTest
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface with an unsupported method signature.
|
||||
/// </summary>
|
||||
[PlugInPoint("Async test", "Bar")]
|
||||
public interface IAsyncPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// An async method.
|
||||
/// </summary>
|
||||
ValueTask MyMethodAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An interface with an unsupported method signature.
|
||||
/// </summary>
|
||||
[PlugInPoint("Foo", "Bar")]
|
||||
internal interface IUnsupportedPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsupported method.
|
||||
/// </summary>
|
||||
/// <returns>Some boolean.</returns>
|
||||
bool UnsupportedMethod();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the proxy creation for <see cref="IExamplePlugIn"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ProxyIsCreated()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
var proxy = generator.GenerateProxy<IExamplePlugIn>(new PlugInManager(null, new NullLoggerFactory(), null, null));
|
||||
|
||||
Assert.That(proxy, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if multiple plugins are executed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask MultiplePlugInsAreExecutedAsync()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
var proxy = generator.GenerateProxy<IExamplePlugIn>(new PlugInManager(null, NullLoggerFactory.Instance, null, null));
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
var firstMock = new Mock<IExamplePlugIn>();
|
||||
var secondMock = new Mock<IExamplePlugIn>();
|
||||
|
||||
firstMock.Setup(p => p.DoStuff(player, command, args)).Verifiable();
|
||||
secondMock.Setup(p => p.DoStuff(player, command, args)).Verifiable();
|
||||
proxy.AddPlugIn(firstMock.Object, true);
|
||||
proxy.AddPlugIn(secondMock.Object, true);
|
||||
|
||||
(proxy as IExamplePlugIn)?.DoStuff(player, command, args);
|
||||
|
||||
firstMock.VerifyAll();
|
||||
secondMock.VerifyAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if multiple plugins are executed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask MultipleAsyncPlugInsAreExecutedAsync()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
var proxy = generator.GenerateProxy<IAsyncPlugIn>(new PlugInManager(null, NullLoggerFactory.Instance, null, null));
|
||||
|
||||
// Forcing to load NitoEx
|
||||
_ = new AsyncReaderWriterLock();
|
||||
_ = new AwaitableDisposable<IDisposable>(Task.FromResult((IDisposable)null!));
|
||||
|
||||
var firstMock = new Mock<IAsyncPlugIn>();
|
||||
var secondMock = new Mock<IAsyncPlugIn>();
|
||||
|
||||
firstMock.Setup(p => p.MyMethodAsync()).Verifiable();
|
||||
secondMock.Setup(p => p.MyMethodAsync()).Verifiable();
|
||||
proxy.AddPlugIn(firstMock.Object, true);
|
||||
proxy.AddPlugIn(secondMock.Object, true);
|
||||
|
||||
await ((IAsyncPlugIn)proxy).MyMethodAsync().ConfigureAwait(false);
|
||||
|
||||
firstMock.VerifyAll();
|
||||
secondMock.VerifyAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if inactive plugins are not executed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask InactivePlugInsAreNotExecutedAsync()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
var proxy = generator.GenerateProxy<IExamplePlugIn>(new PlugInManager(null, NullLoggerFactory.Instance, null, null));
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
var firstMock = new Mock<IExamplePlugIn>();
|
||||
var secondMock = new Mock<IExamplePlugIn>();
|
||||
|
||||
secondMock.Setup(p => p.DoStuff(player, command, args)).Verifiable();
|
||||
proxy.AddPlugIn(firstMock.Object, false);
|
||||
proxy.AddPlugIn(secondMock.Object, true);
|
||||
|
||||
(proxy as IExamplePlugIn)?.DoStuff(player, command, args);
|
||||
|
||||
firstMock.VerifyAll();
|
||||
firstMock.VerifyNoOtherCalls();
|
||||
secondMock.VerifyAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if parameters of <see cref="CancelEventArgs"/> are respected, so that when <see cref="CancelEventArgs.Cancel"/> is <c>true</c>,
|
||||
/// next plugins are not executed anymore.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask CancelEventArgsAreRespectedAsync()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
var proxy = generator.GenerateProxy<IExamplePlugIn>(new PlugInManager(null, NullLoggerFactory.Instance, null, null));
|
||||
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
var command = "test";
|
||||
var args = new MyEventArgs();
|
||||
var firstMock = new Mock<IExamplePlugIn>();
|
||||
var secondMock = new Mock<IExamplePlugIn>();
|
||||
|
||||
firstMock.Setup(p => p.DoStuff(player, command, args)).Callback(() => args.Cancel = true).Verifiable();
|
||||
proxy.AddPlugIn(firstMock.Object, true);
|
||||
proxy.AddPlugIn(secondMock.Object, true);
|
||||
|
||||
(proxy as IExamplePlugIn)?.DoStuff(player, command, args);
|
||||
firstMock.VerifyAll();
|
||||
secondMock.VerifyAll();
|
||||
secondMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the proxy creation fails when a class is passed as proxy interface type.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorForClasses()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
Assert.Throws<ArgumentException>(() => generator.GenerateProxy<ExamplePlugIn>(new PlugInManager(null, new NullLoggerFactory(), null, null)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the proxy creation fails when an interface without <see cref="PlugInPointAttribute"/> is passed as proxy interface type.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorForInterfaceWithoutAttribute()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
Assert.Throws<ArgumentException>(() => generator.GenerateProxy<ICloneable>(new PlugInManager(null, new NullLoggerFactory(), null, null)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the proxy creation fails when an interface with an unsupported method signature is passed as proxy interface type.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorForInterfaceWithUnsupportedMethodSignature()
|
||||
{
|
||||
var generator = new PlugInProxyTypeGenerator();
|
||||
Assert.Throws<ArgumentException>(() => generator.GenerateProxy<IUnsupportedPlugIn>(new PlugInManager(null, new NullLoggerFactory(), null, null)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// <copyright file="PlugInTypeExtensionTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using MUnique.OpenMU.GameServer;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="PlugInTypeExtensions"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PlugInTypeExtensionTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests the maximum client version requirement.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConsiderMaximumClientVersion()
|
||||
{
|
||||
Assert.That(new ClientVersion(6, 3, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeUntilSeason1)), Is.False);
|
||||
Assert.That(new ClientVersion(1, 1, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeUntilSeason1)), Is.False);
|
||||
Assert.That(new ClientVersion(1, 0, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeUntilSeason1)), Is.True);
|
||||
Assert.That(new ClientVersion(0, 99, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeUntilSeason1)), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the minimum client version requirement.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConsiderMinimumClientVersion()
|
||||
{
|
||||
Assert.That(new ClientVersion(6, 3, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeAfterSeason2)), Is.True);
|
||||
Assert.That(new ClientVersion(2, 0, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeAfterSeason2)), Is.True);
|
||||
Assert.That(new ClientVersion(1, 255, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeAfterSeason2)), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the minimum and maximum client version requirements in combination.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConsiderMinimumAndMaximumClientVersion()
|
||||
{
|
||||
Assert.That(new ClientVersion(6, 3, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeBetweenSeason1And2)), Is.False);
|
||||
Assert.That(new ClientVersion(2, 0, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeBetweenSeason1And2)), Is.True);
|
||||
Assert.That(new ClientVersion(1, 0, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeBetweenSeason1And2)), Is.True);
|
||||
Assert.That(new ClientVersion(0, 255, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeBetweenSeason1And2)), Is.False);
|
||||
|
||||
Assert.That(new ClientVersion(1, 1, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeAtExactlySeason1)), Is.False);
|
||||
Assert.That(new ClientVersion(1, 0, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeAtExactlySeason1)), Is.True);
|
||||
Assert.That(new ClientVersion(0, 255, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInTypeAtExactlySeason1)), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if minimum and maximum client version requirements are not inherited from base classes.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DontConsiderInheritedAttributes()
|
||||
{
|
||||
Assert.That(new ClientVersion(1, 1, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInWithInheritedAttribute)), Is.True);
|
||||
Assert.That(new ClientVersion(0, 255, ClientLanguage.English).IsPlugInSuitable(typeof(PlugInWithInheritedAttribute)), Is.True);
|
||||
}
|
||||
|
||||
[MaximumClient(1, 0, ClientLanguage.Invariant)]
|
||||
private class PlugInTypeUntilSeason1
|
||||
{
|
||||
}
|
||||
|
||||
[MinimumClient(2, 0, ClientLanguage.Invariant)]
|
||||
private class PlugInTypeAfterSeason2
|
||||
{
|
||||
}
|
||||
|
||||
[MinimumClient(1, 0, ClientLanguage.Invariant)]
|
||||
[MaximumClient(2, 0, ClientLanguage.Invariant)]
|
||||
private class PlugInTypeBetweenSeason1And2
|
||||
{
|
||||
}
|
||||
|
||||
[MaximumClient(1, 0, ClientLanguage.Invariant)]
|
||||
[MinimumClient(1, 0, ClientLanguage.Invariant)]
|
||||
private class PlugInTypeAtExactlySeason1
|
||||
{
|
||||
}
|
||||
|
||||
private class PlugInWithInheritedAttribute : PlugInTypeAtExactlySeason1
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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.PlugIns.Tests")]
|
||||
16
tests/MUnique.OpenMU.PlugIns.Tests/TestCustomPlugIn.cs
Normal file
16
tests/MUnique.OpenMU.PlugIns.Tests/TestCustomPlugIn.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="TestCustomPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// A test implementation of <see cref="ITestCustomPlugIn"/>.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(TestCustomPlugIn))]
|
||||
[Guid("77CF382A-2F87-4642-889A-85BF6D76E218")]
|
||||
public class TestCustomPlugIn : ITestCustomPlugIn;
|
||||
18
tests/MUnique.OpenMU.PlugIns.Tests/TestCustomPlugIn2.cs
Normal file
18
tests/MUnique.OpenMU.PlugIns.Tests/TestCustomPlugIn2.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// <copyright file="TestCustomPlugIn2.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns.Tests;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// A second test implementation of <see cref="ITestCustomPlugIn"/>.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(TestCustomPlugIn2))]
|
||||
[Guid("9431C449-1F0C-47C1-BE5D-F9E356090DAB")]
|
||||
public class TestCustomPlugIn2 : ITestCustomPlugIn, IAnotherCustomPlugIn
|
||||
{
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user