~ruther/NosSmooth

c6be7bd2a809904ae8a4259b0fa2512051b4d4e1 — František Boháček 3 years ago 8dcf58c
chore: remove NosCore dependency
D .gitmodules => .gitmodules +0 -6
@@ 1,6 0,0 @@
[submodule "libs/NosCore.Packets"]
	path = libs/NosCore.Packets
	url = https://github.com/Rutherther/NosCore.Packets.git
[submodule "libs/NosCore.Shared"]
	path = libs/NosCore.Shared
	url = https://github.com/Rutherther/NosCore.Shared.git

M Core/NosSmooth.Core/Client/BaseNostaleClient.cs => Core/NosSmooth.Core/Client/BaseNostaleClient.cs +75 -74
@@ 1,74 1,75 @@
//
//  BaseNostaleClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosCore.Packets.Interfaces;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using Remora.Results;

namespace NosSmooth.Core.Client;

/// <summary>
/// Represents base class of <see cref="INostaleClient"/>.
/// </summary>
/// <remarks>
/// This class serializes packets and processes commands.
/// </remarks>
public abstract class BaseNostaleClient : INostaleClient
{
    private readonly CommandProcessor _commandProcessor;
    private readonly IPacketSerializer _packetSerializer;

    /// <summary>
    /// Initializes a new instance of the <see cref="BaseNostaleClient"/> class.
    /// </summary>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    protected BaseNostaleClient
    (
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer
    )
    {
        _commandProcessor = commandProcessor;
        _packetSerializer = packetSerializer;
    }

    /// <inheritdoc />
    public abstract Task<Result> RunAsync(CancellationToken stopRequested = default);

    /// <inheritdoc />
    public virtual Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var serialized = _packetSerializer.Serialize(packet);

        return serialized.IsSuccess
            ? SendPacketAsync(serialized.Entity, ct)
            : Task.FromResult(Result.FromError(serialized));
    }

    /// <inheritdoc />
    public abstract Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default);

    /// <inheritdoc />
    public abstract Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default);

    /// <inheritdoc />
    public virtual Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var serialized = _packetSerializer.Serialize(packet);

        return serialized.IsSuccess
            ? ReceivePacketAsync(serialized.Entity, ct)
            : Task.FromResult(Result.FromError(serialized));
    }

    /// <inheritdoc />
    public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default) =>
        _commandProcessor.ProcessCommand(command, ct);
}
\ No newline at end of file
//
//  BaseNostaleClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using NosSmooth.Packets;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Core.Client;

/// <summary>
/// Represents base class of <see cref="INostaleClient"/>.
/// </summary>
/// <remarks>
/// This class serializes packets and processes commands.
/// </remarks>
public abstract class BaseNostaleClient : INostaleClient
{
    private readonly CommandProcessor _commandProcessor;
    private readonly IPacketSerializer _packetSerializer;

    /// <summary>
    /// Initializes a new instance of the <see cref="BaseNostaleClient"/> class.
    /// </summary>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    protected BaseNostaleClient
    (
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer
    )
    {
        _commandProcessor = commandProcessor;
        _packetSerializer = packetSerializer;
    }

    /// <inheritdoc />
    public abstract Task<Result> RunAsync(CancellationToken stopRequested = default);

    /// <inheritdoc />
    public virtual Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var serialized = _packetSerializer.Serialize(packet);

        return serialized.IsSuccess
            ? SendPacketAsync(serialized.Entity, ct)
            : Task.FromResult(Result.FromError(serialized));
    }

    /// <inheritdoc />
    public abstract Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default);

    /// <inheritdoc />
    public abstract Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default);

    /// <inheritdoc />
    public virtual Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var serialized = _packetSerializer.Serialize(packet);

        return serialized.IsSuccess
            ? ReceivePacketAsync(serialized.Entity, ct)
            : Task.FromResult(Result.FromError(serialized));
    }

    /// <inheritdoc />
    public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default) =>
        _commandProcessor.ProcessCommand(command, ct);
}

M Core/NosSmooth.Core/Client/INostaleClient.cs => Core/NosSmooth.Core/Client/INostaleClient.cs +71 -71
@@ 1,71 1,71 @@
//
//  INostaleClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosCore.Packets.Interfaces;
using NosSmooth.Core.Commands;
using Remora.Results;

namespace NosSmooth.Core.Client;

/// <summary>
/// Class representing nostale client that may send and receive packets as well as process commands.
/// </summary>
public interface INostaleClient
{
    /// <summary>
    /// Starts the client.
    /// </summary>
    /// <param name="stopRequested">A cancellation token for stopping the client.</param>
    /// <returns>The result that may or may not have succeeded.</returns>
    public Task<Result> RunAsync(CancellationToken stopRequested = default);

    /// <summary>
    /// Sends the given packet to the server.
    /// </summary>
    /// <param name="packet">The packet to send.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default);

    /// <summary>
    /// Sends the given raw packet string.
    /// </summary>
    /// <param name="packetString">The packed string to send in plain text.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default);

    /// <summary>
    /// Receives the given raw packet string.
    /// </summary>
    /// <param name="packetString">The packet to receive in plain text.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default);

    /// <summary>
    /// Receives the given packet.
    /// </summary>
    /// <param name="packet">The packet to receive.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default);

    /// <summary>
    /// Sends the given command to the client.
    /// </summary>
    /// <remarks>
    /// Commands can be used for doing complex operations like walking that require sending multiple packets
    /// and/or calling some functions of the local client.
    /// This method will not return until the command is finished or it failed.
    /// </remarks>
    /// <param name="command">The command to send.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default);
}
\ No newline at end of file
//
//  INostaleClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosSmooth.Core.Commands;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Core.Client;

/// <summary>
/// Class representing nostale client that may send and receive packets as well as process commands.
/// </summary>
public interface INostaleClient
{
    /// <summary>
    /// Starts the client.
    /// </summary>
    /// <param name="stopRequested">A cancellation token for stopping the client.</param>
    /// <returns>The result that may or may not have succeeded.</returns>
    public Task<Result> RunAsync(CancellationToken stopRequested = default);

    /// <summary>
    /// Sends the given packet to the server.
    /// </summary>
    /// <param name="packet">The packet to send.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default);

    /// <summary>
    /// Sends the given raw packet string.
    /// </summary>
    /// <param name="packetString">The packed string to send in plain text.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default);

    /// <summary>
    /// Receives the given raw packet string.
    /// </summary>
    /// <param name="packetString">The packet to receive in plain text.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default);

    /// <summary>
    /// Receives the given packet.
    /// </summary>
    /// <param name="packet">The packet to receive.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default);

    /// <summary>
    /// Sends the given command to the client.
    /// </summary>
    /// <remarks>
    /// Commands can be used for doing complex operations like walking that require sending multiple packets
    /// and/or calling some functions of the local client.
    /// This method will not return until the command is finished or it failed.
    /// </remarks>
    /// <param name="command">The command to send.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default);
}

M Core/NosSmooth.Core/Extensions/ResultExtensions.cs => Core/NosSmooth.Core/Extensions/ResultExtensions.cs +0 -1
@@ 7,7 7,6 @@
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
using Remora.Results;


M Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs +151 -185
@@ 1,185 1,151 @@
//
//  ServiceCollectionExtensions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NosCore.Packets;
using NosCore.Packets.Attributes;
using NosCore.Packets.Enumerations;
using NosCore.Packets.Interfaces;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using NosSmooth.Core.Packets.Converters;

namespace NosSmooth.Core.Extensions;

/// <summary>
/// Contains extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Adds base packet and command handling for nostale client.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <param name="additionalPacketTypes">Custom types of packets to serialize and deserialize.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleCore
    (
        this IServiceCollection serviceCollection,
        params Type[] additionalPacketTypes
    )
    {
        serviceCollection
            .TryAddSingleton<IPacketHandler, PacketHandler>();

        var clientPacketTypes = typeof(IPacket).Assembly.GetTypes()
            .Where(p => (p.Namespace?.Contains("Client") ?? false) && p.GetInterfaces().Contains(typeof(IPacket)) && p.IsClass && !p.IsAbstract).ToList();
        var serverPacketTypes = typeof(IPacket).Assembly.GetTypes()
            .Where(p => (p.Namespace?.Contains("Server") ?? false) && p.GetInterfaces().Contains(typeof(IPacket)) && p.IsClass && !p.IsAbstract).ToList();

        if (additionalPacketTypes.Length != 0)
        {
            clientPacketTypes.AddRange(additionalPacketTypes);
        }

        serviceCollection.AddSingleton(p =>
            new PacketSerializerProvider(clientPacketTypes, serverPacketTypes, p));
        serviceCollection.AddSingleton(p => p.GetRequiredService<PacketSerializerProvider>().ServerSerializer);

        serviceCollection.AddSingleton<CommandProcessor>();

        serviceCollection.AddSpecificPacketConverter<InPacketSerializer>();

        return serviceCollection;
    }

    /// <summary>
    /// Adds the specified packet responder that will be called upon receiving the given event.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <typeparam name="TPacketResponder">The type of the responder.</typeparam>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddPacketResponder<TPacketResponder>
    (
        this IServiceCollection serviceCollection
    )
        where TPacketResponder : class, IPacketResponder
    {
        return serviceCollection.AddPacketResponder(typeof(TPacketResponder));
    }

    /// <summary>
    /// Adds the specified packet responder that will be called upon receiving the given event.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <param name="responderType">The type of the responder.</param>
    /// <returns>The collection.</returns>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    public static IServiceCollection AddPacketResponder
    (
        this IServiceCollection serviceCollection,
        Type responderType
    )
    {
        if (responderType.GetInterfaces().Any(i => i == typeof(IEveryPacketResponder)))
        {
            return serviceCollection.AddScoped(typeof(IEveryPacketResponder), responderType);
        }

        if (!responderType.GetInterfaces().Any(
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPacketResponder<>)
            ))
        {
            throw new ArgumentException(
                $"{nameof(responderType)} should implement IPacketResponder.",
                nameof(responderType));
        }

        var responderTypeInterfaces = responderType.GetInterfaces();
        var responderInterfaces = responderTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IPacketResponder<>)
        );

        foreach (var responderInterface in responderInterfaces)
        {
            serviceCollection.AddScoped(responderInterface, responderType);
        }

        return serviceCollection;
    }

    /// <summary>
    /// Adds the specified command handler.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <typeparam name="TCommandHandler">The type of the command.</typeparam>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddCommandHandler<TCommandHandler>
    (
        this IServiceCollection serviceCollection
    )
        where TCommandHandler : class, ICommandHandler
    {
        return serviceCollection.AddCommandHandler(typeof(TCommandHandler));
    }

    /// <summary>
    /// Adds the specified command handler.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <param name="commandHandlerType">The type of the command handler.</param>
    /// <returns>The collection.</returns>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    public static IServiceCollection AddCommandHandler
    (
        this IServiceCollection serviceCollection,
        Type commandHandlerType
    )
    {
        if (!commandHandlerType.GetInterfaces().Any(
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<>)
            ))
        {
            throw new ArgumentException(
                $"{nameof(commandHandlerType)} should implement ICommandHandler.",
                nameof(commandHandlerType));
        }

        var handlerTypeInterfaces = commandHandlerType.GetInterfaces();
        var handlerInterfaces = handlerTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(ICommandHandler<>)
        );

        foreach (var handlerInterface in handlerInterfaces)
        {
            serviceCollection.AddScoped(handlerInterface, commandHandlerType);
        }

        return serviceCollection;
    }

    /// <summary>
    /// Adds the specified packet converter.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <typeparam name="TPacketConverter">The type of the packet.</typeparam>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddSpecificPacketConverter<TPacketConverter>(this IServiceCollection serviceCollection)
        where TPacketConverter : class, ISpecificPacketSerializer
    {
        return serviceCollection.AddSingleton<ISpecificPacketSerializer, TPacketConverter>();
    }
}
\ No newline at end of file
//
//  ServiceCollectionExtensions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using NosSmooth.Packets.Extensions;

namespace NosSmooth.Core.Extensions;

/// <summary>
/// Contains extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Adds base packet and command handling for nostale client.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <param name="additionalPacketTypes">Custom types of packets to serialize and deserialize.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleCore
    (
        this IServiceCollection serviceCollection,
        params Type[] additionalPacketTypes
    )
    {
        serviceCollection
            .TryAddSingleton<IPacketHandler, PacketHandler>();

        serviceCollection.AddPacketSerialization();
        serviceCollection.AddSingleton<CommandProcessor>();

        return serviceCollection;
    }

    /// <summary>
    /// Adds the specified packet responder that will be called upon receiving the given event.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <typeparam name="TPacketResponder">The type of the responder.</typeparam>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddPacketResponder<TPacketResponder>
    (
        this IServiceCollection serviceCollection
    )
        where TPacketResponder : class, IPacketResponder
    {
        return serviceCollection.AddPacketResponder(typeof(TPacketResponder));
    }

    /// <summary>
    /// Adds the specified packet responder that will be called upon receiving the given event.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <param name="responderType">The type of the responder.</param>
    /// <returns>The collection.</returns>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    public static IServiceCollection AddPacketResponder
    (
        this IServiceCollection serviceCollection,
        Type responderType
    )
    {
        if (responderType.GetInterfaces().Any(i => i == typeof(IEveryPacketResponder)))
        {
            return serviceCollection.AddScoped(typeof(IEveryPacketResponder), responderType);
        }

        if (!responderType.GetInterfaces().Any(
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPacketResponder<>)
            ))
        {
            throw new ArgumentException(
                $"{nameof(responderType)} should implement IPacketResponder.",
                nameof(responderType));
        }

        var responderTypeInterfaces = responderType.GetInterfaces();
        var responderInterfaces = responderTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IPacketResponder<>)
        );

        foreach (var responderInterface in responderInterfaces)
        {
            serviceCollection.AddScoped(responderInterface, responderType);
        }

        return serviceCollection;
    }

    /// <summary>
    /// Adds the specified command handler.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <typeparam name="TCommandHandler">The type of the command.</typeparam>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddCommandHandler<TCommandHandler>
    (
        this IServiceCollection serviceCollection
    )
        where TCommandHandler : class, ICommandHandler
    {
        return serviceCollection.AddCommandHandler(typeof(TCommandHandler));
    }

    /// <summary>
    /// Adds the specified command handler.
    /// </summary>
    /// <param name="serviceCollection">The service collection to register the responder to.</param>
    /// <param name="commandHandlerType">The type of the command handler.</param>
    /// <returns>The collection.</returns>
    /// <exception cref="ArgumentException">Thrown if the type of the responder is incorrect.</exception>
    public static IServiceCollection AddCommandHandler
    (
        this IServiceCollection serviceCollection,
        Type commandHandlerType
    )
    {
        if (!commandHandlerType.GetInterfaces().Any(
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<>)
            ))
        {
            throw new ArgumentException(
                $"{nameof(commandHandlerType)} should implement ICommandHandler.",
                nameof(commandHandlerType));
        }

        var handlerTypeInterfaces = commandHandlerType.GetInterfaces();
        var handlerInterfaces = handlerTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(ICommandHandler<>)
        );

        foreach (var handlerInterface in handlerInterfaces)
        {
            serviceCollection.AddScoped(handlerInterface, commandHandlerType);
        }

        return serviceCollection;
    }
}

M Core/NosSmooth.Core/NosSmooth.Core.csproj => Core/NosSmooth.Core/NosSmooth.Core.csproj +18 -18
@@ 1,18 1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
      <PackageReference Include="Remora.Results" Version="7.1.0" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\..\libs\NosCore.Packets\src\NosCore.Packets\NosCore.Packets.csproj" />
    </ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
      <PackageReference Include="Remora.Results" Version="7.1.0" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\NosSmooth.Packets\NosSmooth.Packets.csproj" />
    </ItemGroup>

</Project>

M Core/NosSmooth.Core/Packets/Converters/ISpecificPacketSerializer.cs => Core/NosSmooth.Core/Packets/Converters/ISpecificPacketSerializer.cs +1 -132
@@ 1,132 1,1 @@
//
//  ISpecificPacketSerializer.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using NosCore.Packets.Attributes;
using NosCore.Packets.Interfaces;
using Remora.Results;

namespace NosSmooth.Core.Packets.Converters;

/// <summary>
/// Converts packets that cannot be handled easily by the default serializer.
/// </summary>
public interface ISpecificPacketSerializer
{
    /// <summary>
    /// Gets whether this is a serializer.
    /// </summary>
    public bool Serializer { get; }

    /// <summary>
    /// Gets whether this is a deserializer.
    /// </summary>
    public bool Deserializer { get; }

    /// <summary>
    /// Whether the current packet serializer should handle the packet.
    /// </summary>
    /// <param name="packetString">The string of the packet.</param>
    /// <returns>If this serializer should be used for handling.</returns>
    public bool ShouldHandle(string packetString);

    /// <summary>
    /// Whether the current packet serializer should handle the packet.
    /// </summary>
    /// <param name="packet">The packet object.</param>
    /// <returns>If this serializer should be used for handling.</returns>
    public bool ShouldHandle(IPacket packet);

    /// <summary>
    /// Serialize the given packet into string.
    /// </summary>
    /// <param name="packet">The string of the packet.</param>
    /// <returns>The serialized packet or an error.</returns>
    public Result<string> Serialize(IPacket packet);

    /// <summary>
    /// Deserialize the given packet to its type.
    /// </summary>
    /// <param name="packetString">The string of the packet.</param>
    /// <returns>The deserialized packet or an error.</returns>
    public Result<IPacket> Deserialize(string packetString);
}

/// <summary>
/// Converts packets that cannot be handled easily by the default serializer.
/// </summary>
/// <typeparam name="TPacket">The packet.</typeparam>
public abstract class SpecificPacketSerializer<TPacket> : ISpecificPacketSerializer
    where TPacket : IPacket
{
    private string? _packetHeader;

    /// <summary>
    /// Gets the packet header identifier.
    /// </summary>
    public string PacketHeader
    {
        get
        {
            if (_packetHeader is null)
            {
                _packetHeader = typeof(TPacket).GetCustomAttribute<PacketHeaderAttribute>()!.Identification + " ";
            }

            return _packetHeader;
        }
    }

    /// <inheritdoc />
    public abstract bool Serializer { get; }

    /// <inheritdoc />
    public abstract bool Deserializer { get; }

    /// <inheritdoc />
    public bool ShouldHandle(string packetString)
    {
        return packetString.StartsWith(PacketHeader);
    }

    /// <inheritdoc />
    public bool ShouldHandle(IPacket packet)
    {
        return typeof(TPacket) == packet.GetType();
    }

    /// <inheritdoc/>
    Result<string> ISpecificPacketSerializer.Serialize(IPacket packet)
    {
        return Serialize((TPacket)packet);
    }

    /// <inheritdoc/>
    Result<IPacket> ISpecificPacketSerializer.Deserialize(string packetString)
    {
        var result = Deserialize(packetString);
        if (!result.IsSuccess)
        {
            return Result<IPacket>.FromError(result);
        }

        return Result<IPacket>.FromSuccess(result.Entity);
    }

    /// <summary>
    /// Serialize the given packet into string.
    /// </summary>
    /// <param name="packet">The string of the packet.</param>
    /// <returns>The serialized packet or an error.</returns>
    public abstract Result<string> Serialize(TPacket packet);

    /// <summary>
    /// Deserialize the given packet to its type.
    /// </summary>
    /// <param name="packetString">The string of the packet.</param>
    /// <returns>The deserialized packet or an error.</returns>
    public abstract Result<TPacket> Deserialize(string packetString);
}
\ No newline at end of file

\ No newline at end of file

M Core/NosSmooth.Core/Packets/Converters/InPacketSerializer.cs => Core/NosSmooth.Core/Packets/Converters/InPacketSerializer.cs +1 -91
@@ 1,91 1,1 @@
//
//  InPacketSerializer.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Linq;
using NosCore.Packets.ServerPackets.Visibility;
using NosCore.Shared.Enumerations;
using Remora.Results;

namespace NosSmooth.Core.Packets.Converters;

/// <summary>
/// Deserializes InPacket correctly.
/// </summary>
public class InPacketSerializer : SpecificPacketSerializer<InPacket>
{
    private readonly PacketSerializerProvider _packetSerializerProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="InPacketSerializer"/> class.
    /// </summary>
    /// <param name="packetSerializerProvider">The provider of packet serializer.</param>
    public InPacketSerializer(PacketSerializerProvider packetSerializerProvider)
    {
        _packetSerializerProvider = packetSerializerProvider;
    }

    /// <inheritdoc />
    public override bool Serializer => false;

    /// <inheritdoc />
    public override bool Deserializer => true;

    /// <inheritdoc />
    public override Result<string> Serialize(InPacket packet)
    {
        throw new System.NotImplementedException();
    }

    /// <inheritdoc />
    public override Result<InPacket> Deserialize(string packetString)
    {
        try
        {
            var deserializer = _packetSerializerProvider.ServerSerializer.Deserializer;
            var splitted = packetString.Split(new char[] { ' ' }, 9).Skip(1).ToArray();

            if (!Enum.TryParse(splitted[0], out VisualType type))
            {
                return new ArgumentInvalidError(nameof(packetString), "The visual type is incorrect.");
            }

            var startAddress = type == VisualType.Player ? 3 : 2;
            var inPacket = new InPacket
            {
                VisualType = type,
                VNum = type != VisualType.Player ? long.Parse(splitted[1]) : null,
                Name = type == VisualType.Player ? splitted[1] : null,
                VisualId = long.Parse(splitted[startAddress]),
                PositionX = short.Parse(splitted[startAddress + 1]),
                PositionY = short.Parse(splitted[startAddress + 2]),
                Direction = byte.Parse(splitted[startAddress + 3])
            };

            switch (inPacket.VisualType)
            {
                case VisualType.Player:
                    inPacket.InCharacterSubPacket = (InCharacterSubPacket?)deserializer
                        .DeserializeHeaderlessIPacket(typeof(InCharacterSubPacket), splitted[7]);
                    break;
                case VisualType.Object:
                    inPacket.InItemSubPacket = (InItemSubPacket?)deserializer
                        .DeserializeHeaderlessIPacket(typeof(InItemSubPacket), splitted[6] + " " + splitted[7]);
                    break;
                default:
                    inPacket.InNonPlayerSubPacket = (InNonPlayerSubPacket?)deserializer
                        .DeserializeHeaderlessIPacket(typeof(InNonPlayerSubPacket), splitted[6] + " " + splitted[7]);
                    break;
            }

            return inPacket;
        }
        catch (Exception e)
        {
            return e;
        }
    }
}
\ No newline at end of file

\ No newline at end of file

M Core/NosSmooth.Core/Packets/IPacketHandler.cs => Core/NosSmooth.Core/Packets/IPacketHandler.cs +36 -36
@@ 1,36 1,36 @@
//
//  IPacketHandler.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosCore.Packets.Interfaces;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Calls registered responders for the packet that should be handled.
/// </summary>
public interface IPacketHandler
{
    /// <summary>
    /// Calls a responder for the given packet.
    /// </summary>
    /// <param name="packet">The packet.</param>
    /// <param name="packetString">The string of the packet.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> HandleReceivedPacketAsync(IPacket packet, string packetString, CancellationToken ct = default);

    /// <summary>
    /// Calls a responder for the given packet.
    /// </summary>
    /// <param name="packet">The packet.</param>
    /// <param name="packetString">The string of the packet.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> HandleSentPacketAsync(IPacket packet, string packetString, CancellationToken ct = default);
}
\ No newline at end of file
//
//  IPacketHandler.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Calls registered responders for the packet that should be handled.
/// </summary>
public interface IPacketHandler
{
    /// <summary>
    /// Calls a responder for the given packet.
    /// </summary>
    /// <param name="packet">The packet.</param>
    /// <param name="packetString">The string of the packet.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> HandleReceivedPacketAsync(IPacket packet, string packetString, CancellationToken ct = default);

    /// <summary>
    /// Calls a responder for the given packet.
    /// </summary>
    /// <param name="packet">The packet.</param>
    /// <param name="packetString">The string of the packet.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> HandleSentPacketAsync(IPacket packet, string packetString, CancellationToken ct = default);
}

M Core/NosSmooth.Core/Packets/IPacketResponder.cs => Core/NosSmooth.Core/Packets/IPacketResponder.cs +52 -52
@@ 1,52 1,52 @@
//
//  IPacketResponder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosCore.Packets.Interfaces;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Represents interface for classes that respond to packets.
/// </summary>
public interface IPacketResponder
{
}

/// <summary>
/// Represents interface for classes that respond to packets.
/// Responds to <typeparamref name="TPacket"/>.
/// </summary>
/// <typeparam name="TPacket">The packet type this responder responds to.</typeparam>
public interface IPacketResponder<TPacket> : IPacketResponder
    where TPacket : IPacket
{
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond(PacketEventArgs<TPacket> packet, CancellationToken ct = default);
}

/// <summary>
/// Represents interface for classes that respond to every type of packets.
/// </summary>
public interface IEveryPacketResponder : IPacketResponder
{
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <typeparam name="TPacket">The type of the packet.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packet, CancellationToken ct = default)
        where TPacket : IPacket;
}
\ No newline at end of file
//
//  IPacketResponder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Represents interface for classes that respond to packets.
/// </summary>
public interface IPacketResponder
{
}

/// <summary>
/// Represents interface for classes that respond to packets.
/// Responds to <typeparamref name="TPacket"/>.
/// </summary>
/// <typeparam name="TPacket">The packet type this responder responds to.</typeparam>
public interface IPacketResponder<TPacket> : IPacketResponder
    where TPacket : IPacket
{
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond(PacketEventArgs<TPacket> packet, CancellationToken ct = default);
}

/// <summary>
/// Represents interface for classes that respond to every type of packets.
/// </summary>
public interface IEveryPacketResponder : IPacketResponder
{
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <typeparam name="TPacket">The type of the packet.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packet, CancellationToken ct = default)
        where TPacket : IPacket;
}

D Core/NosSmooth.Core/Packets/IPacketSerializer.cs => Core/NosSmooth.Core/Packets/IPacketSerializer.cs +0 -44
@@ 1,44 0,0 @@
//
//  IPacketSerializer.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosCore.Packets;
using NosCore.Packets.Interfaces;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Represents serialiazer and deserializer of <see cref="IPacket"/>.
/// </summary>
public interface IPacketSerializer
{
    /// <summary>
    /// Serializes the given <see cref="IPacket"/> into string.
    /// </summary>
    /// <param name="packet">The packet to serialize.</param>
    /// <returns>The serialized packet.</returns>
    public Result<string> Serialize(IPacket packet);

    /// <summary>
    /// Deserializes the given string into <see cref="IPacket"/>.
    /// </summary>
    /// <param name="packetString">The packet to deserialize.</param>
    /// <returns>The deserialized packet.</returns>
    public Result<IPacket> Deserialize(string packetString);

    /// <summary>
    /// Gets the inner serializer from NosCore.
    /// </summary>
    [Obsolete("May be removed anytime.")]
    public Serializer Serializer { get; }

    /// <summary>
    /// Gets the inner deserializer from NosCore.
    /// </summary>
    [Obsolete("May be removed anytime.")]
    public Deserializer Deserializer { get; }
}
\ No newline at end of file

M Core/NosSmooth.Core/Packets/PacketEventArgs.cs => Core/NosSmooth.Core/Packets/PacketEventArgs.cs +3 -3
@@ 4,14 4,14 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosCore.Packets.Interfaces;
using NosSmooth.Packets.Attributes;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Arguments for <see cref="IPacketResponder{TPacket}"/>
/// </summary>
/// <param name="Type">The type of the packet.</param>
/// <param name="Source">The source of the packet.</param>
/// <param name="Packet">The deserialized packet.</param>
/// <param name="PacketString">The packet string.</param>
public record PacketEventArgs<TPacket>(PacketType Type, TPacket Packet, string PacketString);
\ No newline at end of file
public record PacketEventArgs<TPacket>(PacketSource Source, TPacket Packet, string PacketString);
\ No newline at end of file

M Core/NosSmooth.Core/Packets/PacketHandler.cs => Core/NosSmooth.Core/Packets/PacketHandler.cs +105 -104
@@ 1,104 1,105 @@
//
//  PacketHandler.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NosCore.Packets.Interfaces;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <inheritdoc />
public class PacketHandler : IPacketHandler
{
    private readonly IServiceProvider _provider;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketHandler"/> class.
    /// </summary>
    /// <param name="provider">The dependency injection provider.</param>
    public PacketHandler(IServiceProvider provider)
    {
        _provider = provider;
    }

    /// <inheritdoc />
    public Task<Result> HandleReceivedPacketAsync(IPacket packet, string packetString, CancellationToken ct)
        => HandlePacketAsync(PacketType.Received, packet, packetString, ct);

    /// <inheritdoc />
    public Task<Result> HandleSentPacketAsync(IPacket packet, string packetString, CancellationToken ct)
        => HandlePacketAsync(PacketType.Sent, packet, packetString, ct);

    private Task<Result> HandlePacketAsync
    (
        PacketType packetType,
        IPacket packet,
        string packetString,
        CancellationToken ct = default
    )
    {
        var processMethod = GetType().GetMethod
        (
            nameof(DispatchResponder),
            BindingFlags.NonPublic | BindingFlags.Instance
        );

        if (processMethod is null)
        {
            throw new InvalidOperationException("Could not find process command generic method in command processor.");
        }

        var boundProcessMethod = processMethod.MakeGenericMethod(packet.GetType());
        return (Task<Result>)boundProcessMethod.Invoke(this, new object[]
        {
            packetType,
            packet,
            packetString,
            ct
        })!;
    }

    private async Task<Result> DispatchResponder<TPacket>(
        PacketType packetType,
        TPacket packet,
        string packetString,
        CancellationToken ct
    )
        where TPacket : class, IPacket
    {
        using var scope = _provider.CreateScope();
        var packetResponders = scope.ServiceProvider.GetServices<IPacketResponder<TPacket>>();
        var genericPacketResponders = scope.ServiceProvider.GetServices<IEveryPacketResponder>();

        var packetEventArgs = new PacketEventArgs<TPacket>(packetType, packet, packetString);
        var tasks = packetResponders.Select(responder => responder.Respond(packetEventArgs, ct)).ToList();
        tasks.AddRange(genericPacketResponders.Select(responder => responder.Respond(packetEventArgs, ct)));

        var results = await Task.WhenAll(tasks);

        var errors = new List<Result>();
        foreach (var result in results)
        {
            if (!result.IsSuccess)
            {
                errors.Add(result);
            }
        }

        return errors.Count switch
        {
            0 => Result.FromSuccess(),
            1 => errors[0],
            _ => new AggregateError(errors.Cast<IResult>().ToArray())
        };
    }
}
\ No newline at end of file
//
//  PacketHandler.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Packets.Attributes;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <inheritdoc />
public class PacketHandler : IPacketHandler
{
    private readonly IServiceProvider _provider;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketHandler"/> class.
    /// </summary>
    /// <param name="provider">The dependency injection provider.</param>
    public PacketHandler(IServiceProvider provider)
    {
        _provider = provider;
    }

    /// <inheritdoc />
    public Task<Result> HandleReceivedPacketAsync(IPacket packet, string packetString, CancellationToken ct)
        => HandlePacketAsync(PacketSource.Server, packet, packetString, ct);

    /// <inheritdoc />
    public Task<Result> HandleSentPacketAsync(IPacket packet, string packetString, CancellationToken ct)
        => HandlePacketAsync(PacketSource.Client, packet, packetString, ct);

    private Task<Result> HandlePacketAsync
    (
        PacketSource packetType,
        IPacket packet,
        string packetString,
        CancellationToken ct = default
    )
    {
        var processMethod = GetType().GetMethod
        (
            nameof(DispatchResponder),
            BindingFlags.NonPublic | BindingFlags.Instance
        );

        if (processMethod is null)
        {
            throw new InvalidOperationException("Could not find process command generic method in command processor.");
        }

        var boundProcessMethod = processMethod.MakeGenericMethod(packet.GetType());
        return (Task<Result>)boundProcessMethod.Invoke(this, new object[]
        {
            packetType,
            packet,
            packetString,
            ct
        })!;
    }

    private async Task<Result> DispatchResponder<TPacket>(
        PacketSource packetType,
        TPacket packet,
        string packetString,
        CancellationToken ct
    )
        where TPacket : class, IPacket
    {
        using var scope = _provider.CreateScope();
        var packetResponders = scope.ServiceProvider.GetServices<IPacketResponder<TPacket>>();
        var genericPacketResponders = scope.ServiceProvider.GetServices<IEveryPacketResponder>();

        var packetEventArgs = new PacketEventArgs<TPacket>(packetType, packet, packetString);
        var tasks = packetResponders.Select(responder => responder.Respond(packetEventArgs, ct)).ToList();
        tasks.AddRange(genericPacketResponders.Select(responder => responder.Respond(packetEventArgs, ct)));

        var results = await Task.WhenAll(tasks);

        var errors = new List<Result>();
        foreach (var result in results)
        {
            if (!result.IsSuccess)
            {
                errors.Add(result);
            }
        }

        return errors.Count switch
        {
            0 => Result.FromSuccess(),
            1 => errors[0],
            _ => new AggregateError(errors.Cast<IResult>().ToArray())
        };
    }
}

D Core/NosSmooth.Core/Packets/PacketSerializer.cs => Core/NosSmooth.Core/Packets/PacketSerializer.cs +0 -88
@@ 1,88 0,0 @@
//
//  PacketSerializer.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using NosCore.Packets;
using NosCore.Packets.Interfaces;
using NosSmooth.Core.Packets.Converters;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <inheritdoc />
public class PacketSerializer : IPacketSerializer
{
    private readonly Serializer _serializer;
    private readonly Deserializer _deserializer;
    private readonly IEnumerable<ISpecificPacketSerializer> _specificPacketSerializers;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSerializer"/> class.
    /// </summary>
    /// <param name="serializer">The NosCore serializer.</param>
    /// <param name="deserializer">The NosCore deserializer.</param>
    /// <param name="specificPacketSerializers">The specific packet serializers.</param>
    public PacketSerializer
    (
        Serializer serializer,
        Deserializer deserializer,
        IEnumerable<ISpecificPacketSerializer> specificPacketSerializers
    )
    {
        _serializer = serializer;
        _deserializer = deserializer;
        _specificPacketSerializers = specificPacketSerializers;
    }

    /// <inheritdoc />
    public Result<string> Serialize(IPacket packet)
    {
        try
        {
            foreach (var specificPacketSerializer in _specificPacketSerializers)
            {
                if (specificPacketSerializer.Serializer && specificPacketSerializer.ShouldHandle(packet))
                {
                    return specificPacketSerializer.Serialize(packet);
                }
            }

            return _serializer.Serialize(packet);
        }
        catch (Exception e)
        {
            return e;
        }
    }

    /// <inheritdoc />
    public Result<IPacket> Deserialize(string packetString)
    {
        try
        {
            foreach (var specificPacketSerializer in _specificPacketSerializers)
            {
                if (specificPacketSerializer.Deserializer && specificPacketSerializer.ShouldHandle(packetString))
                {
                    return specificPacketSerializer.Deserialize(packetString);
                }
            }

            return Result<IPacket>.FromSuccess(_deserializer.Deserialize(packetString));
        }
        catch (Exception e)
        {
            return e;
        }
    }

    /// <inheritdoc />
    public Serializer Serializer => _serializer;

    /// <inheritdoc />
    public Deserializer Deserializer => _deserializer;
}
\ No newline at end of file

D Core/NosSmooth.Core/Packets/PacketSerializerProvider.cs => Core/NosSmooth.Core/Packets/PacketSerializerProvider.cs +0 -83
@@ 1,83 0,0 @@
//
//  PacketSerializerProvider.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using NosCore.Packets;
using NosSmooth.Core.Packets.Converters;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Provides serializer for the server or client packets.
/// </summary>
public class PacketSerializerProvider
{
    private readonly List<Type> _clientPacketTypes;
    private readonly List<Type> _serverPacketTypes;

    private IPacketSerializer? _serverSerializer;
    private IPacketSerializer? _clientSerializer;
    private IServiceProvider _serviceProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSerializerProvider"/> class.
    /// </summary>
    /// <param name="clientPacketTypes">The types of client packets.</param>
    /// <param name="serverPacketTypes">The types of server packets.</param>
    /// <param name="serviceProvider">The service provider for dependency injection.</param>
    public PacketSerializerProvider(List<Type> clientPacketTypes, List<Type> serverPacketTypes, IServiceProvider serviceProvider)
    {
        _clientPacketTypes = clientPacketTypes;
        _serverPacketTypes = serverPacketTypes;
        _serviceProvider = serviceProvider;
    }

    /// <summary>
    /// Gets the server serializer.
    /// </summary>
    public IPacketSerializer ServerSerializer
    {
        get
        {
            if (_serverSerializer is null)
            {
                _serverSerializer =
                    new PacketSerializer
                    (
                        new Serializer(_serverPacketTypes),
                        new Deserializer(_serverPacketTypes),
                        _serviceProvider.GetServices<ISpecificPacketSerializer>()
                    );
            }

            return _serverSerializer;
        }
    }

    /// <summary>
    /// Gets the client serializer.
    /// </summary>
    public IPacketSerializer ClientSerializer
    {
        get
        {
            if (_clientSerializer is null)
            {
                _clientSerializer =
                    new PacketSerializer
                    (
                        new Serializer(_clientPacketTypes),
                        new Deserializer(_clientPacketTypes),
                        _serviceProvider.GetServices<ISpecificPacketSerializer>()
                    );
            }

            return _clientSerializer;
        }
    }
}
\ No newline at end of file

D Core/NosSmooth.Core/Packets/PacketType.cs => Core/NosSmooth.Core/Packets/PacketType.cs +0 -23
@@ 1,23 0,0 @@
//
//  PacketType.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Core.Packets;

/// <summary>
/// The type of the packet.
/// </summary>
public enum PacketType
{
    /// <summary>
    /// The packet was sent to the server.
    /// </summary>
    Sent,

    /// <summary>
    /// The packet was received from the server.
    /// </summary>
    Received,
}
\ No newline at end of file

M Core/NosSmooth.Core/Packets/ParsingFailedPacket.cs => Core/NosSmooth.Core/Packets/ParsingFailedPacket.cs +37 -37
@@ 1,37 1,37 @@
//
//  ParsingFailedPacket.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosCore.Packets;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Represents packet that failed to parse correctly.
/// </summary>
public class ParsingFailedPacket : PacketBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ParsingFailedPacket"/> class.
    /// </summary>
    /// <param name="serializerResult">The result from the serializer.</param>
    /// <param name="packet">The full text of the packet.</param>
    public ParsingFailedPacket(IResult serializerResult, string packet)
    {
        SerializerResult = serializerResult;
        Packet = packet;
    }

    /// <summary>
    /// Gets the result from the serializer.
    /// </summary>
    public IResult SerializerResult { get; }

    /// <summary>
    /// Gets he full packet string.
    /// </summary>
    public string Packet { get; }
}
\ No newline at end of file
//
//  ParsingFailedPacket.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Represents packet that failed to parse correctly.
/// </summary>
public class ParsingFailedPacket : IPacket
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ParsingFailedPacket"/> class.
    /// </summary>
    /// <param name="serializerResult">The result from the serializer.</param>
    /// <param name="packet">The full text of the packet.</param>
    public ParsingFailedPacket(IResult serializerResult, string packet)
    {
        SerializerResult = serializerResult;
        Packet = packet;
    }

    /// <summary>
    /// Gets the result from the serializer.
    /// </summary>
    public IResult SerializerResult { get; }

    /// <summary>
    /// Gets he full packet string.
    /// </summary>
    public string Packet { get; }
}

M Core/NosSmooth.Packets/Packets/Server/Entities/InPacket.cs => Core/NosSmooth.Packets/Packets/Server/Entities/InPacket.cs +1 -1
@@ 43,7 43,7 @@ public record InPacket
    [PacketIndex(6)]
    short PositionY,
    [PacketConditionalIndex(7, "EntityType", true, EntityType.Object)]
    byte Direction,
    byte? Direction,
    [PacketConditionalIndex(8, "EntityType", false, EntityType.Player, InnerSeparator = ' ')]
    InPlayerSubPacket? PlayerSubPacket,
    [PacketConditionalIndex(9, "EntityType", false, EntityType.Object, InnerSeparator = ' ')]

M Local/NosSmooth.LocalClient/NostaleLocalClient.cs => Local/NosSmooth.LocalClient/NostaleLocalClient.cs +201 -212
@@ 1,212 1,201 @@
//
//  NostaleLocalClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosCore.Packets;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.LocalClient.Hooks;
using NosSmoothCore;
using Remora.Results;

namespace NosSmooth.LocalClient;

/// <summary>
/// The local nostale client.
/// </summary>
/// <remarks>
/// Client used for living in the same process as NostaleClientX.exe.
/// It hooks the send and receive packet methods.
/// </remarks>
public class NostaleLocalClient : BaseNostaleClient
{
    private readonly PacketSerializerProvider _packetSerializerProvider;
    private readonly NostaleHookManager _hookManager;
    private readonly IPacketHandler _packetHandler;
    private readonly ILogger _logger;
    private readonly IServiceProvider _provider;
    private readonly NosClient _client;
    private readonly LocalClientOptions _options;
    private CancellationToken? _stopRequested;
    private IPacketInterceptor? _interceptor;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleLocalClient"/> class.
    /// </summary>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    /// <param name="packetSerializerProvider">The packet serializer provider.</param>
    /// <param name="hookManager">The hooking manager.</param>
    /// <param name="packetHandler">The packet handler.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="options">The options for the client.</param>
    /// <param name="provider">The dependency injection provider.</param>
    /// <param name="client">The nostale managed client.</param>
    public NostaleLocalClient
    (
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer,
        PacketSerializerProvider packetSerializerProvider,
        NostaleHookManager hookManager,
        IPacketHandler packetHandler,
        ILogger<NostaleLocalClient> logger,
        IOptions<LocalClientOptions> options,
        IServiceProvider provider,
        NosClient client
    )
        : base(commandProcessor, packetSerializer)
    {
        _options = options.Value;
        _packetSerializerProvider = packetSerializerProvider;
        _hookManager = hookManager;
        _packetHandler = packetHandler;
        _logger = logger;
        _provider = provider;
        _client = client;
    }

    /// <inheritdoc />
    public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
    {
        _stopRequested = stopRequested;
        _logger.LogInformation("Starting local client");
        NetworkCallback receiveCallback = ReceiveCallback;
        NetworkCallback sendCallback = SendCallback;

        if (_options.HookPacketReceive)
        {
            _client.GetNetwork().SetReceiveCallback(receiveCallback);
        }

        if (_options.HookPacketSend)
        {
            _client.GetNetwork().SetSendCallback(sendCallback);
        }

        if (_options.HookCharacterWalk)
        {
            _hookManager.HookCharacterWalk();
        }

        _logger.LogInformation("Packet methods hooked successfully");

        try
        {
            await Task.Delay(-1, stopRequested);
        }
        catch
        {
            // ignored
        }

        _client.ResetHooks();

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public override Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
    {
        ReceivePacket(packetString);
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public override Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
    {
        SendPacket(packetString);
        return Task.FromResult(Result.FromSuccess());
    }

    private bool ReceiveCallback(string packet)
    {
        bool accepted = true;
        if (_options.AllowIntercept)
        {
            if (_interceptor is null)
            {
                _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
            }

            accepted = _interceptor.InterceptReceive(ref packet);
        }

        Task.Run(async () => await ProcessPacketAsync(PacketType.Received, packet));

        return accepted;
    }

    private bool SendCallback(string packet)
    {
        bool accepted = true;
        if (_options.AllowIntercept)
        {
            if (_interceptor is null)
            {
                _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
            }

            accepted = _interceptor.InterceptSend(ref packet);
        }

        Task.Run(async () => await ProcessPacketAsync(PacketType.Sent, packet));

        return accepted;
    }

    private void SendPacket(string packetString)
    {
        _client.GetNetwork().SendPacket(packetString);
        _logger.LogDebug($"Sending client packet {packetString}");
    }

    private void ReceivePacket(string packetString)
    {
        _client.GetNetwork().ReceivePacket(packetString);
        _logger.LogDebug($"Receiving client packet {packetString}");
    }

    private async Task ProcessPacketAsync(PacketType type, string packetString)
    {
        IPacketSerializer serializer;
        if (type == PacketType.Received)
        {
            serializer = _packetSerializerProvider.ServerSerializer;
        }
        else
        {
            serializer = _packetSerializerProvider.ClientSerializer;
        }

        var packet = serializer.Deserialize(packetString);
        if (!packet.IsSuccess)
        {
            _logger.LogWarning("Could not parse {Packet}. Reason:", packetString);
            _logger.LogResultError(packet);
            packet = new ParsingFailedPacket(packet, packetString);
        }

        Result result;
        if (type == PacketType.Received)
        {
            result = await _packetHandler.HandleReceivedPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
        }
        else
        {
            result = await _packetHandler.HandleSentPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
        }

        if (!result.IsSuccess)
        {
            _logger.LogError("There was an error whilst handling packet");
            _logger.LogResultError(result);
        }
    }
}
\ No newline at end of file
//
//  NostaleLocalClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.LocalClient.Hooks;
using NosSmooth.Packets;
using NosSmooth.Packets.Attributes;
using NosSmoothCore;
using Remora.Results;

namespace NosSmooth.LocalClient;

/// <summary>
/// The local nostale client.
/// </summary>
/// <remarks>
/// Client used for living in the same process as NostaleClientX.exe.
/// It hooks the send and receive packet methods.
/// </remarks>
public class NostaleLocalClient : BaseNostaleClient
{
    private readonly IPacketSerializer _packetSerializer;
    private readonly NostaleHookManager _hookManager;
    private readonly IPacketHandler _packetHandler;
    private readonly ILogger _logger;
    private readonly IServiceProvider _provider;
    private readonly NosClient _client;
    private readonly LocalClientOptions _options;
    private CancellationToken? _stopRequested;
    private IPacketInterceptor? _interceptor;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleLocalClient"/> class.
    /// </summary>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    /// <param name="hookManager">The hooking manager.</param>
    /// <param name="packetHandler">The packet handler.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="options">The options for the client.</param>
    /// <param name="provider">The dependency injection provider.</param>
    /// <param name="client">The nostale managed client.</param>
    public NostaleLocalClient
    (
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer,
        NostaleHookManager hookManager,
        IPacketHandler packetHandler,
        ILogger<NostaleLocalClient> logger,
        IOptions<LocalClientOptions> options,
        IServiceProvider provider,
        NosClient client
    )
        : base(commandProcessor, packetSerializer)
    {
        _options = options.Value;
        _packetSerializer = packetSerializer;
        _hookManager = hookManager;
        _packetHandler = packetHandler;
        _logger = logger;
        _provider = provider;
        _client = client;
    }

    /// <inheritdoc />
    public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
    {
        _stopRequested = stopRequested;
        _logger.LogInformation("Starting local client");
        NetworkCallback receiveCallback = ReceiveCallback;
        NetworkCallback sendCallback = SendCallback;

        if (_options.HookPacketReceive)
        {
            _client.GetNetwork().SetReceiveCallback(receiveCallback);
        }

        if (_options.HookPacketSend)
        {
            _client.GetNetwork().SetSendCallback(sendCallback);
        }

        if (_options.HookCharacterWalk)
        {
            _hookManager.HookCharacterWalk();
        }

        _logger.LogInformation("Packet methods hooked successfully");

        try
        {
            await Task.Delay(-1, stopRequested);
        }
        catch
        {
            // ignored
        }

        _client.ResetHooks();

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public override Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
    {
        ReceivePacket(packetString);
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public override Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
    {
        SendPacket(packetString);
        return Task.FromResult(Result.FromSuccess());
    }

    private bool ReceiveCallback(string packet)
    {
        bool accepted = true;
        if (_options.AllowIntercept)
        {
            if (_interceptor is null)
            {
                _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
            }

            accepted = _interceptor.InterceptReceive(ref packet);
        }

        Task.Run(async () => await ProcessPacketAsync(PacketSource.Server, packet));

        return accepted;
    }

    private bool SendCallback(string packet)
    {
        bool accepted = true;
        if (_options.AllowIntercept)
        {
            if (_interceptor is null)
            {
                _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
            }

            accepted = _interceptor.InterceptSend(ref packet);
        }

        Task.Run(async () => await ProcessPacketAsync(PacketSource.Client, packet));

        return accepted;
    }

    private void SendPacket(string packetString)
    {
        _client.GetNetwork().SendPacket(packetString);
        _logger.LogDebug($"Sending client packet {packetString}");
    }

    private void ReceivePacket(string packetString)
    {
        _client.GetNetwork().ReceivePacket(packetString);
        _logger.LogDebug($"Receiving client packet {packetString}");
    }

    private async Task ProcessPacketAsync(PacketSource type, string packetString)
    {
        var packet = _packetSerializer.Deserialize(packetString, type);
        if (!packet.IsSuccess)
        {
            _logger.LogWarning("Could not parse {Packet}. Reason:", packetString);
            _logger.LogResultError(packet);
            packet = new ParsingFailedPacket(packet, packetString);
        }

        Result result;
        if (type == PacketSource.Server)
        {
            result = await _packetHandler.HandleReceivedPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
        }
        else
        {
            result = await _packetHandler.HandleSentPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
        }

        if (!result.IsSuccess)
        {
            _logger.LogError("There was an error whilst handling packet");
            _logger.LogResultError(result);
        }
    }
}

M NosSmooth.Unix.sln => NosSmooth.Unix.sln +0 -60
@@ 19,10 19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosSmooth.Extensions", "Cor
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{01B5E872-271F-4D30-A1AA-AD48D81840C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.Packets", "libs\NosCore.Packets\src\NosCore.Packets\NosCore.Packets.csproj", "{27DF38DF-AC58-4039-A91C-824D829ECECD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.Shared", "libs\NosCore.Shared\src\NosCore.Shared\NosCore.Shared.csproj", "{945E9248-C150-4617-AB0F-1450561859E3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".metadata", ".metadata", "{FA63BCED-9D81-4FF7-BA75-A6F3BA31ECDE}"
	ProjectSection(SolutionItems) = preProject
		Directory.Build.props = Directory.Build.props


@@ 32,10 28,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".metadata", ".metadata", "{
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosCore.Packets.Tests", "libs\NosCore.Packets\test\NosCore.Packets.Tests\NosCore.Packets.Tests.csproj", "{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Core.Tests", "Tests\NosSmooth.Core.Tests\NosSmooth.Core.Tests.csproj", "{1A10C624-48E5-425D-938E-31A4CC7AC687}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Packets", "Core\NosSmooth.Packets\NosSmooth.Packets.csproj", "{93C2E14D-2E3F-426C-9AEF-8D0E8CA21BEB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Packets.Tests", "Tests\NosSmooth.Packets.Tests\NosSmooth.Packets.Tests.csproj", "{CE7EE14A-BCC5-4A8B-8D9F-403BF09BD99A}"


@@ 112,54 104,6 @@ Global
		{B6C23EA5-55FE-436A-96EA-FE2974C5881D}.Release|x64.Build.0 = Release|Any CPU
		{B6C23EA5-55FE-436A-96EA-FE2974C5881D}.Release|x86.ActiveCfg = Release|Any CPU
		{B6C23EA5-55FE-436A-96EA-FE2974C5881D}.Release|x86.Build.0 = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x64.ActiveCfg = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x64.Build.0 = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x86.ActiveCfg = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x86.Build.0 = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|Any CPU.Build.0 = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x64.ActiveCfg = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x64.Build.0 = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x86.ActiveCfg = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x86.Build.0 = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x64.ActiveCfg = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x64.Build.0 = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x86.ActiveCfg = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x86.Build.0 = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|Any CPU.Build.0 = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x64.ActiveCfg = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x64.Build.0 = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x86.ActiveCfg = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x86.Build.0 = Release|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Debug|x64.ActiveCfg = Debug|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Debug|x64.Build.0 = Debug|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Debug|x86.ActiveCfg = Debug|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Debug|x86.Build.0 = Debug|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Release|Any CPU.Build.0 = Release|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Release|x64.ActiveCfg = Release|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Release|x64.Build.0 = Release|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Release|x86.ActiveCfg = Release|Any CPU
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0}.Release|x86.Build.0 = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Debug|x64.ActiveCfg = Debug|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Debug|x64.Build.0 = Debug|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Debug|x86.ActiveCfg = Debug|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Debug|x86.Build.0 = Debug|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|Any CPU.Build.0 = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x64.ActiveCfg = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x64.Build.0 = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x86.ActiveCfg = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x86.Build.0 = Release|Any CPU
		{93C2E14D-2E3F-426C-9AEF-8D0E8CA21BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{93C2E14D-2E3F-426C-9AEF-8D0E8CA21BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{93C2E14D-2E3F-426C-9AEF-8D0E8CA21BEB}.Debug|x64.ActiveCfg = Debug|Any CPU


@@ 205,10 149,6 @@ Global
		{19666500-4636-4400-8855-496317F4A7F7} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{05E3039D-EDF6-4CDC-B062-CB67760ACB5F} = {F9EFA63C-0A88-45EB-B36F-C4A9D63BDFD1}
		{06F03B1F-F68A-4C5B-B7FE-128A7CE1A1D7} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{27DF38DF-AC58-4039-A91C-824D829ECECD} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{945E9248-C150-4617-AB0F-1450561859E3} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{1A10C624-48E5-425D-938E-31A4CC7AC687} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{93C2E14D-2E3F-426C-9AEF-8D0E8CA21BEB} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{CE7EE14A-BCC5-4A8B-8D9F-403BF09BD99A} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{854A9D1D-5A7D-494E-B1D9-021A1C085F58} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}

M NosSmooth.sln => NosSmooth.sln +30 -30
@@ 25,10 25,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosSmooth.Extensions", "Cor
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{01B5E872-271F-4D30-A1AA-AD48D81840C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.Packets", "libs\NosCore.Packets\src\NosCore.Packets\NosCore.Packets.csproj", "{27DF38DF-AC58-4039-A91C-824D829ECECD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.Shared", "libs\NosCore.Shared\src\NosCore.Shared\NosCore.Shared.csproj", "{945E9248-C150-4617-AB0F-1450561859E3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".metadata", ".metadata", "{FA63BCED-9D81-4FF7-BA75-A6F3BA31ECDE}"
	ProjectSection(SolutionItems) = preProject
		Directory.build.props = Directory.build.props


@@ 48,6 44,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalkCommands", "Samples\Wal
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Core.Tests", "Tests\NosSmooth.Core.Tests\NosSmooth.Core.Tests.csproj", "{1A10C624-48E5-425D-938E-31A4CC7AC687}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Packets", "Core\NosSmooth.Packets\NosSmooth.Packets.csproj", "{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.PacketSerializersGenerator", "Core\NosSmooth.PacketSerializersGenerator\NosSmooth.PacketSerializersGenerator.csproj", "{C61EBDB6-053C-48C3-B896-58642639C93F}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU


@@ 140,30 140,6 @@ Global
		{B6C23EA5-55FE-436A-96EA-FE2974C5881D}.Release|x64.Build.0 = Release|Any CPU
		{B6C23EA5-55FE-436A-96EA-FE2974C5881D}.Release|x86.ActiveCfg = Release|Any CPU
		{B6C23EA5-55FE-436A-96EA-FE2974C5881D}.Release|x86.Build.0 = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x64.ActiveCfg = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x64.Build.0 = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x86.ActiveCfg = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Debug|x86.Build.0 = Debug|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|Any CPU.Build.0 = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x64.ActiveCfg = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x64.Build.0 = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x86.ActiveCfg = Release|Any CPU
		{27DF38DF-AC58-4039-A91C-824D829ECECD}.Release|x86.Build.0 = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x64.ActiveCfg = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x64.Build.0 = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x86.ActiveCfg = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Debug|x86.Build.0 = Debug|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|Any CPU.Build.0 = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x64.ActiveCfg = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x64.Build.0 = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x86.ActiveCfg = Release|Any CPU
		{945E9248-C150-4617-AB0F-1450561859E3}.Release|x86.Build.0 = Release|Any CPU
		{4017A4F4-5E59-48AA-A7D0-A8518148933A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{4017A4F4-5E59-48AA-A7D0-A8518148933A}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{4017A4F4-5E59-48AA-A7D0-A8518148933A}.Debug|x64.ActiveCfg = Debug|Any CPU


@@ 224,6 200,30 @@ Global
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x64.Build.0 = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x86.ActiveCfg = Release|Any CPU
		{1A10C624-48E5-425D-938E-31A4CC7AC687}.Release|x86.Build.0 = Release|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Debug|x64.ActiveCfg = Debug|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Debug|x64.Build.0 = Debug|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Debug|x86.ActiveCfg = Debug|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Debug|x86.Build.0 = Debug|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Release|Any CPU.Build.0 = Release|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Release|x64.ActiveCfg = Release|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Release|x64.Build.0 = Release|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Release|x86.ActiveCfg = Release|Any CPU
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C}.Release|x86.Build.0 = Release|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Debug|x64.ActiveCfg = Debug|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Debug|x64.Build.0 = Debug|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Debug|x86.ActiveCfg = Debug|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Debug|x86.Build.0 = Debug|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Release|Any CPU.Build.0 = Release|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Release|x64.ActiveCfg = Release|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Release|x64.Build.0 = Release|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Release|x86.ActiveCfg = Release|Any CPU
		{C61EBDB6-053C-48C3-B896-58642639C93F}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE


@@ 235,13 235,13 @@ Global
		{05E3039D-EDF6-4CDC-B062-CB67760ACB5F} = {F9EFA63C-0A88-45EB-B36F-C4A9D63BDFD1}
		{06F03B1F-F68A-4C5B-B7FE-128A7CE1A1D7} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{63E97FF3-7E40-44DE-9E91-F5DEE79AF95F} = {6078AE6E-7CD0-48E4-84E0-EB164D8881DA}
		{27DF38DF-AC58-4039-A91C-824D829ECECD} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{945E9248-C150-4617-AB0F-1450561859E3} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{9025731C-084E-4E82-8CD4-0F52D3AA1F54} = {F20FE754-FDEA-4F3A-93D4-0750CB9EBB33}
		{F96F3AA0-131E-4B6B-AB21-BBE2DEBCEF3A} = {9025731C-084E-4E82-8CD4-0F52D3AA1F54}
		{4017A4F4-5E59-48AA-A7D0-A8518148933A} = {9025731C-084E-4E82-8CD4-0F52D3AA1F54}
		{1A10C624-48E5-425D-938E-31A4CC7AC687} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{C61EBDB6-053C-48C3-B896-58642639C93F} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}

M Tests/NosSmooth.Core.Tests/Packets/InPacketSerializerTest.cs => Tests/NosSmooth.Core.Tests/Packets/InPacketSerializerTest.cs +1 -158
@@ 1,158 1,1 @@
//
//  InPacketSerializerTest.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NosCore.Packets.ServerPackets.Inventory;
using NosCore.Packets.ServerPackets.Visibility;
using NosCore.Shared.Enumerations;
using NosSmooth.Core.Packets;
using NosSmooth.Core.Packets.Converters;
using Xunit;

namespace NosSmooth.Core.Tests.Packets;

/// <summary>
/// Test class for <see cref="InPacketSerializerTest"/>.
/// </summary>
public class InPacketSerializerTest
{
    private readonly InPacketSerializer _inPacketSerializer;

    /// <summary>
    /// Initializes a new instance of the <see cref="InPacketSerializerTest"/> class.
    /// </summary>
    public InPacketSerializerTest()
    {
        var types = new List<Type>(new[]
        {
            typeof(InPacket),
            typeof(InAliveSubPacket),
            typeof(InNonPlayerSubPacket),
            typeof(InCharacterSubPacket),
            typeof(InItemSubPacket),
            typeof(InEquipmentSubPacket),
            typeof(UpgradeRareSubPacket),
            typeof(FamilySubPacket),
        });

        _inPacketSerializer = new ServiceCollection()
            .AddSingleton(
                p => new PacketSerializerProvider(types, types, p)
            )
            .AddSingleton<InPacketSerializer>()
            .BuildServiceProvider()
            .GetRequiredService<InPacketSerializer>();
    }

    /// <summary>
    /// Tests whether the serializer accepts to handle in packet.
    /// </summary>
    [Fact]
    public void AcceptsInPacket()
    {
        var shouldHandle = _inPacketSerializer.ShouldHandle(
            "in 1 dfrfgh - 1 79 2 6 2 1 0 106 2 -1.4480.4452.4468.4840.4132.-1.-1.-1.-1 100 100 0 -1 4 4 0 43 0 0 108 108 -1 - 26 0 0 0 0 99 0 0|0|0 0 0 10 80 0");
        Assert.True(shouldHandle);
    }

    /// <summary>
    /// Tests whether the serializer doesnt accept to handle non in packet.
    /// </summary>
    [Fact]
    public void DoesntAcceptNonInPacket()
    {
        var shouldHandle = _inPacketSerializer.ShouldHandle(
            "sr 5");
        Assert.False(shouldHandle);
    }

    /// <summary>
    /// Tests whether the result is successful when serializing player in packet.
    /// </summary>
    [Fact]
    public void SucceedsDeserializingPlayerIn()
    {
        var result = _inPacketSerializer.Deserialize(
            "in 1 dfrfgh - 1 79 2 6 2 1 0 106 2 -1.4480.4452.4468.4840.4132.-1.-1.-1.-1 50 98 0 -1 4 4 0 43 0 0 108 108 -1 - 26 0 0 0 0 99 0 0|0|0 0 0 10 80 0"
        );
        Assert.True(result.IsSuccess);
    }

    /// <summary>
    /// Tests whether the result is successful when serializing monster in packet.
    /// </summary>
    [Fact]
    public void SucceedsDeserializingMonsterIn()
    {
        var result = _inPacketSerializer.Deserialize(
            "in 2 334 1992 134 112 2 100 100 0 0 0 -1 1 0 -1 - 0 -1 0 0 0 0 0 0 0 0 0 0"
        );
        Assert.True(result.IsSuccess);
    }

    /// <summary>
    /// Tests whether the result of deserializing player is correct.
    /// </summary>
    [Fact]
    public void DeserializesPlayerInCorrectly()
    {
        var result = _inPacketSerializer.Deserialize(
            "in 1 dfrfgh - 55 79 2 6 2 1 0 106 2 -1.4480.4452.4468.4840.4132.-1.-1.-1.-1 50 95 0 -1 4 4 0 43 0 0 108 108 -1 - 26 0 0 0 0 99 0 0|0|0 0 0 10 80 0"
        ); // 55 is id, 50 hp, 95 mp

        Assert.True(result.IsSuccess);
        var inPacket = result.Entity;

        Assert.Equal(VisualType.Player, inPacket.VisualType);
        Assert.NotNull(inPacket.Name);
        Assert.Matches("dfrfgh", inPacket.Name);
        Assert.Equal(55, inPacket.VisualId);
        Assert.Equal(79, inPacket.PositionX);
        Assert.Equal(2, inPacket.PositionY);
        Assert.NotNull(inPacket.Direction);
        Assert.Equal(6, (byte)inPacket.Direction!);
        Assert.NotNull(inPacket.InCharacterSubPacket);
        var characterSubPacket = inPacket.InCharacterSubPacket!;
        Assert.Equal(AuthorityType.GameMaster, characterSubPacket.Authority);
        Assert.Equal(CharacterClassType.Archer, characterSubPacket.Class);
        Assert.NotNull(characterSubPacket.InAliveSubPacket);
        Assert.Equal(50, characterSubPacket.InAliveSubPacket!.Hp);
        Assert.Equal(95, characterSubPacket.InAliveSubPacket!.Mp);

        // TODO: check other things
    }

    /// <summary>
    /// Tests whether the result of deserializing monster is correct.
    /// </summary>
    [Fact]
    public void DeserializesMonsterInCorrectly()
    {
        var result = _inPacketSerializer.Deserialize(
            "in 2 334 1992 134 112 2 100 80 0 0 0 -1 1 0 -1 - 0 -1 0 0 0 0 0 0 0 0 0 0"
        );
        Assert.True(result.IsSuccess);
        var inPacket = result.Entity;
        Assert.Equal(VisualType.Npc, inPacket.VisualType);
        Assert.NotNull(inPacket.VNum);
        Assert.Equal(334, inPacket.VNum!.Value);
        Assert.Equal(1992, inPacket.VisualId);
        Assert.Equal(134, inPacket.PositionX);
        Assert.Equal(112, inPacket.PositionY);
        Assert.NotNull(inPacket.Direction);
        Assert.Equal(2, (byte)inPacket.Direction!);
        Assert.NotNull(inPacket.InNonPlayerSubPacket);
        var nonPlayerSubPacket = inPacket.InNonPlayerSubPacket!;
        Assert.NotNull(nonPlayerSubPacket.InAliveSubPacket);
        Assert.Equal(100, nonPlayerSubPacket.InAliveSubPacket!.Hp);
        Assert.Equal(80, nonPlayerSubPacket.InAliveSubPacket!.Mp);

        // TODO: check other things
    }
}
\ No newline at end of file

\ No newline at end of file

D libs/Directory.Build.props => libs/Directory.Build.props +0 -3
@@ 1,3 0,0 @@
<Project>
    <!-- Only here so that the default Directory.Build.props will not be used. -->
</Project>

D libs/NosCore.Packets => libs/NosCore.Packets +0 -1
@@ 1,1 0,0 @@
Subproject commit ccb432224367fa76403b794d4e5988b241ef19b6

D libs/NosCore.Shared => libs/NosCore.Shared +0 -1
@@ 1,1 0,0 @@
Subproject commit 05c3880af07154903c294a9cf715025b8649ad85

Do not follow this link