baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
25
src/.dockerignore
Normal file
25
src/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
24
src/.editorconfig
Normal file
24
src/.editorconfig
Normal file
@@ -0,0 +1,24 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CS1998.severity = suggestion
|
||||
|
||||
# SA1309: Field names should not begin with underscore
|
||||
dotnet_diagnostic.SA1309.severity = none
|
||||
|
||||
# SA1516: Elements should be separated by blank line
|
||||
dotnet_diagnostic.SA1516.severity = none
|
||||
|
||||
# SA1615: Element return value should be documented
|
||||
dotnet_diagnostic.SA1615.severity = suggestion
|
||||
|
||||
# VSTHRD103: Call async methods when in an async method
|
||||
dotnet_diagnostic.VSTHRD103.severity = error
|
||||
|
||||
# VSTHRD111: Use ConfigureAwait(bool)
|
||||
dotnet_diagnostic.VSTHRD111.severity = warning
|
||||
|
||||
[*.xml]
|
||||
|
||||
tab_width = 2
|
||||
indent_size = 2
|
||||
end_of_line = crlf
|
||||
14
src/Annotations/CloneableAttribute.cs
Normal file
14
src/Annotations/CloneableAttribute.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// <copyright file="CloneableAttribute.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Annotations;
|
||||
|
||||
/// <summary>
|
||||
/// Classes marked with this attribute will get a generic Clonable-Interface
|
||||
/// implemented by a code generator.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class CloneableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
14
src/Annotations/IgnoreWhenCloningAttribute.cs
Normal file
14
src/Annotations/IgnoreWhenCloningAttribute.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// <copyright file="IgnoreWhenCloningAttribute.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Annotations;
|
||||
|
||||
/// <summary>
|
||||
/// Properties marked with this attribute will not get cloned
|
||||
/// implemented by a code generator.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class IgnoreWhenCloningAttribute : Attribute
|
||||
{
|
||||
}
|
||||
12
src/Annotations/MUnique.OpenMU.Annotations.csproj
Normal file
12
src/Annotations/MUnique.OpenMU.Annotations.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
31
src/AttributeSystem/AggregateType.cs
Normal file
31
src/AttributeSystem/AggregateType.cs
Normal 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,
|
||||
}
|
||||
128
src/AttributeSystem/AttributeDefinition.cs
Normal file
128
src/AttributeSystem/AttributeDefinition.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
162
src/AttributeSystem/AttributeRelationship.cs
Normal file
162
src/AttributeSystem/AttributeRelationship.cs
Normal 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)}";
|
||||
}
|
||||
}
|
||||
89
src/AttributeSystem/AttributeRelationshipElement.cs
Normal file
89
src/AttributeSystem/AttributeRelationshipElement.cs
Normal 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"),
|
||||
};
|
||||
}
|
||||
}
|
||||
51
src/AttributeSystem/AttributeRelationshipExtensions.cs
Normal file
51
src/AttributeSystem/AttributeRelationshipExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
259
src/AttributeSystem/AttributeSystem.cs
Normal file
259
src/AttributeSystem/AttributeSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
54
src/AttributeSystem/BaseAttribute.cs
Normal file
54
src/AttributeSystem/BaseAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
33
src/AttributeSystem/BaseStatAttribute.cs
Normal file
33
src/AttributeSystem/BaseStatAttribute.cs
Normal 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; }
|
||||
}
|
||||
48
src/AttributeSystem/CombinedElement.cs
Normal file
48
src/AttributeSystem/CombinedElement.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
97
src/AttributeSystem/ComposableAttribute.cs
Normal file
97
src/AttributeSystem/ComposableAttribute.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
67
src/AttributeSystem/ConstValueAttribute.cs
Normal file
67
src/AttributeSystem/ConstValueAttribute.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
50
src/AttributeSystem/ConstantElement.cs
Normal file
50
src/AttributeSystem/ConstantElement.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
30
src/AttributeSystem/Extensions.cs
Normal file
30
src/AttributeSystem/Extensions.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
16
src/AttributeSystem/IAttribute.cs
Normal file
16
src/AttributeSystem/IAttribute.cs
Normal 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; }
|
||||
}
|
||||
54
src/AttributeSystem/IAttributeSystem.cs
Normal file
54
src/AttributeSystem/IAttributeSystem.cs
Normal 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);
|
||||
}
|
||||
35
src/AttributeSystem/IComposableAttribute.cs
Normal file
35
src/AttributeSystem/IComposableAttribute.cs
Normal 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);
|
||||
}
|
||||
26
src/AttributeSystem/IElement.cs
Normal file
26
src/AttributeSystem/IElement.cs
Normal 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; }
|
||||
}
|
||||
20
src/AttributeSystem/MUnique.OpenMU.AttributeSystem.csproj
Normal file
20
src/AttributeSystem/MUnique.OpenMU.AttributeSystem.csproj
Normal 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>
|
||||
12
src/AttributeSystem/Properties/AssemblyInfo.cs
Normal file
12
src/AttributeSystem/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
56
src/AttributeSystem/Readme.md
Normal file
56
src/AttributeSystem/Readme.md
Normal 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.
|
||||
79
src/AttributeSystem/SimpleElement.cs
Normal file
79
src/AttributeSystem/SimpleElement.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
63
src/AttributeSystem/StatAttribute.cs
Normal file
63
src/AttributeSystem/StatAttribute.cs
Normal 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;
|
||||
}
|
||||
283
src/ChatServer/ChatClient.cs
Normal file
283
src/ChatServer/ChatClient.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
// <copyright file="ChatClient.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using System.Buffers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.Packets;
|
||||
using MUnique.OpenMU.Network.Packets.ChatServer;
|
||||
using MUnique.OpenMU.Network.Xor;
|
||||
|
||||
/// <summary>
|
||||
/// ChatClient implementation, uses socket connections.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Messages are decrypted and encrypted again with the same XOR3 key - in theory we could optimize this (and
|
||||
/// the conversion to a string) away. However, we'll leave it for easier debugging.
|
||||
/// </remarks>
|
||||
internal class ChatClient : IChatClient
|
||||
{
|
||||
private const int TokenOffset = 6;
|
||||
private const int MessageOffset = 5;
|
||||
private static readonly ISpanDecryptor TokenDecryptor = new Xor3Decryptor(TokenOffset);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ISpanDecryptor"/> for chat messages. The incoming chat messages are "encrypted" with the commonly known XOR-3 encryption for reasons we don't know ;).
|
||||
/// </summary>
|
||||
private static readonly ISpanDecryptor MessageDecryptor = new Xor3Decryptor(MessageOffset);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ISpanEncryptor"/> for chat messages. The outgoing chat messages are "encrypted" with the commonly known XOR-3 encryption for reasons we don't know ;).
|
||||
/// </summary>
|
||||
private static readonly ISpanEncryptor MessageEncryptor = new Xor3Encryptor(MessageOffset);
|
||||
|
||||
private readonly ChatRoomManager _manager;
|
||||
private readonly ILogger<ChatClient> _logger;
|
||||
private readonly byte[] _packetBuffer = new byte[0xFF];
|
||||
private IConnection? _connection;
|
||||
private ChatRoom? _room;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClient" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection.</param>
|
||||
/// <param name="manager">The manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ChatClient(IConnection connection, ChatRoomManager manager, ILogger<ChatClient> logger)
|
||||
{
|
||||
this._manager = manager;
|
||||
this._logger = logger;
|
||||
this._connection = connection;
|
||||
this._connection.PacketReceived += this.ReadPacketAsync;
|
||||
this._connection.Disconnected += this.LogOffAsync;
|
||||
|
||||
this.LastActivity = DateTime.Now;
|
||||
_ = this._connection.BeginReceiveAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the client has been disconnected.
|
||||
/// </summary>
|
||||
public event EventHandler? Disconnected;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Index
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? AuthenticationToken { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastActivity { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SendMessageAsync(byte senderId, string message)
|
||||
{
|
||||
if (this._connection is not { } connection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int WritePacket()
|
||||
{
|
||||
var messageLength = (byte)Encoding.UTF8.GetByteCount(message);
|
||||
var length = ChatMessageRef.GetRequiredSize(messageLength);
|
||||
var packet = new ChatMessageRef(connection.Output.GetSpan(length)[..length]);
|
||||
packet.SenderIndex = senderId;
|
||||
packet.MessageLength = messageLength;
|
||||
Encoding.UTF8.GetBytes(message, packet.Message);
|
||||
MessageEncryptor.Encrypt(packet);
|
||||
return length;
|
||||
}
|
||||
|
||||
await this._connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SendChatRoomClientListAsync(IReadOnlyCollection<IChatClient> clients)
|
||||
{
|
||||
if (this._connection is not { } connection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int WritePacket()
|
||||
{
|
||||
var length = ChatRoomClientsRef.GetRequiredSize(clients.Count);
|
||||
var packet = new ChatRoomClientsRef(connection.Output.GetSpan(length)[..length]);
|
||||
packet.ClientCount = (byte)clients.Count;
|
||||
int i = 0;
|
||||
foreach (var client in clients)
|
||||
{
|
||||
var clientBlock = packet[i];
|
||||
clientBlock.Index = client.Index;
|
||||
clientBlock.Name = client.Nickname ?? string.Empty;
|
||||
i++;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
await this._connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SendChatRoomClientUpdateAsync(byte updatedClientId, string updatedClientName, ChatRoomClientUpdateType updateType)
|
||||
{
|
||||
if (this._connection is null || !this._connection.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (updateType == ChatRoomClientUpdateType.Joined)
|
||||
{
|
||||
await this._connection.SendChatRoomClientJoinedAsync(updatedClientId, updatedClientName).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this._connection.SendChatRoomClientLeftAsync(updatedClientId, updatedClientName).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error sending room update");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LogOffAsync()
|
||||
{
|
||||
if (this._connection is null)
|
||||
{
|
||||
this._logger.LogDebug("Client {Nickname} is already disconnected.", this.Nickname);
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.LogDebug("Client {Connection} is going to be disconnected.", this._connection);
|
||||
if (this._room != null)
|
||||
{
|
||||
await this._room.LeaveAsync(this).ConfigureAwait(false);
|
||||
this._room = null;
|
||||
}
|
||||
|
||||
if (this._connection is { } connection)
|
||||
{
|
||||
await connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._connection = null;
|
||||
this.Disconnected?.Invoke(this, EventArgs.Empty);
|
||||
this.Disconnected = null;
|
||||
}
|
||||
|
||||
/// <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 $"Connection:{this._connection}, Client name:{this.Nickname}, Room-ID:{this._room?.RoomId}, Index: {this.Index}";
|
||||
}
|
||||
|
||||
private async ValueTask ReadPacketAsync(ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
if (sequence.Length < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sequence.CopyTo(this._packetBuffer);
|
||||
var packet = this._packetBuffer.AsMemory(0, (int)sequence.Length);
|
||||
if (this._packetBuffer[0] != Authenticate.HeaderType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.LastActivity = DateTime.Now;
|
||||
switch (this._packetBuffer[2])
|
||||
{
|
||||
case 0:
|
||||
await this.AuthenticateAsync(packet).ConfigureAwait(false);
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
// We did never capture such packets, but they don't seem to be wrong (next is 4), so do nothing.
|
||||
break;
|
||||
case 4:
|
||||
if (this._room != null && this.CheckMessage(packet))
|
||||
{
|
||||
MessageDecryptor.Decrypt(packet.Span);
|
||||
var message = packet.Span.ExtractString(5, int.MaxValue, Encoding.UTF8);
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
this._logger.LogDebug("Message received from {Index}: \"{message}\"", this.Index, message);
|
||||
}
|
||||
|
||||
await this._room.SendMessageAsync(this.Index, message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 5:
|
||||
// This is something like a keep-connection-alive packet.
|
||||
// Last activity is always set, so we have to do nothing here.
|
||||
this._logger.LogDebug("Keep-alive received");
|
||||
break;
|
||||
|
||||
case var value:
|
||||
this._logger.LogError("Received unknown packet of type {PacketType}: {PacketSpan}", value, packet.Span.AsString());
|
||||
await this.LogOffAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckMessage(Memory<byte> packet)
|
||||
{
|
||||
return packet.Length > 4 && (packet.Span[4] + 5) <= packet.Length;
|
||||
}
|
||||
|
||||
private async ValueTask AuthenticateAsync(Memory<byte> packet)
|
||||
{
|
||||
var roomId = NumberConversionExtensions.MakeWord(packet.Span[4], packet.Span[5]);
|
||||
var requestedRoom = this._manager.GetChatRoom(roomId);
|
||||
if (requestedRoom is null)
|
||||
{
|
||||
this._logger.LogError("Requested room {RoomId} has not been registered before.", roomId);
|
||||
await this.LogOffAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
TokenDecryptor.Decrypt(packet.Span);
|
||||
var tokenAsString = packet.Span.ExtractString(TokenOffset, 10, Encoding.UTF8);
|
||||
if (!uint.TryParse(tokenAsString, out uint _))
|
||||
{
|
||||
this._logger.LogError("Token '{TokenAsString}' is not a parseable integer.", tokenAsString);
|
||||
await this.LogOffAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.AuthenticationToken = tokenAsString;
|
||||
if (await requestedRoom.TryJoinAsync(this).ConfigureAwait(false))
|
||||
{
|
||||
this._room = requestedRoom;
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.LogOffAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
285
src/ChatServer/ChatRoom.cs
Normal file
285
src/ChatServer/ChatRoom.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
// <copyright file="ChatRoom.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a Chat Room.
|
||||
/// </summary>
|
||||
internal sealed class ChatRoom : IDisposable
|
||||
{
|
||||
private readonly ILogger<ChatRoom> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Nicknames of the registered Clients.
|
||||
/// </summary>
|
||||
private readonly IList<ChatServerAuthenticationInfo> _registeredClients;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IChatClient"/>s which are currently connected to the ChatRoom.
|
||||
/// </summary>
|
||||
private readonly List<IChatClient> _connectedClients;
|
||||
|
||||
private ReaderWriterLockSlim? _lockSlim = new();
|
||||
|
||||
private int _lastUsedClientIndex = -1;
|
||||
|
||||
private bool _isClosing;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatRoom" /> class.
|
||||
/// </summary>
|
||||
/// <param name="roomId">The room identifier.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ChatRoom(ushort roomId, ILogger<ChatRoom> logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._logger.LogDebug("Creating room {RoomId}", roomId);
|
||||
this._connectedClients = new List<IChatClient>(2);
|
||||
this._registeredClients = new List<ChatServerAuthenticationInfo>(2);
|
||||
this.RoomId = roomId;
|
||||
this.AuthenticationRequiredUntil = DateTime.Now.AddSeconds(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently connected clients.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IChatClient> ConnectedClients => this._connectedClients;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id of the Chat Room.
|
||||
/// </summary>
|
||||
public ushort RoomId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a datetime indicating until a authentication is required.
|
||||
/// </summary>
|
||||
public DateTime AuthenticationRequiredUntil { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the room closed event handler.
|
||||
/// </summary>
|
||||
public EventHandler<ChatRoomClosedEventArgs>? RoomClosed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a chat client to the chatroom. this is only called
|
||||
/// by the game server which will send id to the participants
|
||||
/// over the games connection.
|
||||
/// </summary>
|
||||
/// <param name="authenticationInfo">Authentication information of the participant.</param>
|
||||
public void RegisterClient(ChatServerAuthenticationInfo authenticationInfo)
|
||||
{
|
||||
if (this._isClosing)
|
||||
{
|
||||
throw new ObjectDisposedException("Chat room is already disposed.");
|
||||
}
|
||||
|
||||
if (authenticationInfo.RoomId != this.RoomId)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The RoomId of the authentication info ({authenticationInfo.RoomId}) does not match with this RoomId ({this.RoomId}).");
|
||||
}
|
||||
|
||||
this.AuthenticationRequiredUntil = authenticationInfo.AuthenticationRequiredUntil;
|
||||
this._registeredClients.Add(authenticationInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the next client.
|
||||
/// </summary>
|
||||
/// <returns>The index of the next client.</returns>
|
||||
public byte GetNextClientIndex()
|
||||
{
|
||||
var clientIndex = Interlocked.Increment(ref this._lastUsedClientIndex);
|
||||
return (byte)clientIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this chat room by disconnecting all clients.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "lockSlim", Justification = "Null-conditional confuses the code analysis.")]
|
||||
public void Dispose()
|
||||
{
|
||||
var localLockSlim = this._lockSlim;
|
||||
if (this._isClosing || localLockSlim is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._isClosing = true;
|
||||
this._lockSlim = null;
|
||||
this._logger.LogDebug("Disposing room {RoomId}...", this.RoomId);
|
||||
this._registeredClients.Clear();
|
||||
localLockSlim.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
foreach (var connectedClient in this._connectedClients)
|
||||
{
|
||||
await connectedClient.LogOffAsync().ConfigureAwait(false);
|
||||
}
|
||||
}).WaitAndUnwrapException();
|
||||
this._connectedClients.Clear();
|
||||
this.RoomClosed?.Invoke(this, new ChatRoomClosedEventArgs(this));
|
||||
this.RoomClosed = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
localLockSlim.ExitWriteLock();
|
||||
}
|
||||
|
||||
localLockSlim.Dispose();
|
||||
this._logger.LogDebug("Room {RoomId} disposed.", this.RoomId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The specified client will join the chatroom, if its registered. The Nickname is set to the clients object.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client.</param>
|
||||
/// <returns>True, if the <paramref name="chatClient"/> provides the correct registered id with it's token.</returns>
|
||||
internal async ValueTask<bool> TryJoinAsync(IChatClient chatClient)
|
||||
{
|
||||
if (chatClient is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(chatClient));
|
||||
}
|
||||
|
||||
if (this._isClosing)
|
||||
{
|
||||
throw new ObjectDisposedException("Chat room is already disposed.");
|
||||
}
|
||||
|
||||
this._logger.LogDebug("Client {ChatClientIndex} is trying to join the room {RoomId} with token '{AuthenticationToken}'", chatClient.Index, this.RoomId, chatClient.AuthenticationToken);
|
||||
|
||||
this._lockSlim?.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var authenticationInformation = this._registeredClients.FirstOrDefault(info => string.Equals(info.AuthenticationToken, chatClient.AuthenticationToken));
|
||||
if (authenticationInformation != null)
|
||||
{
|
||||
if (authenticationInformation.AuthenticationRequiredUntil < DateTime.Now)
|
||||
{
|
||||
this._logger.LogInformation(
|
||||
"Client {ChatClientIndex} has tried to join the room {RoomId} with token '{AuthenticationToken}', but was too late. It was valid until {AuthenticationRequiredUntil}.",
|
||||
chatClient.Index,
|
||||
this.RoomId,
|
||||
chatClient.AuthenticationToken,
|
||||
authenticationInformation.AuthenticationRequiredUntil);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatClient.Nickname = authenticationInformation.ClientName;
|
||||
chatClient.Index = authenticationInformation.Index;
|
||||
this._registeredClients.Remove(authenticationInformation);
|
||||
await this.SendChatRoomClientUpdateAsync(chatClient, ChatRoomClientUpdateType.Joined).ConfigureAwait(false);
|
||||
this._connectedClients.Add(chatClient);
|
||||
await chatClient.SendChatRoomClientListAsync(this._connectedClients).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogInformation("Client {ChatClientIndex} has tried to join the room {RoomId} with token '{AuthenticationToken}', but was not registered.", chatClient.Index, this.RoomId, chatClient.AuthenticationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lockSlim?.ExitWriteLock();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The specified client will leave the chatroom.
|
||||
/// If the chatroom is empty then, it will be removed from the manager.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client.</param>
|
||||
internal async ValueTask LeaveAsync(IChatClient chatClient)
|
||||
{
|
||||
if (this._isClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.LogDebug($"Chat client ({chatClient}) is leaving.");
|
||||
this._lockSlim?.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
this._connectedClients.Remove(chatClient);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lockSlim?.ExitWriteLock();
|
||||
}
|
||||
|
||||
bool roomIsEmpty;
|
||||
this._lockSlim?.EnterReadLock();
|
||||
try
|
||||
{
|
||||
roomIsEmpty = this._connectedClients.Count < 1;
|
||||
if (!roomIsEmpty)
|
||||
{
|
||||
await this.SendChatRoomClientUpdateAsync(chatClient, ChatRoomClientUpdateType.Left).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lockSlim?.ExitReadLock();
|
||||
}
|
||||
|
||||
if (roomIsEmpty)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a Message to all chat clients.
|
||||
/// </summary>
|
||||
/// <param name="senderId">The sender identifier.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
internal async ValueTask SendMessageAsync(byte senderId, string message)
|
||||
{
|
||||
if (this._isClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._lockSlim?.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var connectedClient in this._connectedClients)
|
||||
{
|
||||
await connectedClient.SendMessageAsync(senderId, message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lockSlim?.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask SendChatRoomClientUpdateAsync(IChatClient updatedClient, ChatRoomClientUpdateType updateType)
|
||||
{
|
||||
foreach (var client in this._connectedClients)
|
||||
{
|
||||
await client.SendChatRoomClientUpdateAsync(updatedClient.Index, updatedClient.Nickname ?? string.Empty, updateType).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/ChatServer/ChatRoomClosedEventArgs.cs
Normal file
26
src/ChatServer/ChatRoomClosedEventArgs.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// <copyright file="ChatRoomClosedEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments which contains the chat room which has been closed.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
internal class ChatRoomClosedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatRoomClosedEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="room">The chat room.</param>
|
||||
public ChatRoomClosedEventArgs(ChatRoom room)
|
||||
{
|
||||
this.ChatRoom = room;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat room which has been closed.
|
||||
/// </summary>
|
||||
public ChatRoom ChatRoom { get; }
|
||||
}
|
||||
78
src/ChatServer/ChatRoomManager.cs
Normal file
78
src/ChatServer/ChatRoomManager.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
// <copyright file="ChatRoomManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// The Chat Room Manager manages the creation and destruction of chat rooms.
|
||||
/// </summary>
|
||||
internal class ChatRoomManager
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// All currently used chat rooms.
|
||||
/// </summary>
|
||||
private readonly IDictionary<ushort, ChatRoom> _rooms = new ConcurrentDictionary<ushort, ChatRoom>();
|
||||
|
||||
private readonly ConcurrentBag<ushort> _freeRoomIds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatRoomManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ChatRoomManager(ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
for (ushort i = 0; i < ushort.MaxValue; ++i)
|
||||
{
|
||||
this._freeRoomIds.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opened rooms.
|
||||
/// </summary>
|
||||
public ICollection<ChatRoom> OpenedRooms => this._rooms.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ChatRoom and returns its Room-ID.
|
||||
/// </summary>
|
||||
/// <returns>The Room-ID of the new room. Returns ushort.MaxValue, if there is no free chat room available.</returns>
|
||||
public ushort CreateChatRoom()
|
||||
{
|
||||
if (!this._freeRoomIds.TryTake(out ushort roomId))
|
||||
{
|
||||
throw new InvalidOperationException("There is no free room id, so the chat room couldn't be created.");
|
||||
}
|
||||
|
||||
var room = new ChatRoom(roomId, this._loggerFactory.CreateLogger<ChatRoom>());
|
||||
room.RoomClosed += this.OnChatRoomClosed;
|
||||
this._rooms.Add(roomId, room);
|
||||
return roomId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the chat room with the corresponding Room-ID.
|
||||
/// Returns null, if ChatRoom wasn't found.
|
||||
/// </summary>
|
||||
/// <param name="roomId">Room-ID.</param>
|
||||
/// <returns>ChatRoom or null.</returns>
|
||||
internal ChatRoom? GetChatRoom(ushort roomId)
|
||||
{
|
||||
this._rooms.TryGetValue(roomId, out var room);
|
||||
return room;
|
||||
}
|
||||
|
||||
private void OnChatRoomClosed(object? sender, ChatRoomClosedEventArgs eventArgs)
|
||||
{
|
||||
var room = eventArgs.ChatRoom;
|
||||
this._rooms.Remove(room.RoomId);
|
||||
this._freeRoomIds.Add(room.RoomId);
|
||||
room.Dispose();
|
||||
}
|
||||
}
|
||||
361
src/ChatServer/ChatServer.cs
Normal file
361
src/ChatServer/ChatServer.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
// <copyright file="ChatServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
/// <summary>
|
||||
/// Chat Server Listener that accepts incoming connections.
|
||||
/// </summary>
|
||||
public sealed class ChatServer : IChatServer, IDisposable
|
||||
{
|
||||
private readonly ChatRoomManager _manager;
|
||||
private readonly ILogger<ChatServer> _logger;
|
||||
private readonly IIpAddressResolver _addressResolver;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly PlugInManager _plugInManager;
|
||||
|
||||
private readonly RandomNumberGenerator _randomNumberGenerator;
|
||||
|
||||
private readonly IList<IChatClient> _connectedClients = new List<IChatClient>();
|
||||
|
||||
private readonly IList<ChatServerListener> _listeners = new List<ChatServerListener>();
|
||||
|
||||
private Timer? _clientCleanupTimer;
|
||||
private Timer? _roomCleanupTimer;
|
||||
|
||||
private ChatServerSettings? _settings;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
private ServerState _serverState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatServer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="addressResolver">The address resolver which returns the address on which the listener will be bound to.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="plugInManager">The plug in manager.</param>
|
||||
public ChatServer(IIpAddressResolver addressResolver, ILoggerFactory loggerFactory, PlugInManager plugInManager)
|
||||
{
|
||||
this._addressResolver = addressResolver;
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._plugInManager = plugInManager;
|
||||
this._logger = loggerFactory.CreateLogger<ChatServer>();
|
||||
this._manager = new ChatRoomManager(loggerFactory);
|
||||
this._randomNumberGenerator = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => this._settings?.Description ?? string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Id => this.Settings?.ServerId ?? SpecialServerIds.ChatServer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ConfigurationId => this._settings?.Id ?? Guid.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServerType Type => ServerType.ChatServer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerState ServerState
|
||||
{
|
||||
get => this._serverState;
|
||||
private set
|
||||
{
|
||||
if (value != this._serverState)
|
||||
{
|
||||
this._serverState = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaximumConnections => this.Settings.MaximumConnections;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CurrentConnections => this._connectedClients.Count;
|
||||
|
||||
private ChatServerSettings Settings => this._settings ?? throw new InvalidOperationException("The server was not initialized before");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ChatServerAuthenticationInfo?> RegisterClientAsync(ushort roomId, string clientName)
|
||||
{
|
||||
var room = this._manager.GetChatRoom(roomId);
|
||||
if (room is null)
|
||||
{
|
||||
var errorMessage = $"RegisterClient: Could not find chat room with id {roomId} for '{clientName}'.";
|
||||
this._logger.LogError(errorMessage);
|
||||
throw new ArgumentException(errorMessage, nameof(roomId));
|
||||
}
|
||||
|
||||
var ipAddress = await this._addressResolver.ResolveIPv4Async().ConfigureAwait(false);
|
||||
var index = room.GetNextClientIndex();
|
||||
var authenticationInfo = new ChatServerAuthenticationInfo(index, roomId, clientName, ipAddress.ToString(), this.GetRandomAuthenticationToken(index));
|
||||
room.RegisterClient(authenticationInfo);
|
||||
return authenticationInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<ushort> CreateChatRoomAsync()
|
||||
{
|
||||
return ValueTask.FromResult(this._manager.CreateChatRoom());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the listener of this chat server instance.
|
||||
/// </summary>
|
||||
public async ValueTask StartAsync()
|
||||
{
|
||||
if (this.ServerState != ServerState.Stopped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Begin starting");
|
||||
var oldState = this.ServerState;
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Starting;
|
||||
try
|
||||
{
|
||||
this.CreateListeners();
|
||||
foreach (var listener in this._listeners)
|
||||
{
|
||||
listener.Start();
|
||||
}
|
||||
|
||||
this.CreateCleanupTimers();
|
||||
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Started;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error while starting");
|
||||
this.ServerState = oldState;
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Finished starting");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.ShutdownAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the server with the specified settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <exception cref="System.InvalidOperationException">Can only initialize when server is stopped.</exception>
|
||||
public void Initialize(ChatServerSettings settings)
|
||||
{
|
||||
if (this.ServerState != ServerState.Stopped)
|
||||
{
|
||||
throw new InvalidOperationException("Can only initialize when server is stopped.");
|
||||
}
|
||||
|
||||
this._settings = settings;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ShutdownAsync()
|
||||
{
|
||||
if (this.ServerState != ServerState.Started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Begin shutdown");
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Stopping;
|
||||
this.RemoveCleanupTimers();
|
||||
foreach (var listener in this._listeners)
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
|
||||
this._listeners.Clear();
|
||||
|
||||
this._logger.LogDebug("Disconnecting all clients");
|
||||
var clients = this._connectedClients.ToList();
|
||||
foreach (var client in clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.LogOffAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error logging client off.");
|
||||
}
|
||||
}
|
||||
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Stopped;
|
||||
this._logger.LogInformation("Finished shutdown");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this._isDisposed)
|
||||
{
|
||||
this._isDisposed = true;
|
||||
this._randomNumberGenerator.Dispose();
|
||||
this._clientCleanupTimer?.Dispose();
|
||||
this._roomCleanupTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateCleanupTimers()
|
||||
{
|
||||
this._clientCleanupTimer = new Timer(this.Settings.ClientCleanUpInterval.TotalMilliseconds);
|
||||
this._clientCleanupTimer.Elapsed += this.ClientCleanupInactiveClients;
|
||||
this._clientCleanupTimer.Start();
|
||||
this._roomCleanupTimer = new Timer(this.Settings.RoomCleanUpInterval.TotalMilliseconds);
|
||||
this._roomCleanupTimer.Elapsed += this.ClientCleanupUnusedRooms;
|
||||
this._roomCleanupTimer.Start();
|
||||
}
|
||||
|
||||
private void RemoveCleanupTimers()
|
||||
{
|
||||
this._clientCleanupTimer?.Stop();
|
||||
this._clientCleanupTimer?.Dispose();
|
||||
this._clientCleanupTimer = null;
|
||||
|
||||
this._roomCleanupTimer?.Stop();
|
||||
this._roomCleanupTimer?.Dispose();
|
||||
this._roomCleanupTimer = null;
|
||||
}
|
||||
|
||||
private void CreateListeners()
|
||||
{
|
||||
foreach (var endpoint in this.Settings.Endpoints)
|
||||
{
|
||||
var listener = new ChatServerListener(endpoint, this._plugInManager, this._loggerFactory);
|
||||
listener.ClientAccepted += this.ChatClientAcceptedAsync;
|
||||
listener.ClientAccepting += this.ChatClientAcceptingAsync;
|
||||
this._listeners.Add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random authentication token.
|
||||
/// </summary>
|
||||
/// <param name="clientIndex">Index of the client.</param>
|
||||
/// <returns>The random authentication token as a string.</returns>
|
||||
/// <remarks>
|
||||
/// This is the original way of generating the token - not especially secure, but to keep it simple, I leave it that way.
|
||||
/// </remarks>
|
||||
private string GetRandomAuthenticationToken(byte clientIndex)
|
||||
{
|
||||
var authenticationToken = new byte[] { clientIndex, 0, 0, 0 };
|
||||
this._randomNumberGenerator.GetBytes(authenticationToken, 2, 2);
|
||||
var tokenAsString = authenticationToken.MakeDwordBigEndian(0).ToString();
|
||||
return tokenAsString;
|
||||
}
|
||||
|
||||
private async ValueTask ChatClientAcceptingAsync(CancelEventArgs e)
|
||||
{
|
||||
if (this.Settings.MaximumConnections == int.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = this.CurrentConnections >= this.Settings.MaximumConnections;
|
||||
}
|
||||
|
||||
private async ValueTask ChatClientAcceptedAsync(ClientAcceptedEventArgs e)
|
||||
{
|
||||
var chatClient = new ChatClient(e.AcceptedConnection, this._manager, this._loggerFactory.CreateLogger<ChatClient>());
|
||||
this._connectedClients.Add(chatClient);
|
||||
this.RaisePropertyChanged(nameof(this.CurrentConnections));
|
||||
chatClient.Disconnected += this.ChatClientDisconnected;
|
||||
}
|
||||
|
||||
private void ChatClientDisconnected(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is IChatClient client)
|
||||
{
|
||||
this._connectedClients.Remove(client);
|
||||
}
|
||||
|
||||
this.RaisePropertyChanged(nameof(this.CurrentConnections));
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void ClientCleanupInactiveClients(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bottomDateTimeMargin = DateTime.Now.Subtract(this.Settings.ClientTimeout);
|
||||
|
||||
for (int i = this._connectedClients.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var client = this._connectedClients[i];
|
||||
if (client.LastActivity >= bottomDateTimeMargin)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this._logger.LogDebug(
|
||||
"Disconnecting client {Client}, because of activity timeout. LastActivity: {ClientLastActivity}", client, client.LastActivity);
|
||||
|
||||
await client.LogOffAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error during checking for inactive clients");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClientCleanupUnusedRooms(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rooms = this._manager.OpenedRooms.Where(room => room.AuthenticationRequiredUntil < DateTime.Now && room.ConnectedClients.Count < 2).ToList();
|
||||
foreach (var room in rooms)
|
||||
{
|
||||
this._logger.LogInformation($"Cleaning up room {room.RoomId}");
|
||||
room.Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error during cleanup of unused rooms");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a property changed.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">Name of the property.</param>
|
||||
private void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
23
src/ChatServer/ChatServerEndpoint.cs
Normal file
23
src/ChatServer/ChatServerEndpoint.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// <copyright file="ChatServerEndpoint.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A client-version-specific endpoint for a chat server.
|
||||
/// </summary>
|
||||
public class ChatServerEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the tcp network port under which the server is listening for new clients.
|
||||
/// </summary>
|
||||
public int NetworkPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client version for which the endpoint is meant for.
|
||||
/// </summary>
|
||||
public ClientVersion ClientVersion { get; set; }
|
||||
}
|
||||
72
src/ChatServer/ChatServerListener.cs
Normal file
72
src/ChatServer/ChatServerListener.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// <copyright file="ChatServerListener.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.IO.Pipelines;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A listener which listens to the specified endpoint and provides the initialized <see cref="IConnection"/> by the event <see cref="ClientAccepted"/>.
|
||||
/// </summary>
|
||||
public class ChatServerListener
|
||||
{
|
||||
private readonly ChatServerEndpoint _endpoint;
|
||||
private readonly PlugInManager _plugInManager;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private Listener? _chatClientListener;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatServerListener" /> class.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint.</param>
|
||||
/// <param name="plugInManager">The plug in manager.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ChatServerListener(ChatServerEndpoint endpoint, PlugInManager plugInManager, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._endpoint = endpoint;
|
||||
this._plugInManager = plugInManager;
|
||||
this._loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a new client was accepted.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<ClientAcceptedEventArgs>? ClientAccepted;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a client has been accepted by the tcp listener, but before a <see cref="Connection"/> is created.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<CancelEventArgs>? ClientAccepting;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the tcp listener of this instance.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
this._chatClientListener = new Listener(this._endpoint.NetworkPort, this.CreateDecryptor, _ => null, this._loggerFactory);
|
||||
this._chatClientListener.ClientAccepted += async args => await this.ClientAccepted.SafeInvokeAsync(args).ConfigureAwait(false);
|
||||
this._chatClientListener.ClientAccepting += async args => await this.ClientAccepting.SafeInvokeAsync(args).ConfigureAwait(false);
|
||||
this._chatClientListener.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops this instance.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
this._chatClientListener?.Stop();
|
||||
}
|
||||
|
||||
private IPipelinedDecryptor? CreateDecryptor(PipeReader pipeReader)
|
||||
{
|
||||
var encryptionFactoryPlugIn = this._plugInManager.GetStrategy<ClientVersion, INetworkEncryptionFactoryPlugIn>(this._endpoint.ClientVersion)
|
||||
?? this._plugInManager.GetStrategy<ClientVersion, INetworkEncryptionFactoryPlugIn>(default);
|
||||
return encryptionFactoryPlugIn?.CreateDecryptor(pipeReader, DataDirection.ClientToServer);
|
||||
}
|
||||
}
|
||||
57
src/ChatServer/ChatServerSettings.cs
Normal file
57
src/ChatServer/ChatServerSettings.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// <copyright file="ChatServerSettings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the <see cref="ChatServer"/>.
|
||||
/// </summary>
|
||||
public class ChatServerSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier for this configuration.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server identifier.
|
||||
/// </summary>
|
||||
public int ServerId { get; set; } = SpecialServerIds.ChatServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = "Chat Server";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum connections.
|
||||
/// </summary>
|
||||
public int MaximumConnections { get; set; } = int.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client timeout. When a client did not send any data in this timespan, it's automatically disconnected.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The client timeout.
|
||||
/// </value>
|
||||
public TimeSpan ClientTimeout { get; set; } = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in which a client clean up takes place.
|
||||
/// For all connected clients it's checked whether or not the <see cref="ClientTimeout"/> has been reached.
|
||||
/// </summary>
|
||||
public TimeSpan ClientCleanUpInterval { get; set; } = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in which empty chat rooms are cleaned up.
|
||||
/// </summary>
|
||||
public TimeSpan RoomCleanUpInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the endpoints under which the chat server is available for specific game clients.
|
||||
/// </summary>
|
||||
public ICollection<ChatServerEndpoint> Endpoints { get; } = new List<ChatServerEndpoint>();
|
||||
}
|
||||
17
src/ChatServer/ExDbConnector/ChatServer.cfg
Normal file
17
src/ChatServer/ExDbConnector/ChatServer.cfg
Normal file
@@ -0,0 +1,17 @@
|
||||
##############################
|
||||
# ChatServer Configuration #
|
||||
##############################
|
||||
|
||||
# The following values are defaults and are even applied if this file or single configuration value-pairs are missing or are in the wrong format:
|
||||
# ChatServerListenerPort=55980
|
||||
# ExDbHost=127.0.0.1
|
||||
# ExDbPort=55906
|
||||
# Xor32Key=AB 11 CD FE 18 23 C5 A3 CA 33 C1 CC 66 67 21 F3 32 12 15 35 29 FF FE 1D 44 EF CD 41 26 3C 4E 4D
|
||||
|
||||
|
||||
ChatServerListenerPort=55980
|
||||
|
||||
ExDbHost=127.0.0.1
|
||||
ExDbPort=55906
|
||||
|
||||
Xor32Key=AB 11 CD FE 18 23 C5 A3 CA 33 C1 CC 66 67 21 F3 32 12 15 35 29 FF FE 1D 44 EF CD 41 26 3C 4E 4D
|
||||
@@ -0,0 +1,55 @@
|
||||
// <copyright file="ConfigurableNetworkEncryptionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.Network.Xor;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A configurable network encryption factory plugin which reads the Xor32 key from the ChatServer.cfg file. Only used by the ExDbConnector project.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = "Configurable encryption plugin", Description = "A configurable network encryption factory plugin which reads the Xor32 key from the ChatServer.cfg file. Only used by the ExDbConnector project.")]
|
||||
[Guid("890997B2-9334-4E9E-8C82-4492A831BCE3")]
|
||||
public class ConfigurableNetworkEncryptionPlugIn : INetworkEncryptionFactoryPlugIn
|
||||
{
|
||||
private readonly byte[] _xor32Key;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurableNetworkEncryptionPlugIn"/> class.
|
||||
/// </summary>
|
||||
public ConfigurableNetworkEncryptionPlugIn()
|
||||
{
|
||||
var settings = new Settings("ChatServer.cfg");
|
||||
this._xor32Key = settings.Xor32Key ?? new byte[32];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version for which this plugin is available.
|
||||
/// </summary>
|
||||
public static ClientVersion Version { get; } = new(byte.MaxValue, byte.MaxValue, ClientLanguage.Invariant);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ClientVersion Key => Version;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPipelinedDecryptor? CreateDecryptor(PipeReader source, DataDirection direction)
|
||||
{
|
||||
return new PipelinedXor32Decryptor(source, this._xor32Key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPipelinedEncryptor? CreateEncryptor(PipeWriter target, DataDirection direction)
|
||||
{
|
||||
// At least until season 6, there is no encryption from server to client.
|
||||
// ex700 may require packet twister here.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
275
src/ChatServer/ExDbConnector/ExDbClient.cs
Normal file
275
src/ChatServer/ExDbConnector/ExDbClient.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
// <copyright file="ExDbClient.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.Buffers;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.Packets;
|
||||
using Pipelines.Sockets.Unofficial;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
/// <summary>
|
||||
/// The connected exDB server. This class includes the communication implementation between chat server and exDB server.
|
||||
/// It registers clients for the chat server and hands back their authentication details.
|
||||
/// </summary>
|
||||
public class ExDbClient
|
||||
{
|
||||
private readonly ILogger<ExDbClient> _logger;
|
||||
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private readonly IChatServer _chatServer;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ushort _chatServerPort;
|
||||
private readonly byte[] _packetBuffer = new byte[0xFF];
|
||||
|
||||
private IConnection? _connection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExDbClient" /> class.
|
||||
/// </summary>
|
||||
/// <param name="host">The host address of the exDB server.</param>
|
||||
/// <param name="port">The host port of the exDB server.</param>
|
||||
/// <param name="chatServer">The chat server.</param>
|
||||
/// <param name="chatServerPort">The chat server port.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ExDbClient(string host, int port, IChatServer chatServer, int chatServerPort, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._host = host;
|
||||
this._port = port;
|
||||
this._chatServer = chatServer;
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._chatServerPort = (ushort)chatServerPort;
|
||||
this._logger = this._loggerFactory.CreateLogger<ExDbClient>();
|
||||
_ = Task.Run(this.ConnectAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the exDB server.
|
||||
/// </summary>
|
||||
public async ValueTask DisconnectAsync()
|
||||
{
|
||||
if (this._connection is { } connection)
|
||||
{
|
||||
await connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask ConnectAsync()
|
||||
{
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
|
||||
while (!socket.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(this._host, this._port).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
this._logger.LogWarning($"Connection to ExDB-Server ({this._host}:{this._port}) failed, trying again in 10 Seconds...");
|
||||
await Task.Delay(10000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Connection to ExDB-Server established");
|
||||
|
||||
this._connection = new Connection(SocketConnection.Create(socket), null, null, this._loggerFactory.CreateLogger<Connection>());
|
||||
this._connection.PacketReceived += this.ExDbPacketReceivedAsync;
|
||||
this._connection.Disconnected += this.ConnectAsync;
|
||||
await this.SendHelloAsync().ConfigureAwait(false);
|
||||
await this._connection!.BeginReceiveAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask SendHelloAsync()
|
||||
{
|
||||
// C1 3A 00 02 AC DA 43 68 61 74 53 65 72 76 65 72 00 ...
|
||||
int Write()
|
||||
{
|
||||
var length = 0x3A;
|
||||
var span = this._connection!.Output.GetSpan(length)[..length];
|
||||
var packet = span;
|
||||
packet[0] = 0xC1;
|
||||
packet[1] = 0x3A;
|
||||
packet[3] = 0x02;
|
||||
packet[4] = this._chatServerPort.GetLowByte();
|
||||
packet[5] = this._chatServerPort.GetHighByte();
|
||||
packet.Slice(6).WriteString("ChatServer", Encoding.UTF8);
|
||||
return length;
|
||||
}
|
||||
|
||||
await this._connection!.SendAsync(Write).ConfigureAwait(false);
|
||||
this._logger.LogInformation("Sent registration packet to ExDB-Server");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is called when a packet is received from the exDB-Server.
|
||||
/// </summary>
|
||||
/// <param name="sequence">The packet.</param>
|
||||
private async ValueTask ExDbPacketReceivedAsync(ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
try
|
||||
{
|
||||
sequence.CopyTo(this._packetBuffer);
|
||||
var packet = this._packetBuffer.AsMemory(0, (int)sequence.Length);
|
||||
var type = packet.Span[0];
|
||||
if (type != 0xC1)
|
||||
{
|
||||
this._logger.LogWarning($"Unknown packet received from ExDB-Server, type: {type}");
|
||||
return;
|
||||
}
|
||||
|
||||
var code = packet.Span[2];
|
||||
switch (code)
|
||||
{
|
||||
case 0xA0:
|
||||
await this.ReadChatRoomCreationAsync(packet).ConfigureAwait(false);
|
||||
break;
|
||||
case 0xA1:
|
||||
await this.ReadChatRoomInvitationAsync(packet).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
this._logger.LogWarning($"Unknown packet received from ExDB-Server, code: {code}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this._logger.LogError(exception, $"An error occurred while processing an incoming packet from ExDB: {this._packetBuffer.AsString()}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the invitation to an existing chat room and registers the invited client.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet.</param>
|
||||
/// <remarks>
|
||||
/// Example: C1 15 A1 00 00 00 61 62 63 64 65 66 67 68 69 6F 20 01 00 01 57
|
||||
/// Index 4 and 5 is the room id, the next 10 bytes is the client name, after that the player id, game server id and a "type".
|
||||
/// The chat server answers this with the same packets as above(ticket 96862210):
|
||||
/// C1 2C A0 01 00 00 61 62 63 64 65 66 67 68 69 6F CC CC CC CC CC CC CC CC CC CC 53 54 55 56 CC CC 02 00 C6 05 CC CC CC CC 57 CC CC CC.
|
||||
/// </remarks>
|
||||
private async ValueTask ReadChatRoomInvitationAsync(Memory<byte> packet)
|
||||
{
|
||||
ushort roomId = 0;
|
||||
string clientName = string.Empty;
|
||||
ushort clientPlayerId = 0;
|
||||
ushort clientServerId = 0;
|
||||
byte type = 0;
|
||||
|
||||
void Extract(Span<byte> packet)
|
||||
{
|
||||
roomId = NumberConversionExtensions.MakeWord(packet[4], packet[5]);
|
||||
clientName = packet.ExtractString(6, 10, Encoding.UTF8);
|
||||
clientPlayerId = packet.TryMakeWordBigEndian(16);
|
||||
clientServerId = packet.TryMakeWordBigEndian(18);
|
||||
type = packet.Length > 20 ? packet[20] : (byte)0x57;
|
||||
}
|
||||
|
||||
Extract(packet.Span);
|
||||
this._logger.LogDebug($"Received request to invite {clientName} to chat room {roomId}, Client-ID: {clientPlayerId}, Server-ID: {clientServerId}");
|
||||
if (await this._chatServer.RegisterClientAsync(roomId, clientName).ConfigureAwait(false) is { } authentication)
|
||||
{
|
||||
await this.SendAuthenticationAsync(authentication, null, clientPlayerId, clientServerId, type).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the chat room creation message, creates a new chat room and registers the clients.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet.</param>
|
||||
/// <remarks>
|
||||
/// For example, we get here the following packet in:
|
||||
/// C1 20 A0 41 42 43 44 45 46 47 48 49 4A 50 51 52 53 54 55 56 57 58 59 00 E0 2E 01 00 E1 2E 01 00
|
||||
/// This packet includes the header and both names of the creator and the invited chat partner (each 10 bytes long).
|
||||
/// The server should then send the following data back to the exDB-Server:
|
||||
/// s | rid ||-----client name-----------||---------other client name-||plid| |svid||---| |-ticket--| |--------???----------|
|
||||
/// C1 2C A0 01 00 00 41 42 43 44 45 46 47 48 49 4A 50 51 52 53 54 55 56 57 58 59 00 00 00 00 CC CC 00 00 11 04 CC CC CC CC 00 CC CC CC
|
||||
/// C1 2C A0 01 00 00 50 51 52 53 54 55 56 57 58 59 41 42 43 44 45 46 47 48 49 4A 00 00 00 00 CC CC 01 00 BB 05 CC CC CC CC 01 CC CC CC.
|
||||
/// </remarks>
|
||||
private async ValueTask ReadChatRoomCreationAsync(Memory<byte> packet)
|
||||
{
|
||||
string clientName = string.Empty;
|
||||
string friendName = string.Empty;
|
||||
ushort clientPlayerId = 0;
|
||||
ushort clientServerId = 0;
|
||||
ushort friendPlayerId = 0;
|
||||
ushort friendServerId = 0;
|
||||
|
||||
void Extract(Span<byte> packet)
|
||||
{
|
||||
clientName = packet.ExtractString(3, 10, Encoding.UTF8);
|
||||
friendName = packet.ExtractString(13, 10, Encoding.UTF8);
|
||||
clientPlayerId = packet.TryMakeWordBigEndian(24);
|
||||
clientServerId = packet.TryMakeWordBigEndian(26);
|
||||
friendPlayerId = packet.TryMakeWordBigEndian(28);
|
||||
friendServerId = packet.TryMakeWordBigEndian(30);
|
||||
}
|
||||
|
||||
Extract(packet.Span);
|
||||
var roomId = await this._chatServer.CreateChatRoomAsync().ConfigureAwait(false);
|
||||
this._logger.LogDebug($"Received request to create chat room for {clientName} and {friendName}; Room-ID: {roomId}; Client-ID: {clientPlayerId}; Server-ID: {clientServerId}; Friend-ID: {friendPlayerId}; Friend-Server: {friendServerId}");
|
||||
var requesterAuthentication = await this._chatServer.RegisterClientAsync(roomId, clientName).ConfigureAwait(false);
|
||||
var friendAuthentication = await this._chatServer.RegisterClientAsync(roomId, friendName).ConfigureAwait(false);
|
||||
if (requesterAuthentication is not null)
|
||||
{
|
||||
await this.SendAuthenticationAsync(requesterAuthentication, friendAuthentication, clientPlayerId, clientServerId, requesterAuthentication.Index).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (friendAuthentication is not null)
|
||||
{
|
||||
await this.SendAuthenticationAsync(friendAuthentication, requesterAuthentication, friendPlayerId, friendServerId, friendAuthentication.Index).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the authentication information back to the ExDB-Server.
|
||||
/// </summary>
|
||||
/// <param name="authenticationInfo">The authentication information.</param>
|
||||
/// <param name="friendAuthenticationInfo">The friend authentication information.</param>
|
||||
/// <param name="clientId">The client identifier on the server where the client plays on.</param>
|
||||
/// <param name="serverId">The server identifier where the client plays on.</param>
|
||||
/// <param name="type">The type. Usually 0 for the player who requested the chat and 1 for the other player.</param>
|
||||
private async ValueTask SendAuthenticationAsync(ChatServerAuthenticationInfo authenticationInfo, ChatServerAuthenticationInfo? friendAuthenticationInfo, ushort clientId, ushort serverId, byte type)
|
||||
{
|
||||
this._logger.LogDebug($"Registered client {authenticationInfo.ClientName} with index {authenticationInfo.Index} and token {authenticationInfo.AuthenticationToken}");
|
||||
var token = uint.Parse(authenticationInfo.AuthenticationToken);
|
||||
uint friendToken = 0;
|
||||
if (friendAuthenticationInfo != null)
|
||||
{
|
||||
friendToken = uint.Parse(friendAuthenticationInfo.AuthenticationToken);
|
||||
}
|
||||
|
||||
var roomId = authenticationInfo.RoomId;
|
||||
|
||||
int Write()
|
||||
{
|
||||
var length = 0x2C;
|
||||
var packet = this._connection!.Output.GetSpan(length);
|
||||
packet[0] = 0xC1;
|
||||
packet[1] = 0x2C;
|
||||
packet[2] = 0xA0;
|
||||
packet[3] = 0x01;
|
||||
WriteUInt16LittleEndian(packet.Slice(4), roomId);
|
||||
packet.Slice(6).WriteString(authenticationInfo.ClientName, Encoding.UTF8);
|
||||
if (friendAuthenticationInfo != null)
|
||||
{
|
||||
packet.Slice(16).WriteString(friendAuthenticationInfo.ClientName, Encoding.UTF8);
|
||||
}
|
||||
|
||||
WriteUInt16LittleEndian(packet.Slice(26), clientId);
|
||||
WriteUInt16LittleEndian(packet.Slice(28), serverId);
|
||||
WriteUInt32LittleEndian(packet.Slice(32), token);
|
||||
WriteUInt32LittleEndian(packet.Slice(36), friendToken);
|
||||
packet[40] = type;
|
||||
return length;
|
||||
}
|
||||
|
||||
await this._connection!.SendAsync(Write).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AssemblyName>ChatServer</AssemblyName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
<ProjectReference Include="..\MUnique.OpenMU.ChatServer.csproj" />
|
||||
<ProjectReference Include="..\..\Network\MUnique.OpenMU.Network.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ChatServer.cfg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AssemblyName>ChatServer</AssemblyName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
<ProjectReference Include="..\MUnique.OpenMU.ChatServer.csproj" />
|
||||
<ProjectReference Include="..\..\Network\MUnique.OpenMU.Network.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ChatServer.cfg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
79
src/ChatServer/ExDbConnector/Program.cs
Normal file
79
src/ChatServer/ExDbConnector/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.ComponentModel.Design;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MUnique.OpenMU.ChatServer;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Serilog;
|
||||
using Serilog.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry class of the application.
|
||||
/// </summary>
|
||||
internal class Program
|
||||
{
|
||||
private static ILogger<Program> _logger = NullLogger<Program>.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments. </param>
|
||||
internal static async Task Main(string[] args)
|
||||
{
|
||||
SelfLog.Enable(Console.Error);
|
||||
var logConfiguration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", false, true)
|
||||
.Build();
|
||||
|
||||
var logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(logConfiguration)
|
||||
.CreateLogger();
|
||||
|
||||
var loggerFactory = new LoggerFactory().AddSerilog(logger);
|
||||
_logger = loggerFactory.CreateLogger<Program>();
|
||||
|
||||
var addressResolver = IpAddressResolverFactory.CreateIpResolver(args, null, loggerFactory);
|
||||
var settings = new Settings("ChatServer.cfg");
|
||||
var serviceContainer = new ServiceContainer();
|
||||
serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
|
||||
|
||||
int chatServerListenerPort = settings.ChatServerListenerPort ?? 55980;
|
||||
int exDbPort = settings.ExDbPort ?? 55906;
|
||||
string exDbHost = settings.ExDbHost ?? "127.0.0.1";
|
||||
|
||||
try
|
||||
{
|
||||
// To make the chat server use our configured encryption key, we need to trick a bit. We add an endpoint with a special client version which is defined in the plugin.
|
||||
var configuration = new ChatServerSettings();
|
||||
configuration.Endpoints.Add(new ChatServerEndpoint { ClientVersion = ConfigurableNetworkEncryptionPlugIn.Version, NetworkPort = chatServerListenerPort });
|
||||
var pluginManager = new PlugInManager(null, loggerFactory, serviceContainer, null);
|
||||
pluginManager.DiscoverAndRegisterPlugInsOf<INetworkEncryptionFactoryPlugIn>();
|
||||
var chatServer = new ChatServer(addressResolver, loggerFactory, pluginManager);
|
||||
chatServer.Initialize(configuration);
|
||||
await chatServer.StartAsync().ConfigureAwait(false);
|
||||
var exDbClient = new ExDbClient(exDbHost, exDbPort, chatServer, chatServerListenerPort, loggerFactory);
|
||||
_logger.LogInformation("ChatServer started and ready");
|
||||
while (Console.ReadLine() != "exit")
|
||||
{
|
||||
// keep application running
|
||||
}
|
||||
|
||||
await exDbClient.DisconnectAsync().ConfigureAwait(false);
|
||||
await chatServer.ShutdownAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "Unexpected error occured");
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/ChatServer/ExDbConnector/Properties/AssemblyInfo.cs
Normal file
12
src/ChatServer/ExDbConnector/Properties/AssemblyInfo.cs
Normal 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.ChatServer.ExDbConnector")]
|
||||
[assembly: InternalsVisibleTo("MUnique.OpenMU.ChatServer.ExDbConnector.Tests")]
|
||||
156
src/ChatServer/ExDbConnector/Readme.md
Normal file
156
src/ChatServer/ExDbConnector/Readme.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# ChatServer ExDB Connector
|
||||
|
||||
This isn't directly a part of the OpenMU project. It's more like a side product
|
||||
to make the ChatServer available to users of the 'classical' private MU Servers.
|
||||
They have - or maybe had :) - the problem that they are bound to use the
|
||||
original closed source ChatServer of Webzen, if they get it working at all.
|
||||
|
||||
So to offer an open source alternative to the original ChatServer of Webzen,
|
||||
you can use this project to connect the OpenMU-ChatServer with your 'classic'
|
||||
ExDB server.
|
||||
|
||||
## Configuration
|
||||
|
||||
To make this work correctly with an existing ExDB-Server, some might do some
|
||||
minor adjustments in the configuration.
|
||||
|
||||
It's all configured in the ChatServer.cfg an should be self-explanatory.
|
||||
|
||||
### ChatServerListenerPort
|
||||
|
||||
It's the port to which the game clients should connect. Default is 55980,
|
||||
but I'm not sure if it can be changed without modifying the client.
|
||||
|
||||
### ExDbHost and Port
|
||||
|
||||
The host and tcp port of the ExDB server. Usually it's on the same server, so
|
||||
127.0.0.1 on port 55906.
|
||||
|
||||
### Xor32Key
|
||||
|
||||
This one is actually very important to get right. Otherwise, the game clients
|
||||
will not be able to connect.
|
||||
It's the same XOR32 key which is used for the 0xC1 packet encryption from game
|
||||
client to game server.
|
||||
|
||||
You can't edit this key at the original ChatServer of Webzen, that's the reason
|
||||
why it's pretty hard to get the ChatServer working on a private server.
|
||||
|
||||
## Communication between ExDB-Server and ChatServer
|
||||
|
||||
The ExDB server usually leaves the tcp port 55906 open, so that the ChatServer
|
||||
(and maybe other kind of subservers?) can connect to it.
|
||||
|
||||
### Registration
|
||||
|
||||
When the ChatServer connects to the ExDB server, it sends a data packet to
|
||||
register itself. It has the following struture:
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x3A | Packet header - length of the packet |
|
||||
| 1 | byte | 0x00 | Packet Type "server registration" |
|
||||
| 1 | byte | 0x02 | Id for "ChatServer" |
|
||||
| 2 | ushort | 0xDAAC | ChatServer client port (default: 55980) |
|
||||
| 11 | string | "ChatServer" | ChatServer name |
|
||||
|
||||
Example: C1 3A 00 02 AC DA 43 68 61 74 53 65 72 76 65 72 00
|
||||
|
||||
From now, the ChatServer will receive chat room creation and invitation
|
||||
requests from the ExDB Server, which were previously requested by the players.
|
||||
|
||||
### Chat Room Creation Request
|
||||
|
||||
When a client requests to create a new chat room, the following data packet is
|
||||
sent from the ExDB Server to the ChatServer.
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x25 | Packet header - length of the packet |
|
||||
| 1 | byte | 0xA0 | Packet Type 'chat room creation' |
|
||||
| 10 | string | | Name of the character who wants to create the room |
|
||||
| 10 | string | | Name of the character who should be invited to the room |
|
||||
| 1 | byte | 0x01 | "Type", not relevant? |
|
||||
| 2 | ushort | | Player id of the character who wants to create the room, big endian |
|
||||
| 2 | ushort | | Server id of the character who wants to create the room, big endian |
|
||||
| 2 | ushort | | Player id of the character who should be invited, big endian |
|
||||
| 2 | ushort | | Server id of the character who should be invited, big endian |
|
||||
|
||||
Example:
|
||||
C1 25 A0 41 42 43 44 45
|
||||
46 47 48 49 4A 50 51 52
|
||||
53 54 55 56 57 58 59 01
|
||||
20 01 00 01 20 02 00 01
|
||||
|
||||
### Chat Room Creation Responses
|
||||
|
||||
For each of both players, there is one data packet sent back to the ExDB Server:
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x2C | Packet header - length of the packet |
|
||||
| 1 | byte | 0xA0 | Packet Type 'chat room creation' |
|
||||
| 1 | byte | 0x01 | Success flag |
|
||||
| 2 | ushort | | Chat room id, big endian |
|
||||
| 10 | string | | Name of the character to which a chat room invitation should be sent |
|
||||
| 10 | string | | Name of the chat partner character |
|
||||
| 2 | ushort | | Player id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 2 | ushort | | Server id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 2 | byte | | Padding bytes for the alignment of the following authentication token |
|
||||
| 4 | uint | | Authentication token of the character to which a chat room invitation should be sent, big endian |
|
||||
| 4 | uint | | Authentication token of the chat partner, big endian |
|
||||
| 1 | byte | | 'Type' |
|
||||
| 3 | byte | | Don't know - padding?|
|
||||
|
||||
Example First Player:
|
||||
C1 2C A0 01 00 00 41 42
|
||||
43 44 45 46 47 48 49 4A
|
||||
50 51 52 53 54 55 56 57
|
||||
58 59 00 00 00 00 CC CC
|
||||
00 00 11 04 01 00 BB 05
|
||||
00 CC CC CC
|
||||
|
||||
Example Second Player:
|
||||
C1 2C A0 01 00 00 50 51
|
||||
52 53 54 55 56 57 58 59
|
||||
41 42 43 44 45 46 47 48
|
||||
49 4A 00 00 00 00 CC CC
|
||||
01 00 BB 05 00 00 11 04
|
||||
01 CC CC CC
|
||||
|
||||
### Chat Room Invitation Request
|
||||
|
||||
When a client requests to invite another friend to an existing chat room, the
|
||||
following data packet is sent from the ExDB Server to the ChatServer.
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x16 | Packet header - length of the packet |
|
||||
| 1 | byte | 0xA1 | Packet Type 'chat room invitation' |
|
||||
| 1 | byte | 0x00 | Padding |
|
||||
| 2 | ushort | | Chat room id, big endian |
|
||||
| 10 | string | | Name of the character who should be invited to the room |
|
||||
| 2 | ushort | | Player id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 2 | ushort | | Server id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 1 | byte | | 'Type' |
|
||||
|
||||
Example:
|
||||
C1 15 A1 00 00 00 61 62
|
||||
63 64 65 66 67 68 69 6F
|
||||
01 20 01 00 57
|
||||
|
||||
The ChatServer answers this with the same packet as above, but without filling
|
||||
the second character name - no wonder, there is more than one player in the
|
||||
room already.
|
||||
|
||||
Example:
|
||||
C1 2C A0 01 00 00 61 62
|
||||
63 64 65 66 67 68 69 6F
|
||||
CC CC CC CC CC CC CC CC
|
||||
CC CC 01 20 01 00 CC CC
|
||||
02 00 C6 05 CC CC CC CC
|
||||
57 CC CC CC
|
||||
121
src/ChatServer/ExDbConnector/Settings.cs
Normal file
121
src/ChatServer/ExDbConnector/Settings.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
// <copyright file="Settings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// A class which reads settings from a file.
|
||||
/// Line Format:
|
||||
/// [Key]=[Value]
|
||||
/// Line comments can be added by starting with "#".
|
||||
/// </summary>
|
||||
internal class Settings
|
||||
{
|
||||
private readonly IDictionary<string, string> _settingsDictionary = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Settings"/> class.
|
||||
/// Reads the file contents in, if the file is available.
|
||||
/// </summary>
|
||||
/// <param name="file">The file.</param>
|
||||
public Settings(string file)
|
||||
{
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var line in File.ReadAllLines(file))
|
||||
{
|
||||
var elements = line.Split('=');
|
||||
if (elements.Length > 1
|
||||
&& !elements[0].StartsWith("#", StringComparison.InvariantCulture)
|
||||
&& !this._settingsDictionary.ContainsKey(elements[0]))
|
||||
{
|
||||
this._settingsDictionary.Add(elements[0], elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured chat server listener port.
|
||||
/// </summary>
|
||||
public int? ChatServerListenerPort
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this["ChatServerListenerPort"] != null && int.TryParse(this["ChatServerListenerPort"], out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured exDb server port.
|
||||
/// </summary>
|
||||
public int? ExDbPort
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this["ExDbPort"] != null)
|
||||
{
|
||||
if (int.TryParse(this["ExDbPort"], out int result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured exDb server host.
|
||||
/// </summary>
|
||||
public string? ExDbHost => this["ExDbHost"];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured xor32 key.
|
||||
/// </summary>
|
||||
public byte[]? Xor32Key
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this["Xor32Key"] != null)
|
||||
{
|
||||
var customXor32KeyList = new List<byte>();
|
||||
var keyAsString = this["Xor32Key"];
|
||||
if (keyAsString is not null)
|
||||
{
|
||||
var bytesAsString = keyAsString.Split(' ');
|
||||
foreach (var byteString in bytesAsString)
|
||||
{
|
||||
customXor32KeyList.Add(byte.Parse(byteString, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
return customXor32KeyList.ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
this._settingsDictionary.TryGetValue(key, out var value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/ChatServer/ExDbConnector/appsettings.json
Normal file
38
src/ChatServer/ExDbConnector/appsettings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Fatal",
|
||||
"System": "Fatal",
|
||||
"Npgsql": "Information",
|
||||
"MUnique.OpenMU.Network.Connection": "Error",
|
||||
"MUnique": "Information"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "logs/log.txt",
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] [{EventId}] {Message}{NewLine}{Exception}",
|
||||
"rollOnFileSizeLimit": true,
|
||||
"fileSizeLimitBytes": 4194304,
|
||||
"retainedFileCountLimit": 48,
|
||||
"rollingInterval": "Hour"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [ "FromLogContext" ],
|
||||
"Properties": {
|
||||
"Application": "MUnique.OpenMU.ChatServer.ExDbConnector"
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/ChatServer/IChatClient.cs
Normal file
73
src/ChatServer/IChatClient.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// <copyright file="IChatClient.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer;
|
||||
|
||||
/// <summary>
|
||||
/// Type of the chat room client update message.
|
||||
/// </summary>
|
||||
public enum ChatRoomClientUpdateType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A client joined the chat room. Then the client which receives the message adds this client to it's local chat room client list.
|
||||
/// </summary>
|
||||
Joined = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A client left the chat room. Then the client which receives the message removes this client from it's local chat room client list.
|
||||
/// </summary>
|
||||
Left = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a chat client.
|
||||
/// </summary>
|
||||
public interface IChatClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the client in the room.
|
||||
/// </summary>
|
||||
byte Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authentication token which was sent by the client.
|
||||
/// </summary>
|
||||
string? AuthenticationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nickname.
|
||||
/// </summary>
|
||||
string? Nickname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last activity.
|
||||
/// </summary>
|
||||
DateTime LastActivity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Logs the chat client off, which means it removes it from it's current chat room and closes the connection.
|
||||
/// </summary>
|
||||
ValueTask LogOffAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Sends the message to this chat client.
|
||||
/// </summary>
|
||||
/// <param name="senderId">The sender identifier.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
ValueTask SendMessageAsync(byte senderId, string message);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the client list of the chat room to this client.
|
||||
/// </summary>
|
||||
/// <param name="clients">The chat room clients.</param>
|
||||
ValueTask SendChatRoomClientListAsync(IReadOnlyCollection<IChatClient> clients);
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the client that another client has joined the chat room.
|
||||
/// </summary>
|
||||
/// <param name="updatedClientId">The joined client identifier.</param>
|
||||
/// <param name="updatedClientName">Name of the joined client.</param>
|
||||
/// <param name="updateType">Type of the update (join or leave).</param>
|
||||
ValueTask SendChatRoomClientUpdateAsync(byte updatedClientId, string updatedClientName, ChatRoomClientUpdateType updateType);
|
||||
}
|
||||
31
src/ChatServer/MUnique.OpenMU.ChatServer.csproj
Normal file
31
src/ChatServer/MUnique.OpenMU.ChatServer.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<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.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Release\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.ChatServer.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="ExDbConnector\**" />
|
||||
<EmbeddedResource Remove="ExDbConnector\**" />
|
||||
<None Remove="ExDbConnector\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\Network\MUnique.OpenMU.Network.csproj" />
|
||||
<ProjectReference Include="..\Network\Packets\MUnique.OpenMU.Network.Packets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
12
src/ChatServer/Properties/AssemblyInfo.cs
Normal file
12
src/ChatServer/Properties/AssemblyInfo.cs
Normal 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.ChatServer")]
|
||||
[assembly: InternalsVisibleTo("MUnique.OpenMU.ChatServer.Tests")]
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.ClientErrorLogDecryptor.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ClientErrorLogDecryptor.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
41
src/ClientErrorLogDecryptor/Program.cs
Normal file
41
src/ClientErrorLogDecryptor/Program.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.IO;
|
||||
|
||||
var logPath = args.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(logPath))
|
||||
{
|
||||
Console.WriteLine("No path to the error log specified as first parameter. Press any key to exit...");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
var decryptionKey = new byte[] { 0x7C, 0xBD, 0x81, 0x9F, 0x3D, 0x93, 0xE2, 0x56, 0x2A, 0x73, 0xD2, 0x3E, 0xF2, 0x83, 0x95, 0xBF };
|
||||
try
|
||||
{
|
||||
var fileBytes = await File.ReadAllBytesAsync(logPath).ConfigureAwait(false);
|
||||
|
||||
var resultBuilder = new StringBuilder();
|
||||
var i = 0;
|
||||
foreach (var character in fileBytes)
|
||||
{
|
||||
var result = (char)(character ^ decryptionKey[i % decryptionKey.Length]);
|
||||
resultBuilder.Append(result);
|
||||
i++;
|
||||
}
|
||||
|
||||
var resultPath = logPath + ".decrypted.txt";
|
||||
File.WriteAllText(resultPath, resultBuilder.ToString());
|
||||
Console.WriteLine($"Decrypted file has been written to {resultPath}. Press any key to exit...");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Unexpected error during decrypting the file '{logPath}':");
|
||||
Console.WriteLine(ex);
|
||||
|
||||
Console.WriteLine($"Press any key to exit...");
|
||||
}
|
||||
|
||||
Console.ReadKey();
|
||||
17
src/ClientErrorLogDecryptor/Readme.md
Normal file
17
src/ClientErrorLogDecryptor/Readme.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Client Error Log Decryptor
|
||||
|
||||
This tool can be used to decrypt the MuError.log files which are created by the
|
||||
game client on every start.
|
||||
|
||||
It might help to find/analyse errors which occured on the client side.
|
||||
|
||||
## Usage
|
||||
|
||||
This is a command line tool. The file name is given as first parameter. If it
|
||||
contains spaces, wrap it by quotes.
|
||||
Example: ```MUnique.OpenMU.ClientErrorLogDecryptor.exe "C:\MU-Season 6E3\MuError.log"```
|
||||
|
||||
On windows you can also just simply drag & drop your MuError.log file on the exe.
|
||||
|
||||
The resulting file is written out into a new file, where the file name is extended
|
||||
by ".decrypted.txt".
|
||||
21
src/ClientLauncher/ClientColorDepth.cs
Normal file
21
src/ClientLauncher/ClientColorDepth.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="ClientColorDepth.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
/// <summary>
|
||||
/// The available color depths for the mu online client.
|
||||
/// </summary>
|
||||
public enum ClientColorDepth
|
||||
{
|
||||
/// <summary>
|
||||
/// The 16 bit color depth.
|
||||
/// </summary>
|
||||
Bit16 = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The 32 bit color depth.
|
||||
/// </summary>
|
||||
Bit32 = 1,
|
||||
}
|
||||
27
src/ClientLauncher/ClientLanguage.cs
Normal file
27
src/ClientLauncher/ClientLanguage.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="ClientLanguage.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
/// <summary>
|
||||
/// The available mu online client languages.
|
||||
/// Only applicable for the global (english) client.
|
||||
/// </summary>
|
||||
public enum ClientLanguage
|
||||
{
|
||||
/// <summary>
|
||||
/// The english language.
|
||||
/// </summary>
|
||||
English = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The portuguese language.
|
||||
/// </summary>
|
||||
Portuguese = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The spanish language.
|
||||
/// </summary>
|
||||
Spanish = 2,
|
||||
}
|
||||
37
src/ClientLauncher/ClientLanguageExtensions.cs
Normal file
37
src/ClientLauncher/ClientLanguageExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// <copyright file="ClientLanguageExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="ClientLanguage"/>.
|
||||
/// </summary>
|
||||
public static class ClientLanguageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The language keys used in the registry.
|
||||
/// </summary>
|
||||
private static readonly string[] LanguageKeys = { "Eng", "Por", "Spn" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string which should be set in the registry for the given <see cref="ClientLanguage"/>.
|
||||
/// </summary>
|
||||
/// <param name="clientLanguage">The client language.</param>
|
||||
/// <returns>The string which should be set in the registry for the given <see cref="ClientLanguage"/>.</returns>
|
||||
public static string? GetString(this ClientLanguage clientLanguage)
|
||||
{
|
||||
var index = (int)clientLanguage;
|
||||
return index < LanguageKeys.Length ? LanguageKeys[index] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ClientLanguage"/> based on the given key, which is set in the registry.
|
||||
/// </summary>
|
||||
/// <param name="languageKey">The language key.</param>
|
||||
/// <returns>The <see cref="ClientLanguage"/> based on the given key, which is set in the registry.</returns>
|
||||
public static ClientLanguage GetLanguage(this string languageKey)
|
||||
{
|
||||
return (ClientLanguage)Array.IndexOf(LanguageKeys, languageKey);
|
||||
}
|
||||
}
|
||||
41
src/ClientLauncher/ClientResolution.cs
Normal file
41
src/ClientLauncher/ClientResolution.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="ClientResolution.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
/// <summary>
|
||||
/// The available mu online client screen resolutions.
|
||||
/// </summary>
|
||||
public class ClientResolution
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientResolution"/> class.
|
||||
/// </summary>
|
||||
public ClientResolution()
|
||||
{
|
||||
this.Index = 0;
|
||||
this.Caption = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientResolution"/> class.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <param name="caption">The caption.</param>
|
||||
public ClientResolution(int index, string caption)
|
||||
{
|
||||
this.Index = index;
|
||||
this.Caption = caption;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index.
|
||||
/// </summary>
|
||||
public int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the caption.
|
||||
/// </summary>
|
||||
public string Caption { get; init; }
|
||||
}
|
||||
94
src/ClientLauncher/ClientSettings.cs
Normal file
94
src/ClientLauncher/ClientSettings.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
// <copyright file="ClientSettings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
/// <summary>
|
||||
/// This class allows to read and write the game client settings in the registry.
|
||||
/// </summary>
|
||||
internal class ClientSettings
|
||||
{
|
||||
private const string DefaultLanguage = "Eng";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client color depth.
|
||||
/// </summary>
|
||||
public ClientColorDepth ClientColorDepth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is music enabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is music enabled; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsMusicEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is sound enabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is sound enabled; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsSoundEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the volume level.
|
||||
/// </summary>
|
||||
public int VolumeLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is window mode active.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is window mode active; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsWindowModeActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution.
|
||||
/// </summary>
|
||||
public int ResolutionIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language selection.
|
||||
/// </summary>
|
||||
public ClientLanguage LangSelection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads the settings from the windows registry.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void Load()
|
||||
{
|
||||
using var currentUserKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32);
|
||||
using var key = currentUserKey.CreateSubKey(@"SOFTWARE\WebZen\Mu\Config");
|
||||
this.ClientColorDepth = (ClientColorDepth)(key.GetValue("ColorDepth") ?? 0);
|
||||
this.IsMusicEnabled = (int)(key.GetValue("MusicOnOff") ?? 0) == 1;
|
||||
this.IsSoundEnabled = (int)(key.GetValue("SoundOnOff") ?? 0) == 1;
|
||||
this.VolumeLevel = (int)(key.GetValue("VolumeLevel") ?? 0);
|
||||
this.IsWindowModeActive = (int)(key.GetValue("WindowMode") ?? 0) == 1;
|
||||
this.ResolutionIndex = (int)(key.GetValue("Resolution") ?? 0);
|
||||
this.LangSelection = ((string?)key.GetValue("LangSelection") ?? DefaultLanguage).GetLanguage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the settings at the windows registry.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void Save()
|
||||
{
|
||||
using var currentUserKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32);
|
||||
using var key = currentUserKey.CreateSubKey(@"SOFTWARE\WebZen\Mu\Config");
|
||||
key.SetValue("ColorDepth", this.ClientColorDepth, RegistryValueKind.DWord);
|
||||
key.SetValue("MusicOnOff", this.IsMusicEnabled, RegistryValueKind.DWord);
|
||||
key.SetValue("SoundOnOff", this.IsSoundEnabled, RegistryValueKind.DWord);
|
||||
key.SetValue("VolumeLevel", this.VolumeLevel, RegistryValueKind.DWord);
|
||||
key.SetValue("WindowMode", this.IsWindowModeActive, RegistryValueKind.DWord);
|
||||
key.SetValue("Resolution", this.ResolutionIndex, RegistryValueKind.DWord);
|
||||
key.SetValue("LangSelection", this.LangSelection.GetString() ?? DefaultLanguage, RegistryValueKind.String);
|
||||
}
|
||||
}
|
||||
248
src/ClientLauncher/ClientSettingsDialog.Designer.cs
generated
Normal file
248
src/ClientLauncher/ClientSettingsDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,248 @@
|
||||
namespace MUnique.OpenMU.ClientLauncher
|
||||
{
|
||||
partial class ClientSettingsDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
windowModeCheckBox = new System.Windows.Forms.CheckBox();
|
||||
musicActiveCheckBox = new System.Windows.Forms.CheckBox();
|
||||
soundActiveCheckBox = new System.Windows.Forms.CheckBox();
|
||||
groupBox3 = new System.Windows.Forms.GroupBox();
|
||||
soundVolumeTrackBar = new System.Windows.Forms.TrackBar();
|
||||
groupBox4 = new System.Windows.Forms.GroupBox();
|
||||
clientLanguageComboBox = new System.Windows.Forms.ComboBox();
|
||||
saveButton = new System.Windows.Forms.Button();
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
groupBox5 = new System.Windows.Forms.GroupBox();
|
||||
clientResolutionComboBox = new System.Windows.Forms.ComboBox();
|
||||
groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
colorDepthComboBox = new System.Windows.Forms.ComboBox();
|
||||
groupBox3.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)soundVolumeTrackBar).BeginInit();
|
||||
groupBox4.SuspendLayout();
|
||||
groupBox5.SuspendLayout();
|
||||
groupBox1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// windowModeCheckBox
|
||||
//
|
||||
windowModeCheckBox.AutoSize = true;
|
||||
windowModeCheckBox.Location = new System.Drawing.Point(22, 286);
|
||||
windowModeCheckBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
windowModeCheckBox.Name = "windowModeCheckBox";
|
||||
windowModeCheckBox.Size = new System.Drawing.Size(104, 19);
|
||||
windowModeCheckBox.TabIndex = 0;
|
||||
windowModeCheckBox.Text = "Window Mode";
|
||||
windowModeCheckBox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// musicActiveCheckBox
|
||||
//
|
||||
musicActiveCheckBox.AutoSize = true;
|
||||
musicActiveCheckBox.Location = new System.Drawing.Point(136, 22);
|
||||
musicActiveCheckBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
musicActiveCheckBox.Name = "musicActiveCheckBox";
|
||||
musicActiveCheckBox.Size = new System.Drawing.Size(125, 19);
|
||||
musicActiveCheckBox.TabIndex = 3;
|
||||
musicActiveCheckBox.Text = "Background Music";
|
||||
musicActiveCheckBox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// soundActiveCheckBox
|
||||
//
|
||||
soundActiveCheckBox.AutoSize = true;
|
||||
soundActiveCheckBox.Location = new System.Drawing.Point(10, 22);
|
||||
soundActiveCheckBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
soundActiveCheckBox.Name = "soundActiveCheckBox";
|
||||
soundActiveCheckBox.Size = new System.Drawing.Size(98, 19);
|
||||
soundActiveCheckBox.TabIndex = 4;
|
||||
soundActiveCheckBox.Text = "Sound Effects";
|
||||
soundActiveCheckBox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// groupBox3
|
||||
//
|
||||
groupBox3.Controls.Add(soundVolumeTrackBar);
|
||||
groupBox3.Controls.Add(musicActiveCheckBox);
|
||||
groupBox3.Controls.Add(soundActiveCheckBox);
|
||||
groupBox3.Location = new System.Drawing.Point(19, 115);
|
||||
groupBox3.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox3.Name = "groupBox3";
|
||||
groupBox3.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox3.Size = new System.Drawing.Size(315, 111);
|
||||
groupBox3.TabIndex = 7;
|
||||
groupBox3.TabStop = false;
|
||||
groupBox3.Text = "Sound";
|
||||
//
|
||||
// soundVolumeTrackBar
|
||||
//
|
||||
soundVolumeTrackBar.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
soundVolumeTrackBar.Location = new System.Drawing.Point(4, 63);
|
||||
soundVolumeTrackBar.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
soundVolumeTrackBar.Name = "soundVolumeTrackBar";
|
||||
soundVolumeTrackBar.Size = new System.Drawing.Size(307, 45);
|
||||
soundVolumeTrackBar.TabIndex = 5;
|
||||
//
|
||||
// groupBox4
|
||||
//
|
||||
groupBox4.Controls.Add(clientLanguageComboBox);
|
||||
groupBox4.Location = new System.Drawing.Point(19, 233);
|
||||
groupBox4.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox4.Name = "groupBox4";
|
||||
groupBox4.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox4.Size = new System.Drawing.Size(315, 46);
|
||||
groupBox4.TabIndex = 8;
|
||||
groupBox4.TabStop = false;
|
||||
groupBox4.Text = "Language";
|
||||
//
|
||||
// clientLanguageComboBox
|
||||
//
|
||||
clientLanguageComboBox.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
clientLanguageComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
clientLanguageComboBox.FormattingEnabled = true;
|
||||
clientLanguageComboBox.Items.AddRange(new object[] { "English", "Portuguese", "Spanish" });
|
||||
clientLanguageComboBox.Location = new System.Drawing.Point(4, 20);
|
||||
clientLanguageComboBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
clientLanguageComboBox.Name = "clientLanguageComboBox";
|
||||
clientLanguageComboBox.Size = new System.Drawing.Size(307, 23);
|
||||
clientLanguageComboBox.TabIndex = 0;
|
||||
//
|
||||
// saveButton
|
||||
//
|
||||
saveButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveButton.DialogResult = System.Windows.Forms.DialogResult.OK;
|
||||
saveButton.Location = new System.Drawing.Point(153, 321);
|
||||
saveButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
saveButton.Name = "saveButton";
|
||||
saveButton.Size = new System.Drawing.Size(88, 27);
|
||||
saveButton.TabIndex = 9;
|
||||
saveButton.Text = "OK";
|
||||
saveButton.UseVisualStyleBackColor = true;
|
||||
saveButton.Click += SaveButtonClick;
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
closeButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
closeButton.Location = new System.Drawing.Point(247, 321);
|
||||
closeButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
closeButton.Name = "closeButton";
|
||||
closeButton.Size = new System.Drawing.Size(88, 27);
|
||||
closeButton.TabIndex = 10;
|
||||
closeButton.Text = "Cancel";
|
||||
closeButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// groupBox5
|
||||
//
|
||||
groupBox5.Controls.Add(clientResolutionComboBox);
|
||||
groupBox5.Location = new System.Drawing.Point(19, 13);
|
||||
groupBox5.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox5.Name = "groupBox5";
|
||||
groupBox5.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox5.Size = new System.Drawing.Size(315, 45);
|
||||
groupBox5.TabIndex = 11;
|
||||
groupBox5.TabStop = false;
|
||||
groupBox5.Text = "Screen Resolution";
|
||||
//
|
||||
// clientResolutionComboBox
|
||||
//
|
||||
clientResolutionComboBox.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
clientResolutionComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
clientResolutionComboBox.FormattingEnabled = true;
|
||||
clientResolutionComboBox.Location = new System.Drawing.Point(4, 19);
|
||||
clientResolutionComboBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
clientResolutionComboBox.Name = "clientResolutionComboBox";
|
||||
clientResolutionComboBox.Size = new System.Drawing.Size(307, 23);
|
||||
clientResolutionComboBox.TabIndex = 1;
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
groupBox1.Controls.Add(colorDepthComboBox);
|
||||
groupBox1.Location = new System.Drawing.Point(19, 65);
|
||||
groupBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox1.Name = "groupBox1";
|
||||
groupBox1.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
groupBox1.Size = new System.Drawing.Size(316, 44);
|
||||
groupBox1.TabIndex = 12;
|
||||
groupBox1.TabStop = false;
|
||||
groupBox1.Text = "Color Depth";
|
||||
//
|
||||
// colorDepthComboBox
|
||||
//
|
||||
colorDepthComboBox.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
colorDepthComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
colorDepthComboBox.FormattingEnabled = true;
|
||||
colorDepthComboBox.Items.AddRange(new object[] { "Min Color (16 bit)", "Max Color (32 bit)" });
|
||||
colorDepthComboBox.Location = new System.Drawing.Point(4, 18);
|
||||
colorDepthComboBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
colorDepthComboBox.Name = "colorDepthComboBox";
|
||||
colorDepthComboBox.Size = new System.Drawing.Size(308, 23);
|
||||
colorDepthComboBox.TabIndex = 2;
|
||||
//
|
||||
// ClientSettingsDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
CancelButton = closeButton;
|
||||
ClientSize = new System.Drawing.Size(349, 361);
|
||||
Controls.Add(groupBox1);
|
||||
Controls.Add(groupBox5);
|
||||
Controls.Add(closeButton);
|
||||
Controls.Add(saveButton);
|
||||
Controls.Add(groupBox4);
|
||||
Controls.Add(groupBox3);
|
||||
Controls.Add(windowModeCheckBox);
|
||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
Name = "ClientSettingsDialog";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Change Client Settings";
|
||||
groupBox3.ResumeLayout(false);
|
||||
groupBox3.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)soundVolumeTrackBar).EndInit();
|
||||
groupBox4.ResumeLayout(false);
|
||||
groupBox5.ResumeLayout(false);
|
||||
groupBox1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.CheckBox windowModeCheckBox;
|
||||
private System.Windows.Forms.CheckBox musicActiveCheckBox;
|
||||
private System.Windows.Forms.CheckBox soundActiveCheckBox;
|
||||
private System.Windows.Forms.GroupBox groupBox3;
|
||||
private System.Windows.Forms.GroupBox groupBox4;
|
||||
private System.Windows.Forms.ComboBox clientLanguageComboBox;
|
||||
private System.Windows.Forms.Button saveButton;
|
||||
private System.Windows.Forms.TrackBar soundVolumeTrackBar;
|
||||
private System.Windows.Forms.Button closeButton;
|
||||
private System.Windows.Forms.GroupBox groupBox5;
|
||||
private System.Windows.Forms.ComboBox clientResolutionComboBox;
|
||||
private System.Windows.Forms.GroupBox groupBox1;
|
||||
private System.Windows.Forms.ComboBox colorDepthComboBox;
|
||||
}
|
||||
}
|
||||
|
||||
83
src/ClientLauncher/ClientSettingsDialog.cs
Normal file
83
src/ClientLauncher/ClientSettingsDialog.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
// <copyright file="ClientSettingsDialog.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Windows.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ClientSettingsDialog"/>.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal partial class ClientSettingsDialog : Form
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientSettingsDialog"/> class.
|
||||
/// </summary>
|
||||
public ClientSettingsDialog()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.clientResolutionComboBox.DisplayMember = nameof(ClientResolution.Caption);
|
||||
this.clientResolutionComboBox.ValueMember = nameof(ClientResolution.Index);
|
||||
this.clientResolutionComboBox.DataSource = LauncherSettings.DefaultResolutions;
|
||||
this.Icon = Icon.FromHandle(Properties.Resources.Settings_16x.GetHicon());
|
||||
var config = new ClientSettings();
|
||||
config.Load();
|
||||
this.ReadConfig(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the available client resolutions.
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public BindingList<ClientResolution>? Resolutions
|
||||
{
|
||||
get => this.clientResolutionComboBox.DataSource as BindingList<ClientResolution>;
|
||||
set
|
||||
{
|
||||
if (value?.Count > 0)
|
||||
{
|
||||
var index = this.clientResolutionComboBox.SelectedIndex;
|
||||
this.clientResolutionComboBox.DataSource = value;
|
||||
if (value?.Count > index)
|
||||
{
|
||||
this.clientResolutionComboBox.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
var config = new ClientSettings();
|
||||
|
||||
this.SetConfig(config);
|
||||
config.Save();
|
||||
}
|
||||
|
||||
private void ReadConfig(ClientSettings clientSettings)
|
||||
{
|
||||
this.colorDepthComboBox.SelectedIndex = clientSettings.ClientColorDepth == 0 ? 0 : 1;
|
||||
this.musicActiveCheckBox.Checked = clientSettings.IsMusicEnabled;
|
||||
this.soundActiveCheckBox.Checked = clientSettings.IsSoundEnabled;
|
||||
this.soundVolumeTrackBar.Value = clientSettings.VolumeLevel;
|
||||
this.windowModeCheckBox.Checked = clientSettings.IsWindowModeActive;
|
||||
this.clientResolutionComboBox.SelectedIndex = clientSettings.ResolutionIndex;
|
||||
this.clientLanguageComboBox.SelectedIndex = (int)clientSettings.LangSelection;
|
||||
}
|
||||
|
||||
private void SetConfig(ClientSettings clientSettings)
|
||||
{
|
||||
clientSettings.ClientColorDepth = (ClientColorDepth)this.colorDepthComboBox.SelectedIndex;
|
||||
clientSettings.IsMusicEnabled = this.musicActiveCheckBox.Checked;
|
||||
clientSettings.IsSoundEnabled = this.soundActiveCheckBox.Checked;
|
||||
clientSettings.VolumeLevel = this.soundVolumeTrackBar.Value;
|
||||
clientSettings.IsWindowModeActive = this.windowModeCheckBox.Checked;
|
||||
clientSettings.ResolutionIndex = this.clientResolutionComboBox.SelectedIndex;
|
||||
clientSettings.LangSelection = (ClientLanguage)this.clientLanguageComboBox.SelectedIndex;
|
||||
}
|
||||
}
|
||||
120
src/ClientLauncher/ClientSettingsDialog.resx
Normal file
120
src/ClientLauncher/ClientSettingsDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
142
src/ClientLauncher/HostConfigurationDialog.Designer.cs
generated
Normal file
142
src/ClientLauncher/HostConfigurationDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,142 @@
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
partial class HostConfigurationDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
_serverPortControl = new System.Windows.Forms.NumericUpDown();
|
||||
_serverAddressTextBox = new System.Windows.Forms.TextBox();
|
||||
label2 = new System.Windows.Forms.Label();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
_descriptionTextBox = new System.Windows.Forms.TextBox();
|
||||
button1 = new System.Windows.Forms.Button();
|
||||
_cancelButton = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)_serverPortControl).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// _serverPortControl
|
||||
//
|
||||
_serverPortControl.Location = new System.Drawing.Point(273, 36);
|
||||
_serverPortControl.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
_serverPortControl.Maximum = new decimal(new int[] { 65535, 0, 0, 0 });
|
||||
_serverPortControl.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
_serverPortControl.Name = "_serverPortControl";
|
||||
_serverPortControl.Size = new System.Drawing.Size(68, 23);
|
||||
_serverPortControl.TabIndex = 10;
|
||||
_serverPortControl.Value = new decimal(new int[] { 44405, 0, 0, 0 });
|
||||
//
|
||||
// _serverAddressTextBox
|
||||
//
|
||||
_serverAddressTextBox.Location = new System.Drawing.Point(113, 35);
|
||||
_serverAddressTextBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
_serverAddressTextBox.Name = "_serverAddressTextBox";
|
||||
_serverAddressTextBox.Size = new System.Drawing.Size(152, 23);
|
||||
_serverAddressTextBox.TabIndex = 9;
|
||||
_serverAddressTextBox.Text = "127.127.127.127";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(13, 38);
|
||||
label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(89, 15);
|
||||
label2.TabIndex = 8;
|
||||
label2.Text = "Server-Address:";
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(13, 9);
|
||||
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(70, 15);
|
||||
label1.TabIndex = 11;
|
||||
label1.Text = "Description:";
|
||||
//
|
||||
// _descriptionTextBox
|
||||
//
|
||||
_descriptionTextBox.Location = new System.Drawing.Point(113, 6);
|
||||
_descriptionTextBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
_descriptionTextBox.Name = "_descriptionTextBox";
|
||||
_descriptionTextBox.PlaceholderText = "<Enter a description here>";
|
||||
_descriptionTextBox.Size = new System.Drawing.Size(228, 23);
|
||||
_descriptionTextBox.TabIndex = 12;
|
||||
//
|
||||
// button1
|
||||
//
|
||||
button1.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
button1.DialogResult = System.Windows.Forms.DialogResult.OK;
|
||||
button1.Location = new System.Drawing.Point(194, 83);
|
||||
button1.Name = "button1";
|
||||
button1.Size = new System.Drawing.Size(75, 23);
|
||||
button1.TabIndex = 13;
|
||||
button1.Text = "OK";
|
||||
button1.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// _cancelButton
|
||||
//
|
||||
_cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
_cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
_cancelButton.Location = new System.Drawing.Point(113, 83);
|
||||
_cancelButton.Name = "_cancelButton";
|
||||
_cancelButton.Size = new System.Drawing.Size(75, 23);
|
||||
_cancelButton.TabIndex = 14;
|
||||
_cancelButton.Text = "Cancel";
|
||||
_cancelButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// HostConfiguration
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(362, 118);
|
||||
Controls.Add(_cancelButton);
|
||||
Controls.Add(button1);
|
||||
Controls.Add(_descriptionTextBox);
|
||||
Controls.Add(label1);
|
||||
Controls.Add(_serverPortControl);
|
||||
Controls.Add(_serverAddressTextBox);
|
||||
Controls.Add(label2);
|
||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
MinimizeBox = false;
|
||||
Name = "HostConfiguration";
|
||||
Text = "Configure Connection";
|
||||
((System.ComponentModel.ISupportInitialize)_serverPortControl).EndInit();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.NumericUpDown _serverPortControl;
|
||||
private System.Windows.Forms.TextBox _serverAddressTextBox;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.TextBox _descriptionTextBox;
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.Button _cancelButton;
|
||||
}
|
||||
46
src/ClientLauncher/HostConfigurationDialog.cs
Normal file
46
src/ClientLauncher/HostConfigurationDialog.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// <copyright file="HostConfigurationDialog.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog for connection settings of a server.
|
||||
/// </summary>
|
||||
public partial class HostConfigurationDialog : Form
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HostConfigurationDialog"/> class.
|
||||
/// </summary>
|
||||
public HostConfigurationDialog()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the settings.
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public ServerHostSettings Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
return new ServerHostSettings
|
||||
{
|
||||
Description = this._descriptionTextBox.Text,
|
||||
Address = this._serverAddressTextBox.Text,
|
||||
Port = (int)this._serverPortControl.Value,
|
||||
};
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this._descriptionTextBox.Text = value.Description;
|
||||
this._serverAddressTextBox.Text = value.Address;
|
||||
this._serverPortControl.Value = value.Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/ClientLauncher/HostConfigurationDialog.resx
Normal file
120
src/ClientLauncher/HostConfigurationDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
31
src/ClientLauncher/ILauncher.cs
Normal file
31
src/ClientLauncher/ILauncher.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// <copyright file="ILauncher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
/// <summary>
|
||||
/// The interface of a launcher.
|
||||
/// </summary>
|
||||
internal interface ILauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host ip.
|
||||
/// </summary>
|
||||
string? HostAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host port.
|
||||
/// </summary>
|
||||
int HostPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the main executable path.
|
||||
/// </summary>
|
||||
string? MainExePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Launches the MU Online client with the specified settings.
|
||||
/// </summary>
|
||||
void LaunchClient();
|
||||
}
|
||||
183
src/ClientLauncher/Launcher.cs
Normal file
183
src/ClientLauncher/Launcher.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
// <copyright file="Launcher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
|
||||
/// <summary>
|
||||
/// The default launcher, which writes
|
||||
/// * host and port into the registry (official way until season 6 at the global server)
|
||||
/// * adds parameters /u and /p (works in some other versions of the game client)
|
||||
/// before starting the main.exe.
|
||||
/// </summary>
|
||||
public class Launcher : ILauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host ip.
|
||||
/// </summary>
|
||||
public string? HostAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host port.
|
||||
/// </summary>
|
||||
public int HostPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the main executable path.
|
||||
/// </summary>
|
||||
public string? MainExePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Launches Mu with the set configuration.
|
||||
/// </summary>
|
||||
public void LaunchClient()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.HostAddress))
|
||||
{
|
||||
MessageBox.Show("Host address is not set.", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(this.MainExePath))
|
||||
{
|
||||
MessageBox.Show("The path to the main.exe is not set.", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ResolveHost() is not { } ipAddress)
|
||||
{
|
||||
MessageBox.Show($"Address '{this.HostAddress} could not be resolved to an IPv4 address.", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
using var localMachineKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
|
||||
using var key = localMachineKey.CreateSubKey(@"SOFTWARE\WebZen\Mu\Connection");
|
||||
key.SetValue("Key", Environment.TickCount, RegistryValueKind.DWord);
|
||||
key.SetValue("ParameterA", this.HostEncode(ipAddress), RegistryValueKind.String);
|
||||
key.SetValue("ParameterB", this.PortEncode(ipAddress), RegistryValueKind.DWord);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MessageBox.Show(
|
||||
"IP and Port couldn't be set, because the operating system is not windows. Try to launch the game client anyway?",
|
||||
string.Empty,
|
||||
MessageBoxButtons.YesNo) != DialogResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var info = new DirectoryInfo(this.MainExePath!);
|
||||
var startInfo = new ProcessStartInfo(this.MainExePath, ["connect", $"/u{ipAddress}", $"/p{this.HostPort}"])
|
||||
{
|
||||
WorkingDirectory = info.Parent!.FullName,
|
||||
UseShellExecute = true,
|
||||
Verb = "open",
|
||||
};
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
startInfo.LoadUserProfile = true;
|
||||
}
|
||||
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the Port, so that the main.exe can read it.
|
||||
/// </summary>
|
||||
/// <returns>Encoded port value which will be put into ParameterB.</returns>
|
||||
private int PortEncode(string ipAddress)
|
||||
{
|
||||
var port = this.HostPort;
|
||||
switch (ipAddress.Length % 4)
|
||||
{
|
||||
case 0:
|
||||
port += 12 - (((port / 4) % 4) * 8);
|
||||
return port;
|
||||
|
||||
case 1:
|
||||
port += 7 - ((port % 8) * 2);
|
||||
return port;
|
||||
|
||||
case 2:
|
||||
port += 3 - ((port % 4) * 2);
|
||||
return port;
|
||||
|
||||
case 3:
|
||||
port += (0x13 - ((port % 4) * 2)) - (((port / 0x10) % 2) * 0x20);
|
||||
return port;
|
||||
default:
|
||||
// we'll hopefully never run into this one
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
private string? ResolveHost()
|
||||
{
|
||||
if (IPAddress.TryParse(this.HostAddress, out _))
|
||||
{
|
||||
return this.HostAddress;
|
||||
}
|
||||
|
||||
var entry = Dns.GetHostEntry(this.HostAddress!);
|
||||
if (entry.AddressList.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork) is { } ipAddress)
|
||||
{
|
||||
var address = ipAddress.ToString();
|
||||
|
||||
// The game client blocks connections to 127.0.0.1 (probably to prevent cheating).
|
||||
return address == "127.0.0.1" ? "127.127.127.127" : address;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the IP Address of the server, so that the main.exe can read it.
|
||||
/// </summary>
|
||||
/// <returns>Encoded string value of the ip address which to put into ParameterA.</returns>
|
||||
private string HostEncode(string ipAddress)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
var counter = 0;
|
||||
foreach (var ch in ipAddress)
|
||||
{
|
||||
var encodedCharacter = '\0';
|
||||
counter++;
|
||||
switch (counter)
|
||||
{
|
||||
case 1:
|
||||
encodedCharacter = (char)(ch + '\f');
|
||||
break;
|
||||
|
||||
case 2:
|
||||
encodedCharacter = (char)(ch + '\a');
|
||||
break;
|
||||
|
||||
case 3:
|
||||
encodedCharacter = (char)(ch + '\x0003');
|
||||
break;
|
||||
|
||||
case 4:
|
||||
encodedCharacter = (char)(ch + '\x0013');
|
||||
counter = 0;
|
||||
break;
|
||||
default:
|
||||
// we should not run into this case, since it's always 1 to 4.
|
||||
break;
|
||||
}
|
||||
|
||||
result.Append(encodedCharacter);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
44
src/ClientLauncher/LauncherSettings.cs
Normal file
44
src/ClientLauncher/LauncherSettings.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// <copyright file="LauncherSettings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
/// <summary>
|
||||
/// Settings of the launcher.
|
||||
/// </summary>
|
||||
public class LauncherSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default resolutions, which are based on the open source MuMain.
|
||||
/// </summary>
|
||||
public static ClientResolution[] DefaultResolutions =>
|
||||
[
|
||||
new(0, "640x480"),
|
||||
new(1, "800x600"),
|
||||
new(2, "1024x768"),
|
||||
new(3, "1280x1024"),
|
||||
new(4, "1600x1200"),
|
||||
new(5, "1864x1400"),
|
||||
new(6, "1600x900"),
|
||||
new(7, "1600x1280"),
|
||||
new(8, "1680x1050"),
|
||||
new(9, "1920x1080"),
|
||||
new(10, "2560x1440"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the main executable path.
|
||||
/// </summary>
|
||||
public string? MainExePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configured hosts.
|
||||
/// </summary>
|
||||
public List<ServerHostSettings> Hosts { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolutions.
|
||||
/// </summary>
|
||||
public List<ClientResolution> AvailableResolutions { get; set; } = [];
|
||||
}
|
||||
69
src/ClientLauncher/MUnique.OpenMU.ClientLauncher.csproj
Normal file
69
src/ClientLauncher/MUnique.OpenMU.ClientLauncher.csproj
Normal file
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.ClientLauncher.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ClientLauncher.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Rocket_16x.ico" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>MUnique.OpenMU.ClientLauncher.exe.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Rocket_16x.ico</ApplicationIcon>
|
||||
<StartupObject>MUnique.OpenMU.ClientLauncher.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="MUnique.OpenMU.ClientLauncher.exe.manifest">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Compile Update="MainForm.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
<Compile Update="ClientSettingsDialog.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
<Compile Update="MainForm.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ClientSettingsDialog.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>ClientSettingsDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="ClientSettingsDialog.resx">
|
||||
<DependentUpon>ClientSettingsDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="MainForm.resx">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC Manifest Options
|
||||
If you want to change the Windows User Account Control level replace the
|
||||
requestedExecutionLevel node with one of the following.
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||
Remove this element if your application requires this virtualization for backwards
|
||||
compatibility.
|
||||
-->
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on and is
|
||||
is designed to work with. Uncomment the appropriate elements and Windows will
|
||||
automatically selected the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
|
||||
<!--
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
-->
|
||||
|
||||
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
|
||||
</assembly>
|
||||
207
src/ClientLauncher/MainForm.Designer.cs
generated
Normal file
207
src/ClientLauncher/MainForm.Designer.cs
generated
Normal file
@@ -0,0 +1,207 @@
|
||||
namespace MUnique.OpenMU.ClientLauncher
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
|
||||
_launchButton = new System.Windows.Forms.Button();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
openFileDialog = new System.Windows.Forms.OpenFileDialog();
|
||||
MainExePathTextBox = new System.Windows.Forms.TextBox();
|
||||
SearchMainExeButton = new System.Windows.Forms.Button();
|
||||
label2 = new System.Windows.Forms.Label();
|
||||
configurationDialogButton = new System.Windows.Forms.Button();
|
||||
_editHostButton = new System.Windows.Forms.Button();
|
||||
_addHostButton = new System.Windows.Forms.Button();
|
||||
_serversComboBox = new System.Windows.Forms.ComboBox();
|
||||
_removeHostButton = new System.Windows.Forms.Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// _launchButton
|
||||
//
|
||||
_launchButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
_launchButton.Location = new System.Drawing.Point(505, 45);
|
||||
_launchButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
_launchButton.Name = "_launchButton";
|
||||
_launchButton.Size = new System.Drawing.Size(147, 27);
|
||||
_launchButton.TabIndex = 0;
|
||||
_launchButton.Text = "Launch Client";
|
||||
_launchButton.UseVisualStyleBackColor = true;
|
||||
_launchButton.Click += LaunchClick;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(14, 17);
|
||||
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(85, 15);
|
||||
label1.TabIndex = 1;
|
||||
label1.Text = "main.exe Path:";
|
||||
//
|
||||
// openFileDialog
|
||||
//
|
||||
openFileDialog.FileName = "main.exe";
|
||||
openFileDialog.Filter = "Executeables|*.exe";
|
||||
//
|
||||
// MainExePathTextBox
|
||||
//
|
||||
MainExePathTextBox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
MainExePathTextBox.Location = new System.Drawing.Point(107, 14);
|
||||
MainExePathTextBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
MainExePathTextBox.Name = "MainExePathTextBox";
|
||||
MainExePathTextBox.Size = new System.Drawing.Size(467, 23);
|
||||
MainExePathTextBox.TabIndex = 2;
|
||||
MainExePathTextBox.Text = "main.exe";
|
||||
//
|
||||
// SearchMainExeButton
|
||||
//
|
||||
SearchMainExeButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
SearchMainExeButton.Location = new System.Drawing.Point(582, 12);
|
||||
SearchMainExeButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
SearchMainExeButton.Name = "SearchMainExeButton";
|
||||
SearchMainExeButton.Size = new System.Drawing.Size(35, 27);
|
||||
SearchMainExeButton.TabIndex = 3;
|
||||
SearchMainExeButton.Text = "...";
|
||||
SearchMainExeButton.UseVisualStyleBackColor = true;
|
||||
SearchMainExeButton.Click += SearchMainExeButtonClick;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(57, 54);
|
||||
label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(42, 15);
|
||||
label2.TabIndex = 4;
|
||||
label2.Text = "Server:";
|
||||
//
|
||||
// configurationDialogButton
|
||||
//
|
||||
configurationDialogButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
configurationDialogButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
|
||||
configurationDialogButton.Image = Properties.Resources.Settings_16x;
|
||||
configurationDialogButton.Location = new System.Drawing.Point(625, 12);
|
||||
configurationDialogButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
configurationDialogButton.Name = "configurationDialogButton";
|
||||
configurationDialogButton.Size = new System.Drawing.Size(27, 27);
|
||||
configurationDialogButton.TabIndex = 4;
|
||||
configurationDialogButton.UseVisualStyleBackColor = true;
|
||||
configurationDialogButton.Click += ConfigurationDialogButtonClick;
|
||||
//
|
||||
// _editHostButton
|
||||
//
|
||||
_editHostButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
_editHostButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
|
||||
_editHostButton.Image = Properties.Resources.Edit_16x;
|
||||
_editHostButton.Location = new System.Drawing.Point(446, 47);
|
||||
_editHostButton.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3);
|
||||
_editHostButton.Name = "_editHostButton";
|
||||
_editHostButton.Size = new System.Drawing.Size(23, 23);
|
||||
_editHostButton.TabIndex = 7;
|
||||
_editHostButton.UseVisualStyleBackColor = true;
|
||||
_editHostButton.Click += OnEditHostButtonClick;
|
||||
//
|
||||
// _addHostButton
|
||||
//
|
||||
_addHostButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
_addHostButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
|
||||
_addHostButton.Image = Properties.Resources.Add_16x;
|
||||
_addHostButton.Location = new System.Drawing.Point(419, 47);
|
||||
_addHostButton.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3);
|
||||
_addHostButton.Name = "_addHostButton";
|
||||
_addHostButton.Size = new System.Drawing.Size(23, 23);
|
||||
_addHostButton.TabIndex = 6;
|
||||
_addHostButton.UseVisualStyleBackColor = true;
|
||||
_addHostButton.Click += OnAddHostButtonClick;
|
||||
//
|
||||
// _serversComboBox
|
||||
//
|
||||
_serversComboBox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
_serversComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
_serversComboBox.FormattingEnabled = true;
|
||||
_serversComboBox.Location = new System.Drawing.Point(107, 48);
|
||||
_serversComboBox.Name = "_serversComboBox";
|
||||
_serversComboBox.Size = new System.Drawing.Size(307, 23);
|
||||
_serversComboBox.TabIndex = 5;
|
||||
_serversComboBox.SelectedValueChanged += OnServersComboBoxSelectedIndexChanged;
|
||||
//
|
||||
// _removeHostButton
|
||||
//
|
||||
_removeHostButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
_removeHostButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
|
||||
_removeHostButton.Image = Properties.Resources.Remove_16x;
|
||||
_removeHostButton.Location = new System.Drawing.Point(473, 46);
|
||||
_removeHostButton.Margin = new System.Windows.Forms.Padding(2, 3, 5, 3);
|
||||
_removeHostButton.Name = "_removeHostButton";
|
||||
_removeHostButton.Size = new System.Drawing.Size(23, 23);
|
||||
_removeHostButton.TabIndex = 8;
|
||||
_removeHostButton.UseVisualStyleBackColor = true;
|
||||
_removeHostButton.Click += OnRemoveHostButtonClick;
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(665, 82);
|
||||
Controls.Add(_removeHostButton);
|
||||
Controls.Add(_serversComboBox);
|
||||
Controls.Add(_addHostButton);
|
||||
Controls.Add(_editHostButton);
|
||||
Controls.Add(configurationDialogButton);
|
||||
Controls.Add(label2);
|
||||
Controls.Add(SearchMainExeButton);
|
||||
Controls.Add(MainExePathTextBox);
|
||||
Controls.Add(label1);
|
||||
Controls.Add(_launchButton);
|
||||
Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon");
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
MaximizeBox = false;
|
||||
MaximumSize = new System.Drawing.Size(1257, 121);
|
||||
Name = "MainForm";
|
||||
Text = "MU Game Client Launcher";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Button _launchButton;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.OpenFileDialog openFileDialog;
|
||||
private System.Windows.Forms.TextBox MainExePathTextBox;
|
||||
private System.Windows.Forms.Button SearchMainExeButton;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Button configurationDialogButton;
|
||||
private System.Windows.Forms.Button _editHostButton;
|
||||
private System.Windows.Forms.Button _addHostButton;
|
||||
private System.Windows.Forms.ComboBox _serversComboBox;
|
||||
private System.Windows.Forms.Button _removeHostButton;
|
||||
}
|
||||
}
|
||||
|
||||
197
src/ClientLauncher/MainForm.cs
Normal file
197
src/ClientLauncher/MainForm.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
// <copyright file="MainForm.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#pragma warning disable CA1416 // This project is compiled for windows
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// The main form of the launcher.
|
||||
/// </summary>
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private const string ConfigFileName = "launcher.config";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the binding list for the configured hosts.
|
||||
/// </summary>
|
||||
private BindingList<ServerHostSettings> _hostsBindingList = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MainForm"/> class.
|
||||
/// </summary>
|
||||
public MainForm()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.LoadOptions();
|
||||
this.UpdateButtonStates();
|
||||
}
|
||||
|
||||
private BindingList<ServerHostSettings> Hosts
|
||||
{
|
||||
get => this._hostsBindingList;
|
||||
set
|
||||
{
|
||||
this._hostsBindingList = value;
|
||||
this._serversComboBox.DataSource = value;
|
||||
}
|
||||
}
|
||||
|
||||
private BindingList<ClientResolution> Resolutions { get; set; } = new(LauncherSettings.DefaultResolutions);
|
||||
|
||||
/// <summary>
|
||||
/// Launches the MU Online client (main.exe) to connect to the configured address.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
|
||||
private void LaunchClick(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selectedHost = (ServerHostSettings)this._serversComboBox.SelectedItem!;
|
||||
var launcher = new Launcher
|
||||
{
|
||||
HostAddress = selectedHost.Address,
|
||||
HostPort = selectedHost.Port,
|
||||
MainExePath = this.MainExePathTextBox.Text,
|
||||
};
|
||||
launcher.LaunchClient();
|
||||
|
||||
this.SaveCurrentOptions();
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
MessageBox.Show("Can't access Windows Registry. To use the launcher, run it as Administrator.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("Error Starting MU. Path correct?" + Environment.NewLine + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadOptions()
|
||||
{
|
||||
this._serversComboBox.DataSource = this.Hosts;
|
||||
if (!File.Exists(ConfigFileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reader = new XmlSerializer(typeof(LauncherSettings));
|
||||
using var file = new StreamReader(ConfigFileName);
|
||||
if (reader.Deserialize(file) is LauncherSettings launcherSettings)
|
||||
{
|
||||
this.MainExePathTextBox.Text = launcherSettings.MainExePath;
|
||||
this.Hosts = new BindingList<ServerHostSettings>(launcherSettings.Hosts);
|
||||
if (launcherSettings.AvailableResolutions?.Any() is true)
|
||||
{
|
||||
this.Resolutions = new(launcherSettings.AvailableResolutions);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Resolutions = new(LauncherSettings.DefaultResolutions);
|
||||
}
|
||||
}
|
||||
|
||||
file.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.Hosts.Clear();
|
||||
this.Hosts.Add(new ServerHostSettings { Description = "Local ConnectServer", Address = "localhost", Port = 44405 });
|
||||
this.Hosts.Add(new ServerHostSettings { Description = "Local GameServer 1", Address = "localhost", Port = 55901 });
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCurrentOptions()
|
||||
{
|
||||
var settings = new LauncherSettings
|
||||
{
|
||||
Hosts = this._hostsBindingList.ToList(),
|
||||
MainExePath = this.MainExePathTextBox.Text,
|
||||
AvailableResolutions = this.Resolutions.ToList(),
|
||||
};
|
||||
|
||||
var writer = new XmlSerializer(typeof(LauncherSettings));
|
||||
using var file = File.Create(ConfigFileName);
|
||||
writer.Serialize(file, settings);
|
||||
file.Close();
|
||||
}
|
||||
|
||||
private void SearchMainExeButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
var dialogResult = this.openFileDialog.ShowDialog(this);
|
||||
if (dialogResult == DialogResult.OK)
|
||||
{
|
||||
this.MainExePathTextBox.Text = this.openFileDialog.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigurationDialogButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
using var configDialog = new ClientSettingsDialog();
|
||||
configDialog.Resolutions = this.Resolutions;
|
||||
configDialog.ShowDialog(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("Changing the configuration of the MU game client is only supported on windows.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddHostButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new HostConfigurationDialog();
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
var settings = dialog.Settings;
|
||||
this._hostsBindingList.Add(settings);
|
||||
this.SaveCurrentOptions();
|
||||
this.UpdateButtonStates();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEditHostButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
var selectedConfiguration = (ServerHostSettings)this._serversComboBox.SelectedItem!;
|
||||
using var dialog = new HostConfigurationDialog();
|
||||
dialog.Settings = selectedConfiguration;
|
||||
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
var editedConfiguration = dialog.Settings;
|
||||
selectedConfiguration.Port = editedConfiguration.Port;
|
||||
selectedConfiguration.Address = editedConfiguration.Address;
|
||||
selectedConfiguration.Description = editedConfiguration.Description;
|
||||
this.SaveCurrentOptions();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemoveHostButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
this._hostsBindingList.RemoveAt(this._serversComboBox.SelectedIndex);
|
||||
this.SaveCurrentOptions();
|
||||
}
|
||||
|
||||
private void OnServersComboBoxSelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
this.UpdateButtonStates();
|
||||
}
|
||||
|
||||
private void UpdateButtonStates()
|
||||
{
|
||||
var isServerSelected = this._serversComboBox.SelectedItem is ServerHostSettings;
|
||||
this._editHostButton.Enabled = isServerSelected;
|
||||
this._removeHostButton.Enabled = isServerSelected;
|
||||
this._launchButton.Enabled = isServerSelected;
|
||||
}
|
||||
}
|
||||
303
src/ClientLauncher/MainForm.resx
Normal file
303
src/ClientLauncher/MainForm.resx
Normal file
@@ -0,0 +1,303 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="openFileDialog.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
AAABAAEAMjIAAAEAIADIKAAAFgAAACgAAAAyAAAAZAAAAAEAIAAAAAAAECcAACMuAAAjLgAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/19fWfAAAAAAAAAAAAAAAAAAAAAPHx8RL29vZ09vb23Pb29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9/f3mv///wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//X19Z8AAAAAAAAAAAAAAAD39/ce9vb23fb29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9fX1nP///wIAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9fX1nwAAAAAAAAAA9/f3Hvb2
|
||||
9t329vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9fX1nP///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v9ubm7/Wlpa/1paWv9aWlr/Wlpa/1paWv9aWlr/Wlpa/1paWv+oqKj/9vb2//b2
|
||||
9v/19fWfAAAAAPf39yD29vbf9vb2//b29v/19fX/paWl/2BgYP9aWlr/Wlpa/1paWv9aWlr/Wlpa/1pa
|
||||
Wv9aWlr/Wlpa/11dXf/MzMz/9vb2//b29v/29vb/9vb2bAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2/1lZWf9CQkL/QkJC/0JCQv9CQkL/QkJC/0JC
|
||||
Qv9CQkL/QkJC/5ycnP/29vb/9vb2//X19Z/4+Pgl9vb24vb29v/29vb/9fX1/42Njf9CQkL/QkJC/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0tLS//U1NT/9vb2//b29v/19fWAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/WVlZ/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/nJyc//b29v/29vb/9vb2wPb29uT29vb/9vb2//X1
|
||||
9f+Pj4//QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/7Ky
|
||||
sv/29vb/9vb2//X19YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v9ZWVn/QkJC/0JCQv+lpaX/xsbG/8bGxv/Gxsb/xsbG/8bGxv/e3t7/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/19fX/jY2N/0JCQv9CQkL/QkJC/2xsbP/Gxsb/xsbG/8bGxv/Gxsb/xsbG/8bG
|
||||
xv/Gxsb/Y2Nj/0JCQv9CQkL/srKy//b29v/29vb/9fX1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2/1lZWf9CQkL/QkJC/8nJyf/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9fX1/4iIiP9CQkL/QkJC/0JCQv9MTEz/5+fn//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v9vb2//QkJC/0JCQv+ysrL/9vb2//b29v/19fWAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/WVlZ/0JC
|
||||
Qv9CQkL/ycnJ//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//X19f+NjY3/QkJC/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9gYGD/2NjY//b29v/29vb/9vb2//b29v/29vb/9vb2/29vb/9CQkL/QkJC/7Ky
|
||||
sv/29vb/9vb2//X19YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v9ZWVn/QkJC/0JCQv/Jycn/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/19fX/iIiI/0JCQv9CQkL/QkJC/0lJSf9CQkL/QkJC/0JCQv9MTEz/ubm5//b29v/29vb/9vb2//b2
|
||||
9v/29vb/b29v/0JCQv9CQkL/srKy//b29v/29vb/9fX1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2/1lZWf9CQkL/QkJC/8nJyf/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9fX1/4iIiP9CQkL/QkJC/0JCQv9tbW3/6enp/5GRkf9DQ0P/QkJC/0JC
|
||||
Qv9CQkL/jo6O//Ly8v/29vb/9vb2//b29v9vb2//QkJC/0JCQv+ysrL/9vb2//b29v/19fWAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/WVlZ/0JC
|
||||
Qv9CQkL/ycnJ//b29v/29vb/9vb2//b29v/29vb/9vb2//X19f+IiIj/QkJC/0JCQv9CQkL/bW1t/+3t
|
||||
7f/29vb/9vb2/8HBwf9OTk7/QkJC/0JCQv9CQkL/b29v/+vr6//29vb/9vb2/29vb/9CQkL/QkJC/7Ky
|
||||
sv/29vb/9vb2//X19YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v+tra3/oqKi/6Kiov/h4eH/9vb2//b29v/29vb/9vb2//b29v/19fX/iIiI/0JC
|
||||
Qv9CQkL/QkJC/21tbf/t7e3/9vb2//b29v/29vb/9vb2/9zc3P9hYWH/QkJC/0JCQv9CQkL/YGBg/+Hh
|
||||
4f/29vb/b29v/0JCQv9CQkL/srKy//b29v/29vb/9fX1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9fX1/4iIiP9CQkL/QkJC/0JCQv9tbW3/7e3t//b29v/29vb/9vb2//b29v/29vb/9vb2/+3t
|
||||
7f99fX3/QkJC/0JCQv9CQkL/VFRU/9XV1f9vb2//QkJC/0JCQv+ysrL/9vb2//b29v/19fWAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//X19f+IiIj/QkJC/0JCQv9CQkL/bW1t/+3t7f/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//X19f+UlJT/QkJC/0JCQv9CQkL/TExM/1VVVf9CQkL/QkJC/7Ky
|
||||
sv/29vb/9vb2//X19YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD39/eZ9/f3mff395n39/eZ9/f3mfX19bv29vb/9vb2//b29v/19fX/jY2N/0JCQv9CQkL/QkJC/2xs
|
||||
bP/s7Oz/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v+oqKj/Q0ND/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9CQkL/srKy//b29v/29vb/9fX1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4+Pgk9vb24/b29v/29vb/9fX1/42N
|
||||
jf9CQkL/QkJC/0JCQv9sbGz/7Ozs//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v+2trb/RkZG/0JCQv9CQkL/QkJC/0JCQv+ysrL/9vb2//b29v/19fWfAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9/f3Ifb2
|
||||
9uP29vb/9vb2//X19f+IiIj/QkJC/0JCQv9CQkL/bm5u/+3t7f/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/AwMD/RkZG/0JCQv9CQkL/QkJC/6ur
|
||||
q//29vb/9vb2//b29v/19fVqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAPf39x729vbd9vb2//b29v/19fX/jY2N/0JCQv9CQkL/QkJC/2xsbP/s7Oz/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v+8vLz/RUVF/0JCQv9CQkL/TExM/9fX1//29vb/9vb2//b29v329vZSAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD39/cg9vb23/b29v/29vb/9fX1/42Njf9CQkL/QkJC/0JC
|
||||
Qv9ubm7/7e3t//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v+xsbH/Q0ND/0JCQv9CQkL/WVlZ/+bm5v/29vb/9vb2//b2
|
||||
9vb29vY2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8fHxEvb29uD29vb/9vb2//X1
|
||||
9f+Ojo7/QkJC/0JCQv9CQkL/TExM/+zs7P/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v+pqan/QkJC/0JC
|
||||
Qv9CQkL/aGho//Hx8f/29vb/9vb2//b29t7v7+8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD29vZ09vb2//b29v/19fX/jo6O/0JCQv9CQkL/QkJC/0JCQv9CQkL/m5ub//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v+cnJz/QkJC/0JCQv9CQkL/ioqK//b29v/29vb/9vb2//b29qwAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29tz29vb/9vb2/6Wlpf9CQkL/QkJC/0JCQv9KSkr/QkJC/0JC
|
||||
Qv9ERET/ysrK//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//X19f99fX3/QkJC/0JCQv9CQkL/s7Oz//b2
|
||||
9v/29vb/9vb2//X19WgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/YGBg/0JC
|
||||
Qv9CQkL/ampq/+Pj4/9fX1//QkJC/0JCQv9TU1P/4+Pj//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2/+7u
|
||||
7v9jY2P/QkJC/0JCQv9JSUn/2dnZ//b29v/29vb/9vb28vb29hsAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v9ZWVn/QkJC/0JCQv/IyMj/9vb2/9fX1/9JSUn/QkJC/0JCQv9oaGj/8PDw//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2/9ra2v9LS0v/QkJC/0JCQv9qamr/9PT0//b29v/29vb/9vb2pgAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2/1lZWf9CQkL/QkJC/8nJyf/29vb/9vb2/7Oz
|
||||
s/9CQkL/QkJC/0JCQv+Ghob/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/8/Pz/6Wlpf9kZGT/RkZG/1ZWVv+QkJD/5OTk//b29v/29vb/9vb2/7Ozs/9CQkL/QkJC/0JC
|
||||
Qv+jo6P/9vb2//b29v/29vb/9/f3QgAAAAAAAAAAAAAAAAAAAAAAAAAA9vb2//b29v/29vb/WVlZ/0JC
|
||||
Qv9CQkL/ycnJ//b29v/29vb/9vb2/4iIiP9CQkL/QkJC/0JCQv+dnZ3/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//Pz8/95eXn/QkJC/0JCQv9CQkL/QkJC/0JCQv9WVlb/2tra//b2
|
||||
9v/29vb/9vb2/319ff9CQkL/QkJC/0hISP/c3Nz/9vb2//b29v/29vbQ////AQAAAAAAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v9ZWVn/QkJC/0JCQv/Jycn/9vb2//b29v/29vb/8PDw/2xsbP9CQkL/QkJC/0JC
|
||||
Qv+vr6//9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/o6Oj/0JCQv9CQkL/QkJC/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9ra2v/9fX1//b29v/29vb/5eXl/05OTv9CQkL/QkJC/3x8fP/29vb/9vb2//b2
|
||||
9v/19fVPAAAAAAAAAAAAAAAAAAAAAPb29v/29vb/9vb2/1lZWf9CQkL/QkJC/8nJyf/29vb/9vb2//b2
|
||||
9v/29vb/6enp/19fX/9CQkL/QkJC/0RERP++vr7/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v9ZWVn/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv/T09P/9vb2//b29v/29vb/sLCw/0JC
|
||||
Qv9CQkL/QkJC/8vLy//29vb/9vb2//b29s7///8BAAAAAAAAAAAAAAAA9vb2//b29v/29vb/WVlZ/0JC
|
||||
Qv9CQkL/ycnJ//b29v/29vb/9vb2//b29v/29vb/39/f/1RUVP9CQkL/QkJC/0hISP/IyMj/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/8PDw/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/7e3
|
||||
t//29vb/9vb2//b29v/19fX/ZGRk/0JCQv9CQkL/Z2dn//X19f/29vb/9vb2//X19UoAAAAAAAAAAAAA
|
||||
AAD29vb/9vb2//b29v9ZWVn/QkJC/0JCQv/Jycn/9vb2//b29v/29vb/9vb2//b29v/29vb/1NTU/0xM
|
||||
TP9CQkL/QkJC/0lJSf/BwcH/9vb2//b29v/29vb/9vb2//b29v/19fX/R0dH/0JCQv9CQkL/QkJC/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9CQkL/wsLC//b29v/29vb/9vb2//b29v/FxcX/QkJC/0JCQv9CQkL/xMTE//b2
|
||||
9v/29vb/9vb2sQAAAAAAAAAAAAAAAPb29v/29vb/9vb2/1lZWf9CQkL/QkJC/2ZmZv9ycnL/cnJy/3Jy
|
||||
cv9ycnL/cnJy/3Jycv9ycnL/VlZW/0JCQv9CQkL/QkJC/0ZGRv+5ubn/9vb2//b29v/29vb/9vb2//b2
|
||||
9v94eHj/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/01NTf/u7u7/9vb2//b29v/29vb/9vb2//b2
|
||||
9v9ycnL/QkJC/0JCQv90dHT/9vb2//b29v/29vb79PT0GAAAAAAAAAAA9vb2//b29v/29vb/XFxc/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0RE
|
||||
RP+wsLD/9vb2//b29v/29vb/9vb2/9fX1/9KSkr/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/rq6u//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2/8TExP9CQkL/QkJC/0NDQ//Y2Nj/9vb2//b29v/29vZzAAAAAAAA
|
||||
AAD29vb/9vb2//b29v/MzMz/S0tL/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JC
|
||||
Qv9CQkL/QkJC/0JCQv9CQkL/QkJC/0NDQ/+goKD/9vb2//b29v/29vb/9vb2/8zMzP9YWFj/QkJC/0JC
|
||||
Qv9CQkL/S0tL/6urq//29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9fX1/2BgYP9CQkL/QkJC/42N
|
||||
jf/29vb/9vb2//b29tAAAAAAAAAAAPf395n29vb/9vb2//b29v/T09P/rq6u/66urv+urq7/rq6u/66u
|
||||
rv+urq7/rq6u/66urv+urq7/rq6u/66urv+urq7/p6en/01NTf9CQkL/QkJC/0JCQv+Dg4P/8PDw//b2
|
||||
9v/29vb/9vb2//Ly8v/ExMT/rq6u/7u7u//q6ur/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/q6ur/0JCQv9CQkL/U1NT//X19f/29vb/9vb2/vb29hsAAAAA////Afb29pX29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/2NjY/1dX
|
||||
V/9CQkL/QkJC/0JCQv9nZ2f/39/f//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/n5+f/RUVF/0JCQv9CQkL/zMzM//b29v/29vb/9/f3YQAA
|
||||
AAAAAAAA////Afb29pX29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/5OTk/2VlZf9CQkL/QkJC/0JCQv9MTEz/ubm5//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v90dHT/QkJC/0JC
|
||||
Qv+RkZH/9vb2//b29v/29vaoAAAAAAAAAAAAAAAA////AfX19Wb29vZ39vb2d/b29nf29vZ39vb2d/b2
|
||||
9nf29vZ39vb2d/b29nf29vZ39vb2d/b29nf19fWc9vb2//b29v/29vb/7+/v/4eHh/9CQkL/QkJC/0JC
|
||||
Qv9CQkL/goKC/+jo6P/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2/66urv9CQkL/QkJC/19fX//29vb/9vb2//b29uoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD29vZs9vb2/fb2
|
||||
9v/29vb/9vb2/7CwsP9ISEj/QkJC/0JCQv9CQkL/UVFR/7a2tv/19fX/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/2dnZ/0JCQv9CQkL/Q0ND/+zs7P/29vb/9vb2//X1
|
||||
9RoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAD29vZR9vb29vb29v/29vb/9vb2/9jY2P9nZ2f/QkJC/0JCQv9CQkL/QkJC/2dn
|
||||
Z//Jycn/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/19fX/Tk5O/0JC
|
||||
Qv9CQkL/zMzM//b29v/29vb/9PT0SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD29vY59vb24fb29v/29vb/9vb2//Pz
|
||||
8/+fn5//RkZG/0JCQv9CQkL/QkJC/0JCQv91dXX/yMjI//b29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29v90dHT/QkJC/0JCQv+oqKj/9vb2//b29v/29vZ3AAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AADx8fES9vb2sPb29v/29vb/9vb2//b29v/Z2dn/d3d3/0JCQv9CQkL/QkJC/0JCQv9CQkL/ZWVl/7Gx
|
||||
sf/r6+v/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2/42Njf9CQkL/QkJC/42Njf/29vb/9vb2//X1
|
||||
9Z8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8B9vb2bfb29vT29vb/9vb2//b29v/29vb/x8fH/2Rk
|
||||
ZP9CQkL/QkJC/0JCQv9CQkL/QkJC/0dHR/96enr/s7Oz/97e3v/29vb/9vb2//b29v/29vb/oqKi/0JC
|
||||
Qv9CQkL/gICA//b29v/29vb/9/f3twAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9fX1Gvb2
|
||||
9qL29vb/9vb2//b29v/29vb/9fX1/8HBwf9wcHD/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/1RU
|
||||
VP97e3v/kZGR/6ioqP+NjY3/QkJC/0JCQv90dHT/9vb2//b29v/29vbNAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPT09Ej19fXW9vb2//b29v/29vb/9vb2//b29v/U1NT/iIiI/05O
|
||||
Tv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/2hoaP/29vb/9vb2//b2
|
||||
9uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wL29vZV9vb20fb2
|
||||
9v/29vb/9vb2//b29v/29vb/8/Pz/8XFxf+Kior/WVlZ/0JCQv9CQkL/QkJC/0JCQv9CQkL/QkJC/0JC
|
||||
Qv9CQkL/ioqK//b29v/29vb/9vb29gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAD///8B9fX1Tvf397b29vb89vb2//b29v/29vb/9vb2//b29v/29vb/6enp/8TE
|
||||
xP+goKD/hYWF/3p6ev9ubm7/YWFh/4iIiP/09PT/9vb2//b29v/29vbkAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPb29hz39/d69vb22Pb2
|
||||
9v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb2//b29v/29vb/9vb26Pn5
|
||||
+SoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAA+Pj4IvX19Wn29vaw9fX18fb29v/29vb/9vb2//b29v/29vb/9vb2//b2
|
||||
9v/29vb/9vb2//b29uf5+fkpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8B+Pj4Ivb2
|
||||
9lL19fWA9vb2qPb29sH19fXW9vb27fb29v/29vbn+fn5KQAAAAAAAAAAAADwAA//wAAAAOAAB//AAAAA
|
||||
wAAD/8AAAACAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/
|
||||
wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wADwAAAAA//AAOAA
|
||||
AAAB/8AAwAAAAAD/wACAAAAAAH/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAB/AAAAAAAAAD8AAAAAAAAAP
|
||||
wAAAAAAAAAfAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAHAAAAAAAAAAcAAAAAAAAABwAAAAAAAAADAAAAA
|
||||
AAAAAMAAAAAAAAAAwAAAAAAAAABAAAAAAAAAAEAAgAAAAAAAQADAAAAAAABAAP//gAAAAAAA///AAAAA
|
||||
AAD//+AAAAAAAP//8AAAAAAA///4AAAAAAD///4AAAAAAP///4AAAAAA////wAAAAAD////wAAAAAP//
|
||||
//4AAAAA/////8AAQAD/////+ADAAA==
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
24
src/ClientLauncher/Program.cs
Normal file
24
src/ClientLauncher/Program.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.Windows.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// The static main program.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
internal static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
}
|
||||
10
src/ClientLauncher/Properties/AssemblyInfo.cs
Normal file
10
src/ClientLauncher/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="AssemblyInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MUnique.OpenMU.ClientLauncher")]
|
||||
103
src/ClientLauncher/Properties/Resources.Designer.cs
generated
Normal file
103
src/ClientLauncher/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,103 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MUnique.OpenMU.ClientLauncher.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap Add_16x {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("Add_16x", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap Edit_16x {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("Edit_16x", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap Remove_16x {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("Remove_16x", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap Settings_16x {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("Settings_16x", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/ClientLauncher/Properties/Resources.resx
Normal file
133
src/ClientLauncher/Properties/Resources.resx
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="Add_16x" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="Edit_16x" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Edit.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="Remove_16x" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Remove.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="Settings_16x" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Settings_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
</root>
|
||||
BIN
src/ClientLauncher/Resources/Add.png
Normal file
BIN
src/ClientLauncher/Resources/Add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 301 B |
BIN
src/ClientLauncher/Resources/Edit.png
Normal file
BIN
src/ClientLauncher/Resources/Edit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 567 B |
BIN
src/ClientLauncher/Resources/Remove.png
Normal file
BIN
src/ClientLauncher/Resources/Remove.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 293 B |
BIN
src/ClientLauncher/Resources/Settings_16x.png
Normal file
BIN
src/ClientLauncher/Resources/Settings_16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 480 B |
BIN
src/ClientLauncher/Rocket_16x.ico
Normal file
BIN
src/ClientLauncher/Rocket_16x.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
90
src/ClientLauncher/ServerHostSettings.cs
Normal file
90
src/ClientLauncher/ServerHostSettings.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// <copyright file="ServerHostSettings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ClientLauncher;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for one server.
|
||||
/// </summary>
|
||||
public class ServerHostSettings : INotifyPropertyChanged
|
||||
{
|
||||
private string? _description;
|
||||
private string? _address;
|
||||
private int _port;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the configuration.
|
||||
/// </summary>
|
||||
public string? Description
|
||||
{
|
||||
get => this._description;
|
||||
set
|
||||
{
|
||||
if (value == this._description)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._description = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host ip.
|
||||
/// </summary>
|
||||
public string? Address
|
||||
{
|
||||
get => this._address;
|
||||
set
|
||||
{
|
||||
if (value == this._address)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._address = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host port.
|
||||
/// </summary>
|
||||
public int Port
|
||||
{
|
||||
get => this._port;
|
||||
set
|
||||
{
|
||||
if (value == this._port)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._port = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Description} ({this.Address}:{this.Port})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="PropertyChanged"/> event for the specified property.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
44
src/ConnectServer/CheckMaximumConnectionsPlugin.cs
Normal file
44
src/ConnectServer/CheckMaximumConnectionsPlugin.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// <copyright file="CheckMaximumConnectionsPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin which checks if the maximum number of connections got exceeded. Refuses the connection to new clients, if that happens.
|
||||
/// </summary>
|
||||
internal class CheckMaximumConnectionsPlugin : IAfterSocketAcceptPlugin
|
||||
{
|
||||
private readonly ILogger<CheckMaximumConnectionsPlugin> _logger;
|
||||
private readonly ClientListener _clientListener;
|
||||
private readonly IConnectServerSettings _connectServerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CheckMaximumConnectionsPlugin" /> class.
|
||||
/// </summary>
|
||||
/// <param name="server">The server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CheckMaximumConnectionsPlugin(ConnectServer server, ILogger<CheckMaximumConnectionsPlugin> logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._clientListener = server.ClientListener;
|
||||
this._connectServerSettings = server.Settings;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnAfterSocketAccept(Socket socket)
|
||||
{
|
||||
var maxConnections = this._connectServerSettings.MaxConnections;
|
||||
if (maxConnections <= this._clientListener.Clients.Count)
|
||||
{
|
||||
this._logger.LogWarning("Connection refused from {0}: maximum connections ({1}) reached.", socket.RemoteEndPoint, maxConnections);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
138
src/ConnectServer/Client.cs
Normal file
138
src/ConnectServer/Client.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
// <copyright file="Client.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.Packets.ConnectServer;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
|
||||
/// <summary>
|
||||
/// The client which connected to the connect server.
|
||||
/// </summary>
|
||||
internal sealed class Client : IDisposable
|
||||
{
|
||||
private readonly ILogger<Client> _logger;
|
||||
private readonly byte[] _receiveBuffer;
|
||||
private readonly Timer _onlineTimer;
|
||||
private readonly IPacketHandler<Client> _packetHandler;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
private DateTime _lastReceive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Client" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection.</param>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <param name="packetHandler">The packet handler.</param>
|
||||
/// <param name="maxPacketSize">Maximum size of the packet. This value is also used to initialize the receive buffer.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public Client(IConnection connection, TimeSpan timeout, IPacketHandler<Client> packetHandler, byte maxPacketSize, ILogger<Client> logger)
|
||||
{
|
||||
this.Connection = connection;
|
||||
this.Connection.PacketReceived += this.OnPacketReceivedAsync;
|
||||
this.Timeout = timeout;
|
||||
this._packetHandler = packetHandler;
|
||||
this._logger = logger;
|
||||
this._lastReceive = DateTime.Now;
|
||||
var checkInterval = new TimeSpan(0, 0, 20);
|
||||
this._onlineTimer = new Timer(this.OnOnlineTimerElapsed, null, checkInterval, checkInterval);
|
||||
this._receiveBuffer = new byte[maxPacketSize];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout after which the client gets disconnected if he is inactive.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server information request count.
|
||||
/// </summary>
|
||||
/// <remarks>Used for DOS protection.</remarks>
|
||||
public int ServerInfoRequestCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FTP request count.
|
||||
/// </summary>
|
||||
/// <remarks>Used for DOS protection.</remarks>
|
||||
public int FtpRequestCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server list request count.
|
||||
/// </summary>
|
||||
/// <remarks>Used for DOS protection.</remarks>
|
||||
public int ServerListRequestCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ip from which the client is connecting.
|
||||
/// </summary>
|
||||
public IPAddress Address { get; set; } = IPAddress.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the port from which the client is connecting.
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection from/to the client.
|
||||
/// </summary>
|
||||
internal IConnection Connection { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this._disposed)
|
||||
{
|
||||
this._disposed = true;
|
||||
this._onlineTimer.Dispose();
|
||||
this.Connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the hello packet.
|
||||
/// </summary>
|
||||
internal ValueTask SendHelloAsync()
|
||||
{
|
||||
return this.Connection.SendHelloAsync();
|
||||
}
|
||||
|
||||
private void OnOnlineTimerElapsed(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.Connection.Connected && DateTime.Now.Subtract(this._lastReceive) > this.Timeout)
|
||||
{
|
||||
this._logger.LogDebug("Connection Timeout ({0}): Address {1}:{2} will be disconnected.", this.Timeout, this.Address, this.Port);
|
||||
this.Connection.DisconnectAsync().AsTask().WaitAndUnwrapException();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error when disconnecting client. Address {1}:{2}", this.Address, this.Port);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask OnPacketReceivedAsync(ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
this._lastReceive = DateTime.Now;
|
||||
if (sequence.Length > this._receiveBuffer.Length)
|
||||
{
|
||||
this._logger.LogInformation($"Client {this.Address}:{this.Port} will be disconnected because it sent a packet which was too big (size of {sequence.Length}");
|
||||
await this.Connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
sequence.CopyTo(this._receiveBuffer);
|
||||
await this._packetHandler
|
||||
.HandlePacketAsync(this, this._receiveBuffer.AsMemory(0, this._receiveBuffer.GetPacketSize()))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
60
src/ConnectServer/ClientConnectionCountPlugin.cs
Normal file
60
src/ConnectServer/ClientConnectionCountPlugin.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
// <copyright file="ClientConnectionCountPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// The client connection count plugin.
|
||||
/// </summary>
|
||||
internal class ClientConnectionCountPlugin : IAfterSocketAcceptPlugin, IAfterDisconnectPlugin
|
||||
{
|
||||
private readonly ILogger<ClientConnectionCountPlugin> _logger;
|
||||
private readonly ClientConnectionCounter _clientCounter;
|
||||
private readonly IConnectServerSettings _connectServerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientConnectionCountPlugin" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServerSettings">The settings.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ClientConnectionCountPlugin(IConnectServerSettings connectServerSettings, ILogger<ClientConnectionCountPlugin> logger)
|
||||
{
|
||||
this._connectServerSettings = connectServerSettings;
|
||||
this._logger = logger;
|
||||
this._clientCounter = new ClientConnectionCounter();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnAfterSocketAccept(Socket socket)
|
||||
{
|
||||
var ipAddress = (socket.RemoteEndPoint as IPEndPoint)?.Address;
|
||||
if (ipAddress is null)
|
||||
{
|
||||
// should never happen - but who knows. In this case, we allow the connection.
|
||||
this._logger.LogDebug($"Non-IPEndPoint connected: {socket.RemoteEndPoint}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._connectServerSettings.CheckMaxConnectionsPerAddress
|
||||
&& this._clientCounter.GetConnectionCount(ipAddress) >= this._connectServerSettings.MaxConnectionsPerAddress)
|
||||
{
|
||||
this._logger.LogWarning("Maximum Connections per IP reached: {0}, Connection refused.", ipAddress);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._clientCounter.AddConnection(ipAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnAfterDisconnect(Client client)
|
||||
{
|
||||
this._clientCounter.RemoveConnection(client.Address);
|
||||
}
|
||||
}
|
||||
72
src/ConnectServer/ClientConnectionCounter.cs
Normal file
72
src/ConnectServer/ClientConnectionCounter.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// <copyright file="ClientConnectionCounter.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Counts the connections per ip address.
|
||||
/// </summary>
|
||||
internal class ClientConnectionCounter
|
||||
{
|
||||
private readonly IDictionary<IPAddress, int> _connections = new Dictionary<IPAddress, int>();
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection count of the specified ip address.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The ip address.</param>
|
||||
/// <returns>The counted connections of the ip address.</returns>
|
||||
public int GetConnectionCount(IPAddress ipAddress)
|
||||
{
|
||||
int count;
|
||||
lock (this._syncRoot)
|
||||
{
|
||||
this._connections.TryGetValue(ipAddress, out count);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the connection and increases its count for the specified ip address.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The ip address.</param>
|
||||
public void AddConnection(IPAddress ipAddress)
|
||||
{
|
||||
lock (this._syncRoot)
|
||||
{
|
||||
if (this._connections.ContainsKey(ipAddress))
|
||||
{
|
||||
this._connections[ipAddress]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._connections.Add(ipAddress, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the connection and decreases its count for the specified ip address.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The ip address.</param>
|
||||
public void RemoveConnection(IPAddress ipAddress)
|
||||
{
|
||||
lock (this._syncRoot)
|
||||
{
|
||||
if (!this._connections.ContainsKey(ipAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._connections[ipAddress]--;
|
||||
if (this._connections[ipAddress] == 0)
|
||||
{
|
||||
this._connections.Remove(ipAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/ConnectServer/ClientListener.cs
Normal file
133
src/ConnectServer/ClientListener.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
// <copyright file="ClientListener.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The listener which is waiting for new connecting clients.
|
||||
/// </summary>
|
||||
internal class ClientListener
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<ClientListener> _logger;
|
||||
private readonly AsyncLock _clientListLock = new();
|
||||
private readonly IConnectServerSettings _connectServerSettings;
|
||||
private readonly IPacketHandler<Client> _packetHandler;
|
||||
private Listener? _listener;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientListener" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ClientListener(IConnectServer connectServer, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._connectServerSettings = connectServer.Settings;
|
||||
this._logger = this._loggerFactory.CreateLogger<ClientListener>();
|
||||
this._packetHandler = new ClientPacketHandler(connectServer, loggerFactory);
|
||||
this.Clients = new List<Client>();
|
||||
this.ClientSocketAcceptPlugins = new List<IAfterSocketAcceptPlugin>();
|
||||
this.ClientSocketDisconnectPlugins = new List<IAfterDisconnectPlugin>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the number of connected clients changed.
|
||||
/// </summary>
|
||||
public event EventHandler? ConnectedClientsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connected clients.
|
||||
/// </summary>
|
||||
public ICollection<Client> Clients { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client socket accept plugins.
|
||||
/// </summary>
|
||||
public IList<IAfterSocketAcceptPlugin> ClientSocketAcceptPlugins { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client socket disconnect plugins.
|
||||
/// </summary>
|
||||
public ICollection<IAfterDisconnectPlugin> ClientSocketDisconnectPlugins { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the listener.
|
||||
/// </summary>
|
||||
public void StartListener()
|
||||
{
|
||||
this._listener = new Listener(this._connectServerSettings.ClientListenerPort, null, null, this._loggerFactory);
|
||||
this._listener.ClientAccepting += this.OnClientAcceptingAsync;
|
||||
this._listener.ClientAccepted += this.OnClientAcceptedAsync;
|
||||
this._listener.Start(this._connectServerSettings.ListenerBacklog);
|
||||
|
||||
this._logger.LogInformation("Client Listener started, Port {0}", this._connectServerSettings.ClientListenerPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the listener.
|
||||
/// </summary>
|
||||
public void StopListener()
|
||||
{
|
||||
this._listener?.Stop();
|
||||
this._logger.LogInformation("Client Listener stopped");
|
||||
}
|
||||
|
||||
private async ValueTask OnClientAcceptingAsync(ClientAcceptingEventArgs e)
|
||||
{
|
||||
for (var i = 0; i < this.ClientSocketAcceptPlugins.Count; ++i)
|
||||
{
|
||||
var plugin = this.ClientSocketAcceptPlugins[i];
|
||||
if (!plugin.OnAfterSocketAccept(e.AcceptingSocket))
|
||||
{
|
||||
e.Cancel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask OnClientAcceptedAsync(ClientAcceptedEventArgs e)
|
||||
{
|
||||
var connection = e.AcceptedConnection;
|
||||
var client = new Client(connection, this._connectServerSettings.Timeout, this._packetHandler, this._connectServerSettings.MaximumReceiveSize, this._loggerFactory.CreateLogger<Client>());
|
||||
var ipEndpoint = connection.EndPoint as IPEndPoint;
|
||||
client.Address = ipEndpoint?.Address ?? IPAddress.None;
|
||||
client.Port = ipEndpoint?.Port ?? 0;
|
||||
client.Timeout = this._connectServerSettings.Timeout;
|
||||
|
||||
using (await this._clientListLock.LockAsync().ConfigureAwait(false))
|
||||
{
|
||||
this.Clients.Add(client);
|
||||
}
|
||||
|
||||
client.Connection.Disconnected += async () => await this.OnClientDisconnectAsync(client).ConfigureAwait(false);
|
||||
this._logger.LogDebug("Client connected: {0}, current client count: {1}", connection.EndPoint, this.Clients.Count);
|
||||
await client.SendHelloAsync().ConfigureAwait(false);
|
||||
_ = Task.Run(() => client.Connection.BeginReceiveAsync());
|
||||
this.ConnectedClientsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async ValueTask OnClientDisconnectAsync(Client client)
|
||||
{
|
||||
foreach (var plugin in this.ClientSocketDisconnectPlugins)
|
||||
{
|
||||
plugin.OnAfterDisconnect(client);
|
||||
}
|
||||
|
||||
this._logger.LogDebug("Connection to Client {0}:{1} disconnected.", client.Address, client.Port);
|
||||
using (await this._clientListLock.LockAsync().ConfigureAwait(false))
|
||||
{
|
||||
this.Clients.Remove(client);
|
||||
}
|
||||
|
||||
this.ConnectedClientsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
242
src/ConnectServer/ConnectServer.cs
Normal file
242
src/ConnectServer/ConnectServer.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
// <copyright file="ConnectServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The connect server.
|
||||
/// </summary>
|
||||
public class ConnectServer : IConnectServer, OpenMU.Interfaces.IConnectServer
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ServerList _serverList;
|
||||
private ServerState _serverState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectServer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServerSettings">The settings.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ConnectServer(IConnectServerSettings connectServerSettings, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
this.Settings = connectServerSettings;
|
||||
this.ClientVersion = new ClientVersion(this.Settings.Client.Season, this.Settings.Client.Episode, ClientLanguage.Invariant);
|
||||
this.ConfigurationId = this.Settings.ConfigurationId;
|
||||
|
||||
this._logger = this._loggerFactory.CreateLogger<ConnectServer>();
|
||||
|
||||
this.ConnectInfos = new ConcurrentDictionary<ushort, byte[]>();
|
||||
this._serverList = new ServerList(this.ClientVersion);
|
||||
|
||||
this.ClientListener = new ClientListener(this, loggerFactory);
|
||||
this.ClientListener.ConnectedClientsChanged += (_, _) =>
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(this.CurrentConnections));
|
||||
};
|
||||
this.CreatePlugins();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerState ServerState
|
||||
{
|
||||
get => this._serverState;
|
||||
private set
|
||||
{
|
||||
if (value != this._serverState)
|
||||
{
|
||||
this._serverState = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServerType Type => ServerType.ConnectServer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => this.Settings.Description;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Id => SpecialServerIds.ConnectServer + this.Settings.ServerId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ConfigurationId { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<ushort, byte[]> ConnectInfos { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
ServerList IConnectServer.ServerList => this._serverList;
|
||||
|
||||
/// <inheritdoc cref="IConnectServer"/>
|
||||
public IConnectServerSettings Settings { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ClientVersion ClientVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum allowed connections.
|
||||
/// </summary>
|
||||
public int MaximumConnections => this.Settings.MaxConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection count.
|
||||
/// </summary>
|
||||
public int CurrentConnections => this.ClientListener.Clients.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current game server connection count.
|
||||
/// </summary>
|
||||
public int CurrentGameServerConnections => this._serverList.TotalConnectionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registered game servers.
|
||||
/// </summary>
|
||||
public IEnumerable<IGameServerEntry> RegisteredGameServers => this._serverList.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client listener.
|
||||
/// </summary>
|
||||
internal ClientListener ClientListener { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask StartAsync()
|
||||
{
|
||||
if (this.ServerState != ServerState.Stopped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Begin starting");
|
||||
var oldState = this.ServerState;
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Starting;
|
||||
try
|
||||
{
|
||||
this.ClientListener.StartListener();
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Started;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, ex.Message);
|
||||
this.ServerState = oldState;
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Finished starting");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.ShutdownAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ShutdownAsync()
|
||||
{
|
||||
this._logger.LogInformation("Begin stopping");
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Stopping;
|
||||
this.ClientListener.StopListener();
|
||||
this.ServerState = OpenMU.Interfaces.ServerState.Stopped;
|
||||
this._logger.LogInformation("Finished stopping");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterGameServer(ServerInfo gameServer, IPEndPoint publicEndPoint)
|
||||
{
|
||||
this._logger.LogInformation("GameServer {0} is registering with endpoint {1}", gameServer, publicEndPoint);
|
||||
try
|
||||
{
|
||||
if (this.ConnectInfos.ContainsKey(gameServer.Id))
|
||||
{
|
||||
this._logger.LogInformation("GameServer {0} was already registered and needs to be removed before...", gameServer);
|
||||
this.UnregisterGameServer(gameServer.Id);
|
||||
}
|
||||
|
||||
var serverListItem = new ServerListItem(this._serverList)
|
||||
{
|
||||
ServerId = gameServer.Id,
|
||||
EndPoint = publicEndPoint,
|
||||
MaximumConnections = gameServer.MaximumConnections,
|
||||
CurrentConnections = gameServer.CurrentConnections,
|
||||
};
|
||||
|
||||
if (this.ConnectInfos.TryAdd(serverListItem.ServerId, serverListItem.ConnectInfo))
|
||||
{
|
||||
this._serverList.Add(serverListItem);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error during registration process");
|
||||
throw;
|
||||
}
|
||||
|
||||
this._logger.LogInformation("GameServer {0} has registered with endpoint {1}", gameServer, publicEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterGameServer(ushort gameServerId)
|
||||
{
|
||||
this._logger.LogInformation("GameServer {0} is unregistering", gameServerId);
|
||||
var serverListItem = this._serverList.GetItem(gameServerId);
|
||||
if (serverListItem != null)
|
||||
{
|
||||
this.ConnectInfos.Remove(serverListItem.ServerId, out _);
|
||||
this._serverList.Remove(serverListItem);
|
||||
}
|
||||
|
||||
this._logger.LogInformation("GameServer {0} has unregistered", gameServerId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
|
||||
{
|
||||
var serverListItem = this._serverList.GetItem(serverId);
|
||||
if (serverListItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
serverListItem.CurrentConnections = currentConnections;
|
||||
}
|
||||
|
||||
private void CreatePlugins()
|
||||
{
|
||||
this._logger.LogDebug("Begin creating plugins");
|
||||
this.ClientListener.ClientSocketAcceptPlugins.Add(new CheckMaximumConnectionsPlugin(this, this._loggerFactory.CreateLogger<CheckMaximumConnectionsPlugin>()));
|
||||
var clientCountPlugin = new ClientConnectionCountPlugin(this.Settings, this._loggerFactory.CreateLogger<ClientConnectionCountPlugin>());
|
||||
this.ClientListener.ClientSocketAcceptPlugins.Add(clientCountPlugin);
|
||||
this.ClientListener.ClientSocketDisconnectPlugins.Add(clientCountPlugin);
|
||||
this._logger.LogDebug("Finished creating plugins");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a property changed.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">Name of the property.</param>
|
||||
private void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
37
src/ConnectServer/ConnectServerFactory.cs
Normal file
37
src/ConnectServer/ConnectServerFactory.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// <copyright file="ConnectServerFactory.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// The connect server factory.
|
||||
/// </summary>
|
||||
public class ConnectServerFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectServerFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ConnectServerFactory(ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new connect server instance.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns>
|
||||
/// The new connect server instance.
|
||||
/// </returns>
|
||||
public OpenMU.Interfaces.IConnectServer CreateConnectServer(IConnectServerSettings settings)
|
||||
{
|
||||
return new ConnectServer(settings, this._loggerFactory);
|
||||
}
|
||||
}
|
||||
17
src/ConnectServer/IAfterDisconnectPlugin.cs
Normal file
17
src/ConnectServer/IAfterDisconnectPlugin.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// <copyright file="IAfterDisconnectPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin which is executed after a client disconnected.
|
||||
/// </summary>
|
||||
internal interface IAfterDisconnectPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Called after a client disconnected.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
void OnAfterDisconnect(Client client);
|
||||
}
|
||||
20
src/ConnectServer/IAfterSocketAcceptPlugin.cs
Normal file
20
src/ConnectServer/IAfterSocketAcceptPlugin.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// <copyright file="IAfterSocketAcceptPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Net.Sockets;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin which is executed when a client socket got accepted by the listener.
|
||||
/// </summary>
|
||||
internal interface IAfterSocketAcceptPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Called after the client socket got accepted by the listener.
|
||||
/// </summary>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <returns>Flag that indicates if the socket is allowed to connect.</returns>
|
||||
bool OnAfterSocketAccept(Socket socket);
|
||||
}
|
||||
35
src/ConnectServer/IConnectServer.cs
Normal file
35
src/ConnectServer/IConnectServer.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// <copyright file="IConnectServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The internal interface of a connect server.
|
||||
/// </summary>
|
||||
internal interface IConnectServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the connect infos.
|
||||
/// </summary>
|
||||
ConcurrentDictionary<ushort, byte[]> ConnectInfos { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server list.
|
||||
/// </summary>
|
||||
ServerList ServerList { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connectServerSettings.
|
||||
/// </summary>
|
||||
IConnectServerSettings Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client version.
|
||||
/// </summary>
|
||||
ClientVersion ClientVersion { get; }
|
||||
}
|
||||
33
src/ConnectServer/IGameServerEntry.cs
Normal file
33
src/ConnectServer/IGameServerEntry.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// <copyright file="IGameServerEntry.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an entry of a gameserver in the connect server.
|
||||
/// </summary>
|
||||
public interface IGameServerEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the server identifier.
|
||||
/// </summary>
|
||||
ushort ServerId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point under which the server is accessible.
|
||||
/// </summary>
|
||||
IPEndPoint EndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server load percentage.
|
||||
/// </summary>
|
||||
byte ServerLoadPercentage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of current connections.
|
||||
/// </summary>
|
||||
int CurrentConnections { get; }
|
||||
}
|
||||
26
src/ConnectServer/MUnique.OpenMU.ConnectServer.csproj
Normal file
26
src/ConnectServer/MUnique.OpenMU.ConnectServer.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<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.ConnectServer.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Release\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.ConnectServer.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\Network\MUnique.OpenMU.Network.csproj" />
|
||||
<ProjectReference Include="..\Network\Packets\MUnique.OpenMU.Network.Packets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
82
src/ConnectServer/PacketHandler/ClientPacketHandler.cs
Normal file
82
src/ConnectServer/PacketHandler/ClientPacketHandler.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// <copyright file="ClientPacketHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
|
||||
using System.Net.Sockets;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using IConnectServer = MUnique.OpenMU.ConnectServer.IConnectServer;
|
||||
|
||||
/// <summary>
|
||||
/// The handler of packets coming from the client.
|
||||
/// </summary>
|
||||
internal class ClientPacketHandler : IPacketHandler<Client>
|
||||
{
|
||||
private readonly ILogger<ClientPacketHandler> _logger;
|
||||
|
||||
private readonly IDictionary<byte, IPacketHandler<Client>> _packetHandlers = new Dictionary<byte, IPacketHandler<Client>>();
|
||||
|
||||
private readonly IConnectServerSettings _connectServerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientPacketHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ClientPacketHandler(IConnectServer connectServer, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._logger = loggerFactory.CreateLogger<ClientPacketHandler>();
|
||||
this._connectServerSettings = connectServer.Settings;
|
||||
|
||||
// TODO: Is 0x05 correct? PatchCheckRequest has Code 0x02
|
||||
this._packetHandlers.Add(0x05, new FtpRequestHandler(connectServer.Settings, loggerFactory.CreateLogger<FtpRequestHandler>()));
|
||||
this._packetHandlers.Add(0xF4, new ServerListHandler(connectServer, loggerFactory));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (packet.Length > this._connectServerSettings.MaximumReceiveSize || packet.Length < 4)
|
||||
{
|
||||
await this.DisconnectClientUnknownPacketAsync(client, packet).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var packetType = packet.Span[2];
|
||||
if (this._packetHandlers.TryGetValue(packetType, out var packetHandler))
|
||||
{
|
||||
await packetHandler.HandlePacketAsync(client, packet).ConfigureAwait(false);
|
||||
}
|
||||
else if (this._connectServerSettings.DisconnectOnUnknownPacket)
|
||||
{
|
||||
await this.DisconnectClientUnknownPacketAsync(client, packet).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
this._logger.LogDebug("SocketException occured in Client.ReceivePacket, Client Address: {0}:{1}, Packet: [{2}], Exception: {3}", client.Address, client.Port, packet.ToArray().ToHexString(), ex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogWarning("Exception occured in Client.ReceivePacket, Client Address: {0}:{1}, Packet: [{2}], Exception: {3}", client.Address, client.Port, packet.ToArray().ToHexString(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask DisconnectClientUnknownPacketAsync(Client client, Memory<byte> packet)
|
||||
{
|
||||
this._logger.LogInformation("Client {0}:{1} will be disconnected because it sent an unknown packet: {2}", client.Address, client.Port, packet.ToArray().ToHexString());
|
||||
await client.Connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
154
src/ConnectServer/PacketHandler/FtpRequestHandler.cs
Normal file
154
src/ConnectServer/PacketHandler/FtpRequestHandler.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
// <copyright file="FtpRequestHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.Packets.ConnectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the ftp related request. The client is sending its version, and the server answers
|
||||
/// with the current version and the ftp address where the client can load a patch.
|
||||
/// </summary>
|
||||
internal class FtpRequestHandler : IPacketHandler<Client>
|
||||
{
|
||||
private static readonly byte[] PatchOk = { 0xC1, 4, 2, 0 };
|
||||
|
||||
private static readonly byte[] Xor3Keys = { 0xFC, 0xCF, 0xAB };
|
||||
|
||||
private readonly IConnectServerSettings _connectServerSettings;
|
||||
private readonly ILogger<FtpRequestHandler> _logger;
|
||||
|
||||
private byte[]? _patchPacket;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FtpRequestHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServerSettings">The settings.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FtpRequestHandler(IConnectServerSettings connectServerSettings, ILogger<FtpRequestHandler> logger)
|
||||
{
|
||||
this._connectServerSettings = connectServerSettings;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The version compare result.
|
||||
/// </summary>
|
||||
private enum VersionCompareResult
|
||||
{
|
||||
VersionTooLow = -1,
|
||||
VersionMatch = 0,
|
||||
VersionHigher = 1,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
|
||||
{
|
||||
if (packet.Length < 6)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void LogVersion(Span<byte> span)
|
||||
{
|
||||
this._logger.LogDebug($"Client {client.Address}:{client.Port} version: {span[3]}.{span[4]}.{span[5]}");
|
||||
}
|
||||
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
LogVersion(packet.Span);
|
||||
}
|
||||
|
||||
if (client.FtpRequestCount >= this._connectServerSettings.MaxFtpRequests)
|
||||
{
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
this._logger.LogDebug("Client {0}:{1} reached maxFtpRequests", client.Address, client.Port);
|
||||
}
|
||||
|
||||
await client.Connection.DisconnectAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int WritePatchPacket()
|
||||
{
|
||||
if (this._patchPacket is { } cachedPacket)
|
||||
{
|
||||
var span = client.Connection.Output.GetSpan(cachedPacket.Length);
|
||||
cachedPacket.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var length = ClientNeedsPatchRef.Length;
|
||||
var span = client.Connection.Output.GetSpan(length)[..length];
|
||||
var packet = new ClientNeedsPatchRef(span);
|
||||
packet.PatchAddress = this._connectServerSettings.PatchAddress;
|
||||
var addressSize = Encoding.UTF8.GetByteCount(this._connectServerSettings.PatchAddress);
|
||||
|
||||
Xor3Bytes(span.Slice(6), addressSize);
|
||||
packet.PatchVersion = this._connectServerSettings.CurrentPatchVersion[2];
|
||||
|
||||
this._patchPacket = span.ToArray();
|
||||
}
|
||||
|
||||
return this._patchPacket.Length;
|
||||
}
|
||||
|
||||
int WriteOkayPacket()
|
||||
{
|
||||
var span = client.Connection.Output.GetSpan(PatchOk.Length)[..PatchOk.Length];
|
||||
PatchOk.CopyTo(span);
|
||||
return PatchOk.Length;
|
||||
}
|
||||
|
||||
if (VersionCompare(this._connectServerSettings.CurrentPatchVersion, 0, packet.Span, 3, this._connectServerSettings.CurrentPatchVersion.Length) == VersionCompareResult.VersionTooLow)
|
||||
{
|
||||
await client.Connection.SendAsync(WritePatchPacket).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.Connection.SendAsync(WriteOkayPacket).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
client.FtpRequestCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the actual version of the client with the expected version.
|
||||
/// </summary>
|
||||
/// <param name="expectedVersion">The expected version.</param>
|
||||
/// <param name="expectedIndex">The expected index.</param>
|
||||
/// <param name="actualVersion">The actual version.</param>
|
||||
/// <param name="actualIndex">The actual index.</param>
|
||||
/// <param name="count">The count.</param>
|
||||
/// <returns>The compare result.</returns>
|
||||
private static VersionCompareResult VersionCompare(byte[] expectedVersion, int expectedIndex, Span<byte> actualVersion, int actualIndex, int count)
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
if (expectedVersion[i + expectedIndex] > actualVersion[i + actualIndex])
|
||||
{
|
||||
return VersionCompareResult.VersionTooLow;
|
||||
}
|
||||
|
||||
if (expectedVersion[i + expectedIndex] < actualVersion[i + actualIndex])
|
||||
{
|
||||
return VersionCompareResult.VersionHigher;
|
||||
}
|
||||
}
|
||||
|
||||
return VersionCompareResult.VersionMatch;
|
||||
}
|
||||
|
||||
private static void Xor3Bytes(Span<byte> data, int size)
|
||||
{
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
data[i] ^= Xor3Keys[i % 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/ConnectServer/PacketHandler/IPacketHandler{T}.cs
Normal file
19
src/ConnectServer/PacketHandler/IPacketHandler{T}.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// <copyright file="IPacketHandler{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
|
||||
/// <summary>
|
||||
/// The interface for a packet handler with a type which is passed as context argument.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the context argument.</typeparam>
|
||||
internal interface IPacketHandler<in T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the packet.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="packet">The packet.</param>
|
||||
ValueTask HandlePacketAsync(T obj, Memory<byte> packet);
|
||||
}
|
||||
115
src/ConnectServer/PacketHandler/ServerInfoRequestHandler.cs
Normal file
115
src/ConnectServer/PacketHandler/ServerInfoRequestHandler.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
// <copyright file="ServerInfoRequestHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.Packets.ConnectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server info request of a client, which means the client wants to know the connect data of the server it just clicked on.
|
||||
/// </summary>
|
||||
internal class ServerInfoRequestHandler : IPacketHandler<Client>
|
||||
{
|
||||
private readonly IConnectServer _connectServer;
|
||||
private readonly ILogger<ServerInfoRequestHandler> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerInfoRequestHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ServerInfoRequestHandler(IConnectServer connectServer, ILogger<ServerInfoRequestHandler> logger)
|
||||
{
|
||||
this._connectServer = connectServer;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
|
||||
{
|
||||
var serverId = GetServerId(packet.Span);
|
||||
this._logger.LogDebug("Client {0}:{1} requested Connection Info of ServerId {2}", client.Address, client.Port, serverId);
|
||||
if (client.ServerInfoRequestCount >= this._connectServer.Settings.MaxIpRequests)
|
||||
{
|
||||
this._logger.LogDebug($"Client {client.Address}:{client.Port} reached max ip requests.");
|
||||
await client.Connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// First we look, if we can just use the IP address which the client connected to.
|
||||
// If the game server is running on the same ip as the connect server, we can use that.
|
||||
// This way, we can be sure, that the client can connect to it, too.
|
||||
var localIpEndPoint = client.Connection.LocalEndPoint as IPEndPoint;
|
||||
var serverItem = this._connectServer.ServerList.GetItem(serverId);
|
||||
var isGameServerOnSameMachineAsConnectServer = (serverItem?.EndPoint.Address).IsOnSameHost();
|
||||
var isClientConnectedOnNonRegisteredAddress = !object.Equals(serverItem?.EndPoint.Address, localIpEndPoint?.Address);
|
||||
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), out var isRunningOnDocker);
|
||||
|
||||
// Only if we can't use the cached data.
|
||||
if (isGameServerOnSameMachineAsConnectServer
|
||||
&& !isRunningOnDocker
|
||||
&& isClientConnectedOnNonRegisteredAddress)
|
||||
{
|
||||
int WritePacket()
|
||||
{
|
||||
var data = client.Connection.Output.GetSpan(ConnectionInfoRef.Length)[..ConnectionInfoRef.Length];
|
||||
_ = new ConnectionInfoRef(data)
|
||||
{
|
||||
IpAddress = localIpEndPoint!.Address.ToString(),
|
||||
Port = (ushort)serverItem!.EndPoint.Port,
|
||||
};
|
||||
return data.Length;
|
||||
}
|
||||
|
||||
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
else if (this._connectServer.ConnectInfos.TryGetValue(serverId, out var connectInfo))
|
||||
{
|
||||
// more optimal way, because the serialized data was cached.
|
||||
int WritePacket()
|
||||
{
|
||||
var span = client.Connection.Output.GetSpan(connectInfo.Length)[..connectInfo.Length];
|
||||
connectInfo.CopyTo(span);
|
||||
return span.Length;
|
||||
}
|
||||
|
||||
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogDebug($"Client {client.Address}:{client.Port}: Connection Info not found, sending Server List instead.");
|
||||
int WritePacket()
|
||||
{
|
||||
var serverList = this._connectServer.ServerList.Serialize();
|
||||
var span = client.Connection.Output.GetSpan(serverList.Length)[..serverList.Length];
|
||||
serverList.CopyTo(span);
|
||||
return span.Length;
|
||||
}
|
||||
|
||||
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await client.SendHelloAsync().ConfigureAwait(false);
|
||||
client.ServerInfoRequestCount++;
|
||||
}
|
||||
|
||||
private static ushort GetServerId(Span<byte> packet)
|
||||
{
|
||||
if (packet.Length == ConnectionInfoRequestRef.Length)
|
||||
{
|
||||
ConnectionInfoRequestRef data = packet;
|
||||
return data.ServerId;
|
||||
}
|
||||
|
||||
if (packet.Length == ConnectionInfoRequest075Ref.Length)
|
||||
{
|
||||
ConnectionInfoRequest075Ref data = packet;
|
||||
return data.ServerId;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unknown packet length C1 {packet.Length} F4 03 ...");
|
||||
}
|
||||
}
|
||||
54
src/ConnectServer/PacketHandler/ServerListHandler.cs
Normal file
54
src/ConnectServer/PacketHandler/ServerListHandler.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// <copyright file="ServerListHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using IConnectServer = MUnique.OpenMU.ConnectServer.IConnectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the requests of server data.
|
||||
/// </summary>
|
||||
internal class ServerListHandler : IPacketHandler<Client>
|
||||
{
|
||||
private readonly ILogger<ServerListHandler> _logger;
|
||||
private readonly IConnectServerSettings _connectServerSettings;
|
||||
private readonly IDictionary<byte, IPacketHandler<Client>> _packetHandlers = new Dictionary<byte, IPacketHandler<Client>>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerListHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ServerListHandler(IConnectServer connectServer, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._logger = loggerFactory.CreateLogger<ServerListHandler>();
|
||||
this._connectServerSettings = connectServer.Settings;
|
||||
this._packetHandlers.Add(0x03, new ServerInfoRequestHandler(connectServer, loggerFactory.CreateLogger<ServerInfoRequestHandler>()));
|
||||
this._packetHandlers.Add(0x06, new ServerListRequestHandler(connectServer, loggerFactory.CreateLogger<ServerListRequestHandler>()));
|
||||
|
||||
// old protocol:
|
||||
this._packetHandlers.Add(0x02, new ServerListRequestHandler(connectServer, loggerFactory.CreateLogger<ServerListRequestHandler>()));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
|
||||
{
|
||||
var packetSubType = packet.Span[3];
|
||||
if (this._packetHandlers.TryGetValue(packetSubType, out var packetHandler))
|
||||
{
|
||||
await packetHandler.HandlePacketAsync(client, packet).ConfigureAwait(false);
|
||||
}
|
||||
else if (this._connectServerSettings.DisconnectOnUnknownPacket)
|
||||
{
|
||||
this._logger.LogInformation("Client {0}:{1} will be disconnected because it sent an unknown packet: {2}", client.Address, client.Port, packet.ToArray().ToHexString());
|
||||
await client.Connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/ConnectServer/PacketHandler/ServerListRequestHandler.cs
Normal file
51
src/ConnectServer/PacketHandler/ServerListRequestHandler.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// <copyright file="ServerListRequestHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the request of the server list.
|
||||
/// </summary>
|
||||
internal class ServerListRequestHandler : IPacketHandler<Client>
|
||||
{
|
||||
private readonly IConnectServer _connectServer;
|
||||
private readonly ILogger<ServerListRequestHandler> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerListRequestHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ServerListRequestHandler(IConnectServer connectServer, ILogger<ServerListRequestHandler> logger)
|
||||
{
|
||||
this._connectServer = connectServer;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
|
||||
{
|
||||
this._logger.LogDebug("Client {0}:{1} requested Server List", client.Address, client.Port);
|
||||
if (client.ServerListRequestCount >= this._connectServer.Settings.MaxServerListRequests)
|
||||
{
|
||||
this._logger.LogDebug("Client {0}:{1} reached maxListRequests", client.Address, client.Port);
|
||||
await client.Connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
client.ServerListRequestCount++;
|
||||
|
||||
int WritePacket()
|
||||
{
|
||||
var serverList = this._connectServer.ServerList.Serialize();
|
||||
var span = client.Connection.Output.GetSpan(serverList.Length)[..serverList.Length];
|
||||
serverList.CopyTo(span);
|
||||
return span.Length;
|
||||
}
|
||||
|
||||
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
10
src/ConnectServer/Properties/AssemblyInfo.cs
Normal file
10
src/ConnectServer/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="AssemblyInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MUnique.OpenMU.ConnectServer")]
|
||||
214
src/ConnectServer/ServerList.cs
Normal file
214
src/ConnectServer/ServerList.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
// <copyright file="ServerList.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer;
|
||||
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.Network.Packets.ConnectServer;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The server list.
|
||||
/// </summary>
|
||||
internal class ServerList
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _lock = new();
|
||||
|
||||
private readonly ICollection<ServerListItem> _servers = new SortedSet<ServerListItem>(new ServerListItemComparer());
|
||||
|
||||
private readonly ClientVersion _clientVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerList" /> class.
|
||||
/// </summary>
|
||||
/// <param name="clientVersion">The client version.</param>
|
||||
public ServerList(ClientVersion clientVersion)
|
||||
{
|
||||
this._clientVersion = clientVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total connection count.
|
||||
/// </summary>
|
||||
public int TotalConnectionCount
|
||||
{
|
||||
get
|
||||
{
|
||||
this._lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return this._servers.Sum(s => s.CurrentConnections);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache of the available servers.
|
||||
/// </summary>
|
||||
public byte[]? Cache { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IGameServerEntry"/>s of this list.
|
||||
/// </summary>
|
||||
public IEnumerable<IGameServerEntry> Items
|
||||
{
|
||||
get
|
||||
{
|
||||
this._lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return this._servers.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified item to this instance.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Add(ServerListItem item)
|
||||
{
|
||||
this._lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
this._servers.Add(item);
|
||||
this.InvalidateCache();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified item from this instance.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Remove(ServerListItem item)
|
||||
{
|
||||
this._lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
this._servers.Remove(item);
|
||||
this.InvalidateCache();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ServerListItem"/> of the specified server id.
|
||||
/// </summary>
|
||||
/// <param name="gameServerId">The game server identifier.</param>
|
||||
/// <returns>The found <see cref="ServerListItem"/>.</returns>
|
||||
public ServerListItem? GetItem(ushort gameServerId)
|
||||
{
|
||||
this._lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return this._servers.FirstOrDefault(s => s.ServerId == gameServerId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes this instance to a server list packet, which can be sent to the client.
|
||||
/// </summary>
|
||||
/// <returns>The serialized server list.</returns>
|
||||
public byte[] Serialize()
|
||||
{
|
||||
var result = this.Cache;
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
this._lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
result = this.Cache;
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
byte[] packet;
|
||||
if (this._clientVersion.Season == 0)
|
||||
{
|
||||
packet = new byte[ServerListResponseOld.GetRequiredSize(this._servers.Count)];
|
||||
var response = new ServerListResponseOld(packet)
|
||||
{
|
||||
ServerCount = (byte)this._servers.Count,
|
||||
};
|
||||
var i = 0;
|
||||
foreach (var server in this._servers)
|
||||
{
|
||||
var serverBlock = response[i];
|
||||
serverBlock.ServerId = (byte)server.ServerId;
|
||||
serverBlock.LoadPercentage = server.ServerLoadPercentage;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
packet = new byte[ServerListResponse.GetRequiredSize(this._servers.Count)];
|
||||
var response = new ServerListResponse(packet)
|
||||
{
|
||||
ServerCount = (ushort)this._servers.Count,
|
||||
};
|
||||
var i = 0;
|
||||
foreach (var server in this._servers)
|
||||
{
|
||||
var serverBlock = response[i];
|
||||
serverBlock.ServerId = server.ServerId;
|
||||
serverBlock.LoadPercentage = server.ServerLoadPercentage;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
this.Cache = packet;
|
||||
return packet;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the cache.
|
||||
/// </summary>
|
||||
private void InvalidateCache()
|
||||
{
|
||||
this.Cache = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Comparer for <see cref="ServerListItem"/>s.
|
||||
/// </summary>
|
||||
private class ServerListItemComparer : IComparer<ServerListItem>
|
||||
{
|
||||
/// <summary>Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.</summary>
|
||||
/// <returns>A signed integer that indicates the relative values of <paramref name="x" /> and <paramref name="y" />, as shown in the following table.Value Meaning Less than zero<paramref name="x" /> is less than <paramref name="y" />.Zero<paramref name="x" /> equals <paramref name="y" />.Greater than zero<paramref name="x" /> is greater than <paramref name="y" />.</returns>
|
||||
/// <param name="x">The first object to compare.</param>
|
||||
/// <param name="y">The second object to compare.</param>
|
||||
public int Compare(ServerListItem? x, ServerListItem? y)
|
||||
{
|
||||
return x?.ServerId.CompareTo(y?.ServerId) ?? int.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user