From ad23f9531971b7e6e7dad1c4b44a3a36eedee2c8 Mon Sep 17 00:00:00 2001 From: BreadDEV Date: Mon, 4 Mar 2024 20:19:32 +0700 Subject: [PATCH] [v0.0.1] very early state server only basic messages, wip. --- Supercell.GUT.Logic/Base/LogicBase.cs | 43 +++ Supercell.GUT.Logic/LogicVersion.cs | 14 + .../Account/CreateAccountFailedMessage.cs | 39 +++ .../Message/Account/CreateAccountMessage.cs | 63 ++++ .../Message/Account/CreateAccountOkMessage.cs | 56 ++++ .../Account/SecureConnectionOkMessage.cs | 51 +++ .../Account/StartSecureConnectionMessage.cs | 38 +++ .../Attributes/VersionedMessageAttribute.cs | 7 + .../Message/GUTMessageFactory.cs | 40 +++ .../Supercell.GUT.Logic.csproj | 13 + .../Debugging/ServerDebuggerListener.cs | 40 +++ Supercell.GUT.Server/GUTServer.cs | 28 ++ .../Network/Connection/ClientConnection.cs | 96 ++++++ .../Connection/ClientConnectionManager.cs | 43 +++ .../Network/Connection/IConnectionListener.cs | 17 + .../Network/IGatewayEventListener.cs | 5 + .../Network/IProtocolEntity.cs | 10 + .../Network/IServerGateway.cs | 6 + .../Network/Options/GatewayOptions.cs | 10 + .../Network/Tcp/SocketExtensions.cs | 17 + .../Network/Tcp/TcpGateway.cs | 60 ++++ .../Network/Tcp/TcpSocketEntity.cs | 30 ++ Supercell.GUT.Server/Program.cs | 42 +++ .../Attributes/MessageHandlerAttribute.cs | 12 + .../Attributes/ServiceNodeAttribute.cs | 12 + .../Extensions/ServiceCollectionExtensions.cs | 20 ++ .../Handlers/AccountMessageHandler.cs | 47 +++ .../Protocol/Handlers/MessageHandlerBase.cs | 36 +++ .../Protocol/MessageManager.cs | 57 ++++ Supercell.GUT.Server/Protocol/Messaging.cs | 124 +++++++ .../Supercell.GUT.Server.csproj | 25 ++ Supercell.GUT.Server/appsettings.json | 6 + Supercell.GUT.Titan/.editorconfig | 4 + Supercell.GUT.Titan/Debugging/Debugger.cs | 40 +++ .../Debugging/IDebuggerListener.cs | 9 + .../Debugging/LogicException.cs | 8 + .../Encoding/ChecksumEncoder.cs | 79 +++++ .../Encoding/Streamed/ByteStream.cs | 302 ++++++++++++++++++ .../Encryption/RC4Encrypter.cs | 54 ++++ Supercell.GUT.Titan/Math/LogicLong.cs | 40 +++ Supercell.GUT.Titan/Math/LogicMath.cs | 151 +++++++++ Supercell.GUT.Titan/Math/LogicRandom.cs | 37 +++ .../Message/LogicMessageFactory.cs | 16 + Supercell.GUT.Titan/Message/PiranhaMessage.cs | 40 +++ .../Message/VersionedMessage.cs | 31 ++ .../Supercell.GUT.Titan.csproj | 9 + Supercell.GUT.Titan/Util/LogicStringUtil.cs | 18 ++ Supercell.GUT.sln | 37 +++ 48 files changed, 1982 insertions(+) create mode 100644 Supercell.GUT.Logic/Base/LogicBase.cs create mode 100644 Supercell.GUT.Logic/LogicVersion.cs create mode 100644 Supercell.GUT.Logic/Message/Account/CreateAccountFailedMessage.cs create mode 100644 Supercell.GUT.Logic/Message/Account/CreateAccountMessage.cs create mode 100644 Supercell.GUT.Logic/Message/Account/CreateAccountOkMessage.cs create mode 100644 Supercell.GUT.Logic/Message/Account/SecureConnectionOkMessage.cs create mode 100644 Supercell.GUT.Logic/Message/Account/StartSecureConnectionMessage.cs create mode 100644 Supercell.GUT.Logic/Message/Attributes/VersionedMessageAttribute.cs create mode 100644 Supercell.GUT.Logic/Message/GUTMessageFactory.cs create mode 100644 Supercell.GUT.Logic/Supercell.GUT.Logic.csproj create mode 100644 Supercell.GUT.Server/Debugging/ServerDebuggerListener.cs create mode 100644 Supercell.GUT.Server/GUTServer.cs create mode 100644 Supercell.GUT.Server/Network/Connection/ClientConnection.cs create mode 100644 Supercell.GUT.Server/Network/Connection/ClientConnectionManager.cs create mode 100644 Supercell.GUT.Server/Network/Connection/IConnectionListener.cs create mode 100644 Supercell.GUT.Server/Network/IGatewayEventListener.cs create mode 100644 Supercell.GUT.Server/Network/IProtocolEntity.cs create mode 100644 Supercell.GUT.Server/Network/IServerGateway.cs create mode 100644 Supercell.GUT.Server/Network/Options/GatewayOptions.cs create mode 100644 Supercell.GUT.Server/Network/Tcp/SocketExtensions.cs create mode 100644 Supercell.GUT.Server/Network/Tcp/TcpGateway.cs create mode 100644 Supercell.GUT.Server/Network/Tcp/TcpSocketEntity.cs create mode 100644 Supercell.GUT.Server/Program.cs create mode 100644 Supercell.GUT.Server/Protocol/Attributes/MessageHandlerAttribute.cs create mode 100644 Supercell.GUT.Server/Protocol/Attributes/ServiceNodeAttribute.cs create mode 100644 Supercell.GUT.Server/Protocol/Extensions/ServiceCollectionExtensions.cs create mode 100644 Supercell.GUT.Server/Protocol/Handlers/AccountMessageHandler.cs create mode 100644 Supercell.GUT.Server/Protocol/Handlers/MessageHandlerBase.cs create mode 100644 Supercell.GUT.Server/Protocol/MessageManager.cs create mode 100644 Supercell.GUT.Server/Protocol/Messaging.cs create mode 100644 Supercell.GUT.Server/Supercell.GUT.Server.csproj create mode 100644 Supercell.GUT.Server/appsettings.json create mode 100644 Supercell.GUT.Titan/.editorconfig create mode 100644 Supercell.GUT.Titan/Debugging/Debugger.cs create mode 100644 Supercell.GUT.Titan/Debugging/IDebuggerListener.cs create mode 100644 Supercell.GUT.Titan/Debugging/LogicException.cs create mode 100644 Supercell.GUT.Titan/Encoding/ChecksumEncoder.cs create mode 100644 Supercell.GUT.Titan/Encoding/Streamed/ByteStream.cs create mode 100644 Supercell.GUT.Titan/Encryption/RC4Encrypter.cs create mode 100644 Supercell.GUT.Titan/Math/LogicLong.cs create mode 100644 Supercell.GUT.Titan/Math/LogicMath.cs create mode 100644 Supercell.GUT.Titan/Math/LogicRandom.cs create mode 100644 Supercell.GUT.Titan/Message/LogicMessageFactory.cs create mode 100644 Supercell.GUT.Titan/Message/PiranhaMessage.cs create mode 100644 Supercell.GUT.Titan/Message/VersionedMessage.cs create mode 100644 Supercell.GUT.Titan/Supercell.GUT.Titan.csproj create mode 100644 Supercell.GUT.Titan/Util/LogicStringUtil.cs create mode 100644 Supercell.GUT.sln diff --git a/Supercell.GUT.Logic/Base/LogicBase.cs b/Supercell.GUT.Logic/Base/LogicBase.cs new file mode 100644 index 0000000..a47f1e1 --- /dev/null +++ b/Supercell.GUT.Logic/Base/LogicBase.cs @@ -0,0 +1,43 @@ +using Supercell.GUT.Titan.Encoding; +using Supercell.GUT.Titan.Encoding.Streamed; + +namespace Supercell.GUT.Logic.Base; + +public abstract class LogicBase +{ + public int LogicDataVersion { get; set; } + + public LogicBase(int logicDataVersion) + { + this.LogicDataVersion = logicDataVersion; + } + + public virtual void Encode(ChecksumEncoder checksumEncoder) + { + checksumEncoder.WriteInt(this.LogicDataVersion); + } + + public virtual void Decode(ByteStream byteStream) + { + this.LogicDataVersion = byteStream.ReadInt(); + } + + public virtual void Destruct() + { + this.LogicDataVersion = 0; + } + + public virtual void Copy(LogicBase logicBase) + { + this.Destruct(); + + ByteStream byteStream = new ByteStream(0); + + logicBase.Encode(byteStream); + byteStream.ResetOffset(); + + this.Decode(byteStream); + + byteStream.Destruct(); + } +} diff --git a/Supercell.GUT.Logic/LogicVersion.cs b/Supercell.GUT.Logic/LogicVersion.cs new file mode 100644 index 0000000..f2f3eb3 --- /dev/null +++ b/Supercell.GUT.Logic/LogicVersion.cs @@ -0,0 +1,14 @@ +namespace Supercell.GUT.Logic; + +public static class LogicVersion +{ + public static int GetVersionNumber(int major, int build, int minor) + { + return minor | (major << 20) | (build << 12); + } + + public static string GetKey() + { + return "9o23ljkmsdfsdippwe0qr2ke1jejhjhjdfb121fpWE802lss"; + } +} diff --git a/Supercell.GUT.Logic/Message/Account/CreateAccountFailedMessage.cs b/Supercell.GUT.Logic/Message/Account/CreateAccountFailedMessage.cs new file mode 100644 index 0000000..6338f5c --- /dev/null +++ b/Supercell.GUT.Logic/Message/Account/CreateAccountFailedMessage.cs @@ -0,0 +1,39 @@ +using Supercell.GUT.Logic.Message.Attributes; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Logic.Message.Account; + +[VersionedMessage(20102)] +public class CreateAccountFailedMessage : VersionedMessage +{ + public int ErrorCode { get; set; } + + public CreateAccountFailedMessage() : base(0) + { + this.ErrorCode = 0; + } + + public override void Encode() + { + base.Encode(); + + this.ByteStream.WriteInt(this.ErrorCode); + } + + public override void Decode() + { + base.Decode(); + + this.ErrorCode = this.ByteStream.ReadInt(); + } + + public override int GetMessageType() + { + return 20102; + } + + public override int GetServiceNodeType() + { + return 1; + } +} diff --git a/Supercell.GUT.Logic/Message/Account/CreateAccountMessage.cs b/Supercell.GUT.Logic/Message/Account/CreateAccountMessage.cs new file mode 100644 index 0000000..c1feb55 --- /dev/null +++ b/Supercell.GUT.Logic/Message/Account/CreateAccountMessage.cs @@ -0,0 +1,63 @@ +using Supercell.GUT.Logic.Message.Attributes; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Logic.Message.Account; + +[VersionedMessage(10103)] +public class CreateAccountMessage : VersionedMessage +{ + public string? FacebookId { get; set; } + public string? GameCenterId { get; set; } + + public CreateAccountMessage() : base(0) + { + this.FacebookId = null; + this.GameCenterId = null; + } + + public CreateAccountMessage(int messageVersion) : base(messageVersion) + { + this.FacebookId = null; + this.GameCenterId = null; + } + + public override void Destruct() + { + base.Destruct(); + + this.FacebookId = null; + this.GameCenterId = null; + } + + public override int GetMessageType() + { + return 10103; + } + + public override int GetServiceNodeType() + { + return 1; + } + + public override void Encode() + { + base.Encode(); + + this.ByteStream.WriteString(null); + this.ByteStream.WriteString(this.FacebookId); + this.ByteStream.WriteString(this.GameCenterId); + this.ByteStream.WriteString(null); + this.ByteStream.WriteString(null); + } + + public override void Decode() + { + base.Decode(); + + this.ByteStream.ReadString(); + this.FacebookId = this.ByteStream.ReadString(); + this.GameCenterId = this.ByteStream.ReadString(); + this.ByteStream.ReadString(); + this.ByteStream.ReadString(); + } +} diff --git a/Supercell.GUT.Logic/Message/Account/CreateAccountOkMessage.cs b/Supercell.GUT.Logic/Message/Account/CreateAccountOkMessage.cs new file mode 100644 index 0000000..28ceaac --- /dev/null +++ b/Supercell.GUT.Logic/Message/Account/CreateAccountOkMessage.cs @@ -0,0 +1,56 @@ +using Supercell.GUT.Logic.Message.Attributes; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Logic.Message.Account; + +[VersionedMessage(20101)] +public class CreateAccountOkMessage : VersionedMessage +{ + public int AccountIdHigherInt { get; set; } + public int AccountIdLowerInt { get; set; } + public string? SessionKey { get; set; } + + public CreateAccountOkMessage() : base(0) + { + this.AccountIdHigherInt = 0; + this.AccountIdLowerInt = 0; + this.SessionKey = null; + } + + public override void Encode() + { + base.Encode(); + + this.ByteStream.WriteInt(this.AccountIdHigherInt); + this.ByteStream.WriteInt(this.AccountIdLowerInt); + this.ByteStream.WriteString(this.SessionKey); + } + + public override void Decode() + { + base.Decode(); + + this.AccountIdHigherInt = this.ByteStream.ReadInt(); + this.AccountIdLowerInt = this.ByteStream.ReadInt(); + this.SessionKey = this.ByteStream.ReadString(); + } + + public override int GetMessageType() + { + return 20101; + } + + public override int GetServiceNodeType() + { + return 1; + } + + public override void Destruct() + { + base.Destruct(); + + this.AccountIdHigherInt = 0; + this.AccountIdLowerInt = 0; + this.SessionKey = null; + } +} diff --git a/Supercell.GUT.Logic/Message/Account/SecureConnectionOkMessage.cs b/Supercell.GUT.Logic/Message/Account/SecureConnectionOkMessage.cs new file mode 100644 index 0000000..9f8f506 --- /dev/null +++ b/Supercell.GUT.Logic/Message/Account/SecureConnectionOkMessage.cs @@ -0,0 +1,51 @@ +using Supercell.GUT.Logic.Message.Attributes; +using Supercell.GUT.Titan.Message; +using Supercell.GUT.Titan.Util; + +namespace Supercell.GUT.Logic.Message.Account; + +[VersionedMessage(20112)] +public class SecureConnectionOkMessage : VersionedMessage +{ + public string Nonce { get; set; } + + public SecureConnectionOkMessage() : base(0) + { + this.Nonce = string.Empty; + } + + public override void Destruct() + { + base.Destruct(); + + this.Nonce = string.Empty; + } + + public override int GetMessageType() + { + return 20112; + } + + public override int GetServiceNodeType() + { + return 1; + } + + public override void Encode() + { + base.Encode(); + + this.ByteStream.WriteString(this.Nonce); + } + + public override void Decode() + { + string nonce = this.Nonce; + + base.Decode(); + + string? value = this.ByteStream.ReadString(); + + this.Nonce = LogicStringUtil.SafeString(nonce, value, string.Empty); + } +} diff --git a/Supercell.GUT.Logic/Message/Account/StartSecureConnectionMessage.cs b/Supercell.GUT.Logic/Message/Account/StartSecureConnectionMessage.cs new file mode 100644 index 0000000..872dd1e --- /dev/null +++ b/Supercell.GUT.Logic/Message/Account/StartSecureConnectionMessage.cs @@ -0,0 +1,38 @@ +using Supercell.GUT.Logic.Message.Attributes; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Logic.Message.Account; + +[VersionedMessage(10105)] +public class StartSecureConnectionMessage : VersionedMessage +{ + public StartSecureConnectionMessage() : base(0) + { + ; + } + + public override void Destruct() + { + base.Destruct(); + } + + public override int GetMessageType() + { + return 10105; + } + + public override int GetServiceNodeType() + { + return 1; + } + + public override void Encode() + { + base.Encode(); + } + + public override void Decode() + { + base.Decode(); + } +} diff --git a/Supercell.GUT.Logic/Message/Attributes/VersionedMessageAttribute.cs b/Supercell.GUT.Logic/Message/Attributes/VersionedMessageAttribute.cs new file mode 100644 index 0000000..2d84954 --- /dev/null +++ b/Supercell.GUT.Logic/Message/Attributes/VersionedMessageAttribute.cs @@ -0,0 +1,7 @@ +namespace Supercell.GUT.Logic.Message.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class VersionedMessageAttribute(int messageType) : Attribute +{ + public int MessageType { get; } = messageType; +} diff --git a/Supercell.GUT.Logic/Message/GUTMessageFactory.cs b/Supercell.GUT.Logic/Message/GUTMessageFactory.cs new file mode 100644 index 0000000..7024631 --- /dev/null +++ b/Supercell.GUT.Logic/Message/GUTMessageFactory.cs @@ -0,0 +1,40 @@ +using Supercell.GUT.Logic.Message.Attributes; +using Supercell.GUT.Titan.Message; +using System.Collections.Immutable; +using System.Reflection; + +namespace Supercell.GUT.Logic.Message; + +public class GUTMessageFactory : LogicMessageFactory +{ + private readonly ImmutableDictionary s_types; + + public GUTMessageFactory() : base() + { + this.s_types = CreateMessageMap(); + } + + public override PiranhaMessage? CreateMessageByType(int messageType) + { + return this.s_types.TryGetValue(messageType, out Type? type) ? + Activator.CreateInstance(type) as PiranhaMessage : null; + } + + private static ImmutableDictionary CreateMessageMap() + { + var builder = ImmutableDictionary.CreateBuilder(); + + IEnumerable types = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null); + + foreach (var type in types) + { + VersionedMessageAttribute attribute = type.GetCustomAttribute()!; + + if (!builder.TryAdd(attribute.MessageType, type)) + throw new Exception($"Piranha message with type {attribute.MessageType} defined twice!"); + } + + return builder.ToImmutable(); + } +} diff --git a/Supercell.GUT.Logic/Supercell.GUT.Logic.csproj b/Supercell.GUT.Logic/Supercell.GUT.Logic.csproj new file mode 100644 index 0000000..b21cae9 --- /dev/null +++ b/Supercell.GUT.Logic/Supercell.GUT.Logic.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Supercell.GUT.Server/Debugging/ServerDebuggerListener.cs b/Supercell.GUT.Server/Debugging/ServerDebuggerListener.cs new file mode 100644 index 0000000..f85ffe9 --- /dev/null +++ b/Supercell.GUT.Server/Debugging/ServerDebuggerListener.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; +using Supercell.GUT.Titan.Debugging; + +namespace Supercell.GUT.Server.Debugging; +internal class ServerDebuggerListener : IDebuggerListener +{ + private readonly ILogger _logger; + private readonly ILogger _hudPrintLogger; + + public ServerDebuggerListener(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("Logic"); + _hudPrintLogger = loggerFactory.CreateLogger("HudPrint"); + } + + public void OnError(string log) + { + _logger.LogError("{debuggerMessage}", log); + } + + public void OnHudPrint(string log) + { + _hudPrintLogger.LogInformation("{debuggerMessage}", log); + } + + public void OnPrint(string log) + { + _logger.LogInformation("{debuggerMessage}", log); + } + + public void OnWarning(string log) + { + _logger.LogWarning("{debuggerMessage}", log); + } + + public void Detach() + { + // Detach. + } +} diff --git a/Supercell.GUT.Server/GUTServer.cs b/Supercell.GUT.Server/GUTServer.cs new file mode 100644 index 0000000..789e649 --- /dev/null +++ b/Supercell.GUT.Server/GUTServer.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Hosting; +using Supercell.GUT.Server.Network; +using Supercell.GUT.Titan.Debugging; + +namespace Supercell.GUT.Server; +internal class GUTServer : IHostedService +{ + private readonly IServerGateway _gateway; + + public GUTServer(IServerGateway serverGateway, IDebuggerListener debuggerListener) + { + _gateway = serverGateway; + + Debugger.SetListener(debuggerListener); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _gateway.Start(); + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _gateway.ShutdownAsync(); + } +} diff --git a/Supercell.GUT.Server/Network/Connection/ClientConnection.cs b/Supercell.GUT.Server/Network/Connection/ClientConnection.cs new file mode 100644 index 0000000..2828436 --- /dev/null +++ b/Supercell.GUT.Server/Network/Connection/ClientConnection.cs @@ -0,0 +1,96 @@ +using Supercell.GUT.Logic; +using Supercell.GUT.Server.Protocol; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Server.Network.Connection; +internal class ClientConnection +{ + private const int ReceiveBufferSize = 16384; + private const int ReceiveTimeoutMs = 30000; + + private readonly IConnectionListener _listener; + private readonly MessageManager _messageManager; + + private readonly byte[] _receiveBuffer; + private IProtocolEntity? _protocolEntity; + private DateTime _lastKeepAliveTime; + + public bool IsAlive => (DateTime.Now - _lastKeepAliveTime).TotalSeconds < 30.0f; + + public ClientConnection(IConnectionListener listener, MessageManager messageManager) + { + _listener = listener; + _listener.OnSend = SendAsync; + _listener.RecvCallback = OnMessage; + + _messageManager = messageManager; + + _receiveBuffer = GC.AllocateUninitializedArray(ReceiveBufferSize); + _lastKeepAliveTime = DateTime.Now; + } + + public async Task RunAsync() + { + int receiveBufferIndex = 0; + Memory receiveBufferMem = _receiveBuffer.AsMemory(); + + while (true) + { + int readAmount = await ReceiveAsync(receiveBufferMem[receiveBufferIndex..], ReceiveTimeoutMs); + if (readAmount == 0) break; + + receiveBufferIndex += readAmount; + int consumedBytes = await _listener.OnReceive(receiveBufferMem, receiveBufferIndex); + + if (consumedBytes > 0) + { + Buffer.BlockCopy(_receiveBuffer, consumedBytes, _receiveBuffer, 0, receiveBufferIndex -= consumedBytes); + } + else if (consumedBytes < 0) break; + } + } + + public async Task SendMessage(PiranhaMessage message) + { + await _listener.Send(message); + } + + public void RefreshKeepAliveTime() + { + _lastKeepAliveTime = DateTime.Now; + } + + public void SetProtocolEntity(IProtocolEntity protocolEntity) + { + _protocolEntity = protocolEntity; + } + + public IProtocolEntity ProtocolEntity + { + get + { + return _protocolEntity ?? throw new InvalidOperationException("Trying to access _protocolEntity when it's NULL!"); + } + } + + private async Task OnMessage(PiranhaMessage message) + { + await _messageManager.ReceiveMessage(message); + } + + private async ValueTask ReceiveAsync(Memory buffer, int timeoutMs) + { + CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(timeoutMs)); + return await ProtocolEntity.ReceiveAsync(buffer, cts.Token); + } + + private async ValueTask SendAsync(Memory buffer) + { + await ProtocolEntity.SendAsync(buffer, default); + } + + public void SetNonce(string nonce) + { + _listener.InitEncryption(LogicVersion.GetKey(), nonce); + } +} diff --git a/Supercell.GUT.Server/Network/Connection/ClientConnectionManager.cs b/Supercell.GUT.Server/Network/Connection/ClientConnectionManager.cs new file mode 100644 index 0000000..3e4bb0e --- /dev/null +++ b/Supercell.GUT.Server/Network/Connection/ClientConnectionManager.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Supercell.GUT.Server.Network.Connection; +internal class ClientConnectionManager : IGatewayEventListener +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + public ClientConnectionManager(ILogger logger, IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + public void OnConnect(IProtocolEntity entity) + { + _logger.LogInformation("New connection from {endPoint}", entity.RemoteEndPoint); + _ = RunSessionAsync(entity); + } + + private async Task RunSessionAsync(IProtocolEntity entity) + { + using IServiceScope scope = _scopeFactory.CreateScope(); + + ClientConnection session = scope.ServiceProvider.GetRequiredService(); + + try + { + session.SetProtocolEntity(entity); + await session.RunAsync(); + } + catch (OperationCanceledException) { /* Operation was canceled. */ } + catch (Exception exception) + { + _logger.LogError("Unhandled exception occurred while processing session, trace:\n{exception}", exception); + } + finally + { + entity.Dispose(); + } + } +} diff --git a/Supercell.GUT.Server/Network/Connection/IConnectionListener.cs b/Supercell.GUT.Server/Network/Connection/IConnectionListener.cs new file mode 100644 index 0000000..a66e147 --- /dev/null +++ b/Supercell.GUT.Server/Network/Connection/IConnectionListener.cs @@ -0,0 +1,17 @@ +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Server.Network.Connection; +internal interface IConnectionListener +{ + public delegate ValueTask SendCallback(Memory buffer); + public delegate Task ReceiveCallback(PiranhaMessage message); + + SendCallback OnSend { set; } + ReceiveCallback RecvCallback { set; } + + ValueTask OnReceive(Memory buffer, int size); + + Task Send(PiranhaMessage message); + + void InitEncryption(string key, string nonce); +} diff --git a/Supercell.GUT.Server/Network/IGatewayEventListener.cs b/Supercell.GUT.Server/Network/IGatewayEventListener.cs new file mode 100644 index 0000000..925b4be --- /dev/null +++ b/Supercell.GUT.Server/Network/IGatewayEventListener.cs @@ -0,0 +1,5 @@ +namespace Supercell.GUT.Server.Network; +internal interface IGatewayEventListener +{ + void OnConnect(IProtocolEntity entity); +} diff --git a/Supercell.GUT.Server/Network/IProtocolEntity.cs b/Supercell.GUT.Server/Network/IProtocolEntity.cs new file mode 100644 index 0000000..93cfb99 --- /dev/null +++ b/Supercell.GUT.Server/Network/IProtocolEntity.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace Supercell.GUT.Server.Network; +public interface IProtocolEntity : IDisposable +{ + EndPoint RemoteEndPoint { get; } + + ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken); + ValueTask SendAsync(Memory buffer, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Supercell.GUT.Server/Network/IServerGateway.cs b/Supercell.GUT.Server/Network/IServerGateway.cs new file mode 100644 index 0000000..0cdbac0 --- /dev/null +++ b/Supercell.GUT.Server/Network/IServerGateway.cs @@ -0,0 +1,6 @@ +namespace Supercell.GUT.Server.Network; +internal interface IServerGateway +{ + void Start(); + Task ShutdownAsync(); +} diff --git a/Supercell.GUT.Server/Network/Options/GatewayOptions.cs b/Supercell.GUT.Server/Network/Options/GatewayOptions.cs new file mode 100644 index 0000000..552a652 --- /dev/null +++ b/Supercell.GUT.Server/Network/Options/GatewayOptions.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace Supercell.GUT.Server.Network.Options; +internal class GatewayOptions +{ + public required string Host { get; set; } + public required int Port { get; set; } + + public IPEndPoint ListenEndPoint => new(IPAddress.Parse(Host), Port); +} diff --git a/Supercell.GUT.Server/Network/Tcp/SocketExtensions.cs b/Supercell.GUT.Server/Network/Tcp/SocketExtensions.cs new file mode 100644 index 0000000..14c0e56 --- /dev/null +++ b/Supercell.GUT.Server/Network/Tcp/SocketExtensions.cs @@ -0,0 +1,17 @@ +using System.Net.Sockets; + +namespace Supercell.GUT.Server.Network.Tcp; +internal static class SocketExtensions +{ + public static async ValueTask AcceptSocketAsync(this Socket socket, CancellationToken ct) + { + try + { + return await socket.AcceptAsync(ct); + } + catch (OperationCanceledException) + { + return null; + } + } +} diff --git a/Supercell.GUT.Server/Network/Tcp/TcpGateway.cs b/Supercell.GUT.Server/Network/Tcp/TcpGateway.cs new file mode 100644 index 0000000..2069388 --- /dev/null +++ b/Supercell.GUT.Server/Network/Tcp/TcpGateway.cs @@ -0,0 +1,60 @@ + +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Supercell.GUT.Server.Network.Options; + +namespace Supercell.GUT.Server.Network.Tcp; +internal class TcpGateway : IServerGateway +{ + private const int TcpBacklog = 100; + + private readonly ILogger _logger; + private readonly IOptions _options; + private readonly IGatewayEventListener _listener; + private readonly Socket _socket; + + private CancellationTokenSource? _listenCancellation; + private Task? _listenTask; + + public TcpGateway(IOptions options, ILogger logger, IGatewayEventListener listener) + { + _logger = logger; + _options = options; + _listener = listener; + _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + } + + public void Start() + { + _socket.Bind(_options.Value.ListenEndPoint); + _socket.Listen(TcpBacklog); + + _listenCancellation = new(); + _listenTask = RunAsync(_listenCancellation.Token); + + _logger.LogInformation("Gateway is listening at {ipEndPoint}", _options.Value.ListenEndPoint); + } + + private async Task RunAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + Socket? socket = await _socket.AcceptSocketAsync(cancellationToken); + if (socket == null) break; + + _listener.OnConnect(new TcpSocketEntity(socket)); + } + } + + public async Task ShutdownAsync() + { + if (_listenCancellation != null) + { + await _listenCancellation.CancelAsync(); + await _listenTask!; + } + + _socket.Close(); + } +} diff --git a/Supercell.GUT.Server/Network/Tcp/TcpSocketEntity.cs b/Supercell.GUT.Server/Network/Tcp/TcpSocketEntity.cs new file mode 100644 index 0000000..af6921b --- /dev/null +++ b/Supercell.GUT.Server/Network/Tcp/TcpSocketEntity.cs @@ -0,0 +1,30 @@ +using System.Net; +using System.Net.Sockets; + +namespace Supercell.GUT.Server.Network.Tcp; +internal class TcpSocketEntity : IProtocolEntity +{ + private readonly Socket _socket; + + public TcpSocketEntity(Socket socket) + { + _socket = socket; + } + + public EndPoint RemoteEndPoint => _socket.RemoteEndPoint!; + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken) + { + return _socket.ReceiveAsync(buffer, cancellationToken); + } + + public ValueTask SendAsync(Memory buffer, CancellationToken cancellationToken) + { + return _socket.SendAsync(buffer, cancellationToken); + } + + public void Dispose() + { + _socket.Close(); + } +} \ No newline at end of file diff --git a/Supercell.GUT.Server/Program.cs b/Supercell.GUT.Server/Program.cs new file mode 100644 index 0000000..095ceb4 --- /dev/null +++ b/Supercell.GUT.Server/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Supercell.GUT.Logic.Message; +using Supercell.GUT.Server.Debugging; +using Supercell.GUT.Server.Network; +using Supercell.GUT.Server.Network.Connection; +using Supercell.GUT.Server.Network.Options; +using Supercell.GUT.Server.Network.Tcp; +using Supercell.GUT.Server.Protocol; +using Supercell.GUT.Server.Protocol.Extensions; +using Supercell.GUT.Titan.Debugging; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Server; + +internal static class Program +{ + private const string GatewayOptionsSection = "Gateway"; + + private static async Task Main(string[] args) + { + Console.Title = "Battle Buddies Server Emulator"; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + + builder.Services.Configure(builder.Configuration.GetRequiredSection(GatewayOptionsSection)); + + builder.Services.AddHandlers(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + + builder.Services.AddHostedService(); + + await builder.Build().RunAsync(); + } +} diff --git a/Supercell.GUT.Server/Protocol/Attributes/MessageHandlerAttribute.cs b/Supercell.GUT.Server/Protocol/Attributes/MessageHandlerAttribute.cs new file mode 100644 index 0000000..a32715c --- /dev/null +++ b/Supercell.GUT.Server/Protocol/Attributes/MessageHandlerAttribute.cs @@ -0,0 +1,12 @@ +namespace Supercell.GUT.Server.Protocol.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +internal class MessageHandlerAttribute : Attribute +{ + public int MessageType { get; } + + public MessageHandlerAttribute(int messageType) + { + MessageType = messageType; + } +} diff --git a/Supercell.GUT.Server/Protocol/Attributes/ServiceNodeAttribute.cs b/Supercell.GUT.Server/Protocol/Attributes/ServiceNodeAttribute.cs new file mode 100644 index 0000000..ebd9679 --- /dev/null +++ b/Supercell.GUT.Server/Protocol/Attributes/ServiceNodeAttribute.cs @@ -0,0 +1,12 @@ +namespace Supercell.GUT.Server.Protocol.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +internal class ServiceNodeAttribute : Attribute +{ + public int ServiceNodeType { get; } + + public ServiceNodeAttribute(int serviceNodeType) + { + ServiceNodeType = serviceNodeType; + } +} diff --git a/Supercell.GUT.Server/Protocol/Extensions/ServiceCollectionExtensions.cs b/Supercell.GUT.Server/Protocol/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..7c7cb64 --- /dev/null +++ b/Supercell.GUT.Server/Protocol/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Supercell.GUT.Server.Protocol.Attributes; + +namespace Supercell.GUT.Server.Protocol.Extensions; +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddHandlers(this IServiceCollection services) + { + IEnumerable types = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null); + + foreach (Type type in types) + { + services.AddScoped(type); + } + + return services; + } +} diff --git a/Supercell.GUT.Server/Protocol/Handlers/AccountMessageHandler.cs b/Supercell.GUT.Server/Protocol/Handlers/AccountMessageHandler.cs new file mode 100644 index 0000000..69b89d7 --- /dev/null +++ b/Supercell.GUT.Server/Protocol/Handlers/AccountMessageHandler.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Supercell.GUT.Logic.Message.Account; +using Supercell.GUT.Server.Network.Connection; +using Supercell.GUT.Server.Protocol.Attributes; + +namespace Supercell.GUT.Server.Protocol.Handlers; + +[ServiceNode(1)] +internal class AccountMessageHandler : MessageHandlerBase +{ + private readonly ILogger _logger; + private readonly ClientConnection _connection; + + public AccountMessageHandler(ClientConnection connection, ILogger logger) + { + _logger = logger; + _connection = connection; + } + + [MessageHandler(10105)] + public async Task OnStartSecureConnection(StartSecureConnectionMessage startSecureConnectionMessage) + { + string nonce = "nonce"; + + await _connection.SendMessage(new SecureConnectionOkMessage() + { + Nonce = nonce + }); + + _connection.SetNonce(nonce); + } + + [MessageHandler(10103)] + public async Task OnCreateAccount(CreateAccountMessage createAccountMessage) + { + _logger.LogInformation("Creating account! FacebookId: {fid} | GameCenterId: {gcid}", + createAccountMessage.FacebookId, + createAccountMessage.GameCenterId); + + await _connection.SendMessage(new CreateAccountOkMessage() + { + AccountIdHigherInt = 0, + AccountIdLowerInt = 1, + SessionKey = "telegram_is_@BL4D3_BR43D" + }); + } +} diff --git a/Supercell.GUT.Server/Protocol/Handlers/MessageHandlerBase.cs b/Supercell.GUT.Server/Protocol/Handlers/MessageHandlerBase.cs new file mode 100644 index 0000000..8f71a41 --- /dev/null +++ b/Supercell.GUT.Server/Protocol/Handlers/MessageHandlerBase.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; +using System.Reflection; +using Supercell.GUT.Server.Protocol.Attributes; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Server.Protocol.Handlers; +internal abstract class MessageHandlerBase +{ + private readonly ImmutableDictionary _handlerMethods; + + public MessageHandlerBase() + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var method in GetType().GetMethods()) + { + MessageHandlerAttribute? attribute = method.GetCustomAttribute(); + if (attribute == null) continue; + + builder.Add(attribute.MessageType, method); + } + + _handlerMethods = builder.ToImmutable(); + } + + public async Task HandleMessage(PiranhaMessage message) + { + if (_handlerMethods.TryGetValue(message.GetMessageType(), out var method)) + { + await (Task)method.Invoke(this, new object[] { message })!; + return true; + } + + return false; + } +} diff --git a/Supercell.GUT.Server/Protocol/MessageManager.cs b/Supercell.GUT.Server/Protocol/MessageManager.cs new file mode 100644 index 0000000..1dc1707 --- /dev/null +++ b/Supercell.GUT.Server/Protocol/MessageManager.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Supercell.GUT.Server.Protocol.Attributes; +using Supercell.GUT.Server.Protocol.Handlers; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Server.Protocol; +internal class MessageManager +{ + private static readonly ImmutableDictionary s_handlerServiceTypes; + + static MessageManager() + { + var builder = ImmutableDictionary.CreateBuilder(); + + IEnumerable types = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null); + + foreach (Type type in types) + { + int serviceNodeType = type.GetCustomAttribute()!.ServiceNodeType; + builder.Add(serviceNodeType, type); + } + + s_handlerServiceTypes = builder.ToImmutable(); + } + + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public MessageManager(IServiceProvider serviceProvider, ILogger logger) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public async Task ReceiveMessage(PiranhaMessage message) + { + int serviceNodeType = message.GetServiceNodeType(); + + if (s_handlerServiceTypes.TryGetValue(serviceNodeType, out Type? handlerType)) + { + MessageHandlerBase handler = (_serviceProvider.GetRequiredService(handlerType) as MessageHandlerBase)!; + + if (!await handler.HandleMessage(message)) + { + _logger.LogWarning("Handler for message {type} not implemented in {svcName}", message.GetMessageType(), handler.GetType().Name); + } + } + else + { + _logger.LogWarning("Handler for service node type {svcType} is not defined!", serviceNodeType); + } + } +} diff --git a/Supercell.GUT.Server/Protocol/Messaging.cs b/Supercell.GUT.Server/Protocol/Messaging.cs new file mode 100644 index 0000000..015e647 --- /dev/null +++ b/Supercell.GUT.Server/Protocol/Messaging.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.Logging; +using Supercell.GUT.Server.Network.Connection; +using Supercell.GUT.Titan.Encryption; +using Supercell.GUT.Titan.Message; + +namespace Supercell.GUT.Server.Protocol; +internal class Messaging : IConnectionListener +{ + private const int HeaderSize = 7; + + private readonly ILogger _logger; + private readonly LogicMessageFactory _factory; + + private IConnectionListener.SendCallback? _sendCallback; + private IConnectionListener.ReceiveCallback? _receiveCallback; + + private RC4Encrypter? _encrypter; + private RC4Encrypter? _decrypter; + + public Messaging(LogicMessageFactory factory, ILogger logger) + { + _logger = logger; + _factory = factory; + } + + public void InitEncryption(string key, string nonce) + { + _decrypter = new RC4Encrypter(key, nonce); + _encrypter = new RC4Encrypter(key, nonce); + } + + public async ValueTask OnReceive(Memory buffer, int size) + { + int consumedBytes = 0; + + while (size >= HeaderSize) + { + ReadHeader(buffer.Span, out int messageType, out int length, out int messageVersion); + if (size < HeaderSize + length) break; + + size -= length + HeaderSize; + consumedBytes += length + HeaderSize; + + byte[] encryptedBytes = buffer.Slice(HeaderSize, length).ToArray(); + buffer = buffer[consumedBytes..]; + + byte[] encodingBytes; + + _decrypter?.Encrypt(encryptedBytes); + + encodingBytes = encryptedBytes; + + int encodingLength = length; + + PiranhaMessage? message = _factory.CreateMessageByType(messageType); + if (message == null) + { + _logger.LogWarning("Ignoring message of unknown type {messageType}", messageType); + continue; + } + + message.MessageVersion = (short)messageVersion; + message.ByteStream.SetByteArray(encodingBytes, encodingLength); + message.Decode(); + + await _receiveCallback!(message); + } + + return consumedBytes; + } + + public async Task Send(PiranhaMessage message) + { + if (message.ByteStream.Offset == 0) message.Encode(); + + byte[] encodingBytes = message.ByteStream.Buffer.Take(message.ByteStream.Offset).ToArray(); + + _encrypter?.Encrypt(encodingBytes); + + byte[] fullPayload = new byte[encodingBytes.Length + HeaderSize]; + + WriteHeader(fullPayload, message, encodingBytes.Length); + encodingBytes.CopyTo(fullPayload, HeaderSize); + + await _sendCallback!(fullPayload); + } + + public IConnectionListener.SendCallback OnSend + { + set + { + _sendCallback = value; + } + } + + public IConnectionListener.ReceiveCallback RecvCallback + { + set + { + _receiveCallback = value; + } + } + + private static void ReadHeader(ReadOnlySpan buffer, out int messageType, out int encodingLength, out int messageVersion) + { + messageType = buffer[0] << 8 | buffer[1]; + encodingLength = buffer[2] << 16 | buffer[3] << 8 | buffer[4]; + messageVersion = buffer[5] << 8 | buffer[6]; + } + + private static void WriteHeader(Span buffer, PiranhaMessage message, int length) + { + int messageType = message.GetMessageType(); + int messageVersion = message.MessageVersion; + + buffer[0] = (byte)(messageType >> 8); + buffer[1] = (byte)messageType; + buffer[2] = (byte)(length >> 16); + buffer[3] = (byte)(length >> 8); + buffer[4] = (byte)length; + buffer[5] = (byte)(messageVersion >> 8); + buffer[6] = (byte)messageVersion; + } +} diff --git a/Supercell.GUT.Server/Supercell.GUT.Server.csproj b/Supercell.GUT.Server/Supercell.GUT.Server.csproj new file mode 100644 index 0000000..ba2b1a8 --- /dev/null +++ b/Supercell.GUT.Server/Supercell.GUT.Server.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Supercell.GUT.Server/appsettings.json b/Supercell.GUT.Server/appsettings.json new file mode 100644 index 0000000..ceed59b --- /dev/null +++ b/Supercell.GUT.Server/appsettings.json @@ -0,0 +1,6 @@ +{ + "Gateway": { + "Host": "0.0.0.0", + "Port": 9339 + } +} \ No newline at end of file diff --git a/Supercell.GUT.Titan/.editorconfig b/Supercell.GUT.Titan/.editorconfig new file mode 100644 index 0000000..75e7a15 --- /dev/null +++ b/Supercell.GUT.Titan/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# IDE0290: Use primary constructor +csharp_style_prefer_primary_constructors = false diff --git a/Supercell.GUT.Titan/Debugging/Debugger.cs b/Supercell.GUT.Titan/Debugging/Debugger.cs new file mode 100644 index 0000000..6f0942a --- /dev/null +++ b/Supercell.GUT.Titan/Debugging/Debugger.cs @@ -0,0 +1,40 @@ +namespace Supercell.GUT.Titan.Debugging; +public static class Debugger +{ + private static IDebuggerListener? _listener; + + public static void Print(string log) + { + _listener?.OnPrint(log); + } + + public static void Warning(string log) + { + _listener?.OnWarning(log); + } + + public static void Error(string log) + { + _listener?.OnError(log); + throw new LogicException(log); + } + + public static void HudPrint(string log) + { + _listener?.OnHudPrint(log); + } + + public static bool DoAssert(bool condition, string message) + { + if (!condition) + Error(message); + + return condition; + } + + public static void SetListener(IDebuggerListener listener) + { + _listener?.Detach(); + _listener = listener; + } +} diff --git a/Supercell.GUT.Titan/Debugging/IDebuggerListener.cs b/Supercell.GUT.Titan/Debugging/IDebuggerListener.cs new file mode 100644 index 0000000..d6dc866 --- /dev/null +++ b/Supercell.GUT.Titan/Debugging/IDebuggerListener.cs @@ -0,0 +1,9 @@ +namespace Supercell.GUT.Titan.Debugging; +public interface IDebuggerListener +{ + void OnPrint(string log); + void OnWarning(string log); + void OnError(string log); + void OnHudPrint(string log); + void Detach(); +} diff --git a/Supercell.GUT.Titan/Debugging/LogicException.cs b/Supercell.GUT.Titan/Debugging/LogicException.cs new file mode 100644 index 0000000..7758612 --- /dev/null +++ b/Supercell.GUT.Titan/Debugging/LogicException.cs @@ -0,0 +1,8 @@ +namespace Supercell.GUT.Titan.Debugging; +public class LogicException : Exception +{ + public LogicException(string message) : base(message) + { + // LogicException. + } +} diff --git a/Supercell.GUT.Titan/Encoding/ChecksumEncoder.cs b/Supercell.GUT.Titan/Encoding/ChecksumEncoder.cs new file mode 100644 index 0000000..c388f33 --- /dev/null +++ b/Supercell.GUT.Titan/Encoding/ChecksumEncoder.cs @@ -0,0 +1,79 @@ +using Supercell.GUT.Titan.Math; +using System.Runtime.CompilerServices; + +namespace Supercell.GUT.Titan.Encoding; +public class ChecksumEncoder +{ + private int _checksum; + private int _snapshotChecksum; + + public ChecksumEncoder() + { + IsChecksumEnabled = true; + } + + public virtual bool IsCheckSumOnlyMode => true; + + public virtual ChecksumEncoder WriteBoolean(bool value) => UpdateCheckSum(value ? 13 : 7); + + public virtual ChecksumEncoder WriteByte(byte value) => UpdateCheckSum(value + 11); + + public virtual ChecksumEncoder WriteShort(short value) => UpdateCheckSum(value + 19); + + public virtual ChecksumEncoder WriteInt(int value) => UpdateCheckSum(value + 9); + + public virtual void WriteLong(LogicLong value) => value.Encode(this); + + public virtual ChecksumEncoder WriteString(string? value) => + UpdateCheckSum(value != null ? value.Length + 28 : 27); + + public virtual ChecksumEncoder WriteBytes(ReadOnlySpan value) => + UpdateCheckSum(value.Length + 28); + + public bool IsChecksumEnabled { get; private set; } + + public int CheckSum => IsChecksumEnabled ? _checksum : _snapshotChecksum; + + public ChecksumEncoder EnableCheckSum(bool enable) + { + if (!IsChecksumEnabled && enable) + { + _checksum = _snapshotChecksum; + } + else + { + _snapshotChecksum = _checksum; + } + + IsChecksumEnabled = enable; + return this; + } + + public ChecksumEncoder ResetCheckSum() + { + _checksum = 0; + return this; + } + + public override bool Equals(object? obj) + { + ChecksumEncoder? otherEncoder = obj as ChecksumEncoder; + return otherEncoder?.CheckSum == CheckSum; + } + + public override int GetHashCode() + { + // original: Debugger::error("ChecksumEncoder hashCode not designed"); + return CheckSum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ChecksumEncoder UpdateCheckSum(int value) + { + _checksum = value + Ror4(_checksum, 31); + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Ror4(int v, int c) => v >> c | v << 32 - c; +} diff --git a/Supercell.GUT.Titan/Encoding/Streamed/ByteStream.cs b/Supercell.GUT.Titan/Encoding/Streamed/ByteStream.cs new file mode 100644 index 0000000..8ccce37 --- /dev/null +++ b/Supercell.GUT.Titan/Encoding/Streamed/ByteStream.cs @@ -0,0 +1,302 @@ +using Supercell.GUT.Titan.Math; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using TextEncoding = System.Text.Encoding; + +namespace Supercell.GUT.Titan.Encoding.Streamed; + +public class ByteStream : ChecksumEncoder +{ + public byte[] Buffer { get; set; } + + private int _bitIndex; + private int _length; + + public int Offset { get; set; } + + public int Length => _length > 0 ? _length : Offset; + + public ByteStream(byte[] buffer, int length) + { + Buffer = buffer; + _length = length; + } + + public ByteStream(int capacity) + { + Buffer = new byte[capacity]; + } + + public ByteStream() + { + Buffer = new byte[10]; + } + + public void SetByteArray(byte[] buffer, int length) + { + Buffer = buffer; + _length = length; + } + + public bool ReadBoolean() + { + if (_bitIndex == 0) + ++Offset; + + bool value = (Buffer[Offset - 1] & 1 << _bitIndex) != 0; + _bitIndex = _bitIndex + 1 & 7; + return value; + } + + public byte ReadByte() + { + _bitIndex = 0; + return Buffer[Offset++]; + } + + public short ReadShort() + { + _bitIndex = 0; + + short ret = BinaryPrimitives.ReadInt16BigEndian(Buffer.AsSpan()[Offset..]); + Offset += 2; + + return ret; + } + + public int ReadInt() + { + _bitIndex = 0; + + int ret = BinaryPrimitives.ReadInt32BigEndian(Buffer.AsSpan()[Offset..]); + Offset += 4; + + return ret; + } + + public int ReadVInt() + { + _bitIndex = 0; + + int value = 0; + byte byteValue = Buffer[Offset++]; + + if ((byteValue & 0x40) != 0) + { + value |= byteValue & 0x3F; + + if ((byteValue & 0x80) != 0) + { + value |= ((byteValue = Buffer[Offset++]) & 0x7F) << 6; + + if ((byteValue & 0x80) != 0) + { + value |= ((byteValue = Buffer[Offset++]) & 0x7F) << 13; + + if ((byteValue & 0x80) != 0) + { + value |= ((byteValue = Buffer[Offset++]) & 0x7F) << 20; + + if ((byteValue & 0x80) != 0) + { + value |= ((_ = Buffer[Offset++]) & 0x7F) << 27; + return (int)(value | 0x80000000); + } + + return (int)(value | 0xF8000000); + } + + return (int)(value | 0xFFF00000); + } + + return (int)(value | 0xFFFFE000); + } + + return (int)(value | 0xFFFFFFC0); + } + + value |= byteValue & 0x3F; + + if ((byteValue & 0x80) != 0) + { + value |= ((byteValue = Buffer[Offset++]) & 0x7F) << 6; + + if ((byteValue & 0x80) != 0) + { + value |= ((byteValue = Buffer[Offset++]) & 0x7F) << 13; + + if ((byteValue & 0x80) != 0) + { + value |= ((byteValue = Buffer[Offset++]) & 0x7F) << 20; + + if ((byteValue & 0x80) != 0) + { + value |= ((_ = Buffer[Offset++]) & 0x7F) << 27; + } + } + } + } + + return value; + } + + public LogicLong ReadLong() + { + LogicLong ll = new(); + ll.Decode(this); + + return ll; + } + + public string? ReadString() + { + int length = ReadInt(); + if (length is < 0 or > 900000) + return null; + else if (length == 0) + return string.Empty; + + string ret = TextEncoding.UTF8.GetString(Buffer, Offset, length); + Offset += length; + + return ret; + } + + public string ReadStringReference() + { + int length = ReadInt(); + if (length is <= 0 or > 900000) return string.Empty; + + string ret = TextEncoding.UTF8.GetString(Buffer, Offset, length); + Offset += length; + + return ret; + } + + public byte[] ReadBytes(int length) + { + _bitIndex = 0; + + byte[] ret = Buffer.Skip(Offset).Take(length).ToArray(); + Offset += length; + + return ret; + } + + public override ChecksumEncoder WriteBoolean(bool value) + { + base.WriteBoolean(value); + + if (_bitIndex == 0) + { + EnsureCapacity(1); + Buffer[Offset++] = 0; + } + + Buffer[Offset - 1] |= (byte)(value ? 1 : 0); + _bitIndex = _bitIndex + 1 & 7; + + return this; + } + + public override ChecksumEncoder WriteByte(byte value) + { + base.WriteByte(value); + EnsureCapacity(1); + + Buffer[Offset++] = value; + + return this; + } + + public override ChecksumEncoder WriteShort(short value) + { + base.WriteShort(value); + EnsureCapacity(2); + + BinaryPrimitives.WriteInt16BigEndian(Buffer.AsSpan()[Offset..], value); + Offset += 2; + + return this; + } + + public override ChecksumEncoder WriteInt(int value) + { + base.WriteInt(value); + EnsureCapacity(4); + + WriteIntToByteArray(value); + return this; + } + + public override ChecksumEncoder WriteString(string? value) + { + base.WriteString(value); + if (value == null) + { + WriteIntToByteArray(-1); + return this; + } + + int size = TextEncoding.UTF8.GetByteCount(value); + WriteIntToByteArray(size); + + EnsureCapacity(size); + TextEncoding.UTF8.GetBytes(value, Buffer.AsSpan()[Offset..]); + + Offset += size; + return this; + } + + public override ChecksumEncoder WriteBytes(ReadOnlySpan value) + { + base.WriteBytes(value); + + WriteIntToByteArray(value.Length); + + EnsureCapacity(value.Length); + value.CopyTo(Buffer.AsSpan()[Offset..]); + Offset += value.Length; + + return this; + } + + public override bool IsCheckSumOnlyMode => false; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteIntToByteArray(int value) + { + EnsureCapacity(4); + + BinaryPrimitives.WriteInt32BigEndian(Buffer.AsSpan()[Offset..], value); + Offset += 4; + } + + public void EnsureCapacity(int capacity) + { + _bitIndex = 0; + int bufferLength = Buffer.Length; + + if (Offset + capacity > bufferLength) + { + byte[] tmpBuffer = new byte[Buffer.Length + capacity + 100]; + System.Buffer.BlockCopy(Buffer, 0, tmpBuffer, 0, bufferLength); + Buffer = tmpBuffer; + } + } + + public bool IsAtEnd() + { + return Offset >= Buffer.Length; + } + + public void ResetOffset() + { + this.Offset = 0; + } + + public void Destruct() + { + ; + } +} \ No newline at end of file diff --git a/Supercell.GUT.Titan/Encryption/RC4Encrypter.cs b/Supercell.GUT.Titan/Encryption/RC4Encrypter.cs new file mode 100644 index 0000000..7fc84e8 --- /dev/null +++ b/Supercell.GUT.Titan/Encryption/RC4Encrypter.cs @@ -0,0 +1,54 @@ +namespace Supercell.GUT.Titan.Encryption; + +public class RC4Encrypter +{ + private byte[]? m_key; + private byte m_x; + private byte m_y; + + public RC4Encrypter(string baseKey, string nonce) + { + this.InitState(baseKey, nonce); + } + + public void InitState(string baseKey, string nonce) + { + string key = baseKey + nonce; + + this.m_key = new byte[256]; + this.m_x = 0; + this.m_y = 0; + + for (int i = 0; i < 256; i++) + { + this.m_key[i] = (byte)i; + } + + for (int i = 0, j = 0; i < 256; i++) + { + j = (byte)(j + this.m_key[i] + key[i % key.Length]); + + (this.m_key[j], this.m_key[i]) = (this.m_key[i], this.m_key[j]); + } + + for (int i = 0; i < key.Length; i++) + { + this.m_x += 1; + this.m_y += this.m_key[this.m_x]; + + (this.m_key[this.m_x], this.m_key[this.m_y]) = (this.m_key[this.m_y], this.m_key[this.m_x]); + } + } + + public void Encrypt(byte[] input) + { + for (int i = 0; i < input.Length; i++) + { + this.m_x += 1; + this.m_y += this.m_key![this.m_x]; + + (this.m_key[this.m_x], this.m_key[this.m_y]) = (this.m_key[this.m_y], this.m_key[this.m_x]); + input[i] = (byte)(input[i] ^ this.m_key[(byte)(this.m_key[this.m_x] + this.m_key[this.m_y])]); + } + } +} diff --git a/Supercell.GUT.Titan/Math/LogicLong.cs b/Supercell.GUT.Titan/Math/LogicLong.cs new file mode 100644 index 0000000..81f8611 --- /dev/null +++ b/Supercell.GUT.Titan/Math/LogicLong.cs @@ -0,0 +1,40 @@ +using Supercell.GUT.Titan.Encoding; +using Supercell.GUT.Titan.Encoding.Streamed; + +namespace Supercell.GUT.Titan.Math; + +public class LogicLong +{ + public int HigherInt { get; set; } + public int LowerInt { get; set; } + + public LogicLong(long longValue) + { + this.HigherInt = (int)(longValue >> 32); + this.LowerInt = (int)longValue; + } + + public LogicLong() + { + this.HigherInt = 0; + this.LowerInt = 0; + } + + public LogicLong(int higherInt, int lowerInt) + { + this.HigherInt = higherInt; + this.LowerInt = lowerInt; + } + + public void Encode(ChecksumEncoder checksumEncoder) + { + checksumEncoder.WriteInt(this.HigherInt); + checksumEncoder.WriteInt(this.LowerInt); + } + + public void Decode(ByteStream byteStream) + { + this.HigherInt = byteStream.ReadInt(); + this.LowerInt = byteStream.ReadInt(); + } +} diff --git a/Supercell.GUT.Titan/Math/LogicMath.cs b/Supercell.GUT.Titan/Math/LogicMath.cs new file mode 100644 index 0000000..0cd26cb --- /dev/null +++ b/Supercell.GUT.Titan/Math/LogicMath.cs @@ -0,0 +1,151 @@ +namespace Supercell.GUT.Titan.Math; + +public static class LogicMath +{ + public static readonly int[] DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30]; + + public static readonly int[] SQRT_TABLE = [ + 0x00, 0x10, 0x16, 0x1B, 0x20, 0x23, 0x27, 0x2A, 0x2D, + 0x30, 0x32, 0x35, 0x37, 0x39, 0x3B, 0x3D, 0x40, 0x41, + 0x43, 0x45, 0x47, 0x49, 0x4B, 0x4C, 0x4E, 0x50, 0x51, + 0x53, 0x54, 0x56, 0x57, 0x59, 0x5A, 0x5B, 0x5D, 0x5E, + 0x60, 0x61, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68, 0x6A, + 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, + 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, + 0x7E, 0x80, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x96, + 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9B, 0x9C, 0x9D, 0x9E, + 0x9F, 0xA0, 0xA0, 0xA1, 0xA2, 0xA3, 0xA3, 0xA4, 0xA5, + 0xA6, 0xA7, 0xA7, 0xA8, 0xA9, 0xAA, 0xAA, 0xAB, 0xAC, + 0xAD, 0xAD, 0xAE, 0xAF, 0xB0, 0xB0, 0xB1, 0xB2, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB5, 0xB6, 0xB7, 0xB7, 0xB8, 0xB9, + 0xB9, 0xBA, 0xBB, 0xBB, 0xBC, 0xBD, 0xBD, 0xBE, 0xBF, + 0xC0, 0xC0, 0xC1, 0xC1, 0xC2, 0xC3, 0xC3, 0xC4, 0xC5, + 0xC5, 0xC6, 0xC7, 0xC7, 0xC8, 0xC9, 0xC9, 0xCA, 0xCB, + 0xCB, 0xCC, 0xCC, 0xCD, 0xCE, 0xCE, 0xCF, 0xD0, 0xD0, + 0xD1, 0xD1, 0xD2, 0xD3, 0xD3, 0xD4, 0xD4, 0xD5, 0xD6, + 0xD6, 0xD7, 0xD7, 0xD8, 0xD9, 0xD9, 0xDA, 0xDA, 0xDB, + 0xDB, 0xDC, 0xDD, 0xDD, 0xDE, 0xDE, 0xDF, 0xE0, 0xE0, + 0xE1, 0xE1, 0xE2, 0xE2, 0xE3, 0xE3, 0xE4, 0xE5, 0xE5, + 0xE6, 0xE6, 0xE7, 0xE7, 0xE8, 0xE8, 0xE9, 0xEA, 0xEA, + 0xEB, 0xEB, 0xEC, 0xEC, 0xED, 0xED, 0xEE, 0xEE, 0xEF, + 0xF0, 0xF0, 0xF1, 0xF1, 0xF2, 0xF2, 0xF3, 0xF3, 0xF4, + 0xF4, 0xF5, 0xF5, 0xF6, 0xF6, 0xF7, 0xF7, 0xF8, 0xF8, + 0xF9, 0xF9, 0xFA, 0xFA, 0xFB, 0xFB, 0xFC, 0xFC, 0xFD, + 0xFD, 0xFE, 0xFE, 0xFF, 0x00, 0x00, 0x00 + ]; + + public static int Max(int a1, int a2) + { + return a1 > a2 ? a1 : a2; + } + + public static int Min(int a1, int a2) + { + return a1 < a2 ? a1 : a2; + } + + public static int GetDaysInMonth(int month, bool isLeapYear) + { + if (month != 1) + return LogicMath.DAYS_IN_MONTH[month]; + + if (!isLeapYear) + return LogicMath.DAYS_IN_MONTH[month]; + + return 29; + } + + public static int Sqrt(int value) + { + int result; + int v4; + int v5; + int v6; + int v7; + + if (value < 0x10000) + { + if (value < 256) + { + result = -1; + if (value >= 0) + return LogicMath.SQRT_TABLE[value] >> 4; + } + else + { + if (value < 4096) + { + if (value < 1024) + v7 = LogicMath.SQRT_TABLE[value & 0xFFFFFFFC] >> 3; + else + v7 = LogicMath.SQRT_TABLE[value >> 4] >> 2; + } + else if (value < 0x4000) + { + v7 = LogicMath.SQRT_TABLE[value >> 6] >> 1; + } + else + { + v7 = LogicMath.SQRT_TABLE[value >> 8]; + } + + result = v7 + 1; + + if (result * result > value) + return v7; + } + } + else + { + if (value < 0x1000000) + { + if (value < 0x100000) + { + if (value < 0x40000) + v6 = 2 * LogicMath.SQRT_TABLE[value >> 0xA]; + else + v6 = 4 * LogicMath.SQRT_TABLE[value >> 0xC]; + } + else if (value < 0x400000) + { + v6 = 8 * LogicMath.SQRT_TABLE[value >> 0xE]; + } + else + { + v6 = 16 * LogicMath.SQRT_TABLE[value >> 0x10]; + } + + v5 = value / v6 + (v6 | 1); + } + else + { + if (value < 0x10000000) + { + if (value < 0x4000000) + v4 = 32 * LogicMath.SQRT_TABLE[value >> 0x12]; + else + v4 = LogicMath.SQRT_TABLE[value >> 0x14] << 6; + } + else if (value < 0x40000000) + { + v4 = LogicMath.SQRT_TABLE[value >> 0x16] << 7; + } + else + { + result = 0xFFFF; + if (value == 0x7FFFFFFF) + return result; + v4 = LogicMath.SQRT_TABLE[value >> 0x18] << 8; + } + + v5 = value / (((v4 | 1) + value / v4) >> 1) + (((v4 | 1) + value / v4) >> 1) + 1; + } + + return (v5 >> 1) - ((v5 >> 1) * (v5 >> 1) > value ? 1 : 0); + } + + return result; + } +} diff --git a/Supercell.GUT.Titan/Math/LogicRandom.cs b/Supercell.GUT.Titan/Math/LogicRandom.cs new file mode 100644 index 0000000..1a8573f --- /dev/null +++ b/Supercell.GUT.Titan/Math/LogicRandom.cs @@ -0,0 +1,37 @@ +namespace Supercell.GUT.Titan.Math; + +public class LogicRandom +{ + public int IteratedRandomSeed { get; set; } + + public LogicRandom() + { + this.IteratedRandomSeed = 0; + } + + public void Destruct() + { + this.IteratedRandomSeed = 0; + } + + public int Rand(int max) + { + if (max >= 1) + { + int v3 = this.IteratedRandomSeed; + if (this.IteratedRandomSeed == 0) + v3 = -1; + int v4 = v3 ^ (v3 << 13) ^ ((v3 ^ (v3 << 13)) >> 0x11); + int v5 = v4 ^ (32 * v4); + this.IteratedRandomSeed = v5; + int temp; + if (v5 > -1) + temp = v5; + else + temp = -v5; + return temp % max; + } + + return 0; + } +} diff --git a/Supercell.GUT.Titan/Message/LogicMessageFactory.cs b/Supercell.GUT.Titan/Message/LogicMessageFactory.cs new file mode 100644 index 0000000..a44d394 --- /dev/null +++ b/Supercell.GUT.Titan/Message/LogicMessageFactory.cs @@ -0,0 +1,16 @@ +namespace Supercell.GUT.Titan.Message; + +public abstract class LogicMessageFactory +{ + public LogicMessageFactory() + { + ; + } + + public virtual void Destruct() + { + ; + } + + public abstract PiranhaMessage? CreateMessageByType(int messageType); +} diff --git a/Supercell.GUT.Titan/Message/PiranhaMessage.cs b/Supercell.GUT.Titan/Message/PiranhaMessage.cs new file mode 100644 index 0000000..a146be0 --- /dev/null +++ b/Supercell.GUT.Titan/Message/PiranhaMessage.cs @@ -0,0 +1,40 @@ +using Supercell.GUT.Titan.Encoding.Streamed; + +namespace Supercell.GUT.Titan.Message; + +public abstract class PiranhaMessage +{ + public ByteStream ByteStream { get; } + + public int MessageVersion { get; set; } + + public PiranhaMessage(int messageVersion) + { + this.ByteStream = new ByteStream(); + this.MessageVersion = messageVersion; + } + + public virtual void Encode() + { + ; + } + + public virtual void Decode() + { + ; + } + + public abstract int GetServiceNodeType(); + public abstract int GetMessageType(); + + public int GetEncodingLength() + { + return this.ByteStream.Length; + } + + public virtual void Destruct() + { + this.ByteStream.Destruct(); + this.MessageVersion = 0; + } +} diff --git a/Supercell.GUT.Titan/Message/VersionedMessage.cs b/Supercell.GUT.Titan/Message/VersionedMessage.cs new file mode 100644 index 0000000..05c010f --- /dev/null +++ b/Supercell.GUT.Titan/Message/VersionedMessage.cs @@ -0,0 +1,31 @@ +namespace Supercell.GUT.Titan.Message; + +public abstract class VersionedMessage : PiranhaMessage +{ + public int Version { get; set; } + + public VersionedMessage() : base(0) + { + this.Version = 0; + } + + public VersionedMessage(int messageVersion) : base(messageVersion) + { + this.Version = 0; + } + + public override void Encode() + { + this.ByteStream.WriteInt(this.Version); + } + + public override void Decode() + { + this.Version = this.ByteStream.ReadInt(); + } + + public void SetVersion(int major, int build, int minor) + { + this.Version = minor | (major << 20) | (build << 12); + } +} diff --git a/Supercell.GUT.Titan/Supercell.GUT.Titan.csproj b/Supercell.GUT.Titan/Supercell.GUT.Titan.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Supercell.GUT.Titan/Supercell.GUT.Titan.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Supercell.GUT.Titan/Util/LogicStringUtil.cs b/Supercell.GUT.Titan/Util/LogicStringUtil.cs new file mode 100644 index 0000000..b849454 --- /dev/null +++ b/Supercell.GUT.Titan/Util/LogicStringUtil.cs @@ -0,0 +1,18 @@ +namespace Supercell.GUT.Titan.Util; + +public static class LogicStringUtil +{ + public static string SafeString(string a1, string? a2, string a3) + { + if (a2 != null) + { + a1 = a2; + } + else + { + a1 = a3; + } + + return a1; + } +} diff --git a/Supercell.GUT.sln b/Supercell.GUT.sln new file mode 100644 index 0000000..f338db2 --- /dev/null +++ b/Supercell.GUT.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34316.72 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Supercell.GUT.Server", "Supercell.GUT.Server\Supercell.GUT.Server.csproj", "{8D194446-D48D-4DF6-9DCE-0DA2E53B2570}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Supercell.GUT.Titan", "Supercell.GUT.Titan\Supercell.GUT.Titan.csproj", "{8CBA9BFB-D58D-48B2-9139-17C15A5142A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Supercell.GUT.Logic", "Supercell.GUT.Logic\Supercell.GUT.Logic.csproj", "{4441B345-1B99-4DE6-9764-B34E3ED8A982}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8D194446-D48D-4DF6-9DCE-0DA2E53B2570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D194446-D48D-4DF6-9DCE-0DA2E53B2570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D194446-D48D-4DF6-9DCE-0DA2E53B2570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D194446-D48D-4DF6-9DCE-0DA2E53B2570}.Release|Any CPU.Build.0 = Release|Any CPU + {8CBA9BFB-D58D-48B2-9139-17C15A5142A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CBA9BFB-D58D-48B2-9139-17C15A5142A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CBA9BFB-D58D-48B2-9139-17C15A5142A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CBA9BFB-D58D-48B2-9139-17C15A5142A6}.Release|Any CPU.Build.0 = Release|Any CPU + {4441B345-1B99-4DE6-9764-B34E3ED8A982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4441B345-1B99-4DE6-9764-B34E3ED8A982}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4441B345-1B99-4DE6-9764-B34E3ED8A982}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4441B345-1B99-4DE6-9764-B34E3ED8A982}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0ACCBE5B-CC82-4584-943D-22CCC07012D2} + EndGlobalSection +EndGlobal