~ruther/NosSmooth

ba49ad7e1b0431be4a22b3209670e439050b2548 — František Boháček 3 years ago 32a1844 + ae8ae07
Merge pull request #14 from Rutherther/packets-span

Rewrite packets string enumerator to use spans, add Injector for .NET 5+
76 files changed, 1814 insertions(+), 457 deletions(-)

M Core/NosSmooth.Core/NosSmooth.Core.csproj
M Core/NosSmooth.Language/NosSmooth.Language.csproj
M Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BoolInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/EnumInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/FallbackInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/ListInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/StringInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs
M Core/NosSmooth.Packets/Converters/BaseStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/BoolStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/ByteStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/CharStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/IntStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/LongStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/ShortStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/StringTypeConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/UIntStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/ULongStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/UShortStringConverter.cs
M Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs
M Core/NosSmooth.Packets/Converters/IStringConverter.cs
M Core/NosSmooth.Packets/Converters/IStringSerializer.cs
M Core/NosSmooth.Packets/Converters/Packets/UpgradeRareSubPacketConverter.cs
M Core/NosSmooth.Packets/Converters/Special/Converters/EnumStringConverter.cs
M Core/NosSmooth.Packets/Converters/Special/Converters/ListStringConverter.cs
M Core/NosSmooth.Packets/Converters/Special/Converters/NullableStringConverter.cs
M Core/NosSmooth.Packets/Converters/Special/StringSerializer.cs
A Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs
M Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs
M Core/NosSmooth.Packets/NosSmooth.Packets.csproj
M Core/NosSmooth.Packets/PacketSerializer.cs
M Core/NosSmooth.Packets/PacketStringEnumerator.cs
M Core/NosSmooth.Packets/PacketToken.cs
A Local/NosSmooth.Inject/NosSmooth.Inject.vcxproj
A Local/NosSmooth.Inject/NosSmooth.Inject.vcxproj.filters
A Local/NosSmooth.Inject/coreclr_delegates.h
A Local/NosSmooth.Inject/dllmain.cpp
A Local/NosSmooth.Inject/framework.h
A Local/NosSmooth.Inject/hostfxr.h
A Local/NosSmooth.Inject/nethost.h
A Local/NosSmooth.Inject/nossmooth.cpp
A Local/NosSmooth.Inject/nossmooth.h
A Local/NosSmooth.Inject/pch.cpp
A Local/NosSmooth.Inject/pch.h
A Local/NosSmooth.Injector.CLI/Commands/InjectCommand.cs
A Local/NosSmooth.Injector.CLI/Commands/ListProcessesCommand.cs
A Local/NosSmooth.Injector.CLI/NosSmooth.Injector.CLI.csproj
A Local/NosSmooth.Injector.CLI/Program.cs
A Local/NosSmooth.Injector.CLI/app.manifest
A Local/NosSmooth.Injector/Errors/InjectionFailedError.cs
A Local/NosSmooth.Injector/Errors/InsufficientPermissionsError.cs
A Local/NosSmooth.Injector/Errors/ProcessNotFoundError.cs
A Local/NosSmooth.Injector/LoadParams.cs
A Local/NosSmooth.Injector/ManagedMemoryAllocation.cs
A Local/NosSmooth.Injector/NosInjector.cs
A Local/NosSmooth.Injector/NosInjectorOptions.cs
A Local/NosSmooth.Injector/NosSmooth.Injector.csproj
M Local/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj
M Local/NosSmooth.LocalClient/Utils/User32.cs
M NosSmooth.sln
M Samples/InterceptNameChanger/DllMain.cs
M Samples/InterceptNameChanger/InterceptNameChanger.csproj
M Samples/SimpleChat/DllMain.cs
M Samples/SimpleChat/SimpleChat.cs
M Samples/SimpleChat/SimpleChat.csproj
M Samples/WalkCommands/ChatPacketInterceptor.cs
M Samples/WalkCommands/DllMain.cs
M Samples/WalkCommands/Startup.cs
M Samples/WalkCommands/WalkCommands.csproj
M Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs
M Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs
M Core/NosSmooth.Core/NosSmooth.Core.csproj => Core/NosSmooth.Core/NosSmooth.Core.csproj +1 -1
@@ 3,7 3,7 @@
    <PropertyGroup>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>

M Core/NosSmooth.Language/NosSmooth.Language.csproj => Core/NosSmooth.Language/NosSmooth.Language.csproj +1 -1
@@ 4,7 4,7 @@
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
    </PropertyGroup>

</Project>

M Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs +1 -1
@@ 94,7 94,7 @@ public class ConverterDeserializationGenerator
        _textWriter.WriteLine($@"while ({_stringEnumeratorVariable}.IsOnLastToken() == false)");
        _textWriter.WriteLine("{");
        _textWriter.Indent++;
        _textWriter.WriteLine($"{_stringEnumeratorVariable}.GetNextToken();");
        _textWriter.WriteLine($"{_stringEnumeratorVariable}.GetNextToken(out _);");
        _textWriter.Indent--;
        _textWriter.WriteLine("}");
    }

M Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs +10 -0
@@ 38,6 38,16 @@ public static class ParameterInfoExtensions
    }

    /// <summary>
    /// Gets the name of the token variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The name of the token variable.</returns>
    public static string GetTokenVariableName(this ParameterInfo parameterInfo)
    {
        return $"{parameterInfo.Name}Token";
    }

    /// <summary>
    /// Gets the name of the error variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs +7 -7
@@ 57,7 57,7 @@ public class BasicInlineConverterGenerator : IInlineConverterGenerator
            throw new Exception("TypeSyntax or TypeSymbol has to be non null.");
        }

        textWriter.WriteLine($"{Constants.HelperClass}.ParseBasic{type}(typeConverter, stringEnumerator);");
        textWriter.WriteLine($"{Constants.HelperClass}.ParseBasic{type}(typeConverter, ref stringEnumerator);");
        return null;
    }



@@ 67,23 67,23 @@ public class BasicInlineConverterGenerator : IInlineConverterGenerator
        foreach (var type in HandleTypes)
        {
            textWriter.WriteMultiline($@"
public static Result<{type}?> ParseBasic{type}(IStringConverter typeConverter, PacketStringEnumerator stringEnumerator)
public static Result<{type}?> ParseBasic{type}(IStringConverter typeConverter, ref PacketStringEnumerator stringEnumerator)
{{
    var tokenResult = stringEnumerator.GetNextToken();
    var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
    if (!tokenResult.IsSuccess)
    {{
        return Result<{type}?>.FromError(tokenResult);
    }}

    var token = tokenResult.Entity.Token;
    if (token == ""-"")
    var token = packetToken.Token;
    if (token[0] == '-' && token.Length == 1)
    {{
        return Result<{type}?>.FromSuccess(null);
    }}

    if (!{type}.TryParse(token, out var val))
    {{
        return new CouldNotConvertError(typeConverter, token, ""Could not convert as {type} in inline converter"");
        return new CouldNotConvertError(typeConverter, token.ToString(), ""Could not convert as {type} in inline converter"");
    }}

    return val;


@@ 91,4 91,4 @@ public static Result<{type}?> ParseBasic{type}(IStringConverter typeConverter, P
");
        }
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BoolInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BoolInlineConverterGenerator.cs +7 -7
@@ 44,7 44,7 @@ public class BoolInlineConverterGenerator : IInlineConverterGenerator
    /// <inheritdoc />
    public IError? CallDeserialize(IndentedTextWriter textWriter, TypeSyntax? typeSyntax, ITypeSymbol? typeSymbol)
    {
        textWriter.WriteLine($"{Constants.HelperClass}.ParseBool(stringEnumerator);");
        textWriter.WriteLine($"{Constants.HelperClass}.ParseBool(ref stringEnumerator);");
        return null;
    }



@@ 52,22 52,22 @@ public class BoolInlineConverterGenerator : IInlineConverterGenerator
    public void GenerateHelperMethods(IndentedTextWriter textWriter)
    {
        textWriter.WriteLine(@"
public static Result<bool?> ParseBool(PacketStringEnumerator stringEnumerator)
public static Result<bool?> ParseBool(ref PacketStringEnumerator stringEnumerator)
{{
    var tokenResult = stringEnumerator.GetNextToken();
    var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
    if (!tokenResult.IsSuccess)
    {{
        return Result<bool?>.FromError(tokenResult);
    }}

    var token = tokenResult.Entity.Token;
    if (token == ""-"")
    var token = packetToken.Token;
    if (token[0] == '-')
    {{
        return Result<bool?>.FromSuccess(null);
    }}

    return token == ""1"" ? true : false;
    return token[0] == '1' ? true : false;
}}
");
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/EnumInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/EnumInlineConverterGenerator.cs +7 -7
@@ 66,7 66,7 @@ public class EnumInlineConverterGenerator : IInlineConverterGenerator

        textWriter.WriteLine
        (
            $"{Constants.HelperClass}.ParseEnum{typeSymbol?.ToString().TrimEnd('?').Replace('.', '_')}(typeConverter, stringEnumerator);"
            $"{Constants.HelperClass}.ParseEnum{typeSymbol?.ToString().TrimEnd('?').Replace('.', '_')}(typeConverter, ref stringEnumerator);"
        );
        return null;
    }


@@ 80,23 80,23 @@ public class EnumInlineConverterGenerator : IInlineConverterGenerator
            textWriter.WriteMultiline
            (
                $@"
public static Result<{type}?> ParseEnum{type.ToString().Replace('.', '_')}(IStringConverter typeConverter, PacketStringEnumerator stringEnumerator)
public static Result<{type}?> ParseEnum{type.ToString().Replace('.', '_')}(IStringConverter typeConverter, ref PacketStringEnumerator stringEnumerator)
{{
    var tokenResult = stringEnumerator.GetNextToken();
    var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
    if (!tokenResult.IsSuccess)
    {{
        return Result<{type}?>.FromError(tokenResult);
    }}

    var token = tokenResult.Entity.Token;
    if (token == ""-"")
    var token = packetToken.Token;
    if (token[0] == '-')
    {{
        return Result<{type}?>.FromSuccess(null);
    }}

    if (!{underlyingType}.TryParse(token, out var val))
    {{
        return new CouldNotConvertError(typeConverter, token, ""Could not convert as {type} in inline converter"");
        return new CouldNotConvertError(typeConverter, token.ToString(), ""Could not convert as {type} in inline converter"");
    }}

    return ({type}?)val;


@@ 105,4 105,4 @@ public static Result<{type}?> ParseEnum{type.ToString().Replace('.', '_')}(IStri
            );
        }
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/FallbackInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/FallbackInlineConverterGenerator.cs +2 -2
@@ 48,7 48,7 @@ public class FallbackInlineConverterGenerator : IInlineConverterGenerator
    {
        textWriter.WriteLine
        (
            $"_stringSerializer.Deserialize<{(typeSyntax?.ToString() ?? typeSymbol!.ToString()).TrimEnd('?')}?>(stringEnumerator);"
            $"_stringSerializer.Deserialize<{(typeSyntax?.ToString() ?? typeSymbol!.ToString()).TrimEnd('?')}?>(ref stringEnumerator);"
        );
        return null;
    }


@@ 58,4 58,4 @@ public class FallbackInlineConverterGenerator : IInlineConverterGenerator
    {
        // ignore
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/ListInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/ListInlineConverterGenerator.cs +3 -3
@@ 89,7 89,7 @@ public class ListInlineConverterGenerator : IInlineConverterGenerator
        }

        textWriter.WriteLine
            ($"{Constants.HelperClass}.{GetMethodName(genericArgument)}(typeConverter, _stringSerializer, stringEnumerator);");
            ($"{Constants.HelperClass}.{GetMethodName(genericArgument)}(typeConverter, _stringSerializer, ref stringEnumerator);");
        return null;
    }



@@ 107,7 107,7 @@ public class ListInlineConverterGenerator : IInlineConverterGenerator
            textWriter.WriteLine
            (
                @$"
public static Result<IReadOnlyList<{type.GetActualType()}>> {GetMethodName(type)}(IStringConverter typeConverter, IStringSerializer _stringSerializer, PacketStringEnumerator stringEnumerator)
public static Result<IReadOnlyList<{type.GetActualType()}>> {GetMethodName(type)}(IStringConverter typeConverter, IStringSerializer _stringSerializer, ref PacketStringEnumerator stringEnumerator)
{{
    var data = new List<{type.GetActualType()}>();



@@ 129,7 129,7 @@ public static Result<IReadOnlyList<{type.GetActualType()}>> {GetMethodName(type)
        // or the packet has more fields.
        while (stringEnumerator.IsOnLastToken() == false)
        {{
            stringEnumerator.GetNextToken();
            stringEnumerator.GetNextToken(out _);
        }}

        stringEnumerator.PopLevel();

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/StringInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/StringInlineConverterGenerator.cs +10 -5
@@ 30,7 30,7 @@ public class StringInlineConverterGenerator : IInlineConverterGenerator
    /// <inheritdoc />
    public IError? CallDeserialize(IndentedTextWriter textWriter, TypeSyntax? typeSyntax, ITypeSymbol? typeSymbol)
    {
        textWriter.WriteLine($"{Constants.HelperClass}.ParseString(stringEnumerator);");
        textWriter.WriteLine($"{Constants.HelperClass}.ParseString(ref stringEnumerator);");
        return null;
    }



@@ 40,17 40,22 @@ public class StringInlineConverterGenerator : IInlineConverterGenerator
        textWriter.WriteLine
        (
            @"
public static Result<string?> ParseString(PacketStringEnumerator stringEnumerator)
public static Result<string?> ParseString(ref PacketStringEnumerator stringEnumerator)
{{
    var tokenResult = stringEnumerator.GetNextToken();
    var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
    if (!tokenResult.IsSuccess)
    {{
        return Result<string?>.FromError(tokenResult);
    }}

    return tokenResult.Entity.Token;
    if (packetToken.Token[0] == '-')
    {{
        return Result<string?>.FromSuccess(null);
    }}

    return packetToken.Token.ToString();
}}
"
        );
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs +3 -3
@@ 95,7 95,7 @@ public override Result Serialize({_packetInfo.Name}? obj, PacketStringBuilder bu
}}

/// <inheritdoc />
public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator stringEnumerator)
public override Result<{_packetInfo.Name}?> Deserialize(ref PacketStringEnumerator stringEnumerator)
{{
    var typeConverter = this;
"


@@ 176,7 176,7 @@ public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator s
                    textWriter.WriteLine("IResultError? skipError;");
                    skipped = true;
                }
                textWriter.WriteLine($@"skipResult = stringEnumerator.GetNextToken();");
                textWriter.WriteLine($@"skipResult = stringEnumerator.GetNextToken(out _);");
                textWriter.WriteLine
                    ("skipError = CheckDeserializationResult(result, \"None\", stringEnumerator, false);");
                textWriter.WriteMultiline


@@ 246,4 246,4 @@ public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator s

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

M Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +5 -0
@@ 130,6 130,11 @@ public class SourceGenerator : ISourceGenerator
                    $"{packetRecord.GetPrefix()}.{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs",
                    stringWriter.GetStringBuilder().ToString()
                );
                File.WriteAllText
                (
                    $"/tmp/{packetRecord.GetPrefix()}.{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs",
                    stringWriter.GetStringBuilder().ToString()
                );
            }
        }


M Core/NosSmooth.Packets/Converters/BaseStringConverter.cs => Core/NosSmooth.Packets/Converters/BaseStringConverter.cs +4 -4
@@ 19,12 19,12 @@ public abstract class BaseStringConverter<TParseType> : IStringConverter<TParseT
    public abstract Result Serialize(TParseType? obj, PacketStringBuilder builder);

    /// <inheritdoc />
    public abstract Result<TParseType?> Deserialize(PacketStringEnumerator stringEnumerator);
    public abstract Result<TParseType?> Deserialize(ref PacketStringEnumerator stringEnumerator);

    /// <inheritdoc/>
    Result<object?> IStringConverter.Deserialize(PacketStringEnumerator stringEnumerator)
    Result<object?> IStringConverter.Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var result = Deserialize(stringEnumerator);
        var result = Deserialize(ref stringEnumerator);
        if (!result.IsSuccess)
        {
            return Result<object?>.FromError(result);


@@ 43,4 43,4 @@ public abstract class BaseStringConverter<TParseType> : IStringConverter<TParseT

        return Serialize(parseType, builder);
    }
}
\ No newline at end of file
}

M Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs +5 -5
@@ 23,20 23,20 @@ public abstract class BasicTypeConverter<TBasicType> : BaseStringConverter<TBasi
    }

    /// <inheritdoc />
    public override Result<TBasicType?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<TBasicType?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var nextTokenResult = stringEnumerator.GetNextToken();
        var nextTokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!nextTokenResult.IsSuccess)
        {
            return Result<TBasicType?>.FromError(nextTokenResult);
        }

        if (nextTokenResult.Entity.Token == "-")
        if (packetToken.Token[0] == '-' && packetToken.Token.Length == 1)
        {
            return Result<TBasicType?>.FromSuccess(default);
        }

        return Deserialize(nextTokenResult.Entity.Token);
        return Deserialize(packetToken.Token);
    }

    /// <summary>


@@ 44,5 44,5 @@ public abstract class BasicTypeConverter<TBasicType> : BaseStringConverter<TBasi
    /// </summary>
    /// <param name="value">The value to deserialize.</param>
    /// <returns>The deserialized value or an error.</returns>
    protected abstract Result<TBasicType?> Deserialize(string value);
    protected abstract Result<TBasicType?> Deserialize(ReadOnlySpan<char> value);
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/Basic/BoolStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/BoolStringConverter.cs +3 -8
@@ 22,19 22,14 @@ public class BoolStringConverter : BaseStringConverter<bool>
    }

    /// <inheritdoc />
    public override Result<bool> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<bool> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var nextTokenResult = stringEnumerator.GetNextToken();
        var nextTokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!nextTokenResult.IsSuccess)
        {
            return Result<bool>.FromError(nextTokenResult);
        }

        if (nextTokenResult.Entity.Token == "-")
        {
            return Result<bool>.FromSuccess(default);
        }

        return nextTokenResult.Entity.Token == "1" ? true : false;
        return packetToken.Token[0] == '1' ? true : false;
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/Basic/ByteStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ByteStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class ByteStringConverter : BasicTypeConverter<byte>
{
    /// <inheritdoc />
    protected override Result<byte> Deserialize(string value)
    protected override Result<byte> Deserialize(ReadOnlySpan<char> value)
    {
        if (!byte.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an byte.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as an byte.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/CharStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/CharStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class CharStringConverter : BasicTypeConverter<char>
{
    /// <inheritdoc />
    protected override Result<char> Deserialize(string value)
    protected override Result<char> Deserialize(ReadOnlySpan<char> value)
    {
        if (value.Length != 1)
        {
            return new CouldNotConvertError(this, value, "The token is not one character long.");
            return new CouldNotConvertError(this, value.ToString(), "The token is not one character long.");
        }

        return value[0];

M Core/NosSmooth.Packets/Converters/Basic/IntStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/IntStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class IntStringConverter : BasicTypeConverter<int>
{
    /// <inheritdoc />
    protected override Result<int> Deserialize(string value)
    protected override Result<int> Deserialize(ReadOnlySpan<char> value)
    {
        if (!int.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as int.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as int.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/LongStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/LongStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class LongStringConverter : BasicTypeConverter<long>
{
    /// <inheritdoc />
    protected override Result<long> Deserialize(string value)
    protected override Result<long> Deserialize(ReadOnlySpan<char> value)
    {
        if (!long.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as a long.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as a long.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/ShortStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ShortStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class ShortStringConverter : BasicTypeConverter<short>
{
    /// <inheritdoc />
    protected override Result<short> Deserialize(string value)
    protected override Result<short> Deserialize(ReadOnlySpan<char> value)
    {
        if (!short.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as short.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as short.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/StringTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/StringTypeConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;


@@ 14,8 15,8 @@ namespace NosSmooth.Packets.Converters.Basic;
public class StringTypeConverter : BasicTypeConverter<string>
{
    /// <inheritdoc />
    protected override Result<string?> Deserialize(string value)
    protected override Result<string?> Deserialize(ReadOnlySpan<char> value)
    {
        return value;
        return value.ToString();
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/Basic/UIntStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/UIntStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class UIntStringConverter : BasicTypeConverter<uint>
{
    /// <inheritdoc />
    protected override Result<uint> Deserialize(string value)
    protected override Result<uint> Deserialize(ReadOnlySpan<char> value)
    {
        if (!uint.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as uint");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as uint");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/ULongStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ULongStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class ULongStringConverter : BasicTypeConverter<ulong>
{
    /// <inheritdoc />
    protected override Result<ulong> Deserialize(string value)
    protected override Result<ulong> Deserialize(ReadOnlySpan<char> value)
    {
        if (!ulong.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an ulong.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as an ulong.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/UShortStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/UShortStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  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 NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class UShortStringConverter : BasicTypeConverter<ushort>
{
    /// <inheritdoc />
    protected override Result<ushort> Deserialize(string value)
    protected override Result<ushort> Deserialize(ReadOnlySpan<char> value)
    {
        if (!ushort.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an ushort.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as an ushort.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs => Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs +4 -4
@@ 29,19 29,19 @@ public class NameStringConverter : BaseStringConverter<NameString>
    }

    /// <inheritdoc />
    public override Result<NameString?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<NameString?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!tokenResult.IsSuccess)
        {
            return Result<NameString?>.FromError(tokenResult);
        }

        if (tokenResult.Entity.Token == "-")
        if (packetToken.Token[0] == '-' && packetToken.Token.Length == 1)
        {
            return Result<NameString?>.FromSuccess(null);
        }

        return NameString.FromPacket(tokenResult.Entity.Token);
        return NameString.FromPacket(packetToken.Token.ToString());
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/IStringConverter.cs => Core/NosSmooth.Packets/Converters/IStringConverter.cs +2 -2
@@ 18,7 18,7 @@ public interface IStringConverter
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<object?> Deserialize(PacketStringEnumerator stringEnumerator);
    public Result<object?> Deserialize(ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.


@@ 43,7 43,7 @@ public interface IStringConverter<TParseType> : IStringConverter
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public new Result<TParseType?> Deserialize(PacketStringEnumerator stringEnumerator);
    public new Result<TParseType?> Deserialize(ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.

M Core/NosSmooth.Packets/Converters/IStringSerializer.cs => Core/NosSmooth.Packets/Converters/IStringSerializer.cs +2 -2
@@ 20,7 20,7 @@ public interface IStringSerializer
    /// <param name="parseType">The type of the object to serialize.</param>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<object?> Deserialize(Type parseType, PacketStringEnumerator stringEnumerator);
    public Result<object?> Deserialize(Type parseType, ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.


@@ 37,7 37,7 @@ public interface IStringSerializer
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <typeparam name="TParseType">The type of the object to serialize.</typeparam>
    /// <returns>The parsed object or an error.</returns>
    public Result<TParseType?> Deserialize<TParseType>(PacketStringEnumerator stringEnumerator);
    public Result<TParseType?> Deserialize<TParseType>(ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.

M Core/NosSmooth.Packets/Converters/Packets/UpgradeRareSubPacketConverter.cs => Core/NosSmooth.Packets/Converters/Packets/UpgradeRareSubPacketConverter.cs +8 -8
@@ 28,31 28,31 @@ public class UpgradeRareSubPacketConverter : BaseStringConverter<UpgradeRareSubP
    }

    /// <inheritdoc />
    public override Result<UpgradeRareSubPacket?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<UpgradeRareSubPacket?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!tokenResult.IsSuccess)
        {
            return Result<UpgradeRareSubPacket?>.FromError(tokenResult);
        }
        var token = packetToken.Token;

        var token = tokenResult.Entity.Token;
        if (token.Length > 3)
        {
            return new CouldNotConvertError(this, token, "The string is not two/three characters long.");
            return new CouldNotConvertError(this, token.ToString(), "The string is not two/three characters long.");
        }

        var upgradeString = token.Substring(0, token.Length - 1);
        var rareString = token[token.Length - 1].ToString();
        var upgradeString = token.Slice(0, token.Length - 1);
        var rareString = token.Slice(token.Length - 1);

        if (!byte.TryParse(upgradeString, out var upgrade))
        {
            return new CouldNotConvertError(this, upgradeString, "Could not parse as byte");
            return new CouldNotConvertError(this, upgradeString.ToString(), "Could not parse as byte");
        }

        if (!sbyte.TryParse(rareString, out var rare))
        {
            return new CouldNotConvertError(this, rareString, "Could not parse as byte");
            return new CouldNotConvertError(this, rareString.ToString(), "Could not parse as byte");
        }

        return new UpgradeRareSubPacket(upgrade, rare);

M Core/NosSmooth.Packets/Converters/Special/Converters/EnumStringConverter.cs => Core/NosSmooth.Packets/Converters/Special/Converters/EnumStringConverter.cs +2 -2
@@ 35,9 35,9 @@ public class EnumStringConverter<TEnum, TUnderlyingType> : BaseStringConverter<T
    }

    /// <inheritdoc />
    public override Result<TEnum?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<TEnum?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var result = _serializer.Deserialize<TUnderlyingType>(stringEnumerator);
        var result = _serializer.Deserialize<TUnderlyingType>(ref stringEnumerator);
        if (!result.IsSuccess)
        {
            return Result<TEnum?>.FromError(result);

M Core/NosSmooth.Packets/Converters/Special/Converters/ListStringConverter.cs => Core/NosSmooth.Packets/Converters/Special/Converters/ListStringConverter.cs +3 -3
@@ 58,7 58,7 @@ public class ListStringConverter<TGeneric> : BaseStringConverter<IReadOnlyList<T
    }

    /// <inheritdoc />
    public override Result<IReadOnlyList<TGeneric>?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<IReadOnlyList<TGeneric>?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var list = new List<TGeneric>();



@@ 69,14 69,14 @@ public class ListStringConverter<TGeneric> : BaseStringConverter<IReadOnlyList<T
                return new ArgumentInvalidError(nameof(stringEnumerator), "The string enumerator has to have a prepared level for all lists.");
            }

            var result = _serializer.Deserialize<TGeneric>(stringEnumerator);
            var result = _serializer.Deserialize<TGeneric>(ref stringEnumerator);

            // If we know that we are not on the last token in the item level, just skip to the end of the item.
            // Note that if this is the case, then that means the converter is either corrupted
            // or the packet has more fields.
            while (stringEnumerator.IsOnLastToken() == false)
            {
                stringEnumerator.GetNextToken();
                stringEnumerator.GetNextToken(out _);
            }

            stringEnumerator.PopLevel();

M Core/NosSmooth.Packets/Converters/Special/Converters/NullableStringConverter.cs => Core/NosSmooth.Packets/Converters/Special/Converters/NullableStringConverter.cs +2 -2
@@ 42,9 42,9 @@ public class NullableStringConverter<T> : BaseStringConverter<Nullable<T>>
    }

    /// <inheritdoc />
    public override Result<T?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<T?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var result = _stringSerializer.Deserialize<T>(stringEnumerator);
        var result = _stringSerializer.Deserialize<T>(ref stringEnumerator);
        if (!result.IsSuccess)
        {
            return Result<T?>.FromError(result);

M Core/NosSmooth.Packets/Converters/Special/StringSerializer.cs => Core/NosSmooth.Packets/Converters/Special/StringSerializer.cs +4 -4
@@ 25,7 25,7 @@ public class StringSerializer : IStringSerializer
    }

    /// <inheritdoc />
    public Result<object?> Deserialize(Type parseType, PacketStringEnumerator stringEnumerator)
    public Result<object?> Deserialize(Type parseType, ref PacketStringEnumerator stringEnumerator)
    {
        var converterResult = _converterRepository.GetTypeConverter(parseType);
        if (!converterResult.IsSuccess)


@@ 33,7 33,7 @@ public class StringSerializer : IStringSerializer
            return Result<object?>.FromError(converterResult);
        }

        var deserializedResult = converterResult.Entity.Deserialize(stringEnumerator);
        var deserializedResult = converterResult.Entity.Deserialize(ref stringEnumerator);
        if (!deserializedResult.IsSuccess)
        {
            return Result<object?>.FromError(deserializedResult);


@@ 55,7 55,7 @@ public class StringSerializer : IStringSerializer
    }

    /// <inheritdoc />
    public Result<TParseType?> Deserialize<TParseType>(PacketStringEnumerator stringEnumerator)
    public Result<TParseType?> Deserialize<TParseType>(ref PacketStringEnumerator stringEnumerator)
    {
        var converterResult = _converterRepository.GetTypeConverter<TParseType>();
        if (!converterResult.IsSuccess)


@@ 63,7 63,7 @@ public class StringSerializer : IStringSerializer
            return Result<TParseType?>.FromError(converterResult);
        }

        return converterResult.Entity.Deserialize(stringEnumerator);
        return converterResult.Entity.Deserialize(ref stringEnumerator);
    }

    /// <inheritdoc />

A Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs => Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs +0 -0
M Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs => Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs +1 -0
@@ 5,6 5,7 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Buffers;
using NosSmooth.Packets.Converters;
using Remora.Results;


M Core/NosSmooth.Packets/NosSmooth.Packets.csproj => Core/NosSmooth.Packets/NosSmooth.Packets.csproj +1 -1
@@ 2,9 2,9 @@

    <PropertyGroup>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
        <AssemblyName>NosSmooth.Packets</AssemblyName>
        <RootNamespace>NosSmooth.Packets</RootNamespace>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>

M Core/NosSmooth.Packets/PacketSerializer.cs => Core/NosSmooth.Packets/PacketSerializer.cs +3 -3
@@ 59,20 59,20 @@ public class PacketSerializer : IPacketSerializer
    public Result<IPacket> Deserialize(string packetString, PacketSource preferredSource)
    {
        var packetStringEnumerator = new PacketStringEnumerator(packetString);
        var headerTokenResult = packetStringEnumerator.GetNextToken();
        var headerTokenResult = packetStringEnumerator.GetNextToken(out var packetToken);
        if (!headerTokenResult.IsSuccess)
        {
            return Result<IPacket>.FromError(headerTokenResult);
        }

        var packetInfoResult = _packetTypesRepository.FindPacketInfo(headerTokenResult.Entity.Token, preferredSource);
        var packetInfoResult = _packetTypesRepository.FindPacketInfo(packetToken.Token.ToString(), preferredSource);
        if (!packetInfoResult.IsSuccess)
        {
            return Result<IPacket>.FromError(packetInfoResult);
        }

        var packetInfo = packetInfoResult.Entity;
        var deserializedResult = packetInfo.PacketConverter.Deserialize(packetStringEnumerator);
        var deserializedResult = packetInfo.PacketConverter.Deserialize(ref packetStringEnumerator);
        if (!deserializedResult.IsSuccess)
        {
            return Result<IPacket>.FromError(deserializedResult);

M Core/NosSmooth.Packets/PacketStringEnumerator.cs => Core/NosSmooth.Packets/PacketStringEnumerator.cs +47 -91
@@ 4,6 4,7 @@
//  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.Text;
using NosSmooth.Packets.Errors;


@@ 14,15 15,18 @@ namespace NosSmooth.Packets;
/// <summary>
/// Enumerator for packet strings.
/// </summary>
public struct PacketStringEnumerator
public ref struct PacketStringEnumerator
{
    private readonly EnumeratorData _data;
    private readonly ReadOnlySpan<char> _data;
    private readonly Dictionary<char, ushort> _numberOfSeparators;
    private EnumeratorLevel _currentLevel;
    private (char Separator, uint? MaxTokens)? _preparedLevel;
    private PacketToken? _currentToken;
    private bool _currentTokenRead;
    private PacketToken _currentToken;
    private bool _readToLast;

    private int _cursor;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringEnumerator"/> struct.
    /// </summary>


@@ 31,30 35,14 @@ public struct PacketStringEnumerator
    public PacketStringEnumerator(string data, char separator = ' ')
    {
        _currentLevel = new EnumeratorLevel(null, separator);
        _data = new EnumeratorData(data);
        _data = new ReadOnlySpan<char>(data.ToCharArray());
        _cursor = 0;
        _numberOfSeparators = new Dictionary<char, ushort>();
        _numberOfSeparators.Add(separator, 1);
        _currentToken = null;
        _preparedLevel = null;
        _readToLast = false;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringEnumerator"/> struct.
    /// </summary>
    /// <param name="data">The data of the enumerator.</param>
    /// <param name="level">The current enumerator level.</param>
    /// <param name="numberOfSeparators">The number of separators.</param>
    private PacketStringEnumerator(EnumeratorData data, EnumeratorLevel level, Dictionary<char, ushort> numberOfSeparators)
    {
        _currentLevel = level;
        _data = data;

        // TODO: use something less heavy than copying everything from the dictionary.
        _numberOfSeparators = new Dictionary<char, ushort>(numberOfSeparators);
        _currentToken = null;
        _currentToken = new PacketToken(default, default, default, default);
        _preparedLevel = null;
        _readToLast = false;
        _currentTokenRead = false;
    }

    /// <summary>


@@ 66,7 54,7 @@ public struct PacketStringEnumerator
    /// <param name="separator">The separator to look for.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _currentToken = null;
        _currentTokenRead = false;
        _currentLevel.SeparatorOnce = separator;
    }



@@ 97,19 85,6 @@ public struct PacketStringEnumerator
    }

    /// <summary>
    /// Create next level with the separator given in the prepared level.
    /// </summary>
    /// <remarks>
    /// Level of the current enumerator will stay the same.
    /// Will return null, if there is not a level prepared.
    /// </remarks>
    /// <returns>An enumerator with the new level pushed.</returns>
    public PacketStringEnumerator? CreatePreparedLevel()
    {
        return _preparedLevel is not null ? CreateLevel(_preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens) : null;
    }

    /// <summary>
    /// Push next level with the separator given in the prepared level.
    /// </summary>
    /// <returns>Whether there is a prepared level present.</returns>


@@ 120,7 95,7 @@ public struct PacketStringEnumerator
            return false;
        }

        _currentToken = null;
        _currentTokenRead = false;
        _currentLevel = new EnumeratorLevel(_currentLevel, _preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd


@@ 136,25 111,6 @@ public struct PacketStringEnumerator
    }

    /// <summary>
    /// Create next level with the given separator and maximum number of tokens.
    /// </summary>
    /// <remarks>
    /// Level of the current enumerator will stay the same.
    /// The maximum number of tokens indicates how many tokens can be read ie. in lists,
    /// the enumerator won't allow reading more than that many tokens, error will be thrown if the user tries to read more.
    /// </remarks>
    /// <param name="separator">The separator of the new level.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    /// <returns>An enumerator with the new level pushed.</returns>
    public PacketStringEnumerator CreateLevel(char separator, uint? maxTokens = default)
    {
        _currentToken = null;
        var stringEnumerator = new PacketStringEnumerator(_data, _currentLevel, _numberOfSeparators);
        stringEnumerator.PushLevel(separator, maxTokens);
        return stringEnumerator;
    }

    /// <summary>
    /// Push new separator level to the stack.
    /// </summary>
    /// <remarks>


@@ 166,7 122,7 @@ public struct PacketStringEnumerator
    public void PushLevel(char separator, uint? maxTokens = default)
    {
        _preparedLevel = null;
        _currentToken = null;
        _currentTokenRead = false;
        _currentLevel = new EnumeratorLevel(_currentLevel, separator, maxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd


@@ 197,36 153,50 @@ public struct PacketStringEnumerator
    }

    /// <summary>
    /// Skip the given amount of characters.
    /// </summary>
    /// <param name="count">The count of characters to skip.</param>
    public void Skip(int count)
    {
        _cursor += count;
    }

    /// <summary>
    /// Get the next token.
    /// </summary>
    /// <param name="packetToken">The resulting token.</param>
    /// <param name="seek">Whether to seek the cursor to the end of the token.</param>
    /// <returns>The found token.</returns>
    public Result<PacketToken> GetNextToken(bool seek = true)
    public Result GetNextToken(out PacketToken packetToken, bool seek = true)
    {
        // The token is cached if seek was false to speed things up.
        if (_currentToken != null)
        if (_currentTokenRead)
        {
            var cachedToken = _currentToken.Value;
            var cachedToken = _currentToken;
            if (seek)
            {
                UpdateCurrentAndParentLevels(cachedToken);
                _currentLevel.TokensRead++;
                _currentToken = null;
                _data.Cursor += cachedToken.Token.Length + 1;
                _currentTokenRead = false;
                _cursor += cachedToken.Token.Length + 1;
                _currentLevel.SeparatorOnce = null;
            }

            return cachedToken;
            packetToken = new PacketToken(default, default, default, default);
            packetToken = cachedToken;
            return Result.FromSuccess();
        }

        if (_data.ReachedEnd || (_currentLevel.ReachedEnd ?? false))
        if ((_cursor >= _data.Length) || (_currentLevel.ReachedEnd ?? false))
        {
            return new PacketEndReachedError(_data.Data, _currentLevel.ReachedEnd ?? false);
            packetToken = new PacketToken(default, default, default, default);
            return new PacketEndReachedError(_data.ToString(), _currentLevel.ReachedEnd ?? false);
        }

        var currentIndex = _data.Cursor;
        char currentCharacter = _data.Data[currentIndex];
        StringBuilder tokenString = new StringBuilder();
        var currentIndex = _cursor;
        var length = 0;
        var startIndex = currentIndex;
        char currentCharacter = _data[currentIndex];

        bool? isLast, encounteredUpperLevel;



@@ 234,27 204,27 @@ public struct PacketStringEnumerator
        // If should read to last, then read until isLast is null or true.
        while (!IsSeparator(currentCharacter, out isLast, out encounteredUpperLevel) || (_readToLast && !(isLast ?? true)))
        {
            tokenString.Append(currentCharacter);
            length++;
            currentIndex++;

            if (currentIndex == _data.Data.Length)
            if (currentIndex >= _data.Length)
            {
                isLast = true;
                encounteredUpperLevel = true;
                break;
            }

            currentCharacter = _data.Data[currentIndex];
            currentCharacter = _data[currentIndex];
        }

        _readToLast = false;
        currentIndex++;

        var token = new PacketToken(tokenString.ToString(), isLast, encounteredUpperLevel, _data.ReachedEnd);
        var token = new PacketToken(_data.Slice(startIndex, length), isLast, encounteredUpperLevel, _cursor >= _data.Length);
        if (seek)
        {
            UpdateCurrentAndParentLevels(token);
            _data.Cursor = currentIndex;
            _cursor = currentIndex;
            _currentLevel.TokensRead++;
        }
        else


@@ 262,7 232,8 @@ public struct PacketStringEnumerator
            _currentToken = token;
        }

        return token;
        packetToken = token;
        return Result.FromSuccess();
    }

    /// <summary>


@@ 312,7 283,7 @@ public struct PacketStringEnumerator
    /// <returns>Whether the last token was read. Null if cannot determine (ie. there are multiple levels with the same separator.)</returns>
    public bool? IsOnLastToken()
    {
        if (_data.ReachedEnd)
        if (_cursor >= _data.Length)
        {
            return true;
        }


@@ 376,21 347,6 @@ public struct PacketStringEnumerator
        return true;
    }

    private class EnumeratorData
    {
        public EnumeratorData(string data)
        {
            Data = data;
            Cursor = 0;
        }

        public string Data { get; }

        public int Cursor { get; set; }

        public bool ReachedEnd => Cursor >= Data.Length;
    }

    private class EnumeratorLevel
    {
        public EnumeratorLevel(EnumeratorLevel? parent, char separator, uint? maxTokens = default)

M Core/NosSmooth.Packets/PacketToken.cs => Core/NosSmooth.Packets/PacketToken.cs +50 -6
@@ 4,6 4,7 @@
//  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.Diagnostics.CodeAnalysis;

namespace NosSmooth.Packets;


@@ 11,9 12,52 @@ namespace NosSmooth.Packets;
/// <summary>
/// The single token from a packet.
/// </summary>
/// <param name="Token">The token.</param>
/// <param name="IsLast">Whether the token is last in the current level. Null if it cannot be determined.</param>
/// <param name="EncounteredUpperLevel">Whether the current separator was from an upper stack level than the parent. That could mean some kind of an error if not etc. at the end of parsing a last entry of a list and last entry of a subpacket.</param>
/// <param name="PacketEndReached">Whether the packet's end was reached.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Record struct creates the underlying properties.")]
public readonly record struct PacketToken(string Token, bool? IsLast, bool? EncounteredUpperLevel, bool PacketEndReached);
\ No newline at end of file
[SuppressMessage
(
    "StyleCop.CSharp.NamingRules",
    "SA1313:Parameter names should begin with lower-case letter",
    Justification = "Record struct creates the underlying properties."
)]
public readonly ref struct PacketToken
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketToken"/> struct.
    /// </summary>
    /// <param name="token">The token.</param>
    /// <param name="isLast">Whether this is the last token in the current level.</param>
    /// <param name="encounteredUpperLevel">Whether upper level separator was encountered.</param>
    /// <param name="packetEndReached">Whether the packet end was reached.</param>
    public PacketToken
    (
        ReadOnlySpan<char> token,
        bool? isLast,
        bool? encounteredUpperLevel,
        bool packetEndReached
    )
    {
        Token = token;
        IsLast = isLast;
        EncounteredUpperLevel = encounteredUpperLevel;
        PacketEndReached = packetEndReached;
    }

    /// <summary>
    /// The token.
    /// </summary>
    public ReadOnlySpan<char> Token { get; }

    /// <summary>
    /// Whether the token is last in the current level. Null if it cannot be determined.
    /// </summary>
    public bool? IsLast { get; }

    /// <summary>
    /// Whether the current separator was from an upper stack level than the parent. That could mean some kind of an error if not etc. at the end of parsing a last entry of a list and last entry of a subpacket.
    /// </summary>
    public bool? EncounteredUpperLevel { get; }

    /// <summary>
    /// Whether the packet's end was reached.
    /// </summary>
    public bool PacketEndReached { get; }
}
\ No newline at end of file

A Local/NosSmooth.Inject/NosSmooth.Inject.vcxproj => Local/NosSmooth.Inject/NosSmooth.Inject.vcxproj +174 -0
@@ 0,0 1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Debug|x64">
      <Configuration>Debug</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|x64">
      <Configuration>Release</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <VCProjectVersion>16.0</VCProjectVersion>
    <Keyword>Win32Proj</Keyword>
    <ProjectGuid>{ca2873d8-bd0b-4583-818d-b94a3c2abba3}</ProjectGuid>
    <RootNamespace>NosSmoothInject</RootNamespace>
    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
    <ConfigurationType>DynamicLibrary</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v143</PlatformToolset>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    <ConfigurationType>DynamicLibrary</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v143</PlatformToolset>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
    <ConfigurationType>DynamicLibrary</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v143</PlatformToolset>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
    <ConfigurationType>DynamicLibrary</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v143</PlatformToolset>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <ImportGroup Label="ExtensionSettings">
  </ImportGroup>
  <ImportGroup Label="Shared">
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <LinkIncremental>true</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <LinkIncremental>false</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <LinkIncremental>true</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <LinkIncremental>false</LinkIncremental>
  </PropertyGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>WIN32;_DEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeader>NotUsing</PrecompiledHeader>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Windows</SubSystem>
      <GenerateDebugInformation>true</GenerateDebugInformation>
      <EnableUAC>false</EnableUAC>
      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
      <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>WIN32;NDEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeader>NotUsing</PrecompiledHeader>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Windows</SubSystem>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
      <GenerateDebugInformation>true</GenerateDebugInformation>
      <EnableUAC>false</EnableUAC>
      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
      <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>_DEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeader>NotUsing</PrecompiledHeader>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Windows</SubSystem>
      <GenerateDebugInformation>true</GenerateDebugInformation>
      <EnableUAC>false</EnableUAC>
      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
      <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>NDEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeader>NotUsing</PrecompiledHeader>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Windows</SubSystem>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
      <GenerateDebugInformation>true</GenerateDebugInformation>
      <EnableUAC>false</EnableUAC>
      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
      <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
    </Link>
  </ItemDefinitionGroup>
  <ItemGroup>
    <ClCompile Include="dllmain.cpp" />
    <ClCompile Include="nossmooth.cpp" />
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="coreclr_delegates.h" />
    <ClInclude Include="nethost.h" />
    <ClInclude Include="hostfxr.h" />
    <ClInclude Include="nossmooth.h" />
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets">
  </ImportGroup>
</Project>
\ No newline at end of file

A Local/NosSmooth.Inject/NosSmooth.Inject.vcxproj.filters => Local/NosSmooth.Inject/NosSmooth.Inject.vcxproj.filters +39 -0
@@ 0,0 1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <Filter Include="Source Files">
      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
      <Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
    </Filter>
    <Filter Include="Header Files">
      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
      <Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
    </Filter>
    <Filter Include="Resource Files">
      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
    </Filter>
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="dllmain.cpp">
      <Filter>Source Files</Filter>
    </ClCompile>
    <ClCompile Include="nossmooth.cpp">
      <Filter>Source Files</Filter>
    </ClCompile>
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="nossmooth.h">
      <Filter>Header Files</Filter>
    </ClInclude>
    <ClInclude Include="hostfxr.h">
      <Filter>Header Files</Filter>
    </ClInclude>
    <ClInclude Include="nethost.h">
      <Filter>Header Files</Filter>
    </ClInclude>
    <ClInclude Include="coreclr_delegates.h">
      <Filter>Header Files</Filter>
    </ClInclude>
  </ItemGroup>
</Project>
\ No newline at end of file

A Local/NosSmooth.Inject/coreclr_delegates.h => Local/NosSmooth.Inject/coreclr_delegates.h +47 -0
@@ 0,0 1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#ifndef __CORECLR_DELEGATES_H__
#define __CORECLR_DELEGATES_H__

#include <stdint.h>

#if defined(_WIN32)
#define CORECLR_DELEGATE_CALLTYPE __stdcall
#ifdef _WCHAR_T_DEFINED
typedef wchar_t char_t;
#else
typedef unsigned short char_t;
#endif
#else
#define CORECLR_DELEGATE_CALLTYPE
typedef char char_t;
#endif

#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1)

// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer
typedef int (CORECLR_DELEGATE_CALLTYPE* load_assembly_and_get_function_pointer_fn)(
    const char_t* assembly_path      /* Fully qualified path to assembly */,
    const char_t* type_name          /* Assembly qualified type name */,
    const char_t* method_name        /* Public static method name compatible with delegateType */,
    const char_t* delegate_type_name /* Assembly qualified delegate type name or null
                                        or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
                                        the UnmanagedCallersOnlyAttribute. */,
    void* reserved           /* Extensibility parameter (currently unused and must be 0) */,
    /*out*/ void** delegate          /* Pointer where to store the function pointer result */);

// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default)
typedef int (CORECLR_DELEGATE_CALLTYPE* component_entry_point_fn)(void* arg, int32_t arg_size_in_bytes);

typedef int (CORECLR_DELEGATE_CALLTYPE* get_function_pointer_fn)(
    const char_t* type_name          /* Assembly qualified type name */,
    const char_t* method_name        /* Public static method name compatible with delegateType */,
    const char_t* delegate_type_name /* Assembly qualified delegate type name or null,
                                        or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
                                        the UnmanagedCallersOnlyAttribute. */,
    void* load_context       /* Extensibility parameter (currently unused and must be 0) */,
    void* reserved           /* Extensibility parameter (currently unused and must be 0) */,
    /*out*/ void** delegate          /* Pointer where to store the function pointer result */);

#endif // __CORECLR_DELEGATES_H__
\ No newline at end of file

A Local/NosSmooth.Inject/dllmain.cpp => Local/NosSmooth.Inject/dllmain.cpp +19 -0
@@ 0,0 1,19 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include <windows.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}


A Local/NosSmooth.Inject/framework.h => Local/NosSmooth.Inject/framework.h +5 -0
@@ 0,0 1,5 @@
#pragma once

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>

A Local/NosSmooth.Inject/hostfxr.h => Local/NosSmooth.Inject/hostfxr.h +323 -0
@@ 0,0 1,323 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#ifndef __HOSTFXR_H__
#define __HOSTFXR_H__

#include <stddef.h>
#include <stdint.h>

#if defined(_WIN32)
#define HOSTFXR_CALLTYPE __cdecl
#ifdef _WCHAR_T_DEFINED
typedef wchar_t char_t;
#else
typedef unsigned short char_t;
#endif
#else
#define HOSTFXR_CALLTYPE
typedef char char_t;
#endif

enum hostfxr_delegate_type
{
    hdt_com_activation,
    hdt_load_in_memory_assembly,
    hdt_winrt_activation,
    hdt_com_register,
    hdt_com_unregister,
    hdt_load_assembly_and_get_function_pointer,
    hdt_get_function_pointer,
};

typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_fn)(const int argc, const char_t** argv);
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_startupinfo_fn)(
    const int argc,
    const char_t** argv,
    const char_t* host_path,
    const char_t* dotnet_root,
    const char_t* app_path);
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)(
    const int argc,
    const char_t** argv,
    const char_t* host_path,
    const char_t* dotnet_root,
    const char_t* app_path,
    int64_t bundle_header_offset);

typedef void(HOSTFXR_CALLTYPE* hostfxr_error_writer_fn)(const char_t* message);

//
// Sets a callback which is to be used to write errors to.
//
// Parameters:
//     error_writer
//         A callback function which will be invoked every time an error is to be reported.
//         Or nullptr to unregister previously registered callback and return to the default behavior.
// Return value:
//     The previously registered callback (which is now unregistered), or nullptr if no previous callback
//     was registered
//
// The error writer is registered per-thread, so the registration is thread-local. On each thread
// only one callback can be registered. Subsequent registrations overwrite the previous ones.
//
// By default no callback is registered in which case the errors are written to stderr.
//
// Each call to the error writer is sort of like writing a single line (the EOL character is omitted).
// Multiple calls to the error writer may occure for one failure.
//
// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer
// will be propagated to hostpolicy for the duration of the call. This means that errors from
// both hostfxr and hostpolicy will be reporter through the same error writer.
//
typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE* hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer);

typedef void* hostfxr_handle;
struct hostfxr_initialize_parameters
{
    size_t size;
    const char_t* host_path;
    const char_t* dotnet_root;
};

//
// Initializes the hosting components for a dotnet command line running an application
//
// Parameters:
//    argc
//      Number of argv arguments
//    argv
//      Command-line arguments for running an application (as if through the dotnet executable).
//      Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported.
//      For example 'app.dll app_argument_1 app_argument_2`.
//    parameters
//      Optional. Additional parameters for initialization
//    host_context_handle
//      On success, this will be populated with an opaque value representing the initialized host context
//
// Return value:
//    Success          - Hosting components were successfully initialized
//    HostInvalidState - Hosting components are already initialized
//
// This function parses the specified command-line arguments to determine the application to run. It will
// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and
// dependencies and prepare everything needed to load the runtime.
//
// This function only supports arguments for running an application. It does not support SDK commands.
//
// This function does not load the runtime.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_dotnet_command_line_fn)(
    int argc,
    const char_t** argv,
    const struct hostfxr_initialize_parameters* parameters,
    /*out*/ hostfxr_handle* host_context_handle);

//
// Initializes the hosting components using a .runtimeconfig.json file
//
// Parameters:
//    runtime_config_path
//      Path to the .runtimeconfig.json file
//    parameters
//      Optional. Additional parameters for initialization
//    host_context_handle
//      On success, this will be populated with an opaque value representing the initialized host context
//
// Return value:
//    Success                            - Hosting components were successfully initialized
//    Success_HostAlreadyInitialized     - Config is compatible with already initialized hosting components
//    Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components
//    CoreHostIncompatibleConfig         - Config is incompatible with already initialized hosting components
//
// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed
// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that
// may be next to the .runtimeconfig.json).
//
// This function does not load the runtime.
//
// If called when the runtime has already been loaded, this function will check if the specified runtime
// config is compatible with the existing runtime.
//
// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful
// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that
// the difference in properties is acceptable.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_runtime_config_fn)(
    const char_t* runtime_config_path,
    const struct hostfxr_initialize_parameters* parameters,
    /*out*/ hostfxr_handle* host_context_handle);

//
// Gets the runtime property value for an initialized host context
//
// Parameters:
//     host_context_handle
//       Handle to the initialized host context
//     name
//       Runtime property name
//     value
//       Out parameter. Pointer to a buffer with the property value.
//
// Return value:
//     The error code result.
//
// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only
// guaranteed until any of the below occur:
//   - a 'run' method is called for the host context
//   - properties are changed via hostfxr_set_runtime_property_value
//   - the host context is closed via 'hostfxr_close'
//
// If host_context_handle is nullptr and an active host context exists, this function will get the
// property value for the active host context.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_property_value_fn)(
    const hostfxr_handle host_context_handle,
    const char_t* name,
    /*out*/ const char_t** value);

//
// Sets the value of a runtime property for an initialized host context
//
// Parameters:
//     host_context_handle
//       Handle to the initialized host context
//     name
//       Runtime property name
//     value
//       Value to set
//
// Return value:
//     The error code result.
//
// Setting properties is only supported for the first host context, before the runtime has been loaded.
//
// If the property already exists in the host context, it will be overwritten. If value is nullptr, the
// property will be removed.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_set_runtime_property_value_fn)(
    const hostfxr_handle host_context_handle,
    const char_t* name,
    const char_t* value);

//
// Gets all the runtime properties for an initialized host context
//
// Parameters:
//     host_context_handle
//       Handle to the initialized host context
//     count
//       [in] Size of the keys and values buffers
//       [out] Number of properties returned (size of keys/values buffers used). If the input value is too
//             small or keys/values is nullptr, this is populated with the number of available properties
//     keys
//       Array of pointers to buffers with runtime property keys
//     values
//       Array of pointers to buffers with runtime property values
//
// Return value:
//     The error code result.
//
// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only
// guaranteed until any of the below occur:
//   - a 'run' method is called for the host context
//   - properties are changed via hostfxr_set_runtime_property_value
//   - the host context is closed via 'hostfxr_close'
//
// If host_context_handle is nullptr and an active host context exists, this function will get the
// properties for the active host context.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_properties_fn)(
    const hostfxr_handle host_context_handle,
    /*inout*/ size_t* count,
    /*out*/ const char_t** keys,
    /*out*/ const char_t** values);

//
// Load CoreCLR and run the application for an initialized host context
//
// Parameters:
//     host_context_handle
//       Handle to the initialized host context
//
// Return value:
//     If the app was successfully run, the exit code of the application. Otherwise, the error code result.
//
// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line.
//
// This function will not return until the managed application exits.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_run_app_fn)(const hostfxr_handle host_context_handle);

//
// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one.
//
// Parameters:
//     host_context_handle
//       Handle to the initialized host context
//     type
//       Type of runtime delegate requested
//     delegate
//       An out parameter that will be assigned the delegate.
//
// Return value:
//     The error code result.
//
// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config,
// then all delegate types are supported.
// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line,
// then only the following delegate types are currently supported:
//     hdt_load_assembly_and_get_function_pointer
//     hdt_get_function_pointer
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_delegate_fn)(
    const hostfxr_handle host_context_handle,
    enum hostfxr_delegate_type type,
    /*out*/ void** delegate);

//
// Closes an initialized host context
//
// Parameters:
//     host_context_handle
//       Handle to the initialized host context
//
// Return value:
//     The error code result.
//
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_close_fn)(const hostfxr_handle host_context_handle);

struct hostfxr_dotnet_environment_sdk_info
{
    size_t size;
    const char_t* version;
    const char_t* path;
};

typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)(
    const struct hostfxr_dotnet_environment_info* info,
    void* result_context);

struct hostfxr_dotnet_environment_framework_info
{
    size_t size;
    const char_t* name;
    const char_t* version;
    const char_t* path;
};

struct hostfxr_dotnet_environment_info
{
    size_t size;

    const char_t* hostfxr_version;
    const char_t* hostfxr_commit_hash;

    size_t sdk_count;
    const hostfxr_dotnet_environment_sdk_info* sdks;

    size_t framework_count;
    const hostfxr_dotnet_environment_framework_info* frameworks;
};

#endif //__HOSTFXR_H__
\ No newline at end of file

A Local/NosSmooth.Inject/nethost.h => Local/NosSmooth.Inject/nethost.h +94 -0
@@ 0,0 1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#ifndef __NETHOST_H__
#define __NETHOST_H__

#include <stddef.h>

#if defined(_WIN32)
#ifdef NETHOST_EXPORT
#define NETHOST_API __declspec(dllexport)
#else
#define NETHOST_API __declspec(dllimport)
#endif

#define NETHOST_CALLTYPE __stdcall
#ifdef _WCHAR_T_DEFINED
typedef wchar_t char_t;
#else
typedef unsigned short char_t;
#endif
#else
#ifdef NETHOST_EXPORT
#define NETHOST_API __attribute__((__visibility__("default")))
#else
#define NETHOST_API
#endif

#define NETHOST_CALLTYPE
typedef char char_t;
#endif

#ifdef __cplusplus
extern "C" {
#endif

    // Parameters for get_hostfxr_path
    //
    // Fields:
    //   size
    //     Size of the struct. This is used for versioning.
    //
    //   assembly_path
    //     Path to the compenent's assembly.
    //     If specified, hostfxr is located as if the assembly_path is the apphost
    //
    //   dotnet_root
    //     Path to directory containing the dotnet executable.
    //     If specified, hostfxr is located as if an application is started using
    //     'dotnet app.dll', which means it will be searched for under the dotnet_root
    //     path and the assembly_path is ignored.
    //
    struct get_hostfxr_parameters {
        size_t size;
        const char_t* assembly_path;
        const char_t* dotnet_root;
    };

    //
    // Get the path to the hostfxr library
    //
    // Parameters:
    //   buffer
    //     Buffer that will be populated with the hostfxr path, including a null terminator.
    //
    //   buffer_size
    //     [in] Size of buffer in char_t units.
    //     [out] Size of buffer used in char_t units. If the input value is too small
    //           or buffer is nullptr, this is populated with the minimum required size
    //           in char_t units for a buffer to hold the hostfxr path
    //
    //   get_hostfxr_parameters
    //     Optional. Parameters that modify the behaviour for locating the hostfxr library.
    //     If nullptr, hostfxr is located using the enviroment variable or global registration
    //
    // Return value:
    //   0 on success, otherwise failure
    //   0x80008098 - buffer is too small (HostApiBufferTooSmall)
    //
    // Remarks:
    //   The full search for the hostfxr library is done on every call. To minimize the need
    //   to call this function multiple times, pass a large buffer (e.g. PATH_MAX).
    //
    NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path(
        char_t* buffer,
        size_t* buffer_size,
        const struct get_hostfxr_parameters* parameters);

#ifdef __cplusplus
} // extern "C"
#endif

#endif // __NETHOST_H__

A Local/NosSmooth.Inject/nossmooth.cpp => Local/NosSmooth.Inject/nossmooth.cpp +125 -0
@@ 0,0 1,125 @@
#include "nossmooth.h"

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

// Standard headers
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <iostream>
#include "nethost.h"
#include "coreclr_delegates.h"
#include "hostfxr.h"

#include <cassert>

#define STR(s) L ## s
#define CH(c) L ## c
#define DIR_SEPARATOR L'\\'

using string_t = std::basic_string<char_t>;

// Globals to hold hostfxr exports
hostfxr_initialize_for_runtime_config_fn init_fptr;
hostfxr_get_runtime_delegate_fn get_delegate_fptr;
hostfxr_close_fn close_fptr;

// Forward declarations
bool load_hostfxr();
load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* assembly);

/********************************************************************************************
 * Function used to load and activate .NET Core
 ********************************************************************************************/

 // Forward declarations
void* load_library(const char_t*);
void* get_export(void*, const char*);

void* load_library(const char_t* path)
{
    HMODULE h = ::LoadLibraryW(path);
    assert(h != nullptr);
    return (void*)h;
}
void* get_export(void* h, const char* name)
{
    void* f = ::GetProcAddress((HMODULE)h, name);
    assert(f != nullptr);
    return f;
}

// Using the nethost library, discover the location of hostfxr and get exports
bool load_hostfxr()
{
    // Pre-allocate a large buffer for the path to hostfxr
    char_t buffer[MAX_PATH];
    size_t buffer_size = sizeof(buffer) / sizeof(char_t);
    int rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
    if (rc != 0)
        return false;

    // Load hostfxr and get desired exports
    void* lib = load_library(buffer);
    init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
    get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
    close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");

    return (init_fptr && get_delegate_fptr && close_fptr);
}

// Load and initialize .NET Core and get desired function pointer for scenario
load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* config_path)
{
    // Load .NET Core
    void* load_assembly_and_get_function_pointer = nullptr;
    hostfxr_handle cxt = nullptr;
    int rc = init_fptr(config_path, nullptr, &cxt);
    if (rc != 0 || cxt == nullptr)
    {
        std::cerr << "Init failed: " << std::hex << std::showbase << rc << std::endl;
        close_fptr(cxt);
        return nullptr;
    }

    // Get the load assembly function pointer
    rc = get_delegate_fptr(
        cxt,
        hdt_load_assembly_and_get_function_pointer,
        &load_assembly_and_get_function_pointer);
    if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
        std::cerr << "Get delegate failed: " << std::hex << std::showbase << rc << std::endl;

    close_fptr(cxt);
    return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
}

bool LoadAndCallMethod(LoadParams* params)
{
    if (!load_hostfxr())
    {
        assert(false && "Failure: load_hostfxr()");
        return false;
    }

    load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
    load_assembly_and_get_function_pointer = get_dotnet_load_assembly(params->runtimeConfigPath);
    assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");

    typedef void (CORECLR_DELEGATE_CALLTYPE* main_entry_point_fn)();
    main_entry_point_fn main = nullptr;
    int rc = load_assembly_and_get_function_pointer(
        params->libraryPath,
        params->typePath,
        params->methodName,
        UNMANAGEDCALLERSONLY_METHOD,
        nullptr,
        (void**)&main);
    assert(rc == 0 && main != nullptr && "Failure: load_assembly_and_get_function_pointer()");
    main();
    return true;
}
\ No newline at end of file

A Local/NosSmooth.Inject/nossmooth.h => Local/NosSmooth.Inject/nossmooth.h +15 -0
@@ 0,0 1,15 @@
#pragma once
#include <Windows.h>

#pragma pack(push, 1)
struct LoadParams
{
    wchar_t *libraryPath;
    wchar_t *runtimeConfigPath;
    wchar_t *typePath;
    wchar_t *methodName;
};
#pragma pack(pop)
#define DllExport extern "C" __declspec( dllexport )

DllExport bool LoadAndCallMethod(LoadParams* params);
\ No newline at end of file

A Local/NosSmooth.Inject/pch.cpp => Local/NosSmooth.Inject/pch.cpp +5 -0
@@ 0,0 1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header

#include "pch.h"

// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

A Local/NosSmooth.Inject/pch.h => Local/NosSmooth.Inject/pch.h +13 -0
@@ 0,0 1,13 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.

#ifndef PCH_H
#define PCH_H

// add headers that you want to pre-compile here
#include "framework.h"

#endif //PCH_H

A Local/NosSmooth.Injector.CLI/Commands/InjectCommand.cs => Local/NosSmooth.Injector.CLI/Commands/InjectCommand.cs +56 -0
@@ 0,0 1,56 @@
//
//  InjectCommand.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.ComponentModel;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Results;

namespace NosSmooth.Injector.CLI.Commands
{
    /// <summary>
    /// Injection command for injecting .NET 5+ libraries with UnmanagedCallersOnly method.
    /// </summary>
    internal class InjectCommand : CommandGroup
    {
        private readonly NosInjector _injector;

        /// <summary>
        /// Initializes a new instance of the <see cref="InjectCommand"/> class.
        /// </summary>
        /// <param name="injector">The nos smooth injector.</param>
        public InjectCommand(NosInjector injector)
        {
            _injector = injector;
        }

        /// <summary>
        /// The command to inject.
        /// </summary>
        /// <param name="processId">The id of the process.</param>
        /// <param name="dllPath">The path to the dll to inject.</param>
        /// <param name="typeName">The full type specifier. Default is LibraryName.DllMain, LibraryName.</param>
        /// <param name="methodName">The name of the UnmanagedCallersOnly method. Default is Main.</param>
        /// <returns>A result that may or may not have succeeded.</returns>
        [Command("inject")]
        public Task<Result> Inject
        (
            [Description("The id of the process to inject into.")]
            int processId,
            [Description("The path to the dll to inject.")]
            string dllPath,
            [Option('t', "type"), Description("The full type specifier. Default is LibraryName.DllMain, LibraryName")]
            string? typeName = null,
            [Option('m', "method"), Description("The name of the UnmanagedCallersOnly method. Default is Main")]
            string? methodName = null
        )
        {
            var dllName = Path.GetFileNameWithoutExtension(dllPath);
            return Task.FromResult
                (_injector.Inject(processId, dllPath, $"{dllName}.DllMain, {dllName}", methodName ?? "Main"));
        }
    }
}
\ No newline at end of file

A Local/NosSmooth.Injector.CLI/Commands/ListProcessesCommand.cs => Local/NosSmooth.Injector.CLI/Commands/ListProcessesCommand.cs +41 -0
@@ 0,0 1,41 @@
//
//  ListProcessesCommand.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.Diagnostics;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Results;

namespace NosSmooth.Injector.CLI.Commands
{
    /// <summary>
    /// Command for listing processes to find id of NosTale process.
    /// </summary>
    internal class ListProcessesCommand : CommandGroup
    {
        /// <summary>
        /// Lists processes by the given criteria.
        /// </summary>
        /// <param name="nameContains">What should the name of the process contain.</param>
        /// <returns>A result that may or may not have succeeded.</returns>
        [Command("list")]
        public Task<Result> List(string nameContains = "Nostale")
        {
            var processes = Process.GetProcesses();
            foreach (var process in processes.Where(x => x.ProcessName.Contains(nameContains, StringComparison.OrdinalIgnoreCase)))
            {
                Console.WriteLine(ProcessToString(process));
            }

            return Task.FromResult(Result.FromSuccess());
        }

        private string ProcessToString(Process process)
        {
            return $"{process.ProcessName} - {process.Id}";
        }
    }
}

A Local/NosSmooth.Injector.CLI/NosSmooth.Injector.CLI.csproj => Local/NosSmooth.Injector.CLI/NosSmooth.Injector.CLI.csproj +28 -0
@@ 0,0 1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
	<Nullable>enable</Nullable>
	<ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
    <PackageReference Include="Remora.Commands" Version="8.0.0" />
  </ItemGroup>

  <ItemGroup>
    <NativeLibs Remove="Program.cs" />
  </ItemGroup>

  <ItemGroup>
    <NativeLibs Remove="app.manifest" />
  </ItemGroup>

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

</Project>

A Local/NosSmooth.Injector.CLI/Program.cs => Local/NosSmooth.Injector.CLI/Program.cs +75 -0
@@ 0,0 1,75 @@
//
//  Program.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 NosSmooth.Injector.CLI.Commands;
using Remora.Commands.Extensions;
using Remora.Commands.Services;
using Remora.Results;

namespace NosSmooth.Injector.CLI
{
    /// <summary>
    /// The entrypoint class.
    /// </summary>
    public static class Program
    {
        /// <summary>
        /// The entrypoint method.
        /// </summary>
        /// <param name="argv">The command line arguments.</param>
        /// <returns>A task that may or may not have succeeded.</returns>
        public static async Task Main(string[] argv)
        {
            var services = CreateServices();
            var commandService = services.GetRequiredService<CommandService>();
            var preparedCommandResult = await commandService.TryPrepareCommandAsync(string.Join(' ', argv), services);
            if (!preparedCommandResult.IsSuccess)
            {
                Console.Error.WriteLine($"There was an error, {preparedCommandResult.Error.Message}");
                return;
            }

            if (preparedCommandResult.Entity is null)
            {
                Console.Error.WriteLine("You must enter a command such ast list or inject.");
                return;
            }

            var executionResult = await commandService.TryExecuteAsync(preparedCommandResult.Entity, services);
            if (!executionResult.Entity.IsSuccess)
            {
                switch (executionResult.Entity.Error)
                {
                    case ExceptionError exc:
                        Console.Error.WriteLine($"There was an exception, {exc.Exception.Message}");
                        break;
                    default:
                        Console.Error.WriteLine($"There was an error, {executionResult.Entity.Error!.Message}");
                        break;
                }

                return;
            }
        }

        private static IServiceProvider CreateServices()
        {
            var collection = new ServiceCollection();
            collection
                .AddSingleton<NosInjector>()
                .AddOptions<NosInjectorOptions>();

            collection
                .AddCommands()
                .AddCommandTree()
                    .WithCommandGroup<InjectCommand>()
                    .WithCommandGroup<ListProcessesCommand>();

            return collection.BuildServiceProvider();
        }
    }
}

A Local/NosSmooth.Injector.CLI/app.manifest => Local/NosSmooth.Injector.CLI/app.manifest +11 -0
@@ 0,0 1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>
\ No newline at end of file

A Local/NosSmooth.Injector/Errors/InjectionFailedError.cs => Local/NosSmooth.Injector/Errors/InjectionFailedError.cs +15 -0
@@ 0,0 1,15 @@
//
//  InjectionFailedError.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 Remora.Results;

namespace NosSmooth.Injector.Errors;

/// <summary>
/// The injection could not be finished successfully.
/// </summary>
/// <param name="DllPath">The path to the dll.</param>
public record InjectionFailedError(string DllPath) : ResultError($"Could not inject {DllPath} dll into the process.");
\ No newline at end of file

A Local/NosSmooth.Injector/Errors/InsufficientPermissionsError.cs => Local/NosSmooth.Injector/Errors/InsufficientPermissionsError.cs +20 -0
@@ 0,0 1,20 @@
//
//  InsufficientPermissionsError.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 Remora.Results;

namespace NosSmooth.Injector.Errors;

/// <summary>
/// The current user has insufficient permissions to inject into the given process.
/// </summary>
/// <param name="ProcessId">The id of the process.</param>
/// <param name="ProcessName">The name of the process.</param>
public record InsufficientPermissionsError(long ProcessId, string ProcessName)
    : ResultError
    (
        $"Insufficient permissions to open process {ProcessId} ({ProcessName}). Try running the injector as administrator."
    );
\ No newline at end of file

A Local/NosSmooth.Injector/Errors/ProcessNotFoundError.cs => Local/NosSmooth.Injector/Errors/ProcessNotFoundError.cs +16 -0
@@ 0,0 1,16 @@
//
//  ProcessNotFoundError.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 Remora.Results;

namespace NosSmooth.Injector.Errors;

/// <summary>
/// The given process was not found.
/// </summary>
/// <param name="ProcessId">The id of the process.</param>
public record ProcessNotFoundError(string ProcessId)
    : NotFoundError($"Could not find process with the given id {ProcessId}.");
\ No newline at end of file

A Local/NosSmooth.Injector/LoadParams.cs => Local/NosSmooth.Injector/LoadParams.cs +44 -0
@@ 0,0 1,44 @@
//
//  LoadParams.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.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace NosSmooth.Injector
{
    /// <summary>
    /// The parameters passed to the inject module.
    /// </summary>
    internal struct LoadParams
    {
        /// <summary>
        /// The full path of the library.
        /// </summary>
        public int LibraryPath;

        /// <summary>
        /// The full path of the library.
        /// </summary>
        public int RuntimeConfigPath;

        /// <summary>
        /// The full path to the type with the method marked as UnsafeCallersOnly.
        /// </summary>
        /// <remarks>
        /// Can be for example "LibraryNamespace.Type, LibraryNamespace".
        /// </remarks>
        public int TypePath;

        /// <summary>
        /// The name of the method to execute.
        /// </summary>
        public int MethodName;
    }
}

A Local/NosSmooth.Injector/ManagedMemoryAllocation.cs => Local/NosSmooth.Injector/ManagedMemoryAllocation.cs +47 -0
@@ 0,0 1,47 @@
//
//  ManagedMemoryAllocation.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.Net.NetworkInformation;
using Reloaded.Memory.Sources;

namespace NosSmooth.Injector;

/// <summary>
/// Represents freeable memory allocation.
/// </summary>
internal class ManagedMemoryAllocation : IDisposable
{
    private readonly IMemory _memory;

    /// <summary>
    /// Initializes a new instance of the <see cref="ManagedMemoryAllocation"/> class.
    /// </summary>
    /// <param name="memory">The memory with allocation.</param>
    /// <param name="pointer">The pointer to allocated memory.</param>
    public ManagedMemoryAllocation(IMemory memory, IntPtr pointer)
    {
        Pointer = pointer;
        _memory = memory;

    }

    /// <summary>
    /// The allocated pointer number.
    /// </summary>
    public IntPtr Pointer { get; private set; }

    /// <summary>
    /// Whether the memory is currently allocated.
    /// </summary>
    public bool Allocated => Pointer != IntPtr.Zero;

    /// <inheritdoc />
    public void Dispose()
    {
        _memory.Free(Pointer);
        Pointer = IntPtr.Zero;
    }
}
\ No newline at end of file

A Local/NosSmooth.Injector/NosInjector.cs => Local/NosSmooth.Injector/NosInjector.cs +155 -0
@@ 0,0 1,155 @@
//
//  NosInjector.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.Diagnostics;
using System.Security;
using System.Text;
using Microsoft.Extensions.Options;
using NosSmooth.Injector.Errors;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.Injector;

/// <summary>
/// Nos smooth injector for .NET 5+ projects.
/// </summary>
/// <remarks>
/// If you want to inject your project into NosTale that
/// uses NosSmooth.LocalClient, use this injector.
/// You must expose static UnmanagedCallersOnly method.
/// </remarks>
public class NosInjector
{
    private readonly NosInjectorOptions _options;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosInjector"/> class.
    /// </summary>
    /// <param name="options">The injector options.</param>
    public NosInjector(IOptions<NosInjectorOptions> options)
    {
        _options = options.Value;
    }

    /// <summary>
    /// Inject the given .NET 5+ dll into NosTale process.
    /// </summary>
    /// <remarks>
    /// The dll must also have .runtimeconfig.json present next to the dll.
    /// </remarks>
    /// <param name="processId">The id of the process to inject to.</param>
    /// <param name="dllPath">The absolute path to the dll to inject.</param>
    /// <param name="classPath">The full path to the class. Such as "MyLibrary.DllMain, MyLibrary".</param>
    /// <param name="methodName">The name of the method to execute. The method should return void and have no parameters.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Inject
    (
        int processId,
        string dllPath,
        string classPath,
        string methodName = "Main"
    )
    {
        using var process = Process.GetProcessById(processId);
        return Inject(process, dllPath, classPath, methodName);
    }

    /// <summary>
    /// Inject the given .NET 5+ dll into NosTale process.
    /// </summary>
    /// <remarks>
    /// The dll must also have .runtimeconfig.json present next to the dll.
    /// </remarks>
    /// <param name="process">The process to inject to.</param>
    /// <param name="dllPath">The absolute path to the dll to inject.</param>
    /// <param name="classPath">The full path to the class. Such as "MyLibrary.DllMain, MyLibrary".</param>
    /// <param name="methodName">The name of the method to execute. The method should return void and have no parameters.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Inject
    (
        Process process,
        string dllPath,
        string classPath,
        string methodName = "Main"
    )
    {
        try
        {
            using var injector = new Reloaded.Injector.Injector(process);
            var memory = new ExternalMemory(process);

            var directoryName = Path.GetDirectoryName(dllPath);
            if (directoryName is null)
            {
                return new GenericError("There was an error obtaining directory name of the dll path.");
            }

            var runtimePath = Path.Combine
                (directoryName, Path.GetFileNameWithoutExtension(dllPath)) + ".runtimeconfig.json";

            using var dllPathMemory = AllocateString(memory, dllPath);
            using var classPathMemory = AllocateString(memory, classPath);
            using var methodNameMemory = AllocateString(memory, methodName);
            using var runtimePathMemory = AllocateString(memory, runtimePath);

            if (!dllPathMemory.Allocated || !classPathMemory.Allocated || !methodNameMemory.Allocated
                || !runtimePathMemory.Allocated)
            {
                return new GenericError("Could not allocate memory in the external process.");
            }

            var loadParams = new LoadParams()
            {
                LibraryPath = (int)dllPathMemory.Pointer,
                MethodName = (int)methodNameMemory.Pointer,
                RuntimeConfigPath = (int)runtimePathMemory.Pointer,
                TypePath = (int)classPathMemory.Pointer
            };

            var nosSmoothInjectPath = Path.GetFullPath(_options.NosSmoothInjectPath);
            var injected = injector.Inject(nosSmoothInjectPath);
            if (injected == 0)
            {
                return new InjectionFailedError(nosSmoothInjectPath);
            }

            var functionResult = injector.CallFunction(nosSmoothInjectPath, "LoadAndCallMethod", loadParams);
            if (functionResult != 0)
            {
                return new InjectionFailedError(dllPath);
            }

            return Result.FromSuccess();
        }
        catch (UnauthorizedAccessException)
        {
            return new InsufficientPermissionsError(process.Id, process.ProcessName);
        }
        catch (SecurityException)
        {
            return new InsufficientPermissionsError(process.Id, process.ProcessName);
        }
        catch (Exception e)
        {
            return e;
        }
    }

    private ManagedMemoryAllocation AllocateString(IMemory memory, string str)
    {
        var bytes = Encoding.Unicode.GetBytes(str);
        var allocated = memory.Allocate(bytes.Length + 1);
        if (allocated == IntPtr.Zero)
        {
            return new ManagedMemoryAllocation(memory, allocated);
        }

        memory.SafeWriteRaw(allocated + bytes.Length, new byte[] { 0 });
        memory.SafeWriteRaw(allocated, bytes);
        return new ManagedMemoryAllocation(memory, allocated);
    }
}
\ No newline at end of file

A Local/NosSmooth.Injector/NosInjectorOptions.cs => Local/NosSmooth.Injector/NosInjectorOptions.cs +32 -0
@@ 0,0 1,32 @@
//
//  NosInjectorOptions.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.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace NosSmooth.Injector
{
    /// <summary>
    /// Options for NosInjector.
    /// </summary>
    public class NosInjectorOptions : IOptions<NosInjectorOptions>
    {
        /// <summary>
        /// Gets or sets the path to the nos smooth inject dll.
        /// </summary>
        /// <remarks>
        /// If not absolute path, then relative path from the current executing process is assumed.
        /// </remarks>
        public string NosSmoothInjectPath { get; set; } = "NosSmooth.Inject.dll";

        /// <inheritdoc/>
        public NosInjectorOptions Value => this;
    }
}

A Local/NosSmooth.Injector/NosSmooth.Injector.csproj => Local/NosSmooth.Injector/NosSmooth.Injector.csproj +15 -0
@@ 0,0 1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
    <PackageReference Include="Reloaded.Injector" Version="1.2.4" />
    <PackageReference Include="Remora.Results" Version="7.1.0" />
  </ItemGroup>

</Project>

M Local/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj => Local/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj +1 -2
@@ 4,8 4,7 @@
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFramework>net48</TargetFramework>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>

M Local/NosSmooth.LocalClient/Utils/User32.cs => Local/NosSmooth.LocalClient/Utils/User32.cs +1 -0
@@ 7,6 7,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS1591

namespace NosSmooth.LocalClient.Utils;


M NosSmooth.sln => NosSmooth.sln +58 -43
@@ 48,11 48,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosSmooth.PacketSerializers
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosSmooth.Packets.Tests", "Tests\NosSmooth.Packets.Tests\NosSmooth.Packets.Tests.csproj", "{9BC56B40-64E3-4A8F-AD49-C52857A35026}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NativeExecutor", "NativeExecutor\NativeExecutor.vcxproj", "{1BA17328-38FE-4830-9F26-F24585D4CAAB}"
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NosSmooth.Inject", "Local\NosSmooth.Inject\NosSmooth.Inject.vcxproj", "{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosSmooth.Injector.CLI", "Local\NosSmooth.Injector.CLI\NosSmooth.Injector.CLI.csproj", "{5D351C91-C631-40F6-82D2-F8D68468B076}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.LocalBinding", "Local\NosSmooth.LocalBinding\NosSmooth.LocalBinding.csproj", "{AA21666C-C2EF-4899-B047-889657CEB2D9}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Injector", "Local\NosSmooth.Injector\NosSmooth.Injector.csproj", "{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.LocalBinding", "Local\NosSmooth.LocalBinding\NosSmooth.LocalBinding.csproj", "{BC0DCD32-E917-4187-8CF3-970D17301ED3}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution


@@ 232,42 234,54 @@ Global
		{9BC56B40-64E3-4A8F-AD49-C52857A35026}.Release|x64.Build.0 = Release|Any CPU
		{9BC56B40-64E3-4A8F-AD49-C52857A35026}.Release|x86.ActiveCfg = Release|Any CPU
		{9BC56B40-64E3-4A8F-AD49-C52857A35026}.Release|x86.Build.0 = Release|Any CPU
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Debug|Any CPU.ActiveCfg = Debug|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Debug|Any CPU.Build.0 = Debug|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Debug|x64.ActiveCfg = Debug|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Debug|x64.Build.0 = Debug|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Debug|x86.ActiveCfg = Debug|Win32
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Debug|x86.Build.0 = Debug|Win32
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Release|Any CPU.ActiveCfg = Release|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Release|Any CPU.Build.0 = Release|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Release|x64.ActiveCfg = Release|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Release|x64.Build.0 = Release|x64
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Release|x86.ActiveCfg = Release|Win32
		{1BA17328-38FE-4830-9F26-F24585D4CAAB}.Release|x86.Build.0 = Release|Win32
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Debug|x64.ActiveCfg = Debug|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Debug|x64.Build.0 = Debug|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Debug|x86.ActiveCfg = Debug|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Debug|x86.Build.0 = Debug|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Release|Any CPU.Build.0 = Release|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Release|x64.ActiveCfg = Release|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Release|x64.Build.0 = Release|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Release|x86.ActiveCfg = Release|Any CPU
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA}.Release|x86.Build.0 = Release|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Debug|x64.ActiveCfg = Debug|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Debug|x64.Build.0 = Debug|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Debug|x86.ActiveCfg = Debug|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Debug|x86.Build.0 = Debug|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Release|Any CPU.Build.0 = Release|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Release|x64.ActiveCfg = Release|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Release|x64.Build.0 = Release|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Release|x86.ActiveCfg = Release|Any CPU
		{AA21666C-C2EF-4899-B047-889657CEB2D9}.Release|x86.Build.0 = Release|Any CPU
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|Any CPU.ActiveCfg = Debug|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|Any CPU.Build.0 = Debug|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|x64.ActiveCfg = Debug|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|x64.Build.0 = Debug|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|x86.ActiveCfg = Debug|Win32
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|x86.Build.0 = Debug|Win32
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|Any CPU.ActiveCfg = Release|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|Any CPU.Build.0 = Release|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|x64.ActiveCfg = Release|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|x64.Build.0 = Release|x64
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|x86.ActiveCfg = Release|Win32
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|x86.Build.0 = Release|Win32
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Debug|x64.ActiveCfg = Debug|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Debug|x64.Build.0 = Debug|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Debug|x86.ActiveCfg = Debug|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Debug|x86.Build.0 = Debug|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Release|Any CPU.Build.0 = Release|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Release|x64.ActiveCfg = Release|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Release|x64.Build.0 = Release|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Release|x86.ActiveCfg = Release|Any CPU
		{5D351C91-C631-40F6-82D2-F8D68468B076}.Release|x86.Build.0 = Release|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Debug|x64.ActiveCfg = Debug|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Debug|x64.Build.0 = Debug|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Debug|x86.ActiveCfg = Debug|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Debug|x86.Build.0 = Debug|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Release|Any CPU.Build.0 = Release|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Release|x64.ActiveCfg = Release|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Release|x64.Build.0 = Release|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Release|x86.ActiveCfg = Release|Any CPU
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE}.Release|x86.Build.0 = Release|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Debug|x64.ActiveCfg = Debug|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Debug|x64.Build.0 = Debug|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Debug|x86.ActiveCfg = Debug|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Debug|x86.Build.0 = Debug|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Release|Any CPU.Build.0 = Release|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Release|x64.ActiveCfg = Release|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Release|x64.Build.0 = Release|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Release|x86.ActiveCfg = Release|Any CPU
		{BC0DCD32-E917-4187-8CF3-970D17301ED3}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE


@@ 282,14 296,15 @@ Global
		{726188BA-F0EA-4ECA-ACF4-CCC066464FF0} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{F96F3AA0-131E-4B6B-AB21-BBE2DEBCEF3A} = {9025731C-084E-4E82-8CD4-0F52D3AA1F54}
		{9025731C-084E-4E82-8CD4-0F52D3AA1F54} = {F20FE754-FDEA-4F3A-93D4-0750CB9EBB33}
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E} = {F20FE754-FDEA-4F3A-93D4-0750CB9EBB33}
		{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}
		{9BC56B40-64E3-4A8F-AD49-C52857A35026} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
		{1BA17328-38FE-4830-9F26-F24585D4CAAB} = {F20FE754-FDEA-4F3A-93D4-0750CB9EBB33}
		{4F7B9EBB-B2F4-4170-ABDA-504D19D807AA} = {F20FE754-FDEA-4F3A-93D4-0750CB9EBB33}
		{AA21666C-C2EF-4899-B047-889657CEB2D9} = {6078AE6E-7CD0-48E4-84E0-EB164D8881DA}
		{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3} = {6078AE6E-7CD0-48E4-84E0-EB164D8881DA}
		{5D351C91-C631-40F6-82D2-F8D68468B076} = {6078AE6E-7CD0-48E4-84E0-EB164D8881DA}
		{7B68DE7D-159B-42A7-8CF8-69F156CD04DE} = {6078AE6E-7CD0-48E4-84E0-EB164D8881DA}
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E} = {F20FE754-FDEA-4F3A-93D4-0750CB9EBB33}
		{BC0DCD32-E917-4187-8CF3-970D17301ED3} = {6078AE6E-7CD0-48E4-84E0-EB164D8881DA}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}

M Samples/InterceptNameChanger/DllMain.cs => Samples/InterceptNameChanger/DllMain.cs +8 -9
@@ 23,22 23,21 @@ namespace InterceptNameChanger
        /// <summary>
        /// The main entrypoint method of the dll.
        /// </summary>
        /// <param name="handle">The handle of the module.</param>
        [DllExport]
        public static void Main(IntPtr handle)
        [UnmanagedCallersOnly(EntryPoint = "Main")]
        public static void Main()
        {
            AllocConsole();
            Console.WriteLine("Hello from InterceptNameChanger DllMain entry point.");

            new Thread(() =>
            {
                try
                {
                    new NameChanger().RunAsync().GetAwaiter().GetResult();
                try
                {
                    new NameChanger().RunAsync().GetAwaiter().GetResult();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
            }).Start();
        }

M Samples/InterceptNameChanger/InterceptNameChanger.csproj => Samples/InterceptNameChanger/InterceptNameChanger.csproj +2 -44
@@ 1,37 1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>10</LangVersion>
  </PropertyGroup>
  <PropertyGroup>
    <DllExportIdent>89E4EE92-5848-4390-A6A7-34972FE923F5</DllExportIdent>
    <DllExportMetaLibName>DllExport.dll</DllExportMetaLibName>
    <DllExportNamespace>InterceptNameChanger</DllExportNamespace>
    <DllExportDDNSCecil>true</DllExportDDNSCecil>
    <PlatformTarget>x86</PlatformTarget>
    <DllExportOrdinalsBase>7</DllExportOrdinalsBase>
    <DllExportGenExpLib>false</DllExportGenExpLib>
    <DllExportOurILAsm>false</DllExportOurILAsm>
    <DllExportSysObjRebase>false</DllExportSysObjRebase>
    <DllExportLeaveIntermediateFiles>false</DllExportLeaveIntermediateFiles>
    <DllExportTimeout>30000</DllExportTimeout>
    <DllExportPeCheck>2</DllExportPeCheck>
    <DllExportPatches>0</DllExportPatches>
    <DllExportPreProcType>0</DllExportPreProcType>
    <DllExportPostProcType>0</DllExportPostProcType>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Costura.Fody" Version="5.7.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="DllExport">
      <Version>1.7.4</Version>
      <Visible>false</Visible>
      <Wz>1</Wz>
    </PackageReference>
    <PackageReference Include="Fody" Version="6.6.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>


@@ 44,24 22,4 @@
    <ProjectReference Include="..\..\Core\NosSmooth.Core\NosSmooth.Core.csproj" />
    <ProjectReference Include="..\..\Local\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
  </ItemGroup>
  <ImportGroup Label=".NET DllExport">
    <Import Project="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Condition="Exists($([MSBuild]::Escape('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')))" Label="8337224c9ad9e356" />
  </ImportGroup>
  <Target Name="DllExportRestorePkg" BeforeTargets="PrepareForBuild">
    <Error Condition="!Exists('$(SolutionDir)DllExport.bat')" Text="DllExport.bat is not found. Path: '$(SolutionDir)' - https://github.com/3F/DllExport" />
    <Exec Condition="('$(DllExportModImported)' != 'true' Or !Exists('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')) And Exists('$(SolutionDir)DllExport.bat')" Command=".\DllExport.bat  -action Restore" WorkingDirectory="$(SolutionDir)" />
    <MSBuild Condition="'$(DllExportModImported)' != 'true'" Projects="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Targets="DllExportMetaXBaseTarget" Properties="TargetFramework=$(TargetFramework)">
      <Output TaskParameter="TargetOutputs" PropertyName="DllExportMetaXBase" />
    </MSBuild>
    <ItemGroup>
      <Reference Include="DllExport, PublicKeyToken=8337224c9ad9e356">
        <HintPath>$(SolutionDir)packages\DllExport.1.7.4\gcache\$(DllExportMetaXBase)\$(DllExportNamespace)\$(DllExportMetaLibName)</HintPath>
        <Private>False</Private>
        <SpecificVersion>False</SpecificVersion>
      </Reference>
    </ItemGroup>
  </Target>
  <Target Name="DllExportRPkgDynamicImport" BeforeTargets="PostBuildEvent" DependsOnTargets="GetFrameworkPaths" Condition="'$(DllExportModImported)' != 'true' And '$(DllExportRPkgDyn)' != 'false'">
    <MSBuild BuildInParallel="true" UseResultsCache="true" Projects="$(MSBuildProjectFullPath)" Properties="DllExportRPkgDyn=true" Targets="Build" />
  </Target>
</Project>
\ No newline at end of file

M Samples/SimpleChat/DllMain.cs => Samples/SimpleChat/DllMain.cs +6 -7
@@ 4,15 4,15 @@
//  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.Runtime.InteropServices;

using System.Runtime.InteropServices;

namespace SimpleChat;

/// <summary>
/// The main entrypoint class of the dll.
/// </summary>
public class DllMain
{
{
    [DllImport("kernel32")]
#pragma warning disable SA1600
    public static extern bool AllocConsole();


@@ 21,10 21,9 @@ public class DllMain
    /// <summary>
    /// The main entrypoint method of the dll.
    /// </summary>
    /// <param name="handle">The handle of the module.</param>
    [DllExport]
    public static void Main(IntPtr handle)
    {
    [UnmanagedCallersOnly(EntryPoint = "Main")]
    public static void Main()
    {
        AllocConsole();
        Console.WriteLine("Hello from SimpleChat DllMain entry point.");


M Samples/SimpleChat/SimpleChat.cs => Samples/SimpleChat/SimpleChat.cs +9 -0
@@ 8,6 8,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding;
using NosSmooth.LocalClient.Extensions;
using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Enums.Chat;


@@ 43,6 44,14 @@ public class SimpleChat
        var logger = provider.GetRequiredService<ILogger<SimpleChat>>();
        logger.LogInformation("Hello world from SimpleChat!");

        var bindingManager = provider.GetRequiredService<NosBindingManager>();
        var initializeResult = bindingManager.Initialize();
        if (!initializeResult.IsSuccess)
        {
            logger.LogError($"Could not initialize the bindings {initializeResult.Error.Message}.");
            return;
        }

        var client = provider.GetRequiredService<INostaleClient>();

        await client.ReceivePacketAsync

M Samples/SimpleChat/SimpleChat.csproj => Samples/SimpleChat/SimpleChat.csproj +2 -43
@@ 1,37 1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>10</LangVersion>
  </PropertyGroup>
  <PropertyGroup>
    <DllExportIdent>89E4EE92-5848-4390-A6A7-34972FE923F5</DllExportIdent>
    <DllExportMetaLibName>DllExport.dll</DllExportMetaLibName>
    <DllExportNamespace>SimpleChat</DllExportNamespace>
    <DllExportDDNSCecil>true</DllExportDDNSCecil>
    <PlatformTarget>x86</PlatformTarget>
    <DllExportOrdinalsBase>7</DllExportOrdinalsBase>
    <DllExportGenExpLib>false</DllExportGenExpLib>
    <DllExportOurILAsm>false</DllExportOurILAsm>
    <DllExportSysObjRebase>false</DllExportSysObjRebase>
    <DllExportLeaveIntermediateFiles>false</DllExportLeaveIntermediateFiles>
    <DllExportTimeout>30000</DllExportTimeout>
    <DllExportPeCheck>2</DllExportPeCheck>
    <DllExportPatches>0</DllExportPatches>
    <DllExportPreProcType>0</DllExportPreProcType>
    <DllExportPostProcType>0</DllExportPostProcType>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Costura.Fody" Version="5.7.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="DllExport">
      <Version>1.7.4</Version>
      <Visible>false</Visible>
      <Wz>1</Wz>
    </PackageReference>
    <PackageReference Include="Fody" Version="6.6.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>


@@ 44,24 23,4 @@
    <ProjectReference Include="..\..\Core\NosSmooth.Core\NosSmooth.Core.csproj" />
    <ProjectReference Include="..\..\Local\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
  </ItemGroup>
  <ImportGroup Label=".NET DllExport">
    <Import Project="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Condition="Exists($([MSBuild]::Escape('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')))" Label="8337224c9ad9e356" />
  </ImportGroup>
  <Target Name="DllExportRestorePkg" BeforeTargets="PrepareForBuild">
    <Error Condition="!Exists('$(SolutionDir)DllExport.bat')" Text="DllExport.bat is not found. Path: '$(SolutionDir)' - https://github.com/3F/DllExport" />
    <Exec Condition="('$(DllExportModImported)' != 'true' Or !Exists('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')) And Exists('$(SolutionDir)DllExport.bat')" Command=".\DllExport.bat  -action Restore" WorkingDirectory="$(SolutionDir)" />
    <MSBuild Condition="'$(DllExportModImported)' != 'true'" Projects="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Targets="DllExportMetaXBaseTarget" Properties="TargetFramework=$(TargetFramework)">
      <Output TaskParameter="TargetOutputs" PropertyName="DllExportMetaXBase" />
    </MSBuild>
    <ItemGroup>
      <Reference Include="DllExport, PublicKeyToken=8337224c9ad9e356">
        <HintPath>$(SolutionDir)packages\DllExport.1.7.4\gcache\$(DllExportMetaXBase)\$(DllExportNamespace)\$(DllExportMetaLibName)</HintPath>
        <Private>False</Private>
        <SpecificVersion>False</SpecificVersion>
      </Reference>
    </ItemGroup>
  </Target>
  <Target Name="DllExportRPkgDynamicImport" BeforeTargets="PostBuildEvent" DependsOnTargets="GetFrameworkPaths" Condition="'$(DllExportModImported)' != 'true' And '$(DllExportRPkgDyn)' != 'false'">
    <MSBuild BuildInParallel="true" UseResultsCache="true" Projects="$(MSBuildProjectFullPath)" Properties="DllExportRPkgDyn=true" Targets="Build" />
  </Target>
</Project>
\ No newline at end of file

M Samples/WalkCommands/ChatPacketInterceptor.cs => Samples/WalkCommands/ChatPacketInterceptor.cs +2 -2
@@ 88,8 88,8 @@ public class ChatPacketInterceptor : IPacketInterceptor
                var walkCommand = scope.ServiceProvider.GetRequiredService<Commands.WalkCommands>();
                result = await walkCommand.HandleWalkToAsync
                (
                    int.Parse(splitted[1]),
                    int.Parse(splitted[2]),
                    ushort.Parse(splitted[1]),
                    ushort.Parse(splitted[2]),
                    splitted.Length > 3 ? bool.Parse(splitted[3]) : true
                );
                break;

M Samples/WalkCommands/DllMain.cs => Samples/WalkCommands/DllMain.cs +1 -1
@@ 21,7 21,7 @@ public class DllMain
    /// <summary>
    /// Represents the dll entrypoint method.
    /// </summary>
    [DllExport]
    [UnmanagedCallersOnly(EntryPoint = "Main")]
    public static void Main()
    {
        AllocConsole();

M Samples/WalkCommands/Startup.cs => Samples/WalkCommands/Startup.cs +12 -0
@@ 7,8 7,12 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.LocalBinding;
using NosSmooth.LocalClient;
using NosSmooth.LocalClient.Extensions;
using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Enums.Chat;
using NosSmooth.Packets.Packets.Server.Chat;
using WalkCommands.Commands;

namespace WalkCommands;


@@ 43,6 47,14 @@ public class Startup
    public async Task RunAsync()
    {
        var provider = BuildServices();
        var bindingManager = provider.GetRequiredService<NosBindingManager>();
        var logger = provider.GetRequiredService<ILogger<Startup>>();
        var initializeResult = bindingManager.Initialize();
        if (!initializeResult.IsSuccess)
        {
            logger.LogError($"Could not initialize {initializeResult.Error.Message}.");
        }

        var mainCancellation = provider.GetRequiredService<CancellationTokenSource>();

        var client = provider.GetRequiredService<INostaleClient>();

M Samples/WalkCommands/WalkCommands.csproj => Samples/WalkCommands/WalkCommands.csproj +6 -43
@@ 1,28 1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AssemblyName>WalkCommands</AssemblyName>
    <RootNamespace>WalkCommands</RootNamespace>
    <LangVersion>10</LangVersion>
  </PropertyGroup>
  <PropertyGroup>
    <DllExportIdent>9C088A1D-54DE-4A9B-9C1B-DBC5BD5F5299</DllExportIdent>
    <DllExportMetaLibName>DllExport.dll</DllExportMetaLibName>
    <DllExportNamespace>WalkCommands</DllExportNamespace>
    <DllExportDDNSCecil>true</DllExportDDNSCecil>
    <PlatformTarget>x86</PlatformTarget>
    <DllExportOrdinalsBase>7</DllExportOrdinalsBase>
    <DllExportGenExpLib>false</DllExportGenExpLib>
    <DllExportOurILAsm>false</DllExportOurILAsm>
    <DllExportSysObjRebase>false</DllExportSysObjRebase>
    <DllExportLeaveIntermediateFiles>false</DllExportLeaveIntermediateFiles>
    <DllExportTimeout>30000</DllExportTimeout>
    <DllExportPeCheck>2</DllExportPeCheck>
    <DllExportPatches>0</DllExportPatches>
    <DllExportPreProcType>0</DllExportPreProcType>
    <DllExportPostProcType>0</DllExportPostProcType>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <IncludeCopyLocalFilesOutputGroup>true</IncludeCopyLocalFilesOutputGroup>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Costura.Fody">


@@ 30,11 15,6 @@
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="DllExport">
      <Version>1.7.4</Version>
      <Visible>false</Visible>
      <Wz>1</Wz>
    </PackageReference>
    <PackageReference Include="Fody">
      <Version>6.6.0</Version>
      <PrivateAssets>all</PrivateAssets>


@@ 49,28 29,11 @@
    <PackageReference Include="Microsoft.Extensions.Logging.Console">
      <Version>6.0.0</Version>
    </PackageReference>
    <PackageReference Include="Remora.Results">
      <Version>7.1.0</Version>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\Local\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
  </ItemGroup>
  <ImportGroup Label=".NET DllExport">
    <Import Project="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Condition="Exists($([MSBuild]::Escape('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')))" Label="8337224c9ad9e356" />
  </ImportGroup>
  <Target Name="DllExportRestorePkg" BeforeTargets="PrepareForBuild">
    <Error Condition="!Exists('$(SolutionDir)DllExport.bat')" Text="DllExport.bat is not found. Path: '$(SolutionDir)' - https://github.com/3F/DllExport" />
    <Exec Condition="('$(DllExportModImported)' != 'true' Or !Exists('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')) And Exists('$(SolutionDir)DllExport.bat')" Command=".\DllExport.bat  -action Restore" WorkingDirectory="$(SolutionDir)" />
    <MSBuild Condition="'$(DllExportModImported)' != 'true'" Projects="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Targets="DllExportMetaXBaseTarget" Properties="TargetFramework=$(TargetFramework)">
      <Output TaskParameter="TargetOutputs" PropertyName="DllExportMetaXBase" />
    </MSBuild>
    <ItemGroup>
      <Reference Include="DllExport, PublicKeyToken=8337224c9ad9e356">
        <HintPath>$(SolutionDir)packages\DllExport.1.7.4\gcache\$(DllExportMetaXBase)\$(DllExportNamespace)\$(DllExportMetaLibName)</HintPath>
        <Private>False</Private>
        <SpecificVersion>False</SpecificVersion>
      </Reference>
    </ItemGroup>
  </Target>
  <Target Name="DllExportRPkgDynamicImport" BeforeTargets="PostBuildEvent" DependsOnTargets="GetFrameworkPaths" Condition="'$(DllExportModImported)' != 'true' And '$(DllExportRPkgDyn)' != 'false'">
    <MSBuild BuildInParallel="true" UseResultsCache="true" Projects="$(MSBuildProjectFullPath)" Properties="DllExportRPkgDyn=true" Targets="Build" />
  </Target>
</Project>
\ No newline at end of file

M Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs => Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs +0 -1
@@ 55,7 55,6 @@ public class PacketStringBuilderTests
        stringBuilder.Append("a");
        stringBuilder.Append("b");
        stringBuilder.Append("c");
        stringBuilder.ReplaceWithParentSeparator();
        stringBuilder.PopLevel();
        stringBuilder.Append("d");


M Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs => Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs +67 -62
@@ 23,64 23,65 @@ public class PacketStringEnumeratorTests
    public void EnumeratorListComplexStringGivesCorrectResult()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1 11.12.13|14.15.16|17.18.19");
        var headerTokenResult = stringEnumerator.GetNextToken();
        var headerTokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(headerTokenResult.IsSuccess);
        Assert.False(headerTokenResult.Entity.PacketEndReached);
        Assert.NotNull(headerTokenResult.Entity.IsLast);
        Assert.NotNull(headerTokenResult.Entity.EncounteredUpperLevel);
        Assert.False(headerTokenResult.Entity.IsLast);
        Assert.False(headerTokenResult.Entity.EncounteredUpperLevel);
        Assert.Matches("in", headerTokenResult.Entity.Token);

        var firstToken = stringEnumerator.GetNextToken();
        Assert.False(packetToken.PacketEndReached);
        Assert.NotNull(packetToken.IsLast);
        Assert.NotNull(packetToken.EncounteredUpperLevel);
        Assert.False(packetToken.IsLast);
        Assert.False(packetToken.EncounteredUpperLevel);
        Assert.Matches("in", packetToken.Token.ToString());

        var firstToken = stringEnumerator.GetNextToken(out packetToken);
        Assert.True(firstToken.IsSuccess);
        Assert.False(firstToken.Entity.PacketEndReached);
        Assert.NotNull(firstToken.Entity.IsLast);
        Assert.NotNull(firstToken.Entity.EncounteredUpperLevel);
        Assert.False(firstToken.Entity.IsLast);
        Assert.False(firstToken.Entity.EncounteredUpperLevel);
        Assert.Matches("1", firstToken.Entity.Token);
        Assert.False(packetToken.PacketEndReached);
        Assert.NotNull(packetToken.IsLast);
        Assert.NotNull(packetToken.EncounteredUpperLevel);
        Assert.False(packetToken.IsLast);
        Assert.False(packetToken.EncounteredUpperLevel);
        Assert.Matches("1", packetToken.Token.ToString());

        var listEnumerator = stringEnumerator.CreateLevel('|');
        listEnumerator.PrepareLevel('.');
        stringEnumerator.PushLevel('|');
        stringEnumerator.PrepareLevel('.');

        for (int i = 0; i < 3; i++)
        {
            var preparedLevel = listEnumerator.CreatePreparedLevel();
            Assert.NotNull(preparedLevel);
            stringEnumerator.PushPreparedLevel();

            for (int j = 0; j < 3; j++)
            {
                string currentNum = (j + (i * 3) + 1 + 10).ToString();
                var currentToken = preparedLevel!.Value.GetNextToken();
                var currentToken = stringEnumerator.GetNextToken(out packetToken);
                Assert.True(currentToken.IsSuccess);
                Assert.False(currentToken.Entity.PacketEndReached);
                Assert.NotNull(currentToken.Entity.IsLast);
                Assert.NotNull(currentToken.Entity.EncounteredUpperLevel);
                Assert.False(packetToken.PacketEndReached);
                Assert.NotNull(packetToken.IsLast);
                Assert.NotNull(packetToken.EncounteredUpperLevel);
                if (j == 2 && i == 2)
                {
                    Assert.True(currentToken.Entity.EncounteredUpperLevel);
                    Assert.True(packetToken.EncounteredUpperLevel);
                }
                else
                {
                    Assert.False(currentToken.Entity.EncounteredUpperLevel);
                    Assert.False(packetToken.EncounteredUpperLevel);
                }

                if (j != 2)
                {
                    Assert.False(currentToken.Entity.IsLast);
                    Assert.False(packetToken.IsLast);
                }
                else
                {
                    Assert.True(currentToken.Entity.IsLast);
                    Assert.True(packetToken.IsLast);
                }

                Assert.Matches(currentNum, currentToken.Entity.Token);
                Assert.Matches(currentNum, packetToken.Token.ToString());
            }

            Assert.True(preparedLevel!.Value.IsOnLastToken());
            Assert.True(stringEnumerator.IsOnLastToken());
            stringEnumerator.PopLevel();
        }

        stringEnumerator.PopLevel();
        Assert.True(stringEnumerator.IsOnLastToken());
    }



@@ 91,18 92,18 @@ public class PacketStringEnumeratorTests
    public void EnumeratorDoesNotAllowOvereachingPacketEnd()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1 2 3 4");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 1
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 2
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 3
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 4

        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
    }


@@ 114,20 115,20 @@ public class PacketStringEnumeratorTests
    public void EnumeratorDoesNotAllowOvereachingListComplexTypeEnd()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2.2|3.3|4.4|5");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.');
        var itemEnumerator = listEnumerator.CreateLevel('|');
        stringEnumerator.PushLevel('.');
        stringEnumerator.PushLevel('|');

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 1

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(tokenResult.IsSuccess); // 2
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(packetToken.IsLast);

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
        Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd);


@@ 140,33 141,37 @@ public class PacketStringEnumeratorTests
    public void EnumeratorDoesNotAllowOvereachingListLength()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2.2|3.3|4.4|5");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.', 2);
        var itemEnumerator = listEnumerator.CreateLevel('|');
        stringEnumerator.PushLevel('.', 2);
        stringEnumerator.PushLevel('|');

        // first item
        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(packetToken.IsLast);

        stringEnumerator.PopLevel();

        // second item
        itemEnumerator = listEnumerator.CreateLevel('|');
        tokenResult = itemEnumerator.GetNextToken();
        stringEnumerator.PushLevel('|');
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        stringEnumerator.GetNextToken(out packetToken);
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(packetToken.IsLast);

        stringEnumerator.PopLevel();

        // cannot reach third item
        Assert.True(listEnumerator.IsOnLastToken());
        itemEnumerator = listEnumerator.CreateLevel('|');
        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(stringEnumerator.IsOnLastToken());
        stringEnumerator.PushLevel('|');
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
        Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd);


@@ 179,18 184,18 @@ public class PacketStringEnumeratorTests
    public void EnumeratorReturnsEncounteredUpperLevel()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2 1");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.');
        var itemEnumerator = listEnumerator.CreateLevel('|');
        stringEnumerator.PushLevel('.');
        stringEnumerator.PushLevel('|');

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(tokenResult.Entity.EncounteredUpperLevel);
        Assert.True(packetToken.IsLast);
        Assert.True(packetToken.EncounteredUpperLevel);
    }
}
\ No newline at end of file

Do not follow this link