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,31 @@
// <copyright file="AggregateType.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// The attribute aggregate type.
/// </summary>
public enum AggregateType
{
/// <summary>
/// Adds the value to the raw base value.
/// </summary>
AddRaw,
/// <summary>
/// Multiplicates the raw base value.
/// </summary>
Multiplicate,
/// <summary>
/// Adds the value to the final value.
/// </summary>
AddFinal,
/// <summary>
/// Adds only the highest available value to the raw base value (jewelry element resistance).
/// </summary>
Maximum,
}

View File

@@ -0,0 +1,128 @@
// <copyright file="AttributeDefinition.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// Defines and Identifies a Attribute.
/// In the future it may also contain additional data, like a maximum limit of the reachable value to do balancing.
/// </summary>
public class AttributeDefinition : IEquatable<AttributeDefinition>
{
/// <summary>
/// Initializes a new instance of the <see cref="AttributeDefinition"/> class.
/// </summary>
public AttributeDefinition()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AttributeDefinition"/> class.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="designation">The designation.</param>
/// <param name="description">The description.</param>
public AttributeDefinition(Guid id, string designation, string description)
{
this.Id = id;
this.Designation = designation;
this.Description = description;
}
/// <summary>
/// Gets or sets the identifier.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the designation.
/// </summary>
public string? Designation { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the maximum value of this attribute, if the value should be capped.
/// </summary>
public float? MaximumValue { get; set; }
/// <summary>
/// Implements the operator ==.
/// </summary>
/// <param name="lhs">The LHS.</param>
/// <param name="rhs">The RHS.</param>
/// <returns>
/// The result of the operator.
/// </returns>
public static bool operator ==(AttributeDefinition? lhs, AttributeDefinition? rhs)
{
if (ReferenceEquals(lhs, rhs))
{
return true;
}
if (lhs is null || rhs is null)
{
return false;
}
return lhs.Equals(rhs);
}
/// <summary>
/// Implements the operator !=.
/// </summary>
/// <param name="lhs">The LHS.</param>
/// <param name="rhs">The RHS.</param>
/// <returns>
/// The result of the operator.
/// </returns>
public static bool operator !=(AttributeDefinition? lhs, AttributeDefinition? rhs)
{
return !(lhs == rhs);
}
/// <summary>
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string? ToString()
{
return this.Designation;
}
/// <summary>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="object" /> to compare with this instance.</param>
/// <returns>
/// <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.
/// </returns>
public override bool Equals(object? obj)
{
return this.Equals(obj as AttributeDefinition);
}
/// <inheritdoc />
public bool Equals(AttributeDefinition? other)
{
return this.Id == other?.Id;
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}

View File

@@ -0,0 +1,162 @@
// <copyright file="AttributeRelationship.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
using System.Globalization;
/// <summary>
/// The operator which is applied between the input attribute and the input operand.
/// </summary>
public enum InputOperator
{
/// <summary>
/// The <see cref="AttributeRelationship.InputAttribute"/> is multiplied with the <see cref="AttributeRelationship.InputOperand"/> before effecting the <see cref="AttributeRelationship.TargetAttribute"/>.
/// </summary>
Multiply,
/// <summary>
/// The <see cref="AttributeRelationship.InputAttribute"/> is increased by the <see cref="AttributeRelationship.InputOperand"/> before effecting the <see cref="AttributeRelationship.TargetAttribute"/>.
/// </summary>
Add,
/// <summary>
/// The <see cref="AttributeRelationship.InputAttribute"/> is exponentiated by the <see cref="AttributeRelationship.InputOperand"/> before effecting the <see cref="AttributeRelationship.TargetAttribute"/>.
/// </summary>
Exponentiate,
/// <summary>
/// The <see cref="AttributeRelationship.InputOperand"/> is exponentiated by the <see cref="AttributeRelationship.InputAttribute"/> before effecting the <see cref="AttributeRelationship.TargetAttribute"/>.
/// </summary>
ExponentiateByAttribute,
/// <summary>
/// The maximum between <see cref="AttributeRelationship.InputAttribute"/> and <see cref="AttributeRelationship.InputOperand"/> is taken before effecting the <see cref="AttributeRelationship.TargetAttribute"/>.
/// </summary>
Maximum,
/// <summary>
/// The minimum between <see cref="AttributeRelationship.InputAttribute"/> and <see cref="AttributeRelationship.InputOperand"/> is taken before effecting the <see cref="AttributeRelationship.TargetAttribute"/>.
/// </summary>
Minimum,
}
/// <summary>
/// Describes a relationship between two attributes.
/// </summary>
public class AttributeRelationship
{
private AttributeDefinition? _targetAttribute;
private AttributeDefinition? _inputAttribute;
private AttributeDefinition? _operandAttribute;
/// <summary>
/// Initializes a new instance of the <see cref="AttributeRelationship"/> class.
/// </summary>
public AttributeRelationship()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AttributeRelationship" /> class.
/// </summary>
/// <param name="targetAttribute">The target attribute.</param>
/// <param name="inputOperand">The multiplier.</param>
/// <param name="inputAttribute">The input attribute.</param>
/// <param name="aggregateType">The type of the aggregate on the <paramref name="targetAttribute"/>.</param>
public AttributeRelationship(AttributeDefinition targetAttribute, float inputOperand, AttributeDefinition inputAttribute, AggregateType aggregateType = AggregateType.AddRaw)
: this(targetAttribute, inputOperand, inputAttribute, InputOperator.Multiply, default, aggregateType)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AttributeRelationship" /> class.
/// </summary>
/// <param name="targetAttribute">The target attribute.</param>
/// <param name="inputOperand">The multiplier.</param>
/// <param name="inputAttribute">The input attribute.</param>
/// <param name="aggregateType">The type of the aggregate on the <paramref name="targetAttribute"/>.</param>
public AttributeRelationship(AttributeDefinition targetAttribute, AttributeDefinition inputOperand, AttributeDefinition inputAttribute, AggregateType aggregateType = AggregateType.AddRaw)
: this(targetAttribute, 1, inputAttribute, InputOperator.Multiply, inputOperand, aggregateType)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AttributeRelationship" /> class.
/// </summary>
/// <param name="targetAttribute">The target attribute.</param>
/// <param name="inputOperand">The multiplier.</param>
/// <param name="inputAttribute">The input attribute.</param>
/// <param name="inputOperator">The input operator.</param>
/// <param name="operandAttribute">The operand attribute.</param>
/// <param name="aggregateType">The type of the aggregate on the <paramref name="targetAttribute"/>.</param>
public AttributeRelationship(AttributeDefinition targetAttribute, float inputOperand, AttributeDefinition inputAttribute, InputOperator inputOperator, AttributeDefinition? operandAttribute = null, AggregateType aggregateType = AggregateType.AddRaw)
{
this.InputOperand = inputOperand;
this.InputOperator = inputOperator;
this.AggregateType = aggregateType;
this._targetAttribute = targetAttribute;
this._inputAttribute = inputAttribute;
this._operandAttribute = operandAttribute;
}
/// <summary>
/// Gets or sets the target attribute which will be affected.
/// </summary>
public virtual AttributeDefinition? TargetAttribute
{
get => this._targetAttribute;
set => this._targetAttribute = value;
}
/// <summary>
/// Gets or sets the input attribute which provides the input value.
/// </summary>
public virtual AttributeDefinition? InputAttribute
{
get => this._inputAttribute;
set => this._inputAttribute = value;
}
/// <summary>
/// Gets or sets the operand attribute which replaces the <see cref="InputOperand"/>, if set.
/// </summary>
public virtual AttributeDefinition? OperandAttribute
{
get => this._operandAttribute;
set => this._operandAttribute = value;
}
/// <summary>
/// Gets or sets the operator which is applied between the input attribute and the input operand.
/// </summary>
public InputOperator InputOperator { get; set; }
/// <summary>
/// Gets or sets the operand which is applied to the input attribute before adding to the target attribute.
/// Has only effect, when <see cref="OperandAttribute"/> is <see langword="null"/>.
/// </summary>
public float InputOperand { get; set; }
/// <summary>
/// Gets or sets the aggregate type with which the relationship should effect the target attribute.
/// </summary>
public AggregateType AggregateType { get; set; }
/// <inheritdoc/>
public override string ToString()
{
if (this.TargetAttribute is null)
{
return $"{this.InputAttribute} {this.InputOperator.AsString()} {this.OperandAttribute?.ToString() ?? this.InputOperand.ToString(CultureInfo.InvariantCulture)}";
}
if (this.InputOperator == InputOperator.ExponentiateByAttribute)
{
return $"{this.TargetAttribute} {(this.AggregateType == AggregateType.Multiplicate ? "*" : "+")}= {this.OperandAttribute?.ToString() ?? this.InputOperand.ToString(CultureInfo.InvariantCulture)} {this.InputOperator.AsString()} {this.InputAttribute}";
}
return $"{this.TargetAttribute} {(this.AggregateType == AggregateType.Multiplicate ? "*" : "+")}= {this.InputAttribute} {this.InputOperator.AsString()} {this.OperandAttribute?.ToString() ?? this.InputOperand.ToString(CultureInfo.InvariantCulture)}";
}
}

View File

@@ -0,0 +1,89 @@
// <copyright file="AttributeRelationshipElement.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An attribute relationship element which takes several input elements which are summed up and multiplied.
/// Calculated values are cached for a better performance.
/// </summary>
public class AttributeRelationshipElement : SimpleElement
{
private float? _cachedValue;
/// <summary>
/// Initializes a new instance of the <see cref="AttributeRelationshipElement" /> class.
/// </summary>
/// <param name="inputElements">The input elements which are summed up.</param>
/// <param name="inputOperand">The operand which is applied to the summed up input elements.</param>
/// <param name="inputOperator">The input operator.</param>
public AttributeRelationshipElement(IEnumerable<IElement> inputElements, IElement inputOperand, InputOperator inputOperator)
{
this.InputElements = inputElements;
this.InputOperand = inputOperand;
this.InputOperator = inputOperator;
foreach (var element in this.InputElements)
{
element.ValueChanged += this.ElementChanged;
}
inputOperand.ValueChanged += this.ElementChanged;
// TODO: Is Dispose required?
}
/// <summary>
/// Gets the input elements.
/// </summary>
public IEnumerable<IElement> InputElements { get; }
/// <summary>
/// Gets or sets the multiplier with which the sum of all input element values are multiplied.
/// </summary>
public IElement InputOperand { get; set; }
/// <summary>
/// Gets or sets the input operator.
/// </summary>
public InputOperator InputOperator { get; set; }
/// <summary>
/// Gets the calculated value.
/// </summary>
public override float Value => this._cachedValue ?? this.GetAndCacheValue();
private void ElementChanged(object? sender, EventArgs eventArgs)
{
this._cachedValue = null;
this.RaiseValueChanged();
}
private float GetAndCacheValue()
{
this._cachedValue = this.CalculateValue();
return this._cachedValue.Value;
}
private float CalculateValue()
{
return this.InputOperator switch
{
InputOperator.Multiply => this.InputElements.Sum(a => a.Value) * this.InputOperand.Value,
InputOperator.Add => this.InputElements.Sum(a => a.Value) + this.InputOperand.Value,
InputOperator.Exponentiate => (float)Math.Pow(
this.InputElements.Sum(a => a.Value),
this.InputOperand.Value),
InputOperator.ExponentiateByAttribute => (float)Math.Pow(
this.InputOperand.Value,
this.InputElements.Sum(a => a.Value)),
InputOperator.Maximum => Math.Max(
this.InputElements.Sum(a => a.Value),
this.InputOperand.Value),
InputOperator.Minimum => Math.Min(
this.InputElements.Sum(a => a.Value),
this.InputOperand.Value),
_ => throw new InvalidOperationException($"Input operator {this.InputOperator} unknown"),
};
}
}

View File

@@ -0,0 +1,51 @@
// <copyright file="AttributeRelationshipExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// Extensions for <see cref="AttributeRelationship"/>.
/// </summary>
public static class AttributeRelationshipExtensions
{
/// <summary>
/// Gets the target attribute and throws an exception if it's not initialized yet.
/// </summary>
/// <param name="relationship">The attribute relationship.</param>
/// <returns>The target attribute definition.</returns>
/// <exception cref="InvalidOperationException">TargetAttribute not initialized.</exception>
public static AttributeDefinition GetTargetAttribute(this AttributeRelationship relationship)
{
return relationship.TargetAttribute ?? throw new InvalidOperationException("TargetAttribute not initialized.");
}
/// <summary>
/// Gets the input attribute and throws an exception if it's not initialized yet.
/// </summary>
/// <param name="relationship">The attribute relationship.</param>
/// <returns>The input attribute definition.</returns>
/// <exception cref="InvalidOperationException">TargetAttribute not initialized.</exception>
public static AttributeDefinition GetInputAttribute(this AttributeRelationship relationship)
{
return relationship.InputAttribute ?? throw new InvalidOperationException("InputAttribute not initialized.");
}
/// <summary>
/// Gets the operand attribute.
/// </summary>
/// <param name="relationship">The attribute relationship.</param>
/// <param name="attributeSystem">The attribute system.</param>
/// <returns>
/// The input attribute definition.
/// </returns>
public static IElement GetOperandElement(this AttributeRelationship relationship, IAttributeSystem attributeSystem)
{
if (relationship.OperandAttribute is { } operandAttribute)
{
return attributeSystem.GetOrCreateAttribute(operandAttribute);
}
return new ConstantElement(relationship.InputOperand);
}
}

View File

@@ -0,0 +1,259 @@
// <copyright file="AttributeSystem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
using System.Collections;
/// <summary>
/// The attribute system which holds all attributes of a character.
/// </summary>
public class AttributeSystem : IAttributeSystem, IEnumerable<IAttribute>
{
private readonly IDictionary<AttributeDefinition, IAttribute> _attributes = new Dictionary<AttributeDefinition, IAttribute>();
/// <summary>
/// Initializes a new instance of the <see cref="AttributeSystem" /> class.
/// </summary>
/// <param name="statAttributes">The stat attributes. These attributes are added just as-is and are not wrapped by a <see cref="ComposableAttribute"/>.</param>
/// <param name="baseAttributes">The initial base attributes. These attributes contain the base values which will be wrapped by a <see cref="ComposableAttribute"/>, so additional elements can contribute to the attributes value. Instead of providing them here, you could also add them to the system by calling <see cref="AddElement"/> later.</param>
/// <param name="attributeRelationships">The initial attribute relationships. Instead of providing them here, you could also add them to the system by calling <see cref="AddAttributeRelationship(AttributeRelationship, IAttributeSystem, AggregateType)"/> later.</param>
public AttributeSystem(IEnumerable<IAttribute> statAttributes, IEnumerable<IAttribute> baseAttributes, IEnumerable<AttributeRelationship> attributeRelationships)
{
foreach (var statAttribute in statAttributes)
{
this._attributes.Add(statAttribute.Definition, statAttribute);
}
foreach (var baseAttribute in baseAttributes)
{
this.AddElement(baseAttribute, baseAttribute.Definition);
}
foreach (var combination in attributeRelationships)
{
this.AddAttributeRelationship(combination);
}
}
/// <inheritdoc/>
public float this[AttributeDefinition? attributeDefinition]
{
get => this.GetValueOfAttribute(attributeDefinition);
set => this.SetStatAttribute(attributeDefinition, value);
}
/// <inheritdoc/>
public void AddAttributeRelationship(AttributeRelationship relationship, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType)
{
if (this.GetOrCreateAttribute(relationship.GetTargetAttribute()) is IComposableAttribute targetAttribute)
{
var relatedElement = this.CreateRelatedAttribute(relationship, sourceAttributeHolder, aggregateType);
targetAttribute.AddElement(relatedElement);
}
}
/// <summary>
/// Creates the related attribute.
/// </summary>
/// <param name="relationship">The relationship.</param>
/// <param name="sourceAttributeHolder">The source attribute holder. This may be the attribute system of another player.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
/// <returns>
/// The newly created relationship element.
/// </returns>
public IElement CreateRelatedAttribute(AttributeRelationship relationship, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType)
{
var inputElements = new[] { sourceAttributeHolder.GetOrCreateAttribute(relationship.GetInputAttribute()) };
return new AttributeRelationshipElement(inputElements, relationship.GetOperandElement(sourceAttributeHolder), relationship.InputOperator)
{
AggregateType = aggregateType,
};
}
/// <summary>
/// Sets the stat attribute, if the <paramref name="attributeDefinition"/> is a stat attribute.
/// </summary>
/// <param name="attributeDefinition">The attribute definition.</param>
/// <param name="newValue">The new value.</param>
/// <returns>The success.</returns>
public bool SetStatAttribute(AttributeDefinition? attributeDefinition, float newValue)
{
if (attributeDefinition is null)
{
return false;
}
if (this._attributes.TryGetValue(attributeDefinition, out var attribute)
&& attribute is StatAttribute statAttribute)
{
statAttribute.Value = newValue;
return true;
}
return false;
}
/// <summary>
/// Gets the composable attribute.
/// </summary>
/// <param name="attributeDefinition">The attribute definition.</param>
/// <returns>The composable attribute.</returns>
public ComposableAttribute? GetComposableAttribute(AttributeDefinition attributeDefinition)
{
return this.GetOrCreateAttribute(attributeDefinition) as ComposableAttribute;
}
/// <inheritdoc/>
public float GetValueOfAttribute(AttributeDefinition? attributeDefinition)
{
var element = this.GetAttribute(attributeDefinition);
if (element != null)
{
var actualDefinition = (element as BaseAttribute)?.Definition ?? attributeDefinition;
if (actualDefinition?.MaximumValue is { } maximumValue && element.Value > maximumValue)
{
return maximumValue;
}
return element.Value;
}
return 0;
}
/// <inheritdoc/>
public void AddElement(IElement element, AttributeDefinition targetAttribute)
{
if (!this._attributes.TryGetValue(targetAttribute, out var attribute))
{
attribute = new ComposableAttribute(targetAttribute);
this._attributes.Add(targetAttribute, attribute);
this.OnAttributeAdded(attribute);
}
if (attribute is IComposableAttribute composableAttribute)
{
composableAttribute.AddElement(element);
}
else
{
throw new ArgumentException($"Attribute {targetAttribute} is not a composable attribute.");
}
}
/// <inheritdoc/>
public void RemoveElement(IElement element, AttributeDefinition targetAttribute)
{
if (this._attributes.TryGetValue(targetAttribute, out var attribute))
{
if (attribute is IComposableAttribute composableAttribute)
{
composableAttribute.RemoveElement(element);
if (!composableAttribute.Elements.Any())
{
this._attributes.Remove(targetAttribute);
}
}
else
{
throw new ArgumentException($"Attribute {targetAttribute} is not a composable attribute.");
}
}
}
/// <inheritdoc/>
public override string ToString()
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("Stat Attributes:");
foreach (var statAttribute in this._attributes.Values.OfType<StatAttribute>())
{
stringBuilder.AppendLine($" {statAttribute.Definition}: {statAttribute.Value}");
}
stringBuilder.AppendLine("Others:");
foreach (var attribute in this._attributes.Values.OfType<IComposableAttribute>())
{
stringBuilder.AppendLine($" {attribute.Definition}: {attribute.Value}");
}
return stringBuilder.ToString();
}
/// <inheritdoc />
public IEnumerator<IAttribute> GetEnumerator()
{
return this._attributes.Values.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/// <summary>
/// Gets or creates the element with the specified attribute.
/// </summary>
/// <param name="attributeDefinition">The attribute definition.</param>
/// <returns>The element of the attribute.</returns>
public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
{
var element = this.GetAttribute(attributeDefinition);
if (element is null)
{
var composableAttribute = new ComposableAttribute(attributeDefinition);
element = composableAttribute;
this._attributes.Add(attributeDefinition, composableAttribute);
this.OnAttributeAdded(composableAttribute);
}
return element;
}
/// <summary>
/// Called when an attribute was added to the system after the initial construction.
/// </summary>
/// <param name="attribute">The attribute.</param>
protected virtual void OnAttributeAdded(IAttribute attribute)
{
// can be overwritten.
}
/// <summary>
/// Called when an attribute was removed from the system.
/// </summary>
/// <param name="attribute">The attribute.</param>
protected virtual void OnAttributeRemoved(IAttribute attribute)
{
// can be overwritten.
}
/// <summary>
/// Adds the attribute relationship.
/// </summary>
/// <param name="combination">The combination.</param>
private void AddAttributeRelationship(AttributeRelationship combination)
{
this.AddAttributeRelationship(combination, this, combination.AggregateType);
}
private IElement? GetAttribute(AttributeDefinition? attributeDefinition)
{
if (attributeDefinition is null)
{
return null;
}
if (this._attributes.TryGetValue(attributeDefinition, out var attribute))
{
return attribute;
}
return null;
}
}

View File

@@ -0,0 +1,54 @@
// <copyright file="BaseAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// The base class for an attribute.
/// </summary>
public abstract class BaseAttribute : IAttribute
{
private AttributeDefinition _definition = null!;
/// <summary>
/// Initializes a new instance of the <see cref="BaseAttribute"/> class.
/// </summary>
/// <param name="definition">The definition.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
protected BaseAttribute(AttributeDefinition definition, AggregateType aggregateType)
{
this.Definition = definition;
this.AggregateType = aggregateType;
}
/// <inheritdoc/>
public event EventHandler? ValueChanged;
/// <inheritdoc/>
public virtual AttributeDefinition Definition
{
get => this._definition;
protected set => this._definition = value;
}
/// <inheritdoc/>
public abstract float Value { get; }
/// <inheritdoc/>
public AggregateType AggregateType { get; }
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Definition?.Designation}: {this.Value}";
}
/// <summary>
/// Raises the value changed event.
/// </summary>
protected void RaiseValueChanged()
{
this.ValueChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,33 @@
// <copyright file="BaseStatAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An attribute which represents an increasable stat attribute (e.g. by level-up points).
/// </summary>
/// <remarks>
/// Intermediate class, needed because we want to add a setter.
/// We do just override the getter here and have to introduce a new Value get/set-property on a derived type.
/// </remarks>
public abstract class BaseStatAttribute : BaseAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="BaseStatAttribute"/> class.
/// </summary>
/// <param name="definition">The definition.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
protected BaseStatAttribute(AttributeDefinition definition, AggregateType aggregateType)
: base(definition, aggregateType)
{
}
/// <inheritdoc/>
public override float Value => this.ValueGetter;
/// <summary>
/// Gets the value.
/// </summary>
protected abstract float ValueGetter { get; }
}

View File

@@ -0,0 +1,48 @@
// <copyright file="CombinedElement.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An element which combines two elements into one.
/// </summary>
public class CombinedElement : IElement
{
private readonly IElement _element1;
private readonly IElement _element2;
/// <summary>
/// Initializes a new instance of the <see cref="CombinedElement"/> class.
/// </summary>
/// <param name="element1">The first element.</param>
/// <param name="element2">The second element.</param>
/// <exception cref="ArgumentException">The aggregate type of both elements need to match.</exception>
public CombinedElement(IElement element1, IElement element2)
{
if (element1.AggregateType != element2.AggregateType)
{
throw new ArgumentException($"The aggregate type of both elements need to match. Element1: {element1.AggregateType}, Element2: {element2.AggregateType}");
}
this._element1 = element1;
this._element2 = element2;
this._element1.ValueChanged += (_, _) => this.ValueChanged?.Invoke(this, EventArgs.Empty);
this._element2.ValueChanged += (_, _) => this.ValueChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public event EventHandler? ValueChanged;
/// <inheritdoc />
public float Value => this._element1.Value + this._element2.Value;
/// <inheritdoc />
public AggregateType AggregateType => this._element1.AggregateType;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Value} ({this.AggregateType})";
}
}

View File

@@ -0,0 +1,97 @@
// <copyright file="ComposableAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An attribute which is a composition of elements.
/// </summary>
public class ComposableAttribute : BaseAttribute, IComposableAttribute
{
private readonly IList<IElement> _elementList;
private float? _maximumValue;
private float? _cachedValue;
/// <summary>
/// Initializes a new instance of the <see cref="ComposableAttribute" /> class.
/// </summary>
/// <param name="definition">The definition.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
/// <param name="maximumValue">The inner maximum value.</param>
public ComposableAttribute(AttributeDefinition definition, AggregateType aggregateType = AggregateType.AddRaw, float? maximumValue = null)
: base(definition, aggregateType)
{
this._elementList = new List<IElement>();
this._maximumValue = maximumValue;
}
/// <inheritdoc/>
public IEnumerable<IElement> Elements => this._elementList;
/// <inheritdoc/>
public override float Value => this._cachedValue ?? this.GetAndCacheValue();
/// <inheritdoc/>
public IComposableAttribute AddElement(IElement element)
{
this._elementList.Add(element);
element.ValueChanged += this.ElementChanged;
this.ElementChanged(element, EventArgs.Empty);
return this;
}
/// <inheritdoc/>
public void RemoveElement(IElement element)
{
if (this._elementList.Remove(element))
{
element.ValueChanged -= this.ElementChanged;
this.ElementChanged(element, EventArgs.Empty);
}
}
private float GetAndCacheValue()
{
if (this._elementList.Count == 0)
{
this._cachedValue = 0;
return 0;
}
var rawValues = this.Elements.Where(e => e.AggregateType == AggregateType.AddRaw).Sum(e => e.Value);
var multiValues = this.Elements.Where(e => e.AggregateType == AggregateType.Multiplicate).Select(e => e.Value).Concat(Enumerable.Repeat(1.0F, 1)).Aggregate((a, b) => a * b);
var finalValues = this.Elements.Where(e => e.AggregateType == AggregateType.AddFinal).Sum(e => e.Value);
var maxValues = this.Elements.Where(e => e.AggregateType == AggregateType.Maximum).MaxBy(e => e.Value)?.Value ?? 0;
rawValues += maxValues;
if (this.Elements.All(e => e.AggregateType == AggregateType.Multiplicate))
{
rawValues = 1;
}
var newValue = (rawValues * multiValues) + finalValues;
if (this._maximumValue.HasValue)
{
newValue = Math.Min(this._maximumValue.Value, newValue);
}
if (this.Definition.MaximumValue.HasValue)
{
newValue = Math.Min(this.Definition.MaximumValue.Value, newValue);
}
this._cachedValue = newValue;
return this._cachedValue.Value;
}
private void ElementChanged(object? sender, EventArgs eventArgs)
{
this._cachedValue = null;
this.RaiseValueChanged();
}
}

View File

@@ -0,0 +1,67 @@
// <copyright file="ConstValueAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An attribute with a constant value.
/// </summary>
public class ConstValueAttribute : IAttribute
{
private AttributeDefinition _definition = null!;
/// <summary>
/// Initializes a new instance of the <see cref="ConstValueAttribute" /> class.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="definition">The definition.</param>
public ConstValueAttribute(float value, AttributeDefinition definition)
{
this.Value = value;
this._definition = definition;
}
/// <summary>
/// Initializes a new instance of the <see cref="ConstValueAttribute"/> class.
/// </summary>
protected ConstValueAttribute()
{
}
/// <inheritdoc/>
/// <remarks>Empty implementation, because the value can't change.</remarks>
public event EventHandler? ValueChanged
{
#pragma warning disable S3237 //Empty implementation, because the value can't change.
add
{
// no action required
}
remove
{
// no action required
}
#pragma warning restore S3237
}
/// <inheritdoc/>
public virtual AttributeDefinition Definition
{
get => this._definition;
protected set => this._definition = value;
}
/// <inheritdoc/>
public float Value { get; protected set; }
/// <inheritdoc/>
public AggregateType AggregateType => AggregateType.AddRaw;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Definition.Designation}: {this.Value}";
}
}

View File

@@ -0,0 +1,50 @@
// <copyright file="ConstantElement.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An element with a constant value.
/// </summary>
public class ConstantElement : IElement
{
/// <summary>
/// Initializes a new instance of the <see cref="ConstantElement" /> class.
/// </summary>
/// <param name="value">The constant value.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
public ConstantElement(float value, AggregateType aggregateType = AggregateType.AddRaw)
{
this.Value = value;
this.AggregateType = aggregateType;
}
/// <summary>
/// Never occurs, so the implementation is empty.
/// </summary>
public event EventHandler? ValueChanged
{
add
{
// do nothing, as the value never changes.
}
remove
{
// do nothing, as the value never changes.
}
}
/// <inheritdoc/>
public float Value { get; }
/// <inheritdoc/>
public AggregateType AggregateType { get; }
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Value} ({this.AggregateType})";
}
}

View File

@@ -0,0 +1,30 @@
// <copyright file="Extensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// Common extension methods.
/// </summary>
public static class Extensions
{
/// <summary>
/// Returns the input operator as string.
/// </summary>
/// <param name="inputOperator">The input operator.</param>
/// <returns>The textual representation of the input operator.</returns>
public static string AsString(this InputOperator inputOperator)
{
return inputOperator switch
{
InputOperator.Add => "+",
InputOperator.Multiply => "*",
InputOperator.Exponentiate => "^",
InputOperator.ExponentiateByAttribute => "^",
InputOperator.Maximum => "<max>",
InputOperator.Minimum => "<min>",
_ => string.Empty,
};
}
}

View File

@@ -0,0 +1,16 @@
// <copyright file="IAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// The interface of an attribute.
/// </summary>
public interface IAttribute : IElement
{
/// <summary>
/// Gets the attribute definition.
/// </summary>
AttributeDefinition Definition { get; }
}

View File

@@ -0,0 +1,54 @@
// <copyright file="IAttributeSystem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An attribute system which holds all attributes of a game object.
/// </summary>
public interface IAttributeSystem
{
/// <summary>
/// Gets or sets the value with the specified attribute definition.
/// </summary>
/// <param name="attributeDefinition">The attribute definition.</param>
/// <returns>The value of the specified attribute.</returns>
float this[AttributeDefinition attributeDefinition] { get; set; }
/// <summary>
/// Gets the value of the attribute.
/// </summary>
/// <param name="attributeDefinition">The attribute definition.</param>
/// <returns>The value of the attribute.</returns>
float GetValueOfAttribute(AttributeDefinition attributeDefinition);
/// <summary>
/// Adds the element.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="targetAttribute">The target attribute.</param>
void AddElement(IElement element, AttributeDefinition targetAttribute);
/// <summary>
/// Removes the element.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="targetAttribute">The target attribute.</param>
void RemoveElement(IElement element, AttributeDefinition targetAttribute);
/// <summary>
/// Adds the attribute relationship.
/// </summary>
/// <param name="relationship">The relationship.</param>
/// <param name="sourceAttributeHolder">The source attribute holder. May be the attribute system of another player.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
void AddAttributeRelationship(AttributeRelationship relationship, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType);
/// <summary>
/// Gets or creates the element with the specified attribute.
/// </summary>
/// <param name="attributeDefinition">The attribute definition.</param>
/// <returns>The element of the attribute.</returns>
IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition);
}

View File

@@ -0,0 +1,35 @@
// <copyright file="IComposableAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// The Attribute... Hint: every attribute could be wrapped into an element to chain them.
/// Example: We have a base strength attribute which could be wrapped into an element,
/// and this element could be added to a total strength attribute, which will
/// also contain the elements gotten from master tree or ancient items.
/// This way we could identify the base stats, which are needed for the character stats packet.
/// Another use could define an aggregate function (either addition or multiplication), and again
/// we could chain this calculations together (keeping commutative property).
/// </summary>
public interface IComposableAttribute : IAttribute
{
/// <summary>
/// Gets the elements, of which this attribute is calculated.
/// </summary>
IEnumerable<IElement> Elements { get; }
/// <summary>
/// Adds the element to the composition.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The composable attribute itself, to be able to chain adding.</returns>
IComposableAttribute AddElement(IElement element);
/// <summary>
/// Removes the element from the composition.
/// </summary>
/// <param name="element">The element.</param>
void RemoveElement(IElement element);
}

View File

@@ -0,0 +1,26 @@
// <copyright file="IElement.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// The interface of an element.
/// </summary>
public interface IElement
{
/// <summary>
/// Occurs when the value has been changed.
/// </summary>
event EventHandler? ValueChanged;
/// <summary>
/// Gets the value.
/// </summary>
float Value { get; }
/// <summary>
/// Gets the type of the aggregate.
/// </summary>
AggregateType AggregateType { get; }
}

View File

@@ -0,0 +1,20 @@
<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.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\bin\Release\</OutputPath>
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.AttributeSystem.xml</DocumentationFile>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
// <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;
using System.Runtime.CompilerServices;
// 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")]
[assembly: InternalsVisibleTo("MUnique.OpenMU.AttributeSystem.Tests")]

View File

@@ -0,0 +1,56 @@
# Attribute System
This project contains all what's required to create a so-called "Attribute System".
I'm sure it could be helpful also for other games, even other kind of games,
because it's pretty generic and not bound to MU Online or RPGs in general.
## What is it?
In OpenMU, every object which attacks and is attackable (interface IAttackable),
has an instance of an Attribute System. This Attribute System contains all attributes
of this entity.
## And how is it used?
For example a attribute system of a player can contain the following attributes:
* Strength
* Agility
* Vitality
* Health
* Level
* Physical Damage (min)
* Physical Damage (max)
Attributes can depend on each other and attributes values are calculated automatically.
For example we can define that Health is calculated by multiplying Vitality by
2 and adding the Level. Or that the minimum physical damage is Strength divided
by 6.
We can define such relationships by AttributeRelationship objects.
To make this work there need to be attributes which don't depend on other ones
and can be influenced by the player. These are called "StatAttributes".
StatAttributes are for example:
* Level (increasable by gaining experience)
* Base Strength (increasable by distributing free level up points)
Attribute values can get increased by equipping items. For example, there are
ancient items which increase Strength when they are equipped. Or more classical,
Swords which would increase the minimum and maximum physical damage attributes.
E.g. for Strength we usually have another attribute which is called "Total Strength",
which is basically the "Base Strength", but it can be a target attribute of an
AttributeRelationship. This "Total Strength" is then used for item requirements
or the further damage calculation. The "Base Strength" is the value which is
usually persisted to the database for a specific player.
In case of this server project, these relationships are all defined the
configuration for each character class. For some examples you can have a look
at the CharacterClassInitialization of the MUnique.OpenMU.Persistence.Initialization
project.
## Tests
This project also includes some unit tests for AttributeSystem, so check it out
if you want to find out how that works as a whole.

View File

@@ -0,0 +1,79 @@
// <copyright file="SimpleElement.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// A simple element with a variable value.
/// </summary>
public class SimpleElement : IElement
{
private float _value;
private AggregateType _aggregateType;
/// <summary>
/// Initializes a new instance of the <see cref="SimpleElement"/> class.
/// </summary>
public SimpleElement()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SimpleElement"/> class.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="aggregateType">Type of the aggregate.</param>
public SimpleElement(float value, AggregateType aggregateType)
{
this._value = value;
this._aggregateType = aggregateType;
}
/// <inheritdoc/>
public event EventHandler? ValueChanged;
/// <inheritdoc/>
public virtual float Value
{
get => this._value;
set
{
if (Math.Abs(this._value - value) > 0.00001f)
{
this._value = value;
this.RaiseValueChanged();
}
}
}
/// <inheritdoc/>
public AggregateType AggregateType
{
get => this._aggregateType;
set
{
if (this._aggregateType != value)
{
this._aggregateType = value;
this.RaiseValueChanged();
}
}
}
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Value} ({this.AggregateType})";
}
/// <summary>
/// Raises the value changed event.
/// </summary>
protected void RaiseValueChanged()
{
this.ValueChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="StatAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.AttributeSystem;
/// <summary>
/// An attribute which represents an increasable stat attribute (e.g. by level-up points).
/// </summary>
public class StatAttribute : BaseStatAttribute
{
private float _statValue;
/// <summary>
/// Initializes a new instance of the <see cref="StatAttribute"/> class.
/// </summary>
public StatAttribute()
: base(null!, AggregateType.AddRaw)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StatAttribute"/> class.
/// </summary>
/// <param name="definition">The definition.</param>
/// <param name="baseValue">The base value.</param>
public StatAttribute(AttributeDefinition definition, float baseValue)
: base(definition, AggregateType.AddRaw)
{
this._statValue = baseValue;
}
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
public new virtual float Value
{
get
{
if (this.Definition?.MaximumValue is { } maxValue)
{
return Math.Min(maxValue, this._statValue);
}
return this._statValue;
}
set
{
if (Math.Abs(this._statValue - value) > 0.01f)
{
this._statValue = value;
this.RaiseValueChanged();
}
}
}
/// <inheritdoc/>
protected override float ValueGetter => this._statValue;
}