baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props, $(MSBuildThisFileDirectory)../src))" />
</Project>

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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>

View File

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

View File

@@ -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);
}
}

View 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));
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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>

View 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);
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,30 @@
// <copyright file="MockViewPlugInContainer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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

View 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;
}
}

View 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>();
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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

View 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);
}
}
}

View 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>();
}
}

View File

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

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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="&lt;Parameter Name='resultFileName' Value='ClientToServerPacketTests.cs'/&gt;&lt;Parameter Name='subNamespace' Value='ClientToServer'/&gt;" />
<XslTransformation OutputPaths="ServerToClientPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ServerToClient\ServerToClientPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="&lt;Parameter Name='resultFileName' Value='ServerToClientPacketTests.cs'/&gt;&lt;Parameter Name='subNamespace' Value='ServerToClient'/&gt;" />
<XslTransformation OutputPaths="ChatServerPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ChatServer\ChatServerPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="&lt;Parameter Name='resultFileName' Value='ChatServerPacketTests.cs'/&gt;&lt;Parameter Name='subNamespace' Value='ChatServer'/&gt;" />
<XslTransformation OutputPaths="ConnectServerPacketTests.cs" XmlInputPaths="..\..\src\Network\Packets\ConnectServer\ConnectServerPackets.xml" XslInputPath="..\..\src\Network\Packets\GenerateTests.xslt" Parameters="&lt;Parameter Name='resultFileName' Value='ConnectServerPacketTests.cs'/&gt;&lt;Parameter Name='subNamespace' Value='ConnectServer'/&gt;" />
</Target>
</Project>

File diff suppressed because it is too large Load Diff

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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")));
}
}

View File

@@ -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>

View 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()]);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -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));
}
}

File diff suppressed because one or more lines are too long

View File

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

View 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();
}
}

View 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;
}
}

View File

@@ -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>

View 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);
}
}

View 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);
}
}

View File

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

View File

@@ -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));
}
}

View File

@@ -0,0 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
<DocumentationFile>bin\Debug\MUnique.OpenMU.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>

View File

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

View File

@@ -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));
}
}

View 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);
}
}

View File

@@ -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;
}
}

View 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
}
}
}

View 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;
}
}

View File

@@ -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
{
}

View 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);
}

View 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);
}

View 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
{
}

View File

@@ -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>

View 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; }
}

View 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;
}
}

View File

@@ -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)));
}
}

View File

@@ -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
{
}
}

View File

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

View 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;

View 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
{
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
// <copyright file="GameContextTestHelper.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests;
using Microsoft.Extensions.Logging.Abstractions;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Persistence.InMemory;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Helper functions to create test game contexts.
/// </summary>
public static class GameContextTestHelper
{
/// <summary>
/// Creates a game context.
/// </summary>
/// <returns>The game context with MuHelperFeaturePlugIn configured.</returns>
public static IGameContext CreateGameContext()
{
var contextProvider = new InMemoryPersistenceContextProvider();
var context = contextProvider.CreateNewContext();
var gameConfig = context.CreateNew<MUnique.OpenMU.Persistence.BasicModel.GameConfiguration>();
var mapDef = context.CreateNew<MUnique.OpenMU.Persistence.BasicModel.GameMapDefinition>();
mapDef.Number = 0;
mapDef.TerrainData = new byte[ushort.MaxValue + 3];
gameConfig.Maps.Add(mapDef);
gameConfig.MaximumPartySize = 5;
gameConfig.RecoveryInterval = int.MaxValue;
gameConfig.MaximumInventoryMoney = int.MaxValue;
gameConfig.ItemDropDuration = TimeSpan.FromMinutes(1);
var mapInitializer = new MapInitializer(gameConfig, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
var plugInConfigurations = new List<PlugInConfiguration>
{
new ()
{
TypeId = new Guid("E90A72C3-0459-4323-B6D3-171F88D35542"), // MuHelperFeaturePlugIn
IsActive = true,
},
};
var plugInManager = new PlugInManager(plugInConfigurations, new NullLoggerFactory(), null, null);
var gameContext = new GameContext(gameConfig, contextProvider, mapInitializer, new NullLoggerFactory(), plugInManager, NullDropGenerator.Instance, new ConfigurationChangeMediator());
mapInitializer.PlugInManager = gameContext.PlugInManager;
mapInitializer.PathFinderPool = gameContext.PathFinderPool;
return gameContext;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
// <copyright file="MockViewPlugInContainer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests;
using Moq;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// A view plugin container which automatically create mocks for requested view plugins.
/// </summary>
public class MockViewPlugInContainer : ICustomPlugInContainer<IViewPlugIn>
{
private readonly Dictionary<Type, IViewPlugIn> _mocks = new();
/// <inheritdoc />
public T GetPlugIn<T>()
where T : class, IViewPlugIn
{
if (!this._mocks.TryGetValue(typeof(T), out var mock))
{
mock = new Mock<T>().Object;
this._mocks.Add(typeof(T), mock);
}
return (T)mock;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More